Compare commits

..

3 Commits

Author SHA1 Message Date
Sam Nolan
ad73ff98cc Merge branch 'develop' into metalog 2022-08-13 13:39:10 +01:00
Sam Nolan
594e6504ca Add comments about metalog distribution math 2022-07-19 17:19:30 +10:00
Sam Nolan
2adaba7d91 Add metalog 2022-07-19 17:05:15 +10:00
326 changed files with 11755 additions and 14970 deletions

20
.github/CODEOWNERS vendored
View File

@ -9,22 +9,22 @@
# This also holds true for GitHub teams. # This also holds true for GitHub teams.
# Rescript # Rescript
*.res @berekuk @OAGr *.res @OAGr
*.resi @berekuk @OAGr *.resi @OAGr
# Typescript # Typescript
*.tsx @Hazelfire @berekuk @OAGr *.tsx @Hazelfire @OAGr
*.ts @Hazelfire @berekuk @OAGr *.ts @Hazelfire @OAGr
# Javascript # Javascript
*.js @Hazelfire @berekuk @OAGr *.js @Hazelfire @OAGr
# Any opsy files # Any opsy files
.github/** @quinn-dougherty @berekuk @OAGr .github/** @quinn-dougherty @OAGr
*.json @quinn-dougherty @Hazelfire @berekuk @OAGr *.json @quinn-dougherty @Hazelfire @OAGr
*.y*ml @quinn-dougherty @berekuk @OAGr *.y*ml @quinn-dougherty @OAGr
*.config.js @Hazelfire @berekuk @OAGr *.config.js @Hazelfire @OAGr
vercel.json @OAGr @berekuk @Hazelfire netlify.toml @quinn-dougherty @OAGr @Hazelfire
# Documentation # Documentation
*.md @quinn-dougherty @OAGr @Hazelfire *.md @quinn-dougherty @OAGr @Hazelfire

View File

@ -12,13 +12,9 @@ updates:
commit-message: commit-message:
prefix: "⬆️" prefix: "⬆️"
open-pull-requests-limit: 100 open-pull-requests-limit: 100
labels:
- "dependencies"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
commit-message: commit-message:
prefix: "⬆️" prefix: "⬆️"
labels:
- "dependencies"

View File

@ -1,87 +0,0 @@
name: Nix build
on:
push:
branches:
- master
- develop
pull_request:
branches:
- master
- develop
- reducer-dev
- epic-reducer-project
jobs:
flake-lints:
name: All lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install nix
uses: cachix/install-nix-action@v17
with:
nix_path: nixpkgs=channel:nixos-22.05
- name: Use cachix
uses: cachix/cachix-action@v10
with:
name: quantified-uncertainty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Check that lang lints
run: nix build .#lang-lint
- name: Check that components lints
run: nix build .#components-lint
- name: Check that website lints
run: nix build .#docusaurus-lint
- name: Check that vscode extension lints
run: nix build .#vscode-lint
- name: Check that cli lints
run: nix build .#cli-lint
flake-packages:
name: Builds, tests, and bundles
runs-on: ubuntu-latest
needs: flake-lints
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install nix
uses: cachix/install-nix-action@v17
with:
nix_path: nixpkgs=channel:nixos-22.05
- name: Use cachix
uses: cachix/cachix-action@v10
with:
name: quantified-uncertainty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Check all lang tests
run: nix build .#lang-test
- name: Check that lang bundles
run: nix build .#lang-bundle
- name: Check that components builds
run: nix build .#components
- name: Check that components bundles
run: nix build .#components-bundle
flake-devshells:
name: Development shell environment
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install nix
uses: cachix/install-nix-action@v17
with:
nix_path: nixpkgs=channel:nixos-22.05
- name: Use cachix
uses: cachix/cachix-action@v10
with:
name: quantified-uncertainty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Build js devshell
run: nix develop .#js --profile just-js
- name: Build js & wasm devshell
run: nix develop --profile full-shell

View File

@ -1,4 +1,4 @@
name: Squiggle packages checks name: Squiggle packages check
on: on:
push: push:
@ -9,40 +9,213 @@ on:
branches: branches:
- master - master
- develop - develop
- reducer-dev
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: quantified-uncertainty
jobs: jobs:
build-test-lint: pre_check:
name: Build, test, lint name: Precheck for skipping redundant jobs
runs-on: ubuntu-latest 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/**"]'
lang-lint:
name: Language lint
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_lang != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/squiggle-lang
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup Node.js environment - name: Install Dependencies
uses: actions/setup-node@v3 run: cd ../../ && yarn
- name: Check rescript lint
run: yarn lint:rescript
- name: Check javascript, typescript, and markdown lint
uses: creyD/prettier_action@v4.2
with: with:
node-version: 16 dry: true
cache: 'yarn' prettier_options: --check packages/squiggle-lang
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Turbo run
run: npx turbo run build test lint bundle
coverage: lang-build-test-bundle:
name: Coverage name: Language build, test, and bundle
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_lang != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/squiggle-lang
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 2 fetch-depth: 2
- name: Setup Node.js environment - name: Install dependencies from monorepo level
uses: actions/setup-node@v2 run: cd ../../ && yarn
- name: Build rescript codebase
run: yarn build
- name: Run rescript tests
run: yarn test:rescript
- name: Run typescript tests
run: yarn test:ts
- name: Run webpack
run: yarn bundle
- name: Upload rescript coverage report
run: yarn coverage:rescript:ci
- name: Upload typescript coverage report
run: yarn coverage:ts:ci
components-lint:
name: Components lint
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_components != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/components
steps:
- uses: actions/checkout@v3
- name: Check javascript, typescript, and markdown lint
uses: creyD/prettier_action@v4.2
with: with:
node-version: 16 dry: true
cache: 'yarn' prettier_options: --check packages/components --ignore-path packages/components/.prettierignore
- name: Install dependencies
run: yarn components-bundle-build:
- name: Coverage name: Components bundle and build
run: npx turbo run coverage runs-on: ubuntu-latest
needs: pre_check
if: ${{ (needs.pre_check.outputs.should_skip_components != 'true') || (needs.pre_check.outputs.should_skip_lang != 'true') }}
defaults:
run:
shell: bash
working-directory: packages/components
steps:
- uses: actions/checkout@v3
- name: Install dependencies from monorepo level
run: cd ../../ && yarn
- name: Build rescript codebase in squiggle-lang
run: cd ../squiggle-lang && yarn build
- name: Run webpack
run: yarn bundle
- name: Build storybook
run: yarn build
website-lint:
name: Website lint
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_website != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/website
steps:
- uses: actions/checkout@v3
- name: Check javascript, typescript, and markdown lint
uses: creyD/prettier_action@v4.2
with:
dry: true
prettier_options: --check packages/website
website-build:
name: Website build
runs-on: ubuntu-latest
needs: pre_check
if: ${{ (needs.pre_check.outputs.should_skip_website != 'true') || (needs.pre_check.outputs.should_skip_lang != 'true') || (needs.pre_check.outputs.should_skip_components != 'true') }}
defaults:
run:
shell: bash
working-directory: packages/website
steps:
- uses: actions/checkout@v3
- name: Install dependencies from monorepo level
run: cd ../../ && yarn
- name: Build rescript in squiggle-lang
run: cd ../squiggle-lang && yarn build
- name: Build components
run: cd ../components && yarn build
- name: Build website assets
run: yarn build
vscode-ext-lint:
name: VS Code extension lint
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_vscodeext != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/vscode-ext
steps:
- uses: actions/checkout@v3
- name: Install dependencies from monorepo level
run: cd ../../ && yarn
- name: Lint the VSCode Extension source code
run: yarn lint
vscode-ext-build:
name: VS Code extension build
runs-on: ubuntu-latest
needs: pre_check
if: ${{ (needs.pre_check.outputs.should_skip_components != 'true') || (needs.pre_check.outputs.should_skip_lang != 'true') }} || (needs.pre_check.outputs.should_skip_vscodeext != 'true') }}
defaults:
run:
shell: bash
working-directory: packages/vscode-ext
steps:
- uses: actions/checkout@v3
- name: Install dependencies from monorepo level
run: cd ../../ && yarn
- name: Build
run: yarn compile
cli-lint:
name: CLI lint
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_cli != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/cli
steps:
- uses: actions/checkout@v3
- name: Check javascript, typescript, and markdown lint
uses: creyD/prettier_action@v4.2
with:
dry: true
prettier_options: --check packages/cli

View File

@ -3,7 +3,7 @@ name: Run Release Please
on: on:
push: push:
branches: branches:
- master - develop
jobs: jobs:
pre_check: pre_check:
@ -18,27 +18,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@v5.2.0 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@v5.2.0 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@v5.2.0 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@v5.2.0 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@v5.2.0 uses: fkirc/skip-duplicate-actions@v4.0.0
with: with:
paths: '["packages/cli/**"]' paths: '["packages/cli/**"]'
@ -55,22 +55,21 @@ jobs:
token: ${{secrets.GITHUB_TOKEN}} token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr command: manifest-pr
path: packages/squiggle-lang path: packages/squiggle-lang
# bump-patch-for-minor-pre-major: true bump-patch-for-minor-pre-major: true
skip-github-release: true skip-github-release: true
- name: Publish- Checkout source # - name: Publish: Checkout source
uses: actions/checkout@v3 # uses: actions/checkout@v2
# these if statements ensure that a publication only occurs when # # these if statements ensure that a publication only occurs when
# a new release is created: # # a new release is created:
if: ${{ steps.release.outputs.release_created }} # if: ${{ steps.release.outputs.release_created }}
- name: Publish- Install dependencies # - name: Publish: Install dependencies
run: yarn # run: yarn
if: ${{ steps.release.outputs.release_created }} # if: ${{ steps.release.outputs.release_created }}
- name: Publish # - name: Publish
run: cd packages/squiggle-lang && yarn publish # run: cd packages/squiggle-lang && yarn publish
env: # env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: ${{ steps.release.outputs.release_created }} # if: ${{ steps.release.outputs.release_created }}
relplz-components: relplz-components:
name: for components name: for components
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -83,20 +82,20 @@ jobs:
token: ${{secrets.GITHUB_TOKEN}} token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr command: manifest-pr
path: packages/components path: packages/components
# bump-patch-for-minor-pre-major: true bump-patch-for-minor-pre-major: true
skip-github-release: true skip-github-release: true
- name: Publish- Checkout source # - name: Publish: Checkout source
uses: actions/checkout@v3 # uses: actions/checkout@v2
# these if statements ensure that a publication only occurs when # # these if statements ensure that a publication only occurs when
# a new release is created: # # a new release is created:
if: ${{ steps.release.outputs.release_created }} # if: ${{ steps.release.outputs.release_created }}
- name: Publish- Install dependencies # - name: Publish: Install dependencies
run: yarn # run: yarn
if: ${{ steps.release.outputs.release_created }} # if: ${{ steps.release.outputs.release_created }}
- name: Publish # - name: Publish
run: cd packages/components && yarn publish # run: cd packages/components && yarn publish
env: # env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
relplz-website: relplz-website:
name: for website name: for website
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -109,7 +108,7 @@ jobs:
token: ${{secrets.GITHUB_TOKEN}} token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr command: manifest-pr
path: packages/website path: packages/website
# bump-patch-for-minor-pre-major: true bump-patch-for-minor-pre-major: true
skip-github-release: true skip-github-release: true
relplz-vscodeext: relplz-vscodeext:
name: for vscode-ext name: for vscode-ext
@ -123,7 +122,7 @@ jobs:
token: ${{secrets.GITHUB_TOKEN}} token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr command: manifest-pr
path: packages/vscode-ext path: packages/vscode-ext
# bump-patch-for-minor-pre-major: true bump-patch-for-minor-pre-major: true
skip-github-release: true skip-github-release: true
relplz-cl: relplz-cl:
name: for cli name: for cli

6
.gitignore vendored
View File

@ -7,9 +7,3 @@ yarn-error.log
**/.sync.ffs_db **/.sync.ffs_db
.direnv .direnv
.log .log
.vscode
todo.txt
result
shell.nix
.turbo

View File

@ -1,16 +1,15 @@
.direnv .direnv
*.bs.js *.bs.js
*.gen.tsx *.gen.tsx
packages/*/dist
packages/components/storybook-static packages/components/storybook-static
node_modules node_modules
packages/*/node_modules packages/*/node_modules
packages/website/.docusaurus packages/website/.docusaurus
packages/squiggle-lang/lib packages/squiggle-lang/lib
packages/squiggle-lang/.nyc_output/
packages/squiggle-lang/coverage/ packages/squiggle-lang/coverage/
packages/squiggle-lang/.cache/ packages/squiggle-lang/.cache/
packages/website/build/ packages/website/build/
packages/squiggle-lang/src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js packages/squiggle-lang/src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js
packages/vscode-ext/media/vendor/ packages/vscode-ext/media/vendor/
packages/squiggle-lang/.nyc_output/
packages/*/dist
result

View File

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

View File

@ -1 +0,0 @@
See the [Changelog.mdx page](./packages/website/docs/Changelog.mdx) for the changelog.

View File

@ -16,7 +16,7 @@ Squiggle is currently pre-alpha.
# Bug reports # Bug reports
Anyone (with a github account) can file an issue at any time. Please allow Slava, Sam, and Ozzie to triage, but otherwise just follow the suggestions in the issue templates. Anyone (with a github account) can file an issue at any time. Please allow Quinn, Sam, and Ozzie to triage, but otherwise just follow the suggestions in the issue templates.
# Project structure # Project structure
@ -28,7 +28,7 @@ Squiggle is a **monorepo** with three **packages**.
# Deployment ops # Deployment ops
We use Vercel, and it should only concern Slava, Sam, and Ozzie. We use netlify, and it should only concern Quinn, Sam, and Ozzie.
# Development environment, building, testing, dev server # Development environment, building, testing, dev server
@ -56,9 +56,9 @@ If you absolutely must, please prefix your commit message with `hotfix: `.
Please work against `develop` branch. **Do not** work against `master`. Please work against `develop` branch. **Do not** work against `master`.
- For rescript code: Slava and Ozzie are reviewers - For rescript code: Quinn and Ozzie are reviewers
- For js or typescript code: Sam and Ozzie are reviewers - For js or typescript code: Sam and Ozzie are reviewers
- For ops code (i.e. yaml, package.json): Slava and Sam are reviewers - For ops code (i.e. yaml, package.json): Quinn and Sam are reviewers
Autopings are set up: if you are not autopinged, you are welcome to comment, but please do not use the formal review feature, send approvals, rejections, or merges. Autopings are set up: if you are not autopinged, you are welcome to comment, but please do not use the formal review feature, send approvals, rejections, or merges.

View File

@ -21,10 +21,10 @@ _An estimation language_.
## Our deployments ## Our deployments
- **website/docs prod**: https://squiggle-language.com - **website/docs prod**: https://squiggle-language.com [![Netlify Status](https://api.netlify.com/api/v1/badges/2139af5c-671d-473d-a9f6-66c96077d8a1/deploy-status)](https://app.netlify.com/sites/squiggle-documentation/deploys)
- **website/docs staging**: https://preview.squiggle-language.com - **website/docs staging**: https://develop--squiggle-documentation.netlify.app/
- **components storybook prod**: https://components.squiggle-language.com - **components storybook prod**: https://squiggle-components.netlify.app/ [![Netlify Status](https://api.netlify.com/api/v1/badges/b7f724aa-6b20-4d0e-bf86-3fcd1a3e9a70/deploy-status)](https://app.netlify.com/sites/squiggle-components/deploys)
- **components storybook staging**: https://preview-components.squiggle-language.com - **components storybook staging**: https://develop--squiggle-components.netlify.app/
- **legacy (2020) playground**: https://playground.squiggle-language.com - **legacy (2020) playground**: https://playground.squiggle-language.com
## Packages ## Packages
@ -51,25 +51,7 @@ For any project in the repo, begin by running `yarn` in the top level
yarn yarn
``` ```
Then use `turbo` to build the specific packages or the entire monorepo: See `packages/*/README.md` to work with whatever project you're interested in.
```sh
turbo run build
```
Or:
```sh
turbo run build --filter=@quri/squiggle-components
```
You can also run specific npm scripts for the package you're working on. See `packages/*/README.md` for the details.
# NixOS users
This repository requires the use of bundled binaries from node_modules, which
are not linked statically. The easiest way to get them working is to enable
[nix-ld](https://github.com/Mic92/nix-ld).
# Contributing # Contributing

View File

@ -1,79 +0,0 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gentype": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1661855866,
"narHash": "sha256-+q0OOTyaq8eOn9BOWdPOCtSDOISW4A59v3mq3JOZyug=",
"owner": "rescript-association",
"repo": "genType",
"rev": "6b5f164b4f6ced456019b7579a0ab7e0a86518ad",
"type": "github"
},
"original": {
"owner": "rescript-association",
"repo": "genType",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1661617163,
"narHash": "sha256-NN9Ky47j8ohgPhA9JZyfkYIbbAo6RJkGz+7h8/exVpE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0ba2543f8c855d7be8e90ef6c8dc89c1617e8a08",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.05",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"gentype": "gentype",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,99 +0,0 @@
{
description = "Squiggle packages";
inputs = {
nixpkgs.url = "nixpkgs/nixos-22.05";
gentype = {
url = "github:rescript-association/genType";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, gentype, flake-utils }:
let
version = builtins.substring 0 8 self.lastModifiedDate;
overlays = [
(final: prev: {
# set the node version here
nodejs = prev.nodejs-18_x;
# The override is the only way to get it into mkYarnModules
})
];
commonFn = pkgs: {
buildInputs = with pkgs; [ nodejs yarn ];
prettier = with pkgs.nodePackages; [ prettier ];
which = [ pkgs.which ];
};
gentypeOutputFn = pkgs: gentype.outputs.packages.${pkgs.system}.default;
langFn = { pkgs, ... }:
# Probably doesn't work on i686-linux
import ./nix/squiggle-lang.nix {
inherit pkgs commonFn gentypeOutputFn;
};
componentsFn = { pkgs, ... }:
import ./nix/squiggle-components.nix { inherit pkgs commonFn langFn; };
websiteFn = { pkgs, ... }:
import ./nix/squiggle-website.nix {
inherit pkgs commonFn langFn componentsFn;
};
vscodeextFn = { pkgs, ... }:
import ./nix/squiggle-vscode.nix {
inherit pkgs commonFn langFn componentsFn;
};
cliFn = { pkgs, ... }:
import ./nix/squiggle-cli.nix {
inherit pkgs commonFn;
};
# local machines
localFlakeOutputs = { pkgs, ... }:
let
lang = langFn pkgs;
components = componentsFn pkgs;
website = websiteFn pkgs;
vscodeext = vscodeextFn pkgs;
cli = cliFn pkgs;
in {
# validating
checks = flake-utils.lib.flattenTree {
lang-lint = lang.lint;
lang-test = lang.test;
components-lint = components.lint;
docusaurus-lint = website.lint;
cli-lint = cli.lint;
};
# building
packages = flake-utils.lib.flattenTree {
default = components.build;
lang = lang.build;
lang-bundle = lang.bundle;
lang-test = lang.test;
components = components.build;
components-bundle = components.bundle;
# Lint
lang-lint = lang.lint;
components-lint = components.lint;
docusaurus-lint = website.lint;
vscode-lint = vscodeext.lint;
cli-lint = cli.lint;
};
# developing
devShells = let shellNix = import ./nix/shell.nix { inherit pkgs; };
in flake-utils.lib.flattenTree {
default = shellNix.all;
js = shellNix.just-js;
};
};
in flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = overlays;
};
in localFlakeOutputs pkgs);
}

View File

@ -1 +0,0 @@
Visit `quantified-uncertainty.cachix.org` for information about how to add our binary cache to your local dev environment.

View File

@ -1,25 +0,0 @@
{ pkgs }:
with pkgs;
let
js = [ yarn nodejs nodePackages.ts-node ];
rust = [
wasm-pack
cargo
rustup
pkg-config
libressl
rustfmt
wasmtime
binaryen
wasm-bindgen-cli
];
in {
all = mkShell {
name = "squiggle_yarn-wasm-devshell";
buildInputs = builtins.concatLists [ js rust [ nixfmt ] ];
};
just-js = mkShell {
name = "squiggle_yarn-devshell";
buildInputs = js ++ [ nixfmt ];
};
}

View File

@ -1,13 +0,0 @@
{ pkgs, commonFn }:
rec {
common = commonFn pkgs;
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-cli-lint";
buildInputs = common.buildInputs ++ common.prettier;
src = ../packages/cli;
buildPhase = "prettier --check .";
installPhase = "mkdir -p $out";
};
}

View File

@ -1,75 +0,0 @@
{ pkgs, commonFn, langFn }:
rec {
common = commonFn pkgs;
lang = langFn pkgs;
componentsPackageJson = let
raw = pkgs.lib.importJSON ../packages/components/package.json;
modified =
pkgs.lib.recursiveUpdate raw { dependencies.react-dom = "^18.2.0"; };
packageJsonString = builtins.toJSON modified;
in pkgs.writeText "packages/components/patched-package.json"
packageJsonString;
yarn-source = pkgs.mkYarnPackage {
name = "squiggle-components_yarnsource";
buildInputs = common.buildInputs;
src = ../packages/components;
packageJSON = componentsPackageJson;
yarnLock = ../yarn.lock;
packageResolutions."@quri/squiggle-lang" = lang.build;
};
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-components-lint";
src = ../packages/components;
buildInputs = common.buildInputs ++ common.prettier;
buildPhase = "yarn lint";
installPhase = "mkdir -p $out";
};
build = pkgs.stdenv.mkDerivation {
name = "squiggle-components-build";
src = yarn-source + "/libexec/@quri/squiggle-components";
buildInputs = common.buildInputs;
buildPhase = ''
cp -r node_modules/@quri/squiggle-lang deps/@quri
pushd deps/@quri/squiggle-components
yarn --offline build:cjs
yarn --offline build:css
popd
'';
installPhase = ''
mkdir -p $out
# annoying hack because permissions on transitive dependencies later on
mv deps/@quri/squiggle-components/node_modules deps/@quri/squiggle-components/NODE_MODULES
mv node_modules deps/@quri/squiggle-components
# patching .gitignore so flake keeps build artefacts
sed -i /dist/d deps/@quri/squiggle-components/.gitignore
cp -r deps/@quri/squiggle-components/. $out
'';
};
bundle = pkgs.stdenv.mkDerivation {
name = "squiggle-components-bundle";
src = yarn-source + "/libexec/@quri/squiggle-components";
buildInputs = common.buildInputs;
buildPhase = ''
cp -r node_modules/@quri/squiggle-lang deps/@quri
pushd deps/@quri/squiggle-components
yarn --offline bundle
popd
'';
installPhase = ''
mkdir -p $out
# annoying hack because permissions on transitive dependencies later on
mv deps/@quri/squiggle-components/node_modules deps/@quri/squiggle-components/NODE_MODULES
mv node_modules deps/@quri/squiggle-components
# patching .gitignore so flake keeps build artefacts
sed -i /dist/d deps/@quri/squiggle-components/.gitignore
cp -r deps/@quri/squiggle-components/. $out
'';
};
}

View File

@ -1,116 +0,0 @@
{ pkgs, commonFn, gentypeOutputFn }:
rec {
common = commonFn pkgs;
langPackageJson = let
raw = pkgs.lib.importJSON ../packages/squiggle-lang/package.json;
modified = pkgs.lib.recursiveUpdate raw {
devDependencies."@types/lodash" = "^4.14.167";
};
packageJsonString = builtins.toJSON modified;
in pkgs.writeText "packages/squiggle-lang/patched-package.json"
packageJsonString;
yarn-source = pkgs.mkYarnPackage {
name = "squiggle-lang_yarnsource";
src = ../packages/squiggle-lang;
packageJSON = langPackageJson;
yarnLock = ../yarn.lock;
pkgConfig = {
rescript = {
buildInputs = common.which
++ (if pkgs.system != "i686-linux" then [ pkgs.gcc_multi ] else [ ]);
postInstall = ''
echo "PATCHELF'ING RESCRIPT EXECUTABLES (INCL NINJA)"
# Patching interpreter for linux/*.exe's
THE_LD=$(patchelf --print-interpreter $(which mkdir))
patchelf --set-interpreter $THE_LD linux/*.exe && echo "- patched interpreter for linux/*.exe's"
# Replacing needed shared library for linux/ninja.exe
THE_SO=$(find /nix/store/*/lib64 -name libstdc++.so.6 | head -n 1)
patchelf --replace-needed libstdc++.so.6 $THE_SO linux/ninja.exe && echo "- replaced needed for linux/ninja.exe"
'';
};
gentype = {
postInstall = ''
mv gentype.exe ELFLESS-gentype.exe
cp ${gentypeOutputFn pkgs}/src/GenType.exe gentype.exe
'';
};
};
};
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-lang-lint";
src = yarn-source + "/libexec/@quri/squiggle-lang/deps/@quri/squiggle-lang";
buildInputs = common.buildInputs ++ common.prettier;
buildPhase = ''
yarn lint:prettier
yarn lint:rescript
'';
installPhase = "mkdir -p $out";
};
build = pkgs.stdenv.mkDerivation {
name = "squiggle-lang-build";
# `peggy` is in the `node_modules` that's adjacent to `deps`.
src = yarn-source + "/libexec/@quri/squiggle-lang";
buildInputs = common.buildInputs;
buildPhase = ''
# so that the path to ppx doesn't need to be patched.
mv node_modules deps
pushd deps/@quri/squiggle-lang
yarn --offline build:peggy
yarn --offline build:rescript
yarn --offline build:typescript
# custom gitignore so that the flake keeps build artefacts
mv .gitignore GITIGNORE
sed -i /Reducer_Peggy_GeneratedParser.js/d GITIGNORE
sed -i /ReducerProject_IncludeParser.js/d GITIGNORE
sed -i /\*.bs.js/d GITIGNORE
sed -i /\*.gen.ts/d GITIGNORE
sed -i /\*.gen.tsx/d GITIGNORE
sed -i /\*.gen.js/d GITIGNORE
sed -i /helpers.js/d GITIGNORE
popd
'';
installPhase = ''
mkdir -p $out
# mkdir -p $out/node_modules
mv deps/@quri/squiggle-lang/GITIGNORE deps/@quri/squiggle-lang/.gitignore
# annoying hack because permissions on transitive dependencies later on
mv deps/@quri/squiggle-lang/node_modules deps/@quri/squiggle-lang/NODE_MODULES
mv deps/node_modules deps/@quri/squiggle-lang
# the proper install phase
cp -r deps/@quri/squiggle-lang/. $out
'';
};
test = pkgs.stdenv.mkDerivation {
name = "squiggle-lang-test";
src = build;
buildInputs = common.buildInputs;
buildPhase = ''
yarn --offline test
'';
installPhase = ''
mkdir -p $out
cp -r . $out
'';
};
bundle = pkgs.stdenv.mkDerivation {
name = "squiggle-lang-bundle";
src = test;
buildInputs = common.buildInputs;
buildPhase = ''
yarn --offline bundle
'';
installPhase = ''
mkdir -p $out
cp -r dist $out
cp *.json $out/dist
'';
};
}

View File

@ -1,24 +0,0 @@
{ pkgs, commonFn, langFn, componentsFn }:
rec {
common = commonFn pkgs;
lang = langFn pkgs;
components = componentsFn pkgs;
yarn-source = pkgs.mkYarnPackage {
name = "squiggle-vscodeext_yarnsource";
src = ../packages/vscode-ext;
packageJson = ../packages/vscode-ext/package.json;
yarnLock = ../yarn.lock;
packageResolutions."@quri/squiggle-lang" = lang.build;
packageResolutions."@quri/squiggle-components" = components.build;
};
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-vscode-lint";
buildInputs = common.buildInputs ++ common.prettier;
src =
../packages/vscode-ext; # yarn-source + "/libexec/vscode-squiggle/deps/vscode-squiggle";
buildPhase = "prettier --check .";
installPhase = "mkdir -p $out";
};
}

View File

@ -1,30 +0,0 @@
{ pkgs, commonFn, langFn, componentsFn }:
rec {
common = commonFn pkgs;
lang = langFn pkgs;
components = componentsFn pkgs;
websitePackageJson = let
raw = pkgs.lib.importJSON ../packages/website/package.json;
modified = pkgs.lib.recursiveUpdate raw {
dependencies.postcss-import = "^14.1.0";
dependencies.tailwindcss = "^3.1.8";
};
packageJsonString = builtins.toJSON modified;
in pkgs.writeText "packages/website/patched-package.json" packageJsonString;
yarn-source = pkgs.mkYarnPackage {
name = "squiggle-website_yarnsource";
src = ../packages/website;
packageJSON = websitePackageJson;
yarnLock = ../yarn.lock;
packageResolutions."@quri/squiggle-lang" = lang.build;
packageResolutions."@quri/squiggle-components" = components.build;
};
lint = pkgs.stdenv.mkDerivation {
name = "squiggle-website-lint";
buildInputs = common.buildInputs ++ common.prettier;
src = ../packages/website;
buildPhase = "yarn lint";
installPhase = "mkdir -p $out";
};
}

18
nixos.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# This script is only relevant if you're rolling nixos.
# Esy (a bisect_ppx dependency/build tool) is borked on nixos without using an FHS shell. https://github.com/esy/esy/issues/858
# We need to patchelf rescript executables. https://github.com/NixOS/nixpkgs/issues/107375
set -x
fhsShellName="squiggle-development"
fhsShellDotNix="{pkgs ? import <nixpkgs> {} }: (pkgs.buildFHSUserEnv { name = \"${fhsShellName}\"; targetPkgs = pkgs: [pkgs.yarn]; runScript = \"yarn\"; }).env"
nix-shell - <<<"$fhsShellDotNix"
theLd=$(patchelf --print-interpreter $(which mkdir))
patchelf --set-interpreter $theLd ./node_modules/gentype/gentype.exe
patchelf --set-interpreter $theLd ./node_modules/rescript/linux/*.exe
patchelf --set-interpreter $theLd ./node_modules/bisect_ppx/ppx
patchelf --set-interpreter $theLd ./node_moduels/bisect_ppx/bisect-ppx-report
theSo=$(find /nix/store/*$fhsShellName*/lib64 -name libstdc++.so.6 | grep $fhsShellName | head -n 1)
patchelf --replace-needed libstdc++.so.6 $theSo ./node_modules/rescript/linux/ninja.exe

View File

@ -2,11 +2,12 @@
"private": true, "private": true,
"name": "squiggle", "name": "squiggle",
"scripts": { "scripts": {
"nodeclean": "rm -r node_modules && rm -r packages/*/node_modules" "nodeclean": "rm -r node_modules && rm -r packages/*/node_modules",
"format:all": "prettier --write . && cd packages/squiggle-lang && yarn format",
"lint:all": "prettier --check . && cd packages/squiggle-lang && yarn lint:rescript"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^2.7.1", "prettier": "^2.7.1"
"turbo": "^1.5.5"
}, },
"workspaces": [ "workspaces": [
"packages/*" "packages/*"

View File

@ -20,30 +20,3 @@ Runs compilation in the current directory and all of its subdirectories.
### `npx squiggle-cli-experimental watch` ### `npx squiggle-cli-experimental watch`
Watches `.squiggleU` files in the current directory (and subdirectories) and rebuilds them when they are saved. Note that this will _not_ rebuild files when their dependencies are changed, just when they are changed directly. Watches `.squiggleU` files in the current directory (and subdirectories) and rebuilds them when they are saved. Note that this will _not_ rebuild files when their dependencies are changed, just when they are changed directly.
## Further instructions
The above requires having node, npm and npx. To install the first two, see [here](https://nodejs.org/en/), to install npx, run:
```
npm install -g npx
```
Alternatively, you can run the following without the need for npx:
```
npm install squiggle-cli-experimental
node node_modules/squiggle-cli-experimental/index.js compile
```
or you can add a script to your `package.json`, like:
```
...
scripts: {
"compile": "squiggle-cli-experimental compile"
}
...
```
This can be run with `npm run compile`. `npm` knows how to reach into the node_modules directly, so it's not necessary to specify that.

View File

@ -7,15 +7,13 @@
"bin": "index.js", "bin": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node .", "start": "node ."
"lint": "prettier --check .",
"format": "prettier --write ."
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^5.1.0", "chalk": "^5.0.1",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"commander": "^9.4.1", "commander": "^9.4.0",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"glob": "^8.0.3", "glob": "^8.0.3",
"indent-string": "^5.0.0" "indent-string": "^5.0.0"

View File

@ -1,6 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
setupFilesAfterEnv: ["<rootDir>/test/setup.js"],
testEnvironment: "jsdom",
};

View File

@ -0,0 +1,8 @@
[build]
base = "packages/components/"
command = "cd ../squiggle-lang && yarn build && cd ../components && yarn build"
publish = "storybook-static/"
ignore = "node -e 'process.exitCode = process.env.BRANCH.includes(\"dependabot\") ? 0 : 1' && git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF . ../squiggle-lang"
[build.environment]
NETLIFY_USE_YARN = "true"

View File

@ -1,73 +1,64 @@
{ {
"name": "@quri/squiggle-components", "name": "@quri/squiggle-components",
"version": "0.5.0", "version": "0.3.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/react-dom": "^1.0.0", "@floating-ui/react-dom": "^1.0.0",
"@floating-ui/react-dom-interactions": "^0.10.1", "@floating-ui/react-dom-interactions": "^0.9.1",
"@headlessui/react": "^1.7.3", "@headlessui/react": "^1.6.6",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@hookform/resolvers": "^2.9.8", "@hookform/resolvers": "^2.9.7",
"@quri/squiggle-lang": "^0.5.0", "@quri/squiggle-lang": "^0.3.0",
"@react-hook/size": "^2.1.2", "@react-hook/size": "^2.1.2",
"@types/uuid": "^8.3.4",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"framer-motion": "^7.5.3", "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.37.0", "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",
"uuid": "^9.0.0",
"vega": "^5.22.1", "vega": "^5.22.1",
"vega-embed": "^6.21.0", "vega-embed": "^6.21.0",
"vega-lite": "^5.5.0", "vega-lite": "^5.4.0",
"vscode-uri": "^3.0.6", "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.12", "@storybook/addon-actions": "^6.5.9",
"@storybook/addon-essentials": "^6.5.12", "@storybook/addon-essentials": "^6.5.10",
"@storybook/addon-links": "^6.5.12", "@storybook/addon-links": "^6.5.10",
"@storybook/builder-webpack5": "^6.5.12", "@storybook/builder-webpack5": "^6.5.10",
"@storybook/manager-webpack5": "^6.5.12", "@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.12", "@storybook/react": "^6.5.10",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.2",
"@types/jest": "^27.5.0", "@types/jest": "^27.5.0",
"@types/lodash": "^4.14.186", "@types/lodash": "^4.14.182",
"@types/node": "^18.8.3", "@types/node": "^18.6.4",
"@types/react": "^18.0.21", "@types/react": "^18.0.9",
"@types/styled-components": "^5.1.26", "@types/styled-components": "^5.1.24",
"@types/uuid": "^8.3.4",
"@types/webpack": "^5.28.0", "@types/webpack": "^5.28.0",
"canvas": "^2.10.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.1.2",
"jsdom": "^20.0.1",
"mini-css-extract-plugin": "^2.6.1", "mini-css-extract-plugin": "^2.6.1",
"postcss-cli": "^10.0.0", "postcss-cli": "^10.0.0",
"postcss-import": "^15.0.0", "postcss-import": "^14.1.0",
"postcss-loader": "^7.0.1", "postcss-loader": "^7.0.1",
"postcss-nesting": "^10.2.0",
"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.8", "tailwindcss": "^3.1.8",
"ts-jest": "^29.0.3", "ts-loader": "^9.3.0",
"ts-loader": "^9.4.1",
"tsconfig-paths-webpack-plugin": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.8.4", "typescript": "^4.7.4",
"web-vitals": "^3.0.3", "web-vitals": "^2.1.4",
"webpack": "^5.74.0", "webpack": "^5.74.0",
"webpack-cli": "^4.10.0", "webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1" "webpack-dev-server": "^4.9.3"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17 || ^18", "react": "^16.8.0 || ^17 || ^18",
@ -75,7 +66,7 @@
}, },
"scripts": { "scripts": {
"start": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public", "start": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public",
"build:cjs": "rm -rf dist/src && rm -f dist/tsconfig.tsbuildinfo && tsc -b", "build:cjs": "tsc -b",
"build:css": "postcss ./src/styles/main.css -o ./dist/main.css", "build:css": "postcss ./src/styles/main.css -o ./dist/main.css",
"build:storybook": "build-storybook -s public", "build:storybook": "build-storybook -s public",
"build": "yarn run build:cjs && yarn run build:css && yarn run build:storybook", "build": "yarn run build:cjs && yarn run build:css && yarn run build:storybook",
@ -83,10 +74,7 @@
"all": "yarn bundle && yarn build", "all": "yarn bundle && yarn build",
"lint": "prettier --check .", "lint": "prettier --check .",
"format": "prettier --write .", "format": "prettier --write .",
"prepack": "yarn run build:cjs && yarn run bundle", "prepack": "yarn bundle && tsc -b"
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"test:profile": "node --cpu-prof node_modules/.bin/jest --runInBand"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@ -24,13 +24,13 @@ export const Alert: React.FC<{
children, children,
}) => { }) => {
return ( return (
<div className={clsx("rounded-md p-4", backgroundColor)} role="status"> <div className={clsx("rounded-md p-4", backgroundColor)}>
<div className="flex"> <div className="flex">
<Icon <Icon
className={clsx("h-5 w-5 flex-shrink-0", iconColor)} className={clsx("h-5 w-5 flex-shrink-0", iconColor)}
aria-hidden="true" aria-hidden="true"
/> />
<div className="ml-3 grow"> <div className="ml-3">
<header className={clsx("text-sm font-medium", headingColor)}> <header className={clsx("text-sm font-medium", headingColor)}>
{heading} {heading}
</header> </header>

View File

@ -5,8 +5,6 @@ import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-golang"; import "ace-builds/src-noconflict/mode-golang";
import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/theme-github";
import { SqLocation } from "@quri/squiggle-lang";
interface CodeEditorProps { interface CodeEditorProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
@ -15,17 +13,15 @@ interface CodeEditorProps {
width?: number; width?: number;
height: number; height: number;
showGutter?: boolean; showGutter?: boolean;
errorLocations?: SqLocation[];
} }
export const CodeEditor: FC<CodeEditorProps> = ({ export const CodeEditor: FC<CodeEditorProps> = ({
value, value,
onChange, onChange,
onSubmit, onSubmit,
height,
oneLine = false, oneLine = false,
showGutter = false, showGutter = false,
errorLocations = [], height,
}) => { }) => {
const lineCount = value.split("\n").length; const lineCount = value.split("\n").length;
const id = useMemo(() => _.uniqueId(), []); const id = useMemo(() => _.uniqueId(), []);
@ -34,11 +30,8 @@ export const CodeEditor: FC<CodeEditorProps> = ({
const onSubmitRef = useRef<typeof onSubmit | null>(null); const onSubmitRef = useRef<typeof onSubmit | null>(null);
onSubmitRef.current = onSubmit; onSubmitRef.current = onSubmit;
const editorEl = useRef<AceEditor | null>(null);
return ( return (
<AceEditor <AceEditor
ref={editorEl}
value={value} value={value}
mode="golang" mode="golang"
theme="github" theme="github"
@ -55,7 +48,10 @@ export const CodeEditor: FC<CodeEditorProps> = ({
editorProps={{ editorProps={{
$blockScrolling: true, $blockScrolling: true,
}} }}
setOptions={{}} setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
}}
commands={[ commands={[
{ {
name: "submit", name: "submit",
@ -63,14 +59,6 @@ export const CodeEditor: FC<CodeEditorProps> = ({
exec: () => onSubmitRef.current?.(), exec: () => onSubmitRef.current?.(),
}, },
]} ]}
markers={errorLocations?.map((location) => ({
startRow: location.start.line - 1,
startCol: location.start.column - 1,
endRow: location.end.line - 1,
endCol: location.end.column - 1,
className: "ace-error-marker",
type: "text",
}))}
/> />
); );
}; };

View File

@ -1,12 +1,9 @@
import * as React from "react"; import * as React from "react";
import { import {
SqDistribution, Distribution,
result, result,
SqDistributionError, distributionError,
resultMap, distributionErrorToString,
SqRecord,
environment,
SqDistributionTag,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { Vega } from "react-vega"; import { Vega } from "react-vega";
import { ErrorAlert } from "./Alert"; import { ErrorAlert } from "./Alert";
@ -17,8 +14,6 @@ import {
DistributionChartSpecOptions, DistributionChartSpecOptions,
} from "../lib/distributionSpecBuilder"; } from "../lib/distributionSpecBuilder";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "./NumberShower";
import { Plot, parsePlot } from "../lib/plotParser";
import { flattenResult } from "../lib/utility";
import { hasMassBelowZero } from "../lib/distributionUtils"; import { hasMassBelowZero } from "../lib/distributionUtils";
export type DistributionPlottingSettings = { export type DistributionPlottingSettings = {
@ -28,79 +23,33 @@ export type DistributionPlottingSettings = {
} & DistributionChartSpecOptions; } & DistributionChartSpecOptions;
export type DistributionChartProps = { export type DistributionChartProps = {
plot: Plot; distribution: Distribution;
environment: environment;
width?: number; width?: number;
height: number; height: number;
xAxisType?: "number" | "dateTime";
} & DistributionPlottingSettings; } & DistributionPlottingSettings;
export function defaultPlot(distribution: SqDistribution): Plot {
return { distributions: [{ name: "default", distribution }] };
}
export function makePlot(record: SqRecord): Plot | void {
const plotResult = parsePlot(record);
if (plotResult.tag === "Ok") {
return plotResult.value;
}
}
export const DistributionChart: React.FC<DistributionChartProps> = (props) => { export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
const { const {
plot, distribution,
environment,
height, height,
showSummary, showSummary,
width, width,
logX, logX,
actions = false, actions = false,
} = props; } = props;
const shape = distribution.pointSet();
const [sized] = useSize((size) => { const [sized] = useSize((size) => {
const shapes = flattenResult( if (shape.tag === "Error") {
plot.distributions.map((x) =>
resultMap(x.distribution.pointSet(environment), (pointSet) => ({
name: x.name,
// color: x.color, // not supported yet
...pointSet.asShape(),
}))
)
);
if (shapes.tag === "Error") {
return ( return (
<ErrorAlert heading="Distribution Error"> <ErrorAlert heading="Distribution Error">
{shapes.value.toString()} {distributionErrorToString(shape.value)}
</ErrorAlert> </ErrorAlert>
); );
} }
// if this is a sample set, include the samples const spec = buildVegaSpec(props);
const samples: number[] = [];
for (const { distribution } of plot?.distributions) {
if (distribution.tag === SqDistributionTag.SampleSet) {
samples.push(...distribution.value());
}
}
const domain = shapes.value.flatMap((shape) => let widthProp = width ? width : size.width;
shape.discrete.concat(shape.continuous)
);
const spec = buildVegaSpec({
...props,
minX: props.minX ?? Math.min(...domain.map((x) => x.x)),
maxX: props.minX ?? Math.max(...domain.map((x) => x.x)),
maxY: Math.max(...domain.map((x) => x.y)),
});
// I think size.width is sometimes not finite due to the component not being in a visible context
// This occurs during testing
let widthProp = width
? width
: Number.isFinite(size.width)
? size.width
: 400;
if (widthProp < 20) { if (widthProp < 20) {
console.warn( console.warn(
`Width of Distribution is set to ${widthProp}, which is too small` `Width of Distribution is set to ${widthProp}, which is too small`
@ -108,30 +57,23 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
widthProp = 20; widthProp = 20;
} }
const vegaData = { data: shapes.value, samples };
return ( return (
<div style={{ width: widthProp }}> <div style={{ width: widthProp }}>
{logX && shapes.value.some(hasMassBelowZero) ? ( {logX && hasMassBelowZero(shape.value) ? (
<ErrorAlert heading="Log Domain Error"> <ErrorAlert heading="Log Domain Error">
Cannot graph distribution with negative values on logarithmic scale. Cannot graph distribution with negative values on logarithmic scale.
</ErrorAlert> </ErrorAlert>
) : ( ) : (
<Vega <Vega
spec={spec} spec={spec}
data={vegaData} data={{ con: shape.value.continuous, dis: shape.value.discrete }}
width={widthProp - 10} width={widthProp - 10}
height={height} height={height}
actions={actions} actions={actions}
/> />
)} )}
<div className="flex justify-center"> <div className="flex justify-center">
{showSummary && plot.distributions.length === 1 && ( {showSummary && <SummaryTable distribution={distribution} />}
<SummaryTable
distribution={plot.distributions[0].distribution}
environment={environment}
/>
)}
</div> </div>
</div> </div>
); );
@ -154,36 +96,32 @@ const Cell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
); );
type SummaryTableProps = { type SummaryTableProps = {
distribution: SqDistribution; distribution: Distribution;
environment: environment;
}; };
const SummaryTable: React.FC<SummaryTableProps> = ({ const SummaryTable: React.FC<SummaryTableProps> = ({ distribution }) => {
distribution, const mean = distribution.mean();
environment, const stdev = distribution.stdev();
}) => { const p5 = distribution.inv(0.05);
const mean = distribution.mean(environment); const p10 = distribution.inv(0.1);
const stdev = distribution.stdev(environment); const p25 = distribution.inv(0.25);
const p5 = distribution.inv(environment, 0.05); const p50 = distribution.inv(0.5);
const p10 = distribution.inv(environment, 0.1); const p75 = distribution.inv(0.75);
const p25 = distribution.inv(environment, 0.25); const p90 = distribution.inv(0.9);
const p50 = distribution.inv(environment, 0.5); const p95 = distribution.inv(0.95);
const p75 = distribution.inv(environment, 0.75);
const p90 = distribution.inv(environment, 0.9);
const p95 = distribution.inv(environment, 0.95);
const hasResult = (x: result<number, SqDistributionError>): boolean => const hasResult = (x: result<number, distributionError>): boolean =>
x.tag === "Ok"; x.tag === "Ok";
const unwrapResult = ( const unwrapResult = (
x: result<number, SqDistributionError> x: result<number, distributionError>
): React.ReactNode => { ): React.ReactNode => {
if (x.tag === "Ok") { if (x.tag === "Ok") {
return <NumberShower number={x.value} />; return <NumberShower number={x.value} />;
} else { } else {
return ( return (
<ErrorAlert heading="Distribution Error"> <ErrorAlert heading="Distribution Error">
{x.value.toString()} {distributionErrorToString(x.value)}
</ErrorAlert> </ErrorAlert>
); );
} }

View File

@ -1,15 +1,14 @@
import * as React from "react"; import * as React from "react";
import { import {
SqLambda, lambdaValue,
environment, environment,
SqValueTag, runForeign,
SqError, errorValueToString,
} from "@quri/squiggle-lang"; } 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";
import { MessageAlert } from "./Alert"; import { ErrorAlert, MessageAlert } from "./Alert";
import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
export type FunctionChartSettings = { export type FunctionChartSettings = {
start: number; start: number;
@ -18,32 +17,13 @@ export type FunctionChartSettings = {
}; };
interface FunctionChartProps { interface FunctionChartProps {
fn: SqLambda; fn: lambdaValue;
chartSettings: FunctionChartSettings; chartSettings: FunctionChartSettings;
distributionPlotSettings: DistributionPlottingSettings; distributionPlotSettings: DistributionPlottingSettings;
environment: environment; environment: environment;
height: number; height: number;
} }
const FunctionCallErrorAlert = ({ error }: { error: SqError }) => {
const [expanded, setExpanded] = React.useState(false);
if (expanded) {
}
return (
<MessageAlert heading="Function Display Failed">
<div className="space-y-2">
<span
className="underline decoration-dashed cursor-pointer"
onClick={() => setExpanded(!expanded)}
>
{expanded ? "Hide" : "Show"} error details
</span>
{expanded ? <SquiggleErrorAlert error={error} /> : null}
</div>
</MessageAlert>
);
};
export const FunctionChart: React.FC<FunctionChartProps> = ({ export const FunctionChart: React.FC<FunctionChartProps> = ({
fn, fn,
chartSettings, chartSettings,
@ -51,16 +31,15 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
distributionPlotSettings, distributionPlotSettings,
height, height,
}) => { }) => {
console.log(fn.parameters().length); if (fn.parameters.length > 1) {
if (fn.parameters().length !== 1) {
return ( return (
<MessageAlert heading="Function Display Not Supported"> <MessageAlert heading="Function Display Not Supported">
Only functions with one parameter are displayed. Only functions with one parameter are displayed.
</MessageAlert> </MessageAlert>
); );
} }
const result1 = fn.call([chartSettings.start]); const result1 = runForeign(fn, [chartSettings.start], environment);
const result2 = fn.call([chartSettings.stop]); const result2 = runForeign(fn, [chartSettings.stop], environment);
const getValidResult = () => { const getValidResult = () => {
if (result1.tag === "Ok") { if (result1.tag === "Ok") {
return result1; return result1;
@ -73,11 +52,15 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
const validResult = getValidResult(); const validResult = getValidResult();
if (validResult.tag === "Error") { if (validResult.tag === "Error") {
return <FunctionCallErrorAlert error={validResult.value} />; return (
<ErrorAlert heading="Error">
{errorValueToString(validResult.value)}
</ErrorAlert>
);
} }
switch (validResult.value.tag) { switch (validResult.value.tag) {
case SqValueTag.Distribution: case "distribution":
return ( return (
<FunctionChart1Dist <FunctionChart1Dist
fn={fn} fn={fn}
@ -87,7 +70,7 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
distributionPlotSettings={distributionPlotSettings} distributionPlotSettings={distributionPlotSettings}
/> />
); );
case SqValueTag.Number: case "number":
return ( return (
<FunctionChart1Number <FunctionChart1Number
fn={fn} fn={fn}

View File

@ -2,20 +2,20 @@ import * as React from "react";
import _ from "lodash"; import _ from "lodash";
import type { Spec } from "vega"; import type { Spec } from "vega";
import { import {
SqDistribution, Distribution,
result, result,
SqLambda, lambdaValue,
environment, environment,
SqError, runForeign,
SqValue, squiggleExpression,
SqValueTag, errorValue,
errorValueToString,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega"; import { createClassFromSpec } from "react-vega";
import * as percentilesSpec from "../vega-specs/spec-percentiles.json"; import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
import { import {
DistributionChart, DistributionChart,
DistributionPlottingSettings, DistributionPlottingSettings,
defaultPlot,
} from "./DistributionChart"; } from "./DistributionChart";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "./NumberShower";
import { ErrorAlert } from "./Alert"; import { ErrorAlert } from "./Alert";
@ -45,7 +45,7 @@ export type FunctionChartSettings = {
}; };
interface FunctionChart1DistProps { interface FunctionChart1DistProps {
fn: SqLambda; fn: lambdaValue;
chartSettings: FunctionChartSettings; chartSettings: FunctionChartSettings;
distributionPlotSettings: DistributionPlottingSettings; distributionPlotSettings: DistributionPlottingSettings;
environment: environment; environment: environment;
@ -76,17 +76,9 @@ type errors = _.Dictionary<
}[] }[]
>; >;
type point = { x: number; value: result<SqDistribution, string> }; type point = { x: number; value: result<Distribution, string> };
let getPercentiles = ({ let getPercentiles = ({ chartSettings, fn, environment }) => {
chartSettings,
fn,
environment,
}: {
chartSettings: FunctionChartSettings;
fn: SqLambda;
environment: environment;
}) => {
let chartPointsToRender = _rangeByCount( let chartPointsToRender = _rangeByCount(
chartSettings.start, chartSettings.start,
chartSettings.stop, chartSettings.stop,
@ -94,9 +86,9 @@ let getPercentiles = ({
); );
let chartPointsData: point[] = chartPointsToRender.map((x) => { let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = fn.call([x]); let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") { if (result.tag === "Ok") {
if (result.value.tag === SqValueTag.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 {
@ -111,13 +103,13 @@ let getPercentiles = ({
} else { } else {
return { return {
x, x,
value: { tag: "Error", value: result.value.toString() }, value: { tag: "Error", value: errorValueToString(result.value) },
}; };
} }
}); });
let initialPartition: [ let initialPartition: [
{ x: number; value: SqDistribution }[], { x: number; value: Distribution }[],
{ x: number; value: string }[] { x: number; value: string }[]
] = [[], []]; ] = [[], []];
@ -133,23 +125,26 @@ let getPercentiles = ({
let groupedErrors: errors = _.groupBy(errors, (x) => x.value); let groupedErrors: errors = _.groupBy(errors, (x) => x.value);
let percentiles: percentiles = functionImage.map(({ x, value }) => { let percentiles: percentiles = functionImage.map(({ x, value }) => {
const res = { // We convert it to to a pointSet distribution first, so that in case its a sample set
// distribution, it doesn't internally convert it to a pointSet distribution for every
// single inv() call.
let toPointSet: Distribution = unwrap(value.toPointSet());
return {
x: x, x: x,
p1: unwrap(value.inv(environment, 0.01)), p1: unwrap(toPointSet.inv(0.01)),
p5: unwrap(value.inv(environment, 0.05)), p5: unwrap(toPointSet.inv(0.05)),
p10: unwrap(value.inv(environment, 0.1)), p10: unwrap(toPointSet.inv(0.1)),
p20: unwrap(value.inv(environment, 0.2)), p20: unwrap(toPointSet.inv(0.2)),
p30: unwrap(value.inv(environment, 0.3)), p30: unwrap(toPointSet.inv(0.3)),
p40: unwrap(value.inv(environment, 0.4)), p40: unwrap(toPointSet.inv(0.4)),
p50: unwrap(value.inv(environment, 0.5)), p50: unwrap(toPointSet.inv(0.5)),
p60: unwrap(value.inv(environment, 0.6)), p60: unwrap(toPointSet.inv(0.6)),
p70: unwrap(value.inv(environment, 0.7)), p70: unwrap(toPointSet.inv(0.7)),
p80: unwrap(value.inv(environment, 0.8)), p80: unwrap(toPointSet.inv(0.8)),
p90: unwrap(value.inv(environment, 0.9)), p90: unwrap(toPointSet.inv(0.9)),
p95: unwrap(value.inv(environment, 0.95)), p95: unwrap(toPointSet.inv(0.95)),
p99: unwrap(value.inv(environment, 0.99)), p99: unwrap(toPointSet.inv(0.99)),
}; };
return res;
}); });
return { percentiles, errors: groupedErrors }; return { percentiles, errors: groupedErrors };
@ -172,20 +167,19 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
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. //TODO: This custom error handling is a bit hacky and should be improved.
let mouseItem: result<SqValue, SqError> = !!mouseOverlay let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
? fn.call([mouseOverlay]) ? runForeign(fn, [mouseOverlay], environment)
: { : {
tag: "Error", tag: "Error",
value: SqError.createOtherError( value: {
"Hover x-coordinate returned NaN. Expected a number." tag: "RETodo",
), value: "Hover x-coordinate returned NaN. Expected a number.",
},
}; };
let showChart = let showChart =
mouseItem.tag === "Ok" && mouseItem.tag === "Ok" && mouseItem.value.tag === "distribution" ? (
mouseItem.value.tag === SqValueTag.Distribution ? (
<DistributionChart <DistributionChart
plot={defaultPlot(mouseItem.value.value)} distribution={mouseItem.value.value}
environment={environment}
width={400} width={400}
height={50} height={50}
{...distributionPlotSettings} {...distributionPlotSettings}

View File

@ -1,11 +1,16 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash"; import _ from "lodash";
import type { Spec } from "vega"; import type { Spec } from "vega";
import { result, SqLambda, environment, SqValueTag } from "@quri/squiggle-lang"; import {
result,
lambdaValue,
environment,
runForeign,
errorValueToString,
} from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega"; import { createClassFromSpec } from "react-vega";
import * as lineChartSpec from "../vega-specs/spec-line-chart.json"; import * as lineChartSpec from "../vega-specs/spec-line-chart.json";
import { ErrorAlert } from "./Alert"; import { ErrorAlert } from "./Alert";
import { squiggleValueTag } from "@quri/squiggle-lang/src/rescript/ForTS/ForTS_SquiggleValue/ForTS_SquiggleValue_tag";
let SquiggleLineChart = createClassFromSpec({ let SquiggleLineChart = createClassFromSpec({
spec: lineChartSpec as Spec, spec: lineChartSpec as Spec,
@ -25,7 +30,7 @@ export type FunctionChartSettings = {
}; };
interface FunctionChart1NumberProps { interface FunctionChart1NumberProps {
fn: SqLambda; fn: lambdaValue;
chartSettings: FunctionChartSettings; chartSettings: FunctionChartSettings;
environment: environment; environment: environment;
height: number; height: number;
@ -33,15 +38,7 @@ interface FunctionChart1NumberProps {
type point = { x: number; value: result<number, string> }; type point = { x: number; value: result<number, string> };
let getFunctionImage = ({ let getFunctionImage = ({ chartSettings, fn, environment }) => {
chartSettings,
fn,
environment,
}: {
chartSettings: FunctionChartSettings;
fn: SqLambda;
environment: environment;
}) => {
let chartPointsToRender = _rangeByCount( let chartPointsToRender = _rangeByCount(
chartSettings.start, chartSettings.start,
chartSettings.stop, chartSettings.stop,
@ -49,9 +46,9 @@ let getFunctionImage = ({
); );
let chartPointsData: point[] = chartPointsToRender.map((x) => { let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = fn.call([x]); let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") { if (result.tag === "Ok") {
if (result.value.tag === SqValueTag.Number) { if (result.value.tag == "number") {
return { x, value: { tag: "Ok", value: result.value.value } }; return { x, value: { tag: "Ok", value: result.value.value } };
} else { } else {
return { return {
@ -65,7 +62,7 @@ let getFunctionImage = ({
} else { } else {
return { return {
x, x,
value: { tag: "Error", value: result.value.toString() }, value: { tag: "Error", value: errorValueToString(result.value) },
}; };
} }
}); });

View File

@ -1,22 +1,25 @@
import * as React from "react"; import * as React from "react";
import { import {
SqValue, squiggleExpression,
bindings,
environment, environment,
SqProject, jsImports,
defaultImports,
defaultBindings,
defaultEnvironment, defaultEnvironment,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { useSquiggle } from "../lib/hooks"; import { useSquiggle } from "../lib/hooks";
import { SquiggleViewer } from "./SquiggleViewer"; import { SquiggleViewer } from "./SquiggleViewer";
import { JsImports } from "../lib/jsImports";
import { getValueToRender } from "../lib/utility";
export type 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 */ /** Allows to re-run the code if code hasn't changed */
executionId?: number; 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 */
environment?: environment;
/** If the result is a function, where the function domain starts */ /** If the result is a function, where the function domain starts */
diagramStart?: number; diagramStart?: number;
/** If the result is a function, where the function domain ends */ /** If the result is a function, where the function domain ends */
@ -24,12 +27,14 @@ export type SquiggleChartProps = {
/** If the result is a function, the amount of stops sampled */ /** If the result is a function, the amount of stops sampled */
diagramCount?: number; diagramCount?: number;
/** When the squiggle code gets reevaluated */ /** When the squiggle code gets reevaluated */
onChange?(expr: SqValue | undefined, sourceName: string): void; onChange?(expr: squiggleExpression | undefined): void;
/** CSS width of the element */ /** CSS width of the element */
width?: number; width?: number;
height?: number; height?: number;
/** Bindings of previous variables declared */
bindings?: bindings;
/** JS imported parameters */ /** JS imported parameters */
jsImports?: JsImports; jsImports?: jsImports;
/** Whether to show a summary of the distribution */ /** Whether to show a summary of the distribution */
showSummary?: boolean; showSummary?: boolean;
/** Set the x scale to be logarithmic by deault */ /** Set the x scale to be logarithmic by deault */
@ -46,35 +51,24 @@ export type SquiggleChartProps = {
minX?: number; minX?: number;
/** Specify the upper bound of the x scale */ /** Specify the upper bound of the x scale */
maxX?: number; maxX?: number;
/** Whether the x-axis should be dates or numbers */
xAxisType?: "number" | "dateTime";
/** 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; enableLocalSettings?: boolean;
} & (StandaloneExecutionProps | ProjectExecutionProps); }
// Props needed for a standalone execution
type StandaloneExecutionProps = {
project?: undefined;
continues?: undefined;
/** The amount of points returned to draw the distribution, not needed if using a project */
environment?: environment;
};
// Props needed when executing inside a project.
type ProjectExecutionProps = {
environment?: undefined;
/** The project that this execution is part of */
project: SqProject;
/** What other squiggle sources from the project to continue. Default [] */
continues?: string[];
};
const defaultOnChange = () => {}; const defaultOnChange = () => {};
const defaultImports: JsImports = {};
export const splitSquiggleChartSettings = (props: SquiggleChartProps) => { export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
const { ({
code = "",
executionId = 0,
environment,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
bindings = defaultBindings,
jsImports = defaultImports,
showSummary = false, showSummary = false,
width,
logX = false, logX = false,
expY = false, expY = false,
diagramStart = 0, diagramStart = 0,
@ -85,72 +79,44 @@ export const splitSquiggleChartSettings = (props: SquiggleChartProps) => {
maxX, maxX,
color, color,
title, title,
xAxisType = "number",
distributionChartActions, distributionChartActions,
} = props; enableLocalSettings = false,
}) => {
const distributionPlotSettings = { const result = useSquiggle({
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
color,
title,
xAxisType,
actions: distributionChartActions,
};
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return { distributionPlotSettings, chartSettings };
};
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
(props) => {
const { distributionPlotSettings, chartSettings } =
splitSquiggleChartSettings(props);
const {
code, code,
jsImports = defaultImports, bindings,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
executionId = 0,
width,
height = 200,
enableLocalSettings = false,
continues,
project,
environment, environment,
} = props;
const resultAndBindings = useSquiggle({
environment,
continues,
project,
code,
jsImports, jsImports,
onChange, onChange,
executionId, executionId,
}); });
const valueToRender = getValueToRender(resultAndBindings); const distributionPlotSettings = {
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
color,
title,
actions: distributionChartActions,
};
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return ( return (
<SquiggleViewer <SquiggleViewer
result={valueToRender} result={result}
width={width} width={width}
height={height} height={height}
distributionPlotSettings={distributionPlotSettings} distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings} chartSettings={chartSettings}
environment={ environment={environment ?? defaultEnvironment}
project ? project.getEnvironment() : environment ?? defaultEnvironment
}
enableLocalSettings={enableLocalSettings} enableLocalSettings={enableLocalSettings}
/> />
); );

View File

@ -1,29 +1,23 @@
import React from "react"; import React from "react";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import { environment, bindings, jsImports } from "@quri/squiggle-lang";
import { defaultImports, defaultBindings } from "@quri/squiggle-lang";
import { SquiggleContainer } from "./SquiggleContainer"; import { SquiggleContainer } from "./SquiggleContainer";
import { import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart";
splitSquiggleChartSettings, import { useSquigglePartial, useMaybeControlledValue } from "../lib/hooks";
SquiggleChartProps, import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
} from "./SquiggleChart";
import { useMaybeControlledValue, useSquiggle } from "../lib/hooks";
import { JsImports } from "../lib/jsImports";
import { defaultEnvironment, SqLocation, SqProject } from "@quri/squiggle-lang";
import { SquiggleViewer } from "./SquiggleViewer";
import { getErrorLocations, getValueToRender } from "../lib/utility";
const WrappedCodeEditor: React.FC<{ const WrappedCodeEditor: React.FC<{
code: string; code: string;
setCode: (code: string) => void; setCode: (code: string) => void;
errorLocations?: SqLocation[]; }> = ({ code, setCode }) => (
}> = ({ code, setCode, errorLocations }) => ( <div className="border border-grey-200 p-2 m-4">
<div className="border border-grey-200 p-2 m-4" data-testid="squiggle-editor">
<CodeEditor <CodeEditor
value={code} value={code}
onChange={setCode} onChange={setCode}
oneLine={true} oneLine={true}
showGutter={false} showGutter={false}
height={20} height={20}
errorLocations={errorLocations}
/> />
</div> </div>
); );
@ -33,9 +27,6 @@ export type SquiggleEditorProps = SquiggleChartProps & {
onCodeChange?: (code: string) => void; onCodeChange?: (code: string) => void;
}; };
const defaultOnChange = () => {};
const defaultImports: JsImports = {};
export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => { export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
const [code, setCode] = useMaybeControlledValue({ const [code, setCode] = useMaybeControlledValue({
value: props.code, value: props.code,
@ -43,50 +34,59 @@ export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
onChange: props.onCodeChange, onChange: props.onCodeChange,
}); });
const { distributionPlotSettings, chartSettings } = let chartProps = { ...props, code };
splitSquiggleChartSettings(props);
const {
environment,
jsImports = defaultImports,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
executionId = 0,
width,
height = 200,
enableLocalSettings = false,
continues,
project,
} = props;
const resultAndBindings = useSquiggle({
environment,
continues,
code,
project,
jsImports,
onChange,
executionId,
});
const valueToRender = getValueToRender(resultAndBindings);
const errorLocations = getErrorLocations(resultAndBindings.result);
return ( return (
<SquiggleContainer> <SquiggleContainer>
<WrappedCodeEditor <WrappedCodeEditor code={code} setCode={setCode} />
code={code} <SquiggleChart {...chartProps} />
setCode={setCode} </SquiggleContainer>
errorLocations={errorLocations} );
/> };
<SquiggleViewer
result={valueToRender} export interface SquigglePartialProps {
width={width} /** The text inside the input (controlled) */
height={height} code?: string;
distributionPlotSettings={distributionPlotSettings} /** The default text inside the input (unControlled) */
chartSettings={chartSettings} defaultCode?: string;
environment={environment ?? defaultEnvironment} /** when the environment changes. Used again for notebook magic*/
enableLocalSettings={enableLocalSettings} onChange?(expr: bindings | undefined): void;
/> /** When the code changes */
onCodeChange?(code: string): void;
/** Previously declared variables */
bindings?: bindings;
/** If the output requires monte carlo sampling, the amount of samples */
environment?: environment;
/** Variables imported from js */
jsImports?: jsImports;
}
export const SquigglePartial: React.FC<SquigglePartialProps> = ({
code: controlledCode,
defaultCode = "",
onChange,
onCodeChange,
bindings = defaultBindings,
environment,
jsImports = defaultImports,
}: SquigglePartialProps) => {
const [code, setCode] = useMaybeControlledValue<string>({
value: controlledCode,
defaultValue: defaultCode,
onChange: onCodeChange,
});
const result = useSquigglePartial({
code,
bindings,
environment,
jsImports,
onChange,
});
return (
<SquiggleContainer>
<WrappedCodeEditor code={code} setCode={setCode} />
{result.tag !== "Ok" ? <SquiggleErrorAlert error={result.value} /> : null}
</SquiggleContainer> </SquiggleContainer>
); );
}; };

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,44 +1,11 @@
import { SqError, SqFrame } from "@quri/squiggle-lang"; import { errorValue, errorValueToString } from "@quri/squiggle-lang";
import React from "react"; import React from "react";
import { ErrorAlert } from "./Alert"; import { ErrorAlert } from "./Alert";
type Props = { type Props = {
error: SqError; error: errorValue;
};
const StackTraceFrame: React.FC<{ frame: SqFrame }> = ({ frame }) => {
const location = frame.location();
return (
<div>
{frame.name()}
{location
? ` at line ${location.start.line}, column ${location.start.column}`
: ""}
</div>
);
};
const StackTrace: React.FC<Props> = ({ error }) => {
const frames = error.getFrameArray();
return frames.length ? (
<div>
<div className="font-medium">Stack trace:</div>
<div className="ml-4">
{frames.map((frame, i) => (
<StackTraceFrame frame={frame} key={i} />
))}
</div>
</div>
) : null;
}; };
export const SquiggleErrorAlert: React.FC<Props> = ({ error }) => { export const SquiggleErrorAlert: React.FC<Props> = ({ error }) => {
return ( return <ErrorAlert heading="Error">{errorValueToString(error)}</ErrorAlert>;
<ErrorAlert heading="Error">
<div className="space-y-4">
<div>{error.toString()}</div>
<StackTrace error={error} />
</div>
</ErrorAlert>
);
}; };

View File

@ -8,11 +8,7 @@ import React, {
} from "react"; } from "react";
import { useForm, UseFormRegister, useWatch } from "react-hook-form"; import { useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup"; import * as yup from "yup";
import { import { useMaybeControlledValue, useRunnerState } from "../lib/hooks";
useMaybeControlledValue,
useRunnerState,
useSquiggle,
} from "../lib/hooks";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { import {
ChartSquareBarIcon, ChartSquareBarIcon,
@ -28,9 +24,9 @@ import {
} from "@heroicons/react/solid"; } from "@heroicons/react/solid";
import clsx from "clsx"; import clsx from "clsx";
import { environment, SqProject } from "@quri/squiggle-lang"; import { defaultBindings, environment } from "@quri/squiggle-lang";
import { SquiggleChartProps } from "./SquiggleChart"; import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import { JsonEditor } from "./JsonEditor"; import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert"; import { ErrorAlert, SuccessAlert } from "./Alert";
@ -41,11 +37,11 @@ import { InputItem } from "./ui/InputItem";
import { Text } from "./ui/Text"; import { Text } from "./ui/Text";
import { ViewSettings, viewSettingsSchema } from "./ViewSettings"; import { ViewSettings, viewSettingsSchema } from "./ViewSettings";
import { HeadedSection } from "./ui/HeadedSection"; import { HeadedSection } from "./ui/HeadedSection";
import { defaultTickFormat } from "../lib/distributionSpecBuilder"; import {
defaultColor,
defaultTickFormat,
} from "../lib/distributionSpecBuilder";
import { Button } from "./ui/Button"; import { Button } from "./ui/Button";
import { JsImports } from "../lib/jsImports";
import { getErrorLocations, getValueToRender } from "../lib/utility";
import { SquiggleViewer } from "./SquiggleViewer";
type PlaygroundProps = SquiggleChartProps & { type PlaygroundProps = SquiggleChartProps & {
/** The initial squiggle string to put in the playground */ /** The initial squiggle string to put in the playground */
@ -119,8 +115,8 @@ const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
); );
const InputVariablesSettings: React.FC<{ const InputVariablesSettings: React.FC<{
initialImports: JsImports; initialImports: any; // TODO - any json type
setImports: (imports: JsImports) => void; setImports: (imports: any) => void;
}> = ({ initialImports, setImports }) => { }> = ({ initialImports, setImports }) => {
const [importString, setImportString] = useState(() => const [importString, setImportString] = useState(() =>
JSON.stringify(initialImports) JSON.stringify(initialImports)
@ -129,7 +125,7 @@ const InputVariablesSettings: React.FC<{
const onChange = (value: string) => { const onChange = (value: string) => {
setImportString(value); setImportString(value);
let imports = {}; let imports = {} as any;
try { try {
imports = JSON.parse(value); imports = JSON.parse(value);
setImportsAreValid(true); setImportsAreValid(true);
@ -182,7 +178,7 @@ const RunControls: React.FC<{
const CurrentPlayIcon = isRunning ? RefreshIcon : PlayIcon; const CurrentPlayIcon = isRunning ? RefreshIcon : PlayIcon;
return ( return (
<div className="flex space-x-1 items-center" data-testid="autorun-controls"> <div className="flex space-x-1 items-center">
{autorunMode ? null : ( {autorunMode ? null : (
<button onClick={run}> <button onClick={run}>
<CurrentPlayIcon <CurrentPlayIcon
@ -238,12 +234,13 @@ export const PlaygroundContext = React.createContext<PlaygroundContextShape>({
export const SquigglePlayground: FC<PlaygroundProps> = ({ export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultCode = "", defaultCode = "",
height = 500, height = 500,
showSummary = true, showSummary = false,
logX = false, logX = false,
expY = false, expY = false,
title, title,
minX, minX,
maxX, maxX,
color = defaultColor,
tickFormat = defaultTickFormat, tickFormat = defaultTickFormat,
distributionChartActions, distributionChartActions,
code: controlledCode, code: controlledCode,
@ -251,8 +248,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
onSettingsChange, onSettingsChange,
showEditor = true, showEditor = true,
showShareButton = false, showShareButton = false,
continues,
project,
}) => { }) => {
const [code, setCode] = useMaybeControlledValue({ const [code, setCode] = useMaybeControlledValue({
value: controlledCode, value: controlledCode,
@ -260,7 +255,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
onChange: onCodeChange, onChange: onCodeChange,
}); });
const [imports, setImports] = useState<JsImports>({}); const [imports, setImports] = useState({});
const { register, control } = useForm({ const { register, control } = useForm({
resolver: yupResolver(schema), resolver: yupResolver(schema),
@ -273,6 +268,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
title, title,
minX, minX,
maxX, maxX,
color,
tickFormat, tickFormat,
distributionChartActions, distributionChartActions,
showSummary, showSummary,
@ -290,7 +286,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
onSettingsChange?.(vars); onSettingsChange?.(vars);
}, [vars, onSettingsChange]); }, [vars, onSettingsChange]);
const environment: environment = useMemo( const env: environment = useMemo(
() => ({ () => ({
sampleCount: Number(vars.sampleCount), sampleCount: Number(vars.sampleCount),
xyPointLength: Number(vars.xyPointLength), xyPointLength: Number(vars.xyPointLength),
@ -307,53 +303,27 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
executionId, executionId,
} = useRunnerState(code); } = useRunnerState(code);
const resultAndBindings = useSquiggle({
environment,
continues,
code: renderedCode,
project,
jsImports: imports,
executionId,
});
const valueToRender = getValueToRender(resultAndBindings);
const squiggleChart = const squiggleChart =
renderedCode === "" ? null : ( renderedCode === "" ? null : (
<div className="relative"> <div className="relative">
{isRunning ? ( {isRunning ? (
<div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" /> <div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" />
) : null} ) : null}
<SquiggleViewer <SquiggleChart
result={valueToRender} code={renderedCode}
environment={environment} executionId={executionId}
height={vars.chartHeight || 150} environment={env}
distributionPlotSettings={{ {...vars}
showSummary: vars.showSummary ?? false, bindings={defaultBindings}
logX: vars.logX ?? false, jsImports={imports}
expY: vars.expY ?? false,
format: vars.tickFormat,
minX: vars.minX,
maxX: vars.maxX,
title: vars.title,
actions: vars.distributionChartActions,
}}
chartSettings={{
start: vars.diagramStart ?? 0,
stop: vars.diagramStop ?? 10,
count: vars.diagramCount ?? 20,
}}
enableLocalSettings={true} enableLocalSettings={true}
/> />
</div> </div>
); );
const errorLocations = getErrorLocations(resultAndBindings.result);
const firstTab = vars.showEditor ? ( const firstTab = vars.showEditor ? (
<div className="border border-slate-200" data-testid="squiggle-editor"> <div className="border border-slate-200">
<CodeEditor <CodeEditor
errorLocations={errorLocations}
value={code} value={code}
onChange={setCode} onChange={setCode}
onSubmit={run} onSubmit={run}
@ -403,9 +373,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
> >
{tabs} {tabs}
</div> </div>
<div className="w-1/2 p-2 pl-4" data-testid="playground-result"> <div className="w-1/2 p-2 pl-4">{squiggleChart}</div>
{squiggleChart}
</div>
</div> </div>
); );

View File

@ -1,17 +1,14 @@
import React, { useContext } from "react"; import React from "react";
import { SqDistributionTag, SqValue, SqValueTag } from "@quri/squiggle-lang"; import { squiggleExpression, declaration } from "@quri/squiggle-lang";
import { NumberShower } from "../NumberShower"; import { NumberShower } from "../NumberShower";
import { DistributionChart, defaultPlot, makePlot } from "../DistributionChart"; import { DistributionChart } from "../DistributionChart";
import { FunctionChart } from "../FunctionChart"; import { FunctionChart, FunctionChartSettings } from "../FunctionChart";
import clsx from "clsx"; import clsx from "clsx";
import { VariableBox } from "./VariableBox"; import { VariableBox } from "./VariableBox";
import { ItemSettingsMenu } from "./ItemSettingsMenu"; import { ItemSettingsMenu } from "./ItemSettingsMenu";
import { hasMassBelowZero } from "../../lib/distributionUtils"; import { hasMassBelowZero } from "../../lib/distributionUtils";
import { MergedItemSettings } from "./utils"; import { MergedItemSettings } from "./utils";
import { ViewerContext } from "./ViewerContext";
/*
// DISABLED FOR 0.4 branch, for now
function getRange<a>(x: declaration<a>) { function getRange<a>(x: declaration<a>) {
const first = x.args[0]; const first = x.args[0];
switch (first.tag) { switch (first.tag) {
@ -34,21 +31,15 @@ function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
count: 20, count: 20,
}; };
} }
*/
const VariableList: React.FC<{ const VariableList: React.FC<{
value: SqValue; path: string[];
heading: string; heading: string;
children: (settings: MergedItemSettings) => React.ReactNode; children: (settings: MergedItemSettings) => React.ReactNode;
}> = ({ value, heading, children }) => ( }> = ({ path, heading, children }) => (
<VariableBox value={value} heading={heading}> <VariableBox path={path} heading={heading}>
{(settings) => ( {(settings) => (
<div <div className={clsx("space-y-3", path.length ? "pt-1 mt-1" : null)}>
className={clsx(
"space-y-3",
value.location.path.items.length ? "pt-1 mt-1" : null
)}
>
{children(settings)} {children(settings)}
</div> </div>
)} )}
@ -57,44 +48,51 @@ const VariableList: React.FC<{
export interface Props { export interface Props {
/** The output of squiggle's run */ /** The output of squiggle's run */
value: SqValue; 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; width?: number;
} }
export const ExpressionViewer: React.FC<Props> = ({ value, width }) => { export const ExpressionViewer: React.FC<Props> = ({
const { getMergedSettings } = useContext(ViewerContext); path,
expression,
switch (value.tag) { width,
case SqValueTag.Number: }) => {
if (typeof expression !== "object") {
return (
<VariableList path={path} heading="Error">
{() => `Unknown expression: ${expression}`}
</VariableList>
);
}
switch (expression.tag) {
case "number":
return ( return (
<VariableBox value={value} heading="Number"> <VariableBox path={path} heading="Number">
{() => ( {() => (
<div className="font-semibold text-slate-600"> <div className="font-semibold text-slate-600">
<NumberShower precision={3} number={value.value} /> <NumberShower precision={3} number={expression.value} />
</div> </div>
)} )}
</VariableBox> </VariableBox>
); );
case SqValueTag.Distribution: { case "distribution": {
const distType = value.value.tag; const distType = expression.value.type();
return ( return (
<VariableBox <VariableBox
value={value} path={path}
heading={`Distribution (${distType})\n${ heading={`Distribution (${distType})\n${
distType === SqDistributionTag.Symbolic distType === "Symbolic" ? expression.value.toString() : ""
? value.value.toString()
: ""
}`} }`}
renderSettingsMenu={({ onChange }) => { renderSettingsMenu={({ onChange }) => {
const shape = value.value.pointSet( const shape = expression.value.pointSet();
getMergedSettings(value.location).environment
);
return ( return (
<ItemSettingsMenu <ItemSettingsMenu
value={value} path={path}
onChange={onChange} onChange={onChange}
disableLogX={ disableLogX={
shape.tag === "Ok" && hasMassBelowZero(shape.value.asShape()) shape.tag === "Ok" && hasMassBelowZero(shape.value)
} }
withFunctionSettings={false} withFunctionSettings={false}
/> />
@ -104,8 +102,7 @@ export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
{(settings) => { {(settings) => {
return ( return (
<DistributionChart <DistributionChart
plot={defaultPlot(value.value)} distribution={expression.value}
environment={settings.environment}
{...settings.distributionPlotSettings} {...settings.distributionPlotSettings}
height={settings.height} height={settings.height}
width={width} width={width}
@ -115,54 +112,77 @@ export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
</VariableBox> </VariableBox>
); );
} }
case SqValueTag.String: case "string":
return ( return (
<VariableBox value={value} heading="String"> <VariableBox path={path} heading="String">
{() => ( {() => (
<> <>
<span className="text-slate-400">"</span> <span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold font-mono"> <span className="text-slate-600 font-semibold font-mono">
{value.value} {expression.value}
</span> </span>
<span className="text-slate-400">"</span> <span className="text-slate-400">"</span>
</> </>
)} )}
</VariableBox> </VariableBox>
); );
case SqValueTag.Bool: case "boolean":
return ( return (
<VariableBox value={value} heading="Boolean"> <VariableBox path={path} heading="Boolean">
{() => value.value.toString()} {() => expression.value.toString()}
</VariableBox> </VariableBox>
); );
case SqValueTag.Date: case "symbol":
return ( return (
<VariableBox value={value} heading="Date"> <VariableBox path={path} heading="Symbol">
{() => value.value.toDateString()} {() => (
<>
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
<span className="text-slate-600">{expression.value}</span>
</>
)}
</VariableBox> </VariableBox>
); );
case SqValueTag.Void: case "call":
return ( return (
<VariableBox value={value} heading="Void"> <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"} {() => "Void"}
</VariableBox> </VariableBox>
); );
case SqValueTag.TimeDuration: { case "timeDuration": {
return ( return (
<VariableBox value={value} heading="Time Duration"> <VariableBox path={path} heading="Time Duration">
{() => <NumberShower precision={3} number={value.value} />} {() => <NumberShower precision={3} number={expression.value} />}
</VariableBox> </VariableBox>
); );
} }
case SqValueTag.Lambda: case "lambda":
return ( return (
<VariableBox <VariableBox
value={value} path={path}
heading="Function" heading="Function"
renderSettingsMenu={({ onChange }) => { renderSettingsMenu={({ onChange }) => {
return ( return (
<ItemSettingsMenu <ItemSettingsMenu
value={value} path={path}
onChange={onChange} onChange={onChange}
withFunctionSettings={true} withFunctionSettings={true}
/> />
@ -171,11 +191,11 @@ export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
> >
{(settings) => ( {(settings) => (
<> <>
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${value.value <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(
.parameters() ","
.join(",")})`}</div> )})`}</div>
<FunctionChart <FunctionChart
fn={value.value} fn={expression.value}
chartSettings={settings.chartSettings} chartSettings={settings.chartSettings}
distributionPlotSettings={settings.distributionPlotSettings} distributionPlotSettings={settings.distributionPlotSettings}
height={settings.height} height={settings.height}
@ -188,118 +208,92 @@ export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
)} )}
</VariableBox> </VariableBox>
); );
case SqValueTag.Declaration: { case "lambdaDeclaration": {
return ( return (
<VariableBox <VariableBox
value={value} path={path}
heading="Function Declaration" heading="Function Declaration"
renderSettingsMenu={({ onChange }) => { renderSettingsMenu={({ onChange }) => {
return ( return (
<ItemSettingsMenu <ItemSettingsMenu
onChange={onChange} onChange={onChange}
value={value} path={path}
withFunctionSettings={true} withFunctionSettings={true}
/> />
); );
}} }}
> >
{(settings) => ( {(settings) => (
<div>NOT IMPLEMENTED IN 0.4 YET</div> <FunctionChart
// <FunctionChart fn={expression.value.fn}
// fn={expression.value.fn} chartSettings={getChartSettings(expression.value)}
// chartSettings={getChartSettings(expression.value)} distributionPlotSettings={settings.distributionPlotSettings}
// distributionPlotSettings={settings.distributionPlotSettings} height={settings.height}
// height={settings.height} environment={{
// environment={{ sampleCount: settings.environment.sampleCount / 10,
// sampleCount: settings.environment.sampleCount / 10, xyPointLength: settings.environment.xyPointLength / 10,
// xyPointLength: settings.environment.xyPointLength / 10, }}
// }} />
// />
)} )}
</VariableBox> </VariableBox>
); );
} }
case SqValueTag.Record: case "module": {
const plot = makePlot(value.value);
if (plot) {
return (
<VariableBox
value={value}
heading="Plot"
renderSettingsMenu={({ onChange }) => {
let disableLogX = plot.distributions.some((x) => {
let pointSet = x.distribution.pointSet(
getMergedSettings(value.location).environment
);
return (
pointSet.tag === "Ok" &&
hasMassBelowZero(pointSet.value.asShape())
);
});
return (
<ItemSettingsMenu
value={value}
onChange={onChange}
disableLogX={disableLogX}
withFunctionSettings={false}
/>
);
}}
>
{(settings) => {
return (
<DistributionChart
plot={plot}
environment={settings.environment}
{...settings.distributionPlotSettings}
height={settings.height}
width={width}
/>
);
}}
</VariableBox>
);
} else {
return (
<VariableList value={value} heading="Record">
{(_) =>
value.value
.entries()
.map(([key, r]) => (
<ExpressionViewer
key={key}
value={r}
width={width !== undefined ? width - 20 : width}
/>
))
}
</VariableList>
);
}
case SqValueTag.Array:
return ( return (
<VariableList value={value} heading="Array"> <VariableList path={path} heading="Module">
{(_) => {(settings) =>
value.value Object.entries(expression.value)
.getValues() .filter(([key, r]) => !key.match(/^(Math|System)\./))
.map((r, i) => ( .map(([key, r]) => (
<ExpressionViewer <ExpressionViewer
key={i} key={key}
value={r} path={[...path, key]}
expression={r}
width={width !== undefined ? width - 20 : width} width={width !== undefined ? width - 20 : width}
/> />
)) ))
} }
</VariableList> </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: { default: {
return ( return (
<VariableList value={value} heading="Error"> <VariableList path={path} heading="Error">
{() => ( {() => (
<div> <div>
<span>No display for type: </span>{" "} <span>No display for type: </span>{" "}
<span className="font-semibold text-slate-600"> <span className="font-semibold text-slate-600">
{(value as { tag: string }).tag} {expression.tag}
</span> </span>
</div> </div>
)} )}

View File

@ -4,14 +4,16 @@ import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { Modal } from "../ui/Modal"; import { Modal } from "../ui/Modal";
import { ViewSettings, viewSettingsSchema } from "../ViewSettings"; import { ViewSettings, viewSettingsSchema } from "../ViewSettings";
import { Path, pathAsString } from "./utils";
import { ViewerContext } from "./ViewerContext"; import { ViewerContext } from "./ViewerContext";
import { defaultTickFormat } from "../../lib/distributionSpecBuilder"; import {
defaultColor,
defaultTickFormat,
} from "../../lib/distributionSpecBuilder";
import { PlaygroundContext } from "../SquigglePlayground"; import { PlaygroundContext } from "../SquigglePlayground";
import { SqValue } from "@quri/squiggle-lang";
import { locationAsString } from "./utils";
type Props = { type Props = {
value: SqValue; path: Path;
onChange: () => void; onChange: () => void;
disableLogX?: boolean; disableLogX?: boolean;
withFunctionSettings: boolean; withFunctionSettings: boolean;
@ -20,7 +22,7 @@ type Props = {
const ItemSettingsModal: React.FC< const ItemSettingsModal: React.FC<
Props & { close: () => void; resetScroll: () => void } Props & { close: () => void; resetScroll: () => void }
> = ({ > = ({
value, path,
onChange, onChange,
disableLogX, disableLogX,
withFunctionSettings, withFunctionSettings,
@ -30,7 +32,7 @@ const ItemSettingsModal: React.FC<
const { setSettings, getSettings, getMergedSettings } = const { setSettings, getSettings, getMergedSettings } =
useContext(ViewerContext); useContext(ViewerContext);
const mergedSettings = getMergedSettings(value.location); const mergedSettings = getMergedSettings(path);
const { register, watch } = useForm({ const { register, watch } = useForm({
resolver: yupResolver(viewSettingsSchema), resolver: yupResolver(viewSettingsSchema),
@ -44,6 +46,7 @@ const ItemSettingsModal: React.FC<
tickFormat: tickFormat:
mergedSettings.distributionPlotSettings.format || defaultTickFormat, mergedSettings.distributionPlotSettings.format || defaultTickFormat,
title: mergedSettings.distributionPlotSettings.title, title: mergedSettings.distributionPlotSettings.title,
color: mergedSettings.distributionPlotSettings.color || defaultColor,
minX: mergedSettings.distributionPlotSettings.minX, minX: mergedSettings.distributionPlotSettings.minX,
maxX: mergedSettings.distributionPlotSettings.maxX, maxX: mergedSettings.distributionPlotSettings.maxX,
distributionChartActions: mergedSettings.distributionPlotSettings.actions, distributionChartActions: mergedSettings.distributionPlotSettings.actions,
@ -54,8 +57,8 @@ const ItemSettingsModal: React.FC<
}); });
useEffect(() => { useEffect(() => {
const subscription = watch((vars) => { const subscription = watch((vars) => {
const settings = getSettings(value.location); // get the latest version const settings = getSettings(path); // get the latest version
setSettings(value.location, { setSettings(path, {
...settings, ...settings,
distributionPlotSettings: { distributionPlotSettings: {
showSummary: vars.showSummary, showSummary: vars.showSummary,
@ -63,6 +66,7 @@ const ItemSettingsModal: React.FC<
expY: vars.expY, expY: vars.expY,
format: vars.tickFormat, format: vars.tickFormat,
title: vars.title, title: vars.title,
color: vars.color,
minX: vars.minX, minX: vars.minX,
maxX: vars.maxX, maxX: vars.maxX,
actions: vars.distributionChartActions, actions: vars.distributionChartActions,
@ -76,7 +80,7 @@ const ItemSettingsModal: React.FC<
onChange(); onChange();
}); });
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();
}, [getSettings, setSettings, onChange, value.location, watch]); }, [getSettings, setSettings, onChange, path, watch]);
const { getLeftPanelElement } = useContext(PlaygroundContext); const { getLeftPanelElement } = useContext(PlaygroundContext);
@ -84,7 +88,7 @@ const ItemSettingsModal: React.FC<
<Modal container={getLeftPanelElement()} close={close}> <Modal container={getLeftPanelElement()} close={close}>
<Modal.Header> <Modal.Header>
Chart settings Chart settings
{value.location.path.items.length ? ( {path.length ? (
<> <>
{" for "} {" for "}
<span <span
@ -92,7 +96,7 @@ const ItemSettingsModal: React.FC<
className="cursor-pointer" className="cursor-pointer"
onClick={resetScroll} onClick={resetScroll}
> >
{locationAsString(value.location)} {pathAsString(path)}
</span>{" "} </span>{" "}
</> </>
) : ( ) : (
@ -121,7 +125,7 @@ export const ItemSettingsMenu: React.FC<Props> = (props) => {
if (!enableLocalSettings) { if (!enableLocalSettings) {
return null; return null;
} }
const settings = getSettings(props.value.location); const settings = getSettings(props.path);
const resetScroll = () => { const resetScroll = () => {
if (!ref.current) return; if (!ref.current) return;
@ -140,7 +144,7 @@ export const ItemSettingsMenu: React.FC<Props> = (props) => {
{settings.distributionPlotSettings || settings.chartSettings ? ( {settings.distributionPlotSettings || settings.chartSettings ? (
<button <button
onClick={() => { onClick={() => {
setSettings(props.value.location, { setSettings(props.path, {
...settings, ...settings,
distributionPlotSettings: undefined, distributionPlotSettings: undefined,
chartSettings: undefined, chartSettings: undefined,

View File

@ -1,4 +1,3 @@
import { SqValue } from "@quri/squiggle-lang";
import React, { useContext, useReducer } from "react"; import React, { useContext, useReducer } from "react";
import { Tooltip } from "../ui/Tooltip"; import { Tooltip } from "../ui/Tooltip";
import { LocalItemSettings, MergedItemSettings } from "./utils"; import { LocalItemSettings, MergedItemSettings } from "./utils";
@ -9,14 +8,14 @@ type SettingsMenuParams = {
}; };
type VariableBoxProps = { type VariableBoxProps = {
value: SqValue; path: string[];
heading: string; heading: string;
renderSettingsMenu?: (params: SettingsMenuParams) => React.ReactNode; renderSettingsMenu?: (params: SettingsMenuParams) => React.ReactNode;
children: (settings: MergedItemSettings) => React.ReactNode; children: (settings: MergedItemSettings) => React.ReactNode;
}; };
export const VariableBox: React.FC<VariableBoxProps> = ({ export const VariableBox: React.FC<VariableBoxProps> = ({
value: { location }, path,
heading = "Error", heading = "Error",
renderSettingsMenu, renderSettingsMenu,
children, children,
@ -28,10 +27,10 @@ export const VariableBox: React.FC<VariableBoxProps> = ({
// So we use `forceUpdate` to force rerendering. // So we use `forceUpdate` to force rerendering.
const [_, forceUpdate] = useReducer((x) => x + 1, 0); const [_, forceUpdate] = useReducer((x) => x + 1, 0);
const settings = getSettings(location); const settings = getSettings(path);
const setSettingsAndUpdate = (newSettings: LocalItemSettings) => { const setSettingsAndUpdate = (newSettings: LocalItemSettings) => {
setSettings(location, newSettings); setSettings(path, newSettings);
forceUpdate(); forceUpdate();
}; };
@ -39,13 +38,11 @@ export const VariableBox: React.FC<VariableBoxProps> = ({
setSettingsAndUpdate({ ...settings, collapsed: !settings.collapsed }); setSettingsAndUpdate({ ...settings, collapsed: !settings.collapsed });
}; };
const isTopLevel = location.path.items.length === 0; const isTopLevel = path.length === 0;
const name = isTopLevel const name = isTopLevel ? "Result" : path[path.length - 1];
? { result: "Result", bindings: "Bindings" }[location.path.root]
: location.path.items[location.path.items.length - 1];
return ( return (
<div role={isTopLevel ? "status" : undefined}> <div>
<header className="inline-flex space-x-1"> <header className="inline-flex space-x-1">
<Tooltip text={heading}> <Tooltip text={heading}>
<span <span
@ -68,13 +65,13 @@ export const VariableBox: React.FC<VariableBoxProps> = ({
</header> </header>
{settings.collapsed ? null : ( {settings.collapsed ? null : (
<div className="flex w-full"> <div className="flex w-full">
{location.path.items.length ? ( {path.length ? (
<div <div
className="shrink-0 border-l-2 border-slate-200 hover:border-indigo-600 w-4 cursor-pointer" className="border-l-2 border-slate-200 hover:border-indigo-600 w-4 cursor-pointer"
onClick={toggleCollapsed} onClick={toggleCollapsed}
></div> ></div>
) : null} ) : null}
<div className="grow">{children(getMergedSettings(location))}</div> <div className="grow">{children(getMergedSettings(path))}</div>
</div> </div>
)} )}
</div> </div>

View File

@ -1,14 +1,14 @@
import { defaultEnvironment, SqValueLocation } from "@quri/squiggle-lang"; import { defaultEnvironment } from "@quri/squiggle-lang";
import React from "react"; import React from "react";
import { LocalItemSettings, MergedItemSettings } from "./utils"; import { LocalItemSettings, MergedItemSettings, Path } from "./utils";
type ViewerContextShape = { 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). // 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. // 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. // See ./SquiggleViewer.tsx and ./VariableBox.tsx for other implementation details on this.
getSettings(location: SqValueLocation): LocalItemSettings; getSettings(path: Path): LocalItemSettings;
getMergedSettings(location: SqValueLocation): MergedItemSettings; getMergedSettings(path: Path): MergedItemSettings;
setSettings(location: SqValueLocation, value: LocalItemSettings): void; setSettings(path: Path, value: LocalItemSettings): void;
enableLocalSettings: boolean; // show local settings icon in the UI enableLocalSettings: boolean; // show local settings icon in the UI
}; };

View File

@ -1,20 +1,21 @@
import React, { useCallback, useRef } from "react"; import React, { useCallback, useRef } from "react";
import { environment, SqValueLocation } from "@quri/squiggle-lang"; import { environment } from "@quri/squiggle-lang";
import { DistributionPlottingSettings } from "../DistributionChart"; import { DistributionPlottingSettings } from "../DistributionChart";
import { FunctionChartSettings } from "../FunctionChart"; import { FunctionChartSettings } from "../FunctionChart";
import { ExpressionViewer } from "./ExpressionViewer"; import { ExpressionViewer } from "./ExpressionViewer";
import { ViewerContext } from "./ViewerContext"; import { ViewerContext } from "./ViewerContext";
import { import {
LocalItemSettings, LocalItemSettings,
locationAsString,
MergedItemSettings, MergedItemSettings,
Path,
pathAsString,
} from "./utils"; } from "./utils";
import { useSquiggle } from "../../lib/hooks"; import { useSquiggle } from "../../lib/hooks";
import { SquiggleErrorAlert } from "../SquiggleErrorAlert"; import { SquiggleErrorAlert } from "../SquiggleErrorAlert";
type Props = { type Props = {
/** The output of squiggle's run */ /** The output of squiggle's run */
result: ReturnType<typeof useSquiggle>["result"]; result: ReturnType<typeof useSquiggle>;
width?: number; width?: number;
height: number; height: number;
distributionPlotSettings: DistributionPlottingSettings; distributionPlotSettings: DistributionPlottingSettings;
@ -44,22 +45,22 @@ export const SquiggleViewer: React.FC<Props> = ({
const settingsRef = useRef<Settings>({}); const settingsRef = useRef<Settings>({});
const getSettings = useCallback( const getSettings = useCallback(
(location: SqValueLocation) => { (path: Path) => {
return settingsRef.current[locationAsString(location)] || defaultSettings; return settingsRef.current[pathAsString(path)] || defaultSettings;
}, },
[settingsRef] [settingsRef]
); );
const setSettings = useCallback( const setSettings = useCallback(
(location: SqValueLocation, value: LocalItemSettings) => { (path: Path, value: LocalItemSettings) => {
settingsRef.current[locationAsString(location)] = value; settingsRef.current[pathAsString(path)] = value;
}, },
[settingsRef] [settingsRef]
); );
const getMergedSettings = useCallback( const getMergedSettings = useCallback(
(location: SqValueLocation) => { (path: Path) => {
const localSettings = getSettings(location); const localSettings = getSettings(path);
const result: MergedItemSettings = { const result: MergedItemSettings = {
distributionPlotSettings: { distributionPlotSettings: {
...distributionPlotSettings, ...distributionPlotSettings,
@ -90,7 +91,7 @@ export const SquiggleViewer: React.FC<Props> = ({
}} }}
> >
{result.tag === "Ok" ? ( {result.tag === "Ok" ? (
<ExpressionViewer value={result.value} width={width} /> <ExpressionViewer path={[]} expression={result.value} width={width} />
) : ( ) : (
<SquiggleErrorAlert error={result.value} /> <SquiggleErrorAlert error={result.value} />
)} )}

View File

@ -1,6 +1,6 @@
import { DistributionPlottingSettings } from "../DistributionChart"; import { DistributionPlottingSettings } from "../DistributionChart";
import { FunctionChartSettings } from "../FunctionChart"; import { FunctionChartSettings } from "../FunctionChart";
import { environment, SqValueLocation } from "@quri/squiggle-lang"; import { environment } from "@quri/squiggle-lang";
export type LocalItemSettings = { export type LocalItemSettings = {
collapsed: boolean; collapsed: boolean;
@ -17,5 +17,6 @@ export type MergedItemSettings = {
environment: environment; environment: environment;
}; };
export const locationAsString = (location: SqValueLocation) => export type Path = string[];
location.path.items.join(".");
export const pathAsString = (path: Path) => path.join(".");

View File

@ -5,7 +5,10 @@ import { InputItem } from "./ui/InputItem";
import { Checkbox } from "./ui/Checkbox"; import { Checkbox } from "./ui/Checkbox";
import { HeadedSection } from "./ui/HeadedSection"; import { HeadedSection } from "./ui/HeadedSection";
import { Text } from "./ui/Text"; import { Text } from "./ui/Text";
import { defaultTickFormat } from "../lib/distributionSpecBuilder"; import {
defaultColor,
defaultTickFormat,
} from "../lib/distributionSpecBuilder";
export const viewSettingsSchema = yup.object({}).shape({ export const viewSettingsSchema = yup.object({}).shape({
chartHeight: yup.number().required().positive().integer().default(350), chartHeight: yup.number().required().positive().integer().default(350),
@ -15,6 +18,7 @@ export const viewSettingsSchema = yup.object({}).shape({
expY: yup.boolean().required(), expY: yup.boolean().required(),
tickFormat: yup.string().default(defaultTickFormat), tickFormat: yup.string().default(defaultTickFormat),
title: yup.string(), title: yup.string(),
color: yup.string().default(defaultColor).required(),
minX: yup.number(), minX: yup.number(),
maxX: yup.number(), maxX: yup.number(),
distributionChartActions: yup.boolean(), distributionChartActions: yup.boolean(),
@ -110,6 +114,12 @@ export const ViewSettings: React.FC<{
register={register} register={register}
label="Tick Format" label="Tick Format"
/> />
<InputItem
name="color"
type="color"
register={register}
label="Color"
/>
</div> </div>
</HeadedSection> </HeadedSection>
</div> </div>

View File

@ -1,8 +1,8 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react"; import React from "react";
import { Path, UseFormRegister, FieldValues } from "react-hook-form"; import { Path, UseFormRegister } from "react-hook-form";
export function Checkbox<T extends FieldValues>({ export function Checkbox<T>({
name, name,
label, label,
register, register,

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Path, UseFormRegister, FieldValues } from "react-hook-form"; import { Path, UseFormRegister } from "react-hook-form";
export function InputItem<T extends FieldValues>({ export function InputItem<T>({
name, name,
label, label,
type, type,

View File

@ -1,5 +1,7 @@
export { SqProject } from "@quri/squiggle-lang/";
export { SquiggleChart } from "./components/SquiggleChart"; export { SquiggleChart } from "./components/SquiggleChart";
export { SquiggleEditor } 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";

View File

@ -1,5 +1,5 @@
import { VisualizationSpec } from "react-vega"; import { VisualizationSpec } from "react-vega";
import type { LogScale, LinearScale, PowScale, TimeScale } from "vega"; import type { LogScale, LinearScale, PowScale } from "vega";
export type DistributionChartSpecOptions = { export type DistributionChartSpecOptions = {
/** Set the x scale to be logarithmic by deault */ /** Set the x scale to be logarithmic by deault */
@ -10,25 +10,54 @@ export type DistributionChartSpecOptions = {
minX?: number; minX?: number;
/** The maximum x coordinate shown on the chart */ /** The maximum x coordinate shown on the chart */
maxX?: number; maxX?: number;
/** The color of the chart */
color?: string;
/** The title of the chart */ /** The title of the chart */
title?: string; title?: string;
/** The formatting of the ticks */ /** The formatting of the ticks */
format?: string; format?: string;
/** Whether the x-axis should be dates or numbers */
xAxisType?: "number" | "dateTime";
}; };
/** X Scales */ export let linearXScale: LinearScale = {
export const linearXScale: LinearScale = {
name: "xscale", name: "xscale",
clamp: true, clamp: true,
type: "linear", type: "linear",
range: "width", range: "width",
zero: false, zero: false,
nice: false, nice: false,
domain: {
fields: [
{
data: "con",
field: "x",
},
{
data: "dis",
field: "x",
},
],
},
};
export let linearYScale: LinearScale = {
name: "yscale",
type: "linear",
range: "height",
zero: true,
domain: {
fields: [
{
data: "con",
field: "y",
},
{
data: "dis",
field: "y",
},
],
},
}; };
export const logXScale: LogScale = { export let logXScale: LogScale = {
name: "xscale", name: "xscale",
type: "log", type: "log",
range: "width", range: "width",
@ -36,114 +65,82 @@ export const logXScale: LogScale = {
base: 10, base: 10,
nice: false, nice: false,
clamp: true, clamp: true,
domain: {
fields: [
{
data: "con",
field: "x",
},
{
data: "dis",
field: "x",
},
],
},
}; };
export const timeXScale: TimeScale = { export let expYScale: PowScale = {
name: "xscale",
clamp: true,
type: "time",
range: "width",
nice: false,
};
/** Y Scales */
export const linearYScale: LinearScale = {
name: "yscale",
type: "linear",
range: "height",
zero: true,
};
export const expYScale: PowScale = {
name: "yscale", name: "yscale",
type: "pow", type: "pow",
exponent: 0.1, exponent: 0.1,
range: "height", range: "height",
zero: true, zero: true,
nice: false, nice: false,
domain: {
fields: [
{
data: "con",
field: "y",
},
{
data: "dis",
field: "y",
},
],
},
}; };
export const defaultTickFormat = ".9~s"; export const defaultTickFormat = ".9~s";
export const timeTickFormat = "%b %d, %Y %H:%M"; export const defaultColor = "#739ECC";
const width = 500;
export function buildVegaSpec( export function buildVegaSpec(
specOptions: DistributionChartSpecOptions & { maxY: number } specOptions: DistributionChartSpecOptions
): VisualizationSpec { ): VisualizationSpec {
const { let {
format = defaultTickFormat,
color = defaultColor,
title, title,
minX, minX,
maxX, maxX,
logX, logX,
expY, expY,
xAxisType = "number",
maxY,
} = specOptions; } = specOptions;
const dateTime = xAxisType === "dateTime"; let xScale = logX ? logXScale : linearXScale;
if (minX !== undefined && Number.isFinite(minX)) {
xScale = { ...xScale, domainMin: minX };
}
// some fallbacks if (maxX !== undefined && Number.isFinite(maxX)) {
const format = specOptions?.format xScale = { ...xScale, domainMax: maxX };
? specOptions.format }
: dateTime
? timeTickFormat
: defaultTickFormat;
let xScale = dateTime ? timeXScale : logX ? logXScale : linearXScale; let spec: VisualizationSpec = {
xScale = {
...xScale,
domain: [minX ?? 0, maxX ?? 1],
domainMin: minX,
domainMax: maxX,
};
let yScale = expY ? expYScale : linearYScale;
yScale = { ...yScale, domain: [0, maxY ?? 1], domainMin: 0, domainMax: maxY };
const spec: VisualizationSpec = {
$schema: "https://vega.github.io/schema/vega/v5.json", $schema: "https://vega.github.io/schema/vega/v5.json",
description: "Squiggle plot chart", description: "A basic area chart example",
width: width, width: 500,
height: 100, height: 100,
padding: 5, padding: 5,
data: [{ name: "data" }, { name: "domain" }, { name: "samples" }], data: [
signals: [
{ {
name: "hover", name: "con",
value: null,
on: [
{ events: "mouseover", update: "datum" },
{ events: "mouseout", update: "null" },
],
}, },
{ {
name: "position", name: "dis",
value: "[0, 0]",
on: [
{ events: "mousemove", update: "xy() " },
{ events: "mouseout", update: "null" },
],
},
{
name: "position_scaled",
value: null,
update: "isArray(position) ? invert('xscale', position[0]) : ''",
},
],
scales: [
xScale,
yScale,
{
name: "color",
type: "ordinal",
domain: {
data: "data",
field: "name",
},
range: { scheme: "blues" },
}, },
], ],
signals: [],
scales: [xScale, expY ? expYScale : linearYScale],
axes: [ axes: [
{ {
orient: "bottom", orient: "bottom",
@ -154,245 +151,109 @@ export function buildVegaSpec(
domainColor: "#fff", domainColor: "#fff",
domainOpacity: 0.0, domainOpacity: 0.0,
format: format, format: format,
tickCount: dateTime ? 3 : 10, tickCount: 10,
labelOverlap: "greedy",
}, },
], ],
marks: [ marks: [
{ {
name: "all_distributions", type: "area",
type: "group",
from: { from: {
facet: { data: "con",
name: "distribution_facet",
data: "data",
groupby: ["name"],
},
}, },
marks: [
{
name: "continuous_distribution",
type: "group",
from: {
facet: {
name: "continuous_facet",
data: "distribution_facet",
field: "continuous",
},
},
encode: {
update: {},
},
marks: [
{
name: "continuous_area",
type: "area",
from: {
data: "continuous_facet",
},
encode: {
update: {
interpolate: { value: "linear" },
x: {
scale: "xscale",
field: "x",
},
y: {
scale: "yscale",
field: "y",
},
fill: {
scale: "color",
field: { parent: "name" },
},
y2: {
scale: "yscale",
value: 0,
},
fillOpacity: {
value: 1,
},
},
},
},
],
},
{
name: "discrete_distribution",
type: "group",
from: {
facet: {
name: "discrete_facet",
data: "distribution_facet",
field: "discrete",
},
},
marks: [
{
type: "rect",
from: {
data: "discrete_facet",
},
encode: {
enter: {
width: {
value: 1,
},
},
update: {
x: {
scale: "xscale",
field: "x",
},
y: {
scale: "yscale",
field: "y",
},
y2: {
scale: "yscale",
value: 0,
},
fill: {
scale: "color",
field: { parent: "name" },
},
},
},
},
{
type: "symbol",
from: {
data: "discrete_facet",
},
encode: {
enter: {
shape: {
value: "circle",
},
size: [{ value: 100 }],
tooltip: {
signal: dateTime
? "{ probability: datum.y, value: datetime(datum.x) }"
: "{ probability: datum.y, value: datum.x }",
},
},
update: {
x: {
scale: "xscale",
field: "x",
offset: 0.5, // if this is not included, the circles are slightly left of center.
},
y: {
scale: "yscale",
field: "y",
},
fill: {
scale: "color",
field: { parent: "name" },
},
},
},
},
],
},
],
},
{
name: "sampleset",
type: "rect",
from: { data: "samples" },
encode: { encode: {
enter: {
x: { scale: "xscale", field: "data" },
width: { value: 0.1 },
y: { value: 25, offset: { signal: "height" } },
height: { value: 5 },
},
},
},
{
type: "text",
name: "announcer",
interactive: false,
encode: {
enter: {
x: { signal: String(width), offset: 1 }, // vega would prefer its internal ` "width" ` variable, but that breaks the squiggle playground. Just setting it to the same var as used elsewhere in the spec achieves the same result.
fill: { value: "black" },
fontSize: { value: 20 },
align: { value: "right" },
},
update: { update: {
text: { interpolate: { value: "linear" },
signal: dateTime x: {
? "position_scaled ? utcyear(position_scaled) + '-' + utcmonth(position_scaled) + '-' + utcdate(position_scaled) + 'T' + utchours(position_scaled)+':' +utcminutes(position_scaled) : ''" scale: "xscale",
: "position_scaled ? format(position_scaled, ',.4r') : ''", field: "x",
},
y: {
scale: "yscale",
field: "y",
},
y2: {
scale: "yscale",
value: 0,
},
fill: {
value: color,
},
fillOpacity: {
value: 1,
}, },
}, },
}, },
}, },
{ {
type: "rule", type: "rect",
interactive: false, from: {
data: "dis",
},
encode: { encode: {
enter: { enter: {
x: { value: 0 }, width: {
y: { scale: "yscale", value: 0 }, value: 1,
y2: {
signal: "height",
offset: 2,
}, },
strokeDash: { value: [5, 5] },
}, },
update: { update: {
x: { x: {
signal: scale: "xscale",
"position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null", field: "x",
}, },
y: {
opacity: { scale: "yscale",
signal: field: "y",
"position ? position[0] < 0 ? 0 : position[0] > width ? 0 : 1 : 0", },
y2: {
scale: "yscale",
value: 0,
},
fill: {
value: "#2f65a7",
}, },
}, },
}, },
}, },
],
legends: [
{ {
fill: "color", type: "symbol",
orient: "top", from: {
labelFontSize: 12, data: "dis",
},
encode: { encode: {
symbols: { enter: {
update: { shape: {
fill: [ value: "circle",
{ test: "length(domain('color')) == 1", value: "transparent" }, },
{ scale: "color", field: "value" }, size: [{ value: 100 }],
], tooltip: {
signal: "{ probability: datum.y, value: datum.x }",
}, },
}, },
labels: { update: {
interactive: true, x: {
update: { scale: "xscale",
fill: [ field: "x",
{ test: "length(domain('color')) == 1", value: "transparent" }, },
{ value: "black" }, y: {
], scale: "yscale",
field: "y",
},
fill: {
value: "#1e4577",
}, },
}, },
}, },
}, },
], ],
...(title && { };
if (title) {
spec = {
...spec,
title: { title: {
text: title, text: title,
}, },
}), };
}; }
return spec; return spec;
} }

View File

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

View File

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

View File

@ -1,97 +1,53 @@
import { import {
result, bindings,
SqError,
SqProject,
SqRecord,
SqValue,
environment, environment,
jsImports,
run,
runPartial,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { JsImports, jsImportsToSquiggleCode } from "../jsImports";
import * as uuid from "uuid";
type SquiggleArgs = { type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = {
environment?: environment;
code: string; code: string;
executionId?: number; executionId?: number;
jsImports?: JsImports; bindings?: bindings;
project?: SqProject; jsImports?: jsImports;
continues?: string[]; environment?: environment;
onChange?: (expr: SqValue | undefined, sourceName: string) => void; onChange?: (expr: Extract<T, { tag: "Ok" }>["value"] | undefined) => void;
}; };
export type ResultAndBindings = { const useSquiggleAny = <T extends ReturnType<typeof run | typeof runPartial>>(
result: result<SqValue, SqError>; args: SquiggleArgs<T>,
bindings: SqRecord; f: (...args: Parameters<typeof run>) => T
}; ) => {
const result: T = useMemo<T>(
const importSourceName = (sourceName: string) => "imports-" + sourceName; () => f(args.code, args.bindings, args.environment, args.jsImports),
const defaultContinues = [];
export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
const project = useMemo(() => {
if (args.project) {
return args.project;
} else {
const p = SqProject.create();
if (args.environment) {
p.setEnvironment(args.environment);
}
return p;
}
}, [args.project, args.environment]);
const sourceName = useMemo(() => uuid.v4(), []);
const env = project.getEnvironment();
const continues = args.continues || defaultContinues;
const result = useMemo(
() => {
project.setSource(sourceName, args.code);
let fullContinues = continues;
if (args.jsImports && Object.keys(args.jsImports).length) {
const importsSource = jsImportsToSquiggleCode(args.jsImports);
project.setSource(importSourceName(sourceName), importsSource);
fullContinues = continues.concat(importSourceName(sourceName));
}
project.setContinues(sourceName, fullContinues);
project.run(sourceName);
const result = project.getResult(sourceName);
const bindings = project.getBindings(sourceName);
return { result, bindings };
},
// This complains about executionId not being used inside the function body.
// This is on purpose, as executionId simply allows you to run the squiggle
// code again
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[ [
f,
args.code, args.code,
args.bindings,
args.environment,
args.jsImports, args.jsImports,
args.executionId, args.executionId,
sourceName,
continues,
project,
env,
] ]
); );
const { onChange } = args; const { onChange } = args;
useEffect(() => { useEffect(() => {
onChange?.( onChange?.(result.tag === "Ok" ? result.value : undefined);
result.result.tag === "Ok" ? result.result.value : undefined, }, [result, onChange]);
sourceName
);
}, [result, onChange, sourceName]);
useEffect(() => {
return () => {
project.removeSource(sourceName);
if (project.getSource(importSourceName(sourceName)))
project.removeSource(importSourceName(sourceName));
};
}, [project, sourceName]);
return result; return result;
}; };
export const useSquigglePartial = (
args: SquiggleArgs<ReturnType<typeof runPartial>>
) => {
return useSquiggleAny(args, runPartial);
};
export const useSquiggle = (args: SquiggleArgs<ReturnType<typeof run>>) => {
return useSquiggleAny(args, run);
};

View File

@ -1,51 +0,0 @@
type JsImportsValue =
| number
| string
| JsImportsValue[]
| {
[k: string]: JsImportsValue;
};
export type JsImports = {
[k: string]: JsImportsValue;
};
const quote = (arg: string) => `"${arg.replace(new RegExp('"', "g"), '\\"')}"`;
const jsImportsValueToSquiggleCode = (v: JsImportsValue): string => {
if (typeof v === "number") {
return String(v);
} else if (typeof v === "string") {
return quote(v);
} else if (v instanceof Array) {
return "[" + v.map((x) => jsImportsValueToSquiggleCode(x)) + "]";
} else {
if (Object.keys(v).length) {
return (
"{" +
Object.entries(v)
.map(([k, v]) => `${quote(k)}:${jsImportsValueToSquiggleCode(v)},`)
.join("") +
"}"
);
} else {
return "0"; // squiggle doesn't support empty `{}`
}
}
};
export const jsImportsToSquiggleCode = (v: JsImports) => {
const validId = new RegExp("[a-zA-Z][[a-zA-Z0-9]*");
let result = Object.entries(v)
.map(([k, v]) => {
if (!k.match(validId)) {
return ""; // skipping without warnings; can be improved
}
return `$${k} = ${jsImportsValueToSquiggleCode(v)}\n`;
})
.join("");
if (!result) {
result = "$__no_valid_imports__ = 1"; // without this generated squiggle code can be invalid
}
return result;
};

View File

@ -1,83 +0,0 @@
import * as yup from "yup";
import {
SqValue,
SqValueTag,
SqDistribution,
result,
SqRecord,
} from "@quri/squiggle-lang";
export type LabeledDistribution = {
name: string;
distribution: SqDistribution;
color?: string;
};
export type Plot = {
distributions: LabeledDistribution[];
};
function error<a, b>(err: b): result<a, b> {
return { tag: "Error", value: err };
}
function ok<a, b>(x: a): result<a, b> {
return { tag: "Ok", value: x };
}
const schema = yup
.object()
.noUnknown()
.strict()
.shape({
distributions: yup
.array()
.required()
.of(
yup.object().required().shape({
name: yup.string().required(),
distribution: yup.mixed().required(),
})
),
});
type JsonObject =
| string
| { [key: string]: JsonObject }
| JsonObject[]
| SqDistribution;
function toJson(val: SqValue): JsonObject {
if (val.tag === SqValueTag.String) {
return val.value;
} else if (val.tag === SqValueTag.Record) {
return toJsonRecord(val.value);
} else if (val.tag === SqValueTag.Array) {
return val.value.getValues().map(toJson);
} else if (val.tag === SqValueTag.Distribution) {
return val.value;
} else {
throw new Error("Could not parse object of type " + val.tag);
}
}
function toJsonRecord(val: SqRecord): JsonObject {
let recordObject: JsonObject = {};
val.entries().forEach(([key, value]) => (recordObject[key] = toJson(value)));
return recordObject;
}
export function parsePlot(record: SqRecord): result<Plot, string> {
try {
const plotRecord = schema.validateSync(toJsonRecord(record));
if (plotRecord.distributions) {
return ok({ distributions: plotRecord.distributions.map((x) => x) });
} else {
// I have no idea why yup's typings thinks this is possible
return error("no distributions field. Should never get here");
}
} catch (e) {
const message = e instanceof Error ? e.message : "Unknown error";
return error(message);
}
}

View File

@ -1,53 +0,0 @@
import { result, resultMap, SqValueTag } from "@quri/squiggle-lang";
import { ResultAndBindings } from "./hooks/useSquiggle";
export function flattenResult<a, b>(x: result<a, b>[]): result<a[], b> {
if (x.length === 0) {
return { tag: "Ok", value: [] };
} else {
if (x[0].tag === "Error") {
return x[0];
} else {
let rest = flattenResult(x.splice(1));
if (rest.tag === "Error") {
return rest;
} else {
return { tag: "Ok", value: [x[0].value].concat(rest.value) };
}
}
}
}
export function resultBind<a, b, c>(
x: result<a, b>,
fn: (y: a) => result<c, b>
): result<c, b> {
if (x.tag === "Ok") {
return fn(x.value);
} else {
return x;
}
}
export function all(arr: boolean[]): boolean {
return arr.reduce((x, y) => x && y, true);
}
export function some(arr: boolean[]): boolean {
return arr.reduce((x, y) => x || y, false);
}
export function getValueToRender({ result, bindings }: ResultAndBindings) {
return resultMap(result, (value) =>
value.tag === SqValueTag.Void ? bindings.asValue() : value
);
}
export function getErrorLocations(result: ResultAndBindings["result"]) {
if (result.tag === "Error") {
const location = result.value.location();
return location ? [location] : [];
} else {
return [];
}
}

View File

@ -79,22 +79,6 @@ could be continuous, discrete or mixed.
</Story> </Story>
</Canvas> </Canvas>
### Date Distribution
<Canvas>
<Story
name="Date Distribution"
args={{
code: "mx(1661819770311, 1661829770311, 1661839770311)",
width,
xAxisType: "dateTime",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
## Mixed distributions ## Mixed distributions
<Canvas> <Canvas>
@ -109,33 +93,6 @@ could be continuous, discrete or mixed.
</Story> </Story>
</Canvas> </Canvas>
## Multiple plots
<Canvas>
<Story
name="Multiple plots"
args={{
code: `
{
distributions: [
{
name: "one",
distribution: mx(0.5, normal(0,1))
},
{
name: "two",
distribution: mx(2, normal(5, 2)),
}
]
}
`,
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
## Constants ## Constants
A constant is a simple number as a result. This has special formatting rules A constant is a simple number as a result. This has special formatting rules

View File

@ -0,0 +1,51 @@
import { SquigglePartial, SquiggleEditor } from "../components/SquiggleEditor";
import { useState } from "react";
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
<Meta title="Squiggle/SquigglePartial" component={SquigglePartial} />
export const Template = (props) => <SquigglePartial {...props} />;
# Squiggle Partial
A Squiggle Partial is an editor that does not return a graph to the user, but
instead returns bindings that can be used by further Squiggle Editors.
<Canvas>
<Story
name="Standalone"
args={{
defaultCode: "x = normal(5,2)",
}}
>
{Template.bind({})}
</Story>
</Canvas>
<Canvas>
<Story
name="With Editor"
args={{
initialPartialString: "x = normal(5,2)",
initialEditorString: "x",
}}
>
{(props) => {
let [bindings, setBindings] = useState({});
return (
<>
<SquigglePartial
{...props}
defaultCode={props.initialPartialString}
onChange={setBindings}
/>
<SquiggleEditor
{...props}
defaultCode={props.initialEditorString}
bindings={bindings}
/>
</>
);
}}
</Story>
</Canvas>

View File

@ -22,8 +22,3 @@ but this line is still necessary for proper initialization of `--tw-*` variables
.ace_cursor { .ace_cursor {
border-left: 2px solid !important; border-left: 2px solid !important;
} }
.ace-error-marker {
position: absolute;
border-bottom: 1px solid red;
}

View File

@ -1,55 +0,0 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import * as React from "react";
import "@testing-library/jest-dom";
import { SquigglePlayground } from "../src/index";
test("Autorun is default", async () => {
render(<SquigglePlayground code="70*30" />);
await waitFor(() =>
expect(screen.getByTestId("playground-result")).toHaveTextContent("2100")
);
});
test("Autorun can be switched off", async () => {
const user = userEvent.setup();
render(<SquigglePlayground code="70*30" />);
expect(screen.getByTestId("autorun-controls")).toHaveTextContent("Autorun");
await waitFor(() =>
expect(screen.getByTestId("playground-result")).toHaveTextContent("2100")
);
await user.click(screen.getByText("Autorun")); // disable
expect(screen.getByTestId("autorun-controls")).toHaveTextContent("Paused");
expect(screen.getByTestId("autorun-controls")).not.toHaveTextContent(
"Autorun"
);
await user.click(screen.getByText("Paused")); // enable autorun again
expect(screen.getByTestId("autorun-controls")).toHaveTextContent("Autorun");
// we should replace the code here, but it's hard to update react-ace state via user events: https://github.com/securingsincity/react-ace/issues/923
// ...or replace react-ace with something else
// TODO:
/*
const editor = screen
.getByTestId("squiggle-editor")
.querySelector(".ace_editor") as HTMLElement;
editor.focus();
// await user.clear(editor);
await userEvent.paste("40*40"); // https://github.com/securingsincity/react-ace/issues/923#issuecomment-755502696
screen.debug(editor);
// this makes the tests slower, but it's hard to test otherwise that the code _didn't_ execute
await new Promise((r) => setTimeout(r, 300));
expect(screen.getByTestId("playground-result")).toHaveTextContent("2100"); // still the old value
await waitFor(() =>
expect(screen.getByTestId("playground-result")).toHaveTextContent("1600")
);
*/
});

View File

@ -1,53 +0,0 @@
import { render, screen } from "@testing-library/react";
import React from "react";
import "@testing-library/jest-dom";
import {
SquiggleChart,
SquiggleEditor,
SquigglePlayground,
} from "../src/index";
import { SqProject } from "@quri/squiggle-lang";
test("Chart logs nothing on render", async () => {
const { unmount } = render(<SquiggleChart code={"normal(0, 1)"} />);
unmount();
expect(console.log).not.toBeCalled();
expect(console.warn).not.toBeCalled();
expect(console.error).not.toBeCalled();
});
test("Editor logs nothing on render", async () => {
const { unmount } = render(<SquiggleEditor code={"normal(0, 1)"} />);
unmount();
expect(console.log).not.toBeCalled();
expect(console.warn).not.toBeCalled();
expect(console.error).not.toBeCalled();
});
test("Project dependencies work in editors", async () => {
const project = SqProject.create();
render(<SquiggleEditor code={"x = 1"} project={project} />);
const source = project.getSourceIds()[0];
const { container } = render(
<SquiggleEditor code={"x + 1"} project={project} continues={[source]} />
);
expect(container).toHaveTextContent("2");
});
test("Project dependencies work in playgrounds", async () => {
const project = SqProject.create();
project.setSource("depend", "x = 1");
render(
<SquigglePlayground
code={"x + 1"}
project={project}
continues={["depend"]}
/>
);
// We must await here because SquigglePlayground loads results asynchronously
expect(await screen.findByRole("status")).toHaveTextContent("2");
});

View File

@ -1,39 +0,0 @@
import { render } from "@testing-library/react";
import React from "react";
import "@testing-library/jest-dom";
import { SquiggleChart } from "../src/index";
import { SqProject } from "@quri/squiggle-lang";
test("Creates and cleans up source", async () => {
const project = SqProject.create();
const { unmount } = render(
<SquiggleChart code={"normal(0, 1)"} project={project} />
);
expect(project.getSourceIds().length).toBe(1);
const sourceId = project.getSourceIds()[0];
expect(project.getSource(sourceId)).toBe("normal(0, 1)");
unmount();
expect(project.getSourceIds().length).toBe(0);
expect(project.getSource(sourceId)).toBe(undefined);
});
test("Creates and cleans up source and imports", async () => {
const project = SqProject.create();
const { unmount } = render(
<SquiggleChart
code={"normal($x, 1)"}
project={project}
jsImports={{ x: 3 }}
/>
);
expect(project.getSourceIds().length).toBe(2);
unmount();
expect(project.getSourceIds()).toStrictEqual([]);
});

View File

@ -1,8 +0,0 @@
global.console = {
...console,
log: jest.fn(console.log),
debug: jest.fn(console.debug),
info: jest.fn(console.info),
warn: jest.fn(console.warn),
error: jest.fn(console.error),
};

View File

@ -1,4 +0,0 @@
{
"buildCommand": "cd ../.. && npx turbo run build --filter=@quri/squiggle-components",
"outputDirectory": "storybook-static"
}

View File

@ -23,4 +23,3 @@ coverage
.nyc_output/ .nyc_output/
src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js
src/rescript/Reducer/Reducer_Peggy/helpers.js src/rescript/Reducer/Reducer_Peggy/helpers.js
src/rescript/ReducerProject/ReducerProject_IncludeParser.js

View File

@ -3,8 +3,6 @@ lib
*.bs.js *.bs.js
*.gen.tsx *.gen.tsx
.nyc_output/ .nyc_output/
coverage/ _coverage/
.cache/ .cache/
Reducer_Peggy_GeneratedParser.js Reducer_Peggy_GeneratedParser.js
ReducerProject_IncludeParser.js
src/rescript/Reducer/Reducer_Peggy/helpers.js

View File

@ -32,29 +32,25 @@ describe("dotSubtract", () => {
*/ */
Skip.test("mean of normal minus exponential (property)", () => { Skip.test("mean of normal minus exponential (property)", () => {
assert_( assert_(
property2( property2(float_(), floatRange(1e-5, 1e5), (mean, rate) => {
float_(), // We limit ourselves to stdev=1 so that the integral is trivial
floatRange(1e-5, 1e5), let dotDifference = DistributionOperation.Constructors.pointwiseSubtract(
(mean, rate) => { ~env,
// We limit ourselves to stdev=1 so that the integral is trivial mkNormal(mean, 1.0),
let dotDifference = DistributionOperation.Constructors.pointwiseSubtract( mkExponential(rate),
~env, )
mkNormal(mean, 1.0), let meanResult = E.R2.bind(DistributionOperation.Constructors.mean(~env), dotDifference)
mkExponential(rate), // according to algebra or random variables,
let meanAnalytical =
mean -.
SymbolicDist.Exponential.mean({rate: rate})->E.R2.toExn(
"On trusted input this should never happen",
) )
let meanResult = E.R2.bind(DistributionOperation.Constructors.mean(~env), dotDifference) switch meanResult {
// according to algebra or random variables, | Ok(meanValue) => abs_float(meanValue -. meanAnalytical) /. abs_float(meanValue) < 1e-2 // 1% relative error
let meanAnalytical = | Error(err) => err === DistributionTypes.OperationError(DivisionByZeroError)
mean -. }
SymbolicDist.Exponential.mean({rate: rate})->E.R2.toExn( }),
"On trusted input this should never happen",
)
switch meanResult {
| Ok(meanValue) => abs_float(meanValue -. meanAnalytical) /. abs_float(meanValue) < 1e-2 // 1% relative error
| Error(err) => err === DistributionTypes.OperationError(DivisionByZeroError)
}
},
),
) )
pass pass
}) })

View File

@ -40,60 +40,51 @@ let algebraicPower = algebraicPower(~env)
describe("(Algebraic) addition of distributions", () => { describe("(Algebraic) addition of distributions", () => {
describe("mean", () => { describe("mean", () => {
test( test("normal(mean=5) + normal(mean=20)", () => {
"normal(mean=5) + normal(mean=20)", normalDist5
() => { ->algebraicAdd(normalDist20)
normalDist5 ->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->algebraicAdd(normalDist20) ->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
->expect
->toBe(Some(2.5e1))
})
test("uniform(low=9, high=10) + beta(alpha=2, beta=5)", () => {
// let uniformMean = (9.0 +. 10.0) /. 2.0
// let betaMean = 1.0 /. (1.0 +. 5.0 /. 2.0)
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean) ->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->E.R2.fmap(run) ->E.R2.fmap(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _) ->E.R.toExn("Expected float", _)
->expect switch received {
->toBe(Some(2.5e1)) | None => "algebraicAdd has"->expect->toBe("failed")
}, // This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
) // sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(9.786831807237022, ~digits=1) // (uniformMean +. betaMean)
test( }
"uniform(low=9, high=10) + beta(alpha=2, beta=5)", })
() => { test("beta(alpha=2, beta=5) + uniform(low=9, high=10)", () => {
// let uniformMean = (9.0 +. 10.0) /. 2.0 // let uniformMean = (9.0 +. 10.0) /. 2.0
// let betaMean = 1.0 /. (1.0 +. 5.0 /. 2.0) // let betaMean = 1.0 /. (1.0 +. 5.0 /. 2.0)
let received = let received =
uniformDist betaDist
->algebraicAdd(betaDist) ->algebraicAdd(uniformDist)
->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean) ->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->E.R2.fmap(run) ->E.R2.fmap(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _) ->E.R.toExn("Expected float", _)
switch received { switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad. // This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=2. // sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(9.786831807237022, ~digits=1) // (uniformMean +. betaMean) | Some(x) => x->expect->toBeSoCloseTo(9.784290207736126, ~digits=1) // (uniformMean +. betaMean)
} }
}, })
)
test(
"beta(alpha=2, beta=5) + uniform(low=9, high=10)",
() => {
// let uniformMean = (9.0 +. 10.0) /. 2.0
// let betaMean = 1.0 /. (1.0 +. 5.0 /. 2.0)
let received =
betaDist
->algebraicAdd(uniformDist)
->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(9.784290207736126, ~digits=1) // (uniformMean +. betaMean)
}
},
)
}) })
describe("pdf", () => { describe("pdf", () => {
// TEST IS WRONG. SEE STDEV ADDITION EXPRESSION. // TEST IS WRONG. SEE STDEV ADDITION EXPRESSION.
@ -131,282 +122,247 @@ describe("(Algebraic) addition of distributions", () => {
} }
}, },
) )
test( test("(normal(mean=10) + normal(mean=10)).pdf(1.9e1)", () => {
"(normal(mean=10) + normal(mean=10)).pdf(1.9e1)", let received =
() => { normalDist20
let received = ->Ok
normalDist20 ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1.9e1))
->Ok ->E.R2.fmap(run)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1.9e1)) ->E.R2.fmap(toFloat)
->E.R2.fmap(run) ->E.R.toOption
->E.R2.fmap(toFloat) ->E.O.flatten
->E.R.toOption let calculated =
->E.O.flatten normalDist10
let calculated = ->algebraicAdd(normalDist10)
normalDist10 ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1.9e1))
->algebraicAdd(normalDist10) ->E.R2.fmap(run)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1.9e1)) ->E.R2.fmap(toFloat)
->E.R2.fmap(run) ->E.R.toOption
->E.R2.fmap(toFloat) ->E.O.flatten
->E.R.toOption switch received {
->E.O.flatten | None =>
switch received { "this branch occurs when the dispatch to Jstat on trusted input fails."
| None => ->expect
"this branch occurs when the dispatch to Jstat on trusted input fails." ->toBe("never")
->expect | Some(x) =>
->toBe("never") switch calculated {
| Some(x) =>
switch calculated {
| None => "algebraicAdd has"->expect->toBe("failed")
| Some(y) => x->expect->toBeSoCloseTo(y, ~digits=1)
}
}
},
)
test(
"(uniform(low=9, high=10) + beta(alpha=2, beta=5)).pdf(10)",
() => {
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad. | Some(y) => x->expect->toBeSoCloseTo(y, ~digits=1)
// sometimes it works with ~digits=4.
// This value was calculated by a python script
| Some(x) => x->expect->toBeSoCloseTo(0.979023, ~digits=0)
} }
}, }
) })
test( test("(uniform(low=9, high=10) + beta(alpha=2, beta=5)).pdf(10)", () => {
"(beta(alpha=2, beta=5) + uniform(low=9, high=10)).pdf(10)", let received =
() => { uniformDist
let received = ->algebraicAdd(betaDist)
betaDist ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1e1))
->algebraicAdd(uniformDist) ->E.R2.fmap(run)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1e1)) ->E.R2.fmap(toFloat)
->E.R2.fmap(run) ->E.R.toExn("Expected float", _)
->E.R2.fmap(toFloat) switch received {
->E.R.toExn("Expected float", _) | None => "algebraicAdd has"->expect->toBe("failed")
switch received { // This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
| None => "algebraicAdd has"->expect->toBe("failed") // sometimes it works with ~digits=4.
// This is nondeterministic. // This value was calculated by a python script
| Some(x) => x->expect->toBeSoCloseTo(0.979023, ~digits=0) | Some(x) => x->expect->toBeSoCloseTo(0.979023, ~digits=0)
} }
}, })
) test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).pdf(10)", () => {
let received =
betaDist
->algebraicAdd(uniformDist)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic.
| Some(x) => x->expect->toBeSoCloseTo(0.979023, ~digits=0)
}
})
}) })
describe("cdf", () => { describe("cdf", () => {
testAll( testAll("(normal(mean=5) + normal(mean=5)).cdf (imprecise)", list{6e0, 8e0, 1e1, 1.2e1}, x => {
"(normal(mean=5) + normal(mean=5)).cdf (imprecise)", let received =
list{6e0, 8e0, 1e1, 1.2e1}, normalDist10
x => { ->Ok
let received = ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x))
normalDist10 ->E.R2.fmap(run)
->Ok ->E.R2.fmap(toFloat)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x)) ->E.R.toOption
->E.R2.fmap(run) ->E.O.flatten
->E.R2.fmap(toFloat) let calculated =
->E.R.toOption normalDist5
->E.O.flatten ->algebraicAdd(normalDist5)
let calculated = ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x))
normalDist5 ->E.R2.fmap(run)
->algebraicAdd(normalDist5) ->E.R2.fmap(toFloat)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x)) ->E.R.toOption
->E.R2.fmap(run) ->E.O.flatten
->E.R2.fmap(toFloat)
->E.R.toOption
->E.O.flatten
switch received { switch received {
| None => | None =>
"this branch occurs when the dispatch to Jstat on trusted input fails." "this branch occurs when the dispatch to Jstat on trusted input fails."
->expect ->expect
->toBe("never") ->toBe("never")
| Some(x) => | Some(x) =>
switch calculated { switch calculated {
| None => "algebraicAdd has"->expect->toBe("failed")
| Some(y) => x->expect->toBeSoCloseTo(y, ~digits=0)
}
}
},
)
test(
"(normal(mean=10) + normal(mean=10)).cdf(1.25e1)",
() => {
let received =
normalDist20
->Ok
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1.25e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
->E.O.flatten
let calculated =
normalDist10
->algebraicAdd(normalDist10)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1.25e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
->E.O.flatten
switch received {
| None =>
"this branch occurs when the dispatch to Jstat on trusted input fails."
->expect
->toBe("never")
| Some(x) =>
switch calculated {
| None => "algebraicAdd has"->expect->toBe("failed")
| Some(y) => x->expect->toBeSoCloseTo(y, ~digits=2)
}
}
},
)
test(
"(uniform(low=9, high=10) + beta(alpha=2, beta=5)).cdf(10)",
() => {
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad. | Some(y) => x->expect->toBeSoCloseTo(y, ~digits=0)
// The value was calculated externally using a python script
| Some(x) => x->expect->toBeSoCloseTo(0.71148, ~digits=1)
} }
}, }
) })
test( test("(normal(mean=10) + normal(mean=10)).cdf(1.25e1)", () => {
"(beta(alpha=2, beta=5) + uniform(low=9, high=10)).cdf(10)", let received =
() => { normalDist20
let received = ->Ok
betaDist ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1.25e1))
->algebraicAdd(uniformDist) ->E.R2.fmap(run)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1e1)) ->E.R2.fmap(toFloat)
->E.R2.fmap(run) ->E.R.toOption
->E.R2.fmap(toFloat) ->E.O.flatten
->E.R.toExn("Expected float", _) let calculated =
switch received { normalDist10
->algebraicAdd(normalDist10)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1.25e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
->E.O.flatten
switch received {
| None =>
"this branch occurs when the dispatch to Jstat on trusted input fails."
->expect
->toBe("never")
| Some(x) =>
switch calculated {
| None => "algebraicAdd has"->expect->toBe("failed") | None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad. | Some(y) => x->expect->toBeSoCloseTo(y, ~digits=2)
// The value was calculated externally using a python script
| Some(x) => x->expect->toBeSoCloseTo(0.71148, ~digits=1)
} }
}, }
) })
test("(uniform(low=9, high=10) + beta(alpha=2, beta=5)).cdf(10)", () => {
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// The value was calculated externally using a python script
| Some(x) => x->expect->toBeSoCloseTo(0.71148, ~digits=1)
}
})
test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).cdf(10)", () => {
let received =
betaDist
->algebraicAdd(uniformDist)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// The value was calculated externally using a python script
| Some(x) => x->expect->toBeSoCloseTo(0.71148, ~digits=1)
}
})
}) })
describe("inv", () => { describe("inv", () => {
testAll( testAll("(normal(mean=5) + normal(mean=5)).inv (imprecise)", list{5e-2, 4.2e-3, 9e-3}, x => {
"(normal(mean=5) + normal(mean=5)).inv (imprecise)", let received =
list{5e-2, 4.2e-3, 9e-3}, normalDist10
x => { ->Ok
let received = ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x))
normalDist10 ->E.R2.fmap(run)
->Ok ->E.R2.fmap(toFloat)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x)) ->E.R.toOption
->E.R2.fmap(run) ->E.O.flatten
->E.R2.fmap(toFloat) let calculated =
->E.R.toOption normalDist5
->E.O.flatten ->algebraicAdd(normalDist5)
let calculated = ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x))
normalDist5 ->E.R2.fmap(run)
->algebraicAdd(normalDist5) ->E.R2.fmap(toFloat)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x)) ->E.R.toOption
->E.R2.fmap(run) ->E.O.flatten
->E.R2.fmap(toFloat)
->E.R.toOption
->E.O.flatten
switch received { switch received {
| None => | None =>
"this branch occurs when the dispatch to Jstat on trusted input fails." "this branch occurs when the dispatch to Jstat on trusted input fails."
->expect ->expect
->toBe("never") ->toBe("never")
| Some(x) => | Some(x) =>
switch calculated { switch calculated {
| None => "algebraicAdd has"->expect->toBe("failed")
| Some(y) => x->expect->toBeSoCloseTo(y, ~digits=-1)
}
}
},
)
test(
"(normal(mean=10) + normal(mean=10)).inv(1e-1)",
() => {
let received =
normalDist20
->Ok
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 1e-1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
->E.O.flatten
let calculated =
normalDist10
->algebraicAdd(normalDist10)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 1e-1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
->E.O.flatten
switch received {
| None =>
"this branch occurs when the dispatch to Jstat on trusted input fails."
->expect
->toBe("never")
| Some(x) =>
switch calculated {
| None => "algebraicAdd has"->expect->toBe("failed")
| Some(y) => x->expect->toBeSoCloseTo(y, ~digits=-1)
}
}
},
)
test(
"(uniform(low=9, high=10) + beta(alpha=2, beta=5)).inv(2e-2)",
() => {
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 2e-2))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad. | Some(y) => x->expect->toBeSoCloseTo(y, ~digits=-1)
// sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(9.179319623146968, ~digits=0)
} }
}, }
) })
test( test("(normal(mean=10) + normal(mean=10)).inv(1e-1)", () => {
"(beta(alpha=2, beta=5) + uniform(low=9, high=10)).inv(2e-2)", let received =
() => { normalDist20
let received = ->Ok
betaDist ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 1e-1))
->algebraicAdd(uniformDist) ->E.R2.fmap(run)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 2e-2)) ->E.R2.fmap(toFloat)
->E.R2.fmap(run) ->E.R.toOption
->E.R2.fmap(toFloat) ->E.O.flatten
->E.R.toExn("Expected float", _) let calculated =
switch received { normalDist10
->algebraicAdd(normalDist10)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 1e-1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
->E.O.flatten
switch received {
| None =>
"this branch occurs when the dispatch to Jstat on trusted input fails."
->expect
->toBe("never")
| Some(x) =>
switch calculated {
| None => "algebraicAdd has"->expect->toBe("failed") | None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad. | Some(y) => x->expect->toBeSoCloseTo(y, ~digits=-1)
// sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(9.190872365862756, ~digits=0)
} }
}, }
) })
test("(uniform(low=9, high=10) + beta(alpha=2, beta=5)).inv(2e-2)", () => {
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 2e-2))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(9.179319623146968, ~digits=0)
}
})
test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).inv(2e-2)", () => {
let received =
betaDist
->algebraicAdd(uniformDist)
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 2e-2))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(9.190872365862756, ~digits=0)
}
})
}) })
}) })

View File

@ -3,7 +3,7 @@ This is the most basic file in our invariants family of tests.
Validate that the addition of means equals the mean of the addition, similar for subtraction and multiplication. Validate that the addition of means equals the mean of the addition, similar for subtraction and multiplication.
Details in https://squiggle-language.com/docs/internal/invariants/ Details in https://develop--squiggle-documentation.netlify.app/docs/internal/invariants/
Note: epsilon of 1e3 means the invariants are, in general, not being satisfied. Note: epsilon of 1e3 means the invariants are, in general, not being satisfied.
*/ */
@ -87,22 +87,14 @@ describe("Means are invariant", () => {
let testAddInvariant = (t1, t2) => let testAddInvariant = (t1, t2) =>
E.R.liftM2(testAdditionMean, t1, t2)->E.R.toExn("Means were not invariant", _) E.R.liftM2(testAdditionMean, t1, t2)->E.R.toExn("Means were not invariant", _)
testAll( testAll("with two of the same distribution", distributions, dist => {
"with two of the same distribution", testAddInvariant(dist, dist)
distributions, })
dist => {
testAddInvariant(dist, dist)
},
)
testAll( testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
"with two different distributions", let (dist1, dist2) = dists
pairsOfDifferentDistributions, testAddInvariant(dist1, dist2)
dists => { })
let (dist1, dist2) = dists
testAddInvariant(dist1, dist2)
},
)
testAll( testAll(
"with two different distributions in swapped order", "with two different distributions in swapped order",
@ -124,22 +116,14 @@ describe("Means are invariant", () => {
let testSubtractInvariant = (t1, t2) => let testSubtractInvariant = (t1, t2) =>
E.R.liftM2(testSubtractionMean, t1, t2)->E.R.toExn("Means were not invariant", _) E.R.liftM2(testSubtractionMean, t1, t2)->E.R.toExn("Means were not invariant", _)
testAll( testAll("with two of the same distribution", distributions, dist => {
"with two of the same distribution", testSubtractInvariant(dist, dist)
distributions, })
dist => {
testSubtractInvariant(dist, dist)
},
)
testAll( testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
"with two different distributions", let (dist1, dist2) = dists
pairsOfDifferentDistributions, testSubtractInvariant(dist1, dist2)
dists => { })
let (dist1, dist2) = dists
testSubtractInvariant(dist1, dist2)
},
)
testAll( testAll(
"with two different distributions in swapped order", "with two different distributions in swapped order",
@ -161,22 +145,14 @@ describe("Means are invariant", () => {
let testMultiplicationInvariant = (t1, t2) => let testMultiplicationInvariant = (t1, t2) =>
E.R.liftM2(testMultiplicationMean, t1, t2)->E.R.toExn("Means were not invariant", _) E.R.liftM2(testMultiplicationMean, t1, t2)->E.R.toExn("Means were not invariant", _)
testAll( testAll("with two of the same distribution", distributions, dist => {
"with two of the same distribution", testMultiplicationInvariant(dist, dist)
distributions, })
dist => {
testMultiplicationInvariant(dist, dist)
},
)
testAll( testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
"with two different distributions", let (dist1, dist2) = dists
pairsOfDifferentDistributions, testMultiplicationInvariant(dist1, dist2)
dists => { })
let (dist1, dist2) = dists
testMultiplicationInvariant(dist1, dist2)
},
)
testAll( testAll(
"with two different distributions in swapped order", "with two different distributions in swapped order",

View File

@ -17,9 +17,10 @@ describe("klDivergence: continuous -> continuous -> float", () => {
let answer = let answer =
uniformMakeR(lowAnswer, highAnswer)->E.R2.errMap(s => DistributionTypes.ArgumentError(s)) uniformMakeR(lowAnswer, highAnswer)->E.R2.errMap(s => DistributionTypes.ArgumentError(s))
let prediction = let prediction =
uniformMakeR(lowPrediction, highPrediction)->E.R2.errMap( uniformMakeR(
s => DistributionTypes.ArgumentError(s), lowPrediction,
) highPrediction,
)->E.R2.errMap(s => DistributionTypes.ArgumentError(s))
// integral along the support of the answer of answer.pdf(x) times log of prediction.pdf(x) divided by answer.pdf(x) dx // integral along the support of the answer of answer.pdf(x) times log of prediction.pdf(x) divided by answer.pdf(x) dx
let analyticalKl = Js.Math.log((highPrediction -. lowPrediction) /. (highAnswer -. lowAnswer)) let analyticalKl = Js.Math.log((highPrediction -. lowPrediction) /. (highAnswer -. lowAnswer))
let kl = E.R.liftJoin2(klDivergence, prediction, answer) let kl = E.R.liftJoin2(klDivergence, prediction, answer)
@ -182,9 +183,9 @@ describe("combineAlongSupportOfSecondArgument0", () => {
let answer = let answer =
uniformMakeR(lowAnswer, highAnswer)->E.R2.errMap(s => DistributionTypes.ArgumentError(s)) uniformMakeR(lowAnswer, highAnswer)->E.R2.errMap(s => DistributionTypes.ArgumentError(s))
let prediction = let prediction =
uniformMakeR(lowPrediction, highPrediction)->E.R2.errMap( uniformMakeR(lowPrediction, highPrediction)->E.R2.errMap(s => DistributionTypes.ArgumentError(
s => DistributionTypes.ArgumentError(s), s,
) ))
let answerWrapped = E.R.fmap(a => run(FromDist(#ToDist(ToPointSet), a)), answer) let answerWrapped = E.R.fmap(a => run(FromDist(#ToDist(ToPointSet), a)), answer)
let predictionWrapped = E.R.fmap(a => run(FromDist(#ToDist(ToPointSet), a)), prediction) let predictionWrapped = E.R.fmap(a => run(FromDist(#ToDist(ToPointSet), a)), prediction)

View File

@ -3,7 +3,7 @@ open Expect
open TestHelpers open TestHelpers
// TODO: use Normal.make (but preferably after teh new validation dispatch is in) // TODO: use Normal.make (but preferably after teh new validation dispatch is in)
let mkNormal = (mean, stdev) => DistributionTypes.Symbolic(#Normal({mean, stdev})) let mkNormal = (mean, stdev) => DistributionTypes.Symbolic(#Normal({mean: mean, stdev: stdev}))
describe("(Symbolic) normalize", () => { describe("(Symbolic) normalize", () => {
testAll("has no impact on normal distributions", list{-1e8, -1e-2, 0.0, 1e-4, 1e16}, mean => { testAll("has no impact on normal distributions", list{-1e8, -1e-2, 0.0, 1e-4, 1e16}, mean => {
@ -47,7 +47,10 @@ describe("(Symbolic) mean", () => {
tup => { tup => {
let (low, medium, high) = tup let (low, medium, high) = tup
let meanValue = run( let meanValue = run(
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Triangular({low, medium, high}))), FromDist(
#ToFloat(#Mean),
DistributionTypes.Symbolic(#Triangular({low: low, medium: medium, high: high})),
),
) )
meanValue->unpackFloat->expect->toBeCloseTo((low +. medium +. high) /. 3.0) // https://www.statology.org/triangular-distribution/ meanValue->unpackFloat->expect->toBeCloseTo((low +. medium +. high) /. 3.0) // https://www.statology.org/triangular-distribution/
}, },
@ -60,7 +63,7 @@ describe("(Symbolic) mean", () => {
tup => { tup => {
let (alpha, beta) = tup let (alpha, beta) = tup
let meanValue = run( let meanValue = run(
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Beta({alpha, beta}))), FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Beta({alpha: alpha, beta: beta}))),
) )
meanValue->unpackFloat->expect->toBeCloseTo(1.0 /. (1.0 +. beta /. alpha)) // https://en.wikipedia.org/wiki/Beta_distribution#Mean meanValue->unpackFloat->expect->toBeCloseTo(1.0 /. (1.0 +. beta /. alpha)) // https://en.wikipedia.org/wiki/Beta_distribution#Mean
}, },
@ -81,8 +84,8 @@ describe("(Symbolic) mean", () => {
let (mean, stdev) = tup let (mean, stdev) = tup
let betaDistribution = SymbolicDist.Beta.fromMeanAndStdev(mean, stdev) let betaDistribution = SymbolicDist.Beta.fromMeanAndStdev(mean, stdev)
let meanValue = let meanValue =
betaDistribution->E.R2.fmap( betaDistribution->E.R2.fmap(d =>
d => run(FromDist(#ToFloat(#Mean), d->DistributionTypes.Symbolic)), run(FromDist(#ToFloat(#Mean), d->DistributionTypes.Symbolic))
) )
switch meanValue { switch meanValue {
| Ok(value) => value->unpackFloat->expect->toBeCloseTo(mean) | Ok(value) => value->unpackFloat->expect->toBeCloseTo(mean)
@ -97,7 +100,7 @@ describe("(Symbolic) mean", () => {
tup => { tup => {
let (mu, sigma) = tup let (mu, sigma) = tup
let meanValue = run( let meanValue = run(
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Lognormal({mu, sigma}))), FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Lognormal({mu: mu, sigma: sigma}))),
) )
meanValue->unpackFloat->expect->toBeCloseTo(Js.Math.exp(mu +. sigma ** 2.0 /. 2.0)) // https://brilliant.org/wiki/log-normal-distribution/ meanValue->unpackFloat->expect->toBeCloseTo(Js.Math.exp(mu +. sigma ** 2.0 /. 2.0)) // https://brilliant.org/wiki/log-normal-distribution/
}, },
@ -109,7 +112,7 @@ describe("(Symbolic) mean", () => {
tup => { tup => {
let (low, high) = tup let (low, high) = tup
let meanValue = run( let meanValue = run(
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Uniform({low, high}))), FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Uniform({low: low, high: high}))),
) )
meanValue->unpackFloat->expect->toBeCloseTo((low +. high) /. 2.0) // https://en.wikipedia.org/wiki/Continuous_uniform_distribution#Moments meanValue->unpackFloat->expect->toBeCloseTo((low +. high) /. 2.0) // https://en.wikipedia.org/wiki/Continuous_uniform_distribution#Moments
}, },

View File

@ -1,21 +0,0 @@
open Jest
open TestHelpers
describe("E.A.getByFmap", () => {
makeTest("Empty list returns None", E.A.getByFmap([], x => x + 1, x => mod(x, 2) == 0), None)
makeTest(
"Never predicate returns None",
E.A.getByFmap([1, 2, 3, 4, 5, 6], x => x + 1, _ => false),
None,
)
makeTest(
"function evaluates",
E.A.getByFmap([1, 1, 1, 1, 1, 1, 1, 2, 1, 1], x => 3 * x, x => x > 4),
Some(6),
)
makeTest(
"always predicate returns fn(fst(a))",
E.A.getByFmap([0, 1, 2, 3, 4, 5, 6], x => 10 + x, _ => true),
Some(10),
)
})

View File

@ -9,28 +9,22 @@ let prepareInputs = (ar, minWeight) =>
describe("Continuous and discrete splits", () => { describe("Continuous and discrete splits", () => {
makeTest( makeTest(
"is empty, with no common elements", "is empty, with no common elements",
prepareInputs([1.33455, 1.432, 2.0], 2), prepareInputs([1.432, 1.33455, 2.0], 2),
([1.33455, 1.432, 2.0], []), ([1.33455, 1.432, 2.0], []),
) )
makeTest( makeTest(
"only stores 3.5 as discrete when minWeight is 3", "only stores 3.5 as discrete when minWeight is 3",
prepareInputs([1.33455, 1.432, 2.0, 2.0, 3.5, 3.5, 3.5], 3), prepareInputs([1.432, 1.33455, 2.0, 2.0, 3.5, 3.5, 3.5], 3),
([1.33455, 1.432, 2.0, 2.0], [(3.5, 3.0)]), ([1.33455, 1.432, 2.0, 2.0], [(3.5, 3.0)]),
) )
makeTest( makeTest(
"doesn't store 3.5 as discrete when minWeight is 5", "doesn't store 3.5 as discrete when minWeight is 5",
prepareInputs([1.33455, 1.432, 2.0, 2.0, 3.5, 3.5, 3.5], 5), prepareInputs([1.432, 1.33455, 2.0, 2.0, 3.5, 3.5, 3.5], 5),
([1.33455, 1.432, 2.0, 2.0, 3.5, 3.5, 3.5], []), ([1.33455, 1.432, 2.0, 2.0, 3.5, 3.5, 3.5], []),
) )
makeTest(
"more general test",
prepareInputs([10., 10., 11., 11., 11., 12., 13., 13., 13., 13., 13., 14.], 3),
([10., 10., 12., 14.], [(11., 3.), (13., 5.)]),
)
let makeDuplicatedArray = count => { let makeDuplicatedArray = count => {
let arr = Belt.Array.range(1, count) |> E.A.fmap(float_of_int) let arr = Belt.Array.range(1, count) |> E.A.fmap(float_of_int)
let sorted = arr |> Belt.SortArray.stableSortBy(_, compare) let sorted = arr |> Belt.SortArray.stableSortBy(_, compare)

View File

@ -0,0 +1,20 @@
open Jest
open Expect
let makeTest = (~only=false, str, item1, item2) =>
only
? Only.test(str, () => expect(item1)->toEqual(item2))
: test(str, () => expect(item1)->toEqual(item2))
describe("Lodash", () =>
describe("Lodash", () => {
makeTest("min", Lodash.min([1, 3, 4]), 1)
makeTest("max", Lodash.max([1, 3, 4]), 4)
makeTest("uniq", Lodash.uniq([1, 3, 4, 4]), [1, 3, 4])
makeTest(
"countBy",
Lodash.countBy([1, 3, 4, 4], r => r),
Js.Dict.fromArray([("1", 1), ("3", 1), ("4", 2)]),
)
})
)

View File

@ -1,50 +0,0 @@
@@warning("-44")
module Bindings = Reducer_Bindings
module Namespace = Reducer_Namespace
open Jest
open Expect
open Expect.Operators
describe("Bindings", () => {
let value = Reducer_T.IEvNumber(1967.0)
let bindings = Bindings.make()->Bindings.set("value", value)
test("get", () => {
expect(bindings->Bindings.get("value")) == Some(value)
})
test("get nonexisting value", () => {
expect(bindings->Bindings.get("nosuchvalue")) == None
})
test("get on extended", () => {
expect(bindings->Bindings.extend->Bindings.get("value")) == Some(value)
})
test("locals", () => {
expect(bindings->Bindings.locals->Namespace.get("value")) == Some(value)
})
test("locals on extendeed", () => {
expect(bindings->Bindings.extend->Bindings.locals->Namespace.get("value")) == None
})
describe("extend", () => {
let value2 = Reducer_T.IEvNumber(5.)
let extendedBindings = bindings->Bindings.extend->Bindings.set("value", value2)
test(
"get on extended",
() => {
expect(extendedBindings->Bindings.get("value")) == Some(value2)
},
)
test(
"get on original",
() => {
expect(bindings->Bindings.get("value")) == Some(value)
},
)
})
})

View File

@ -0,0 +1,146 @@
open Jest
// open Expect
open Reducer_Expression_ExpressionBuilder
open Reducer_TestMacroHelpers
module ExpressionT = Reducer_Expression_T
let exampleExpression = eNumber(1.)
let exampleExpressionY = eSymbol("y")
let exampleStatementY = eLetStatement("y", eNumber(1.))
let exampleStatementX = eLetStatement("y", eSymbol("x"))
let exampleStatementZ = eLetStatement("z", eSymbol("y"))
// If it is not a macro then it is not expanded
testMacro([], exampleExpression, "Ok(1)")
describe("bindStatement", () => {
// A statement is bound by the bindings created by the previous statement
testMacro(
[],
eBindStatement(eBindings([]), exampleStatementY),
"Ok((:$_setBindings_$ @{} :y 1) context: @{})",
)
// Then it answers the bindings for the next statement when reduced
testMacroEval([], eBindStatement(eBindings([]), exampleStatementY), "Ok(@{y: 1})")
// Now let's feed a binding to see what happens
testMacro(
[],
eBindStatement(eBindings([("x", IEvNumber(2.))]), exampleStatementX),
"Ok((:$_setBindings_$ @{x: 2} :y 2) context: @{x: 2})",
)
// An expression does not return a binding, thus error
testMacro([], eBindStatement(eBindings([]), exampleExpression), "Assignment expected")
// When bindings from previous statement are missing the context is injected. This must be the first statement of a block
testMacro(
[("z", IEvNumber(99.))],
eBindStatementDefault(exampleStatementY),
"Ok((:$_setBindings_$ @{z: 99} :y 1) context: @{z: 99})",
)
})
describe("bindExpression", () => {
// x is simply bound in the expression
testMacro(
[],
eBindExpression(eBindings([("x", IEvNumber(2.))]), eSymbol("x")),
"Ok(2 context: @{x: 2})",
)
// When an let statement is the end expression then bindings are returned
testMacro(
[],
eBindExpression(eBindings([("x", IEvNumber(2.))]), exampleStatementY),
"Ok((:$_exportBindings_$ (:$_setBindings_$ @{x: 2} :y 1)) context: @{x: 2})",
)
// Now let's reduce that expression
testMacroEval(
[],
eBindExpression(eBindings([("x", IEvNumber(2.))]), exampleStatementY),
"Ok(@{x: 2,y: 1})",
)
// When bindings are missing the context is injected. This must be the first and last statement of a block
testMacroEval(
[("z", IEvNumber(99.))],
eBindExpressionDefault(exampleStatementY),
"Ok(@{y: 1,z: 99})",
)
})
describe("block", () => {
// Block with a single expression
testMacro([], eBlock(list{exampleExpression}), "Ok((:$$_bindExpression_$$ 1))")
testMacroEval([], eBlock(list{exampleExpression}), "Ok(1)")
// Block with a single statement
testMacro([], eBlock(list{exampleStatementY}), "Ok((:$$_bindExpression_$$ (:$_let_$ :y 1)))")
testMacroEval([], eBlock(list{exampleStatementY}), "Ok(@{y: 1})")
// Block with a statement and an expression
testMacro(
[],
eBlock(list{exampleStatementY, exampleExpressionY}),
"Ok((:$$_bindExpression_$$ (:$$_bindStatement_$$ (:$_let_$ :y 1)) :y))",
)
testMacroEval([], eBlock(list{exampleStatementY, exampleExpressionY}), "Ok(1)")
// Block with a statement and another statement
testMacro(
[],
eBlock(list{exampleStatementY, exampleStatementZ}),
"Ok((:$$_bindExpression_$$ (:$$_bindStatement_$$ (:$_let_$ :y 1)) (:$_let_$ :z :y)))",
)
testMacroEval([], eBlock(list{exampleStatementY, exampleStatementZ}), "Ok(@{y: 1,z: 1})")
// Block inside a block
testMacro([], eBlock(list{eBlock(list{exampleExpression})}), "Ok((:$$_bindExpression_$$ {1}))")
testMacroEval([], eBlock(list{eBlock(list{exampleExpression})}), "Ok(1)")
// Block assigned to a variable
testMacro(
[],
eBlock(list{eLetStatement("z", eBlock(list{eBlock(list{exampleExpressionY})}))}),
"Ok((:$$_bindExpression_$$ (:$_let_$ :z {{:y}})))",
)
testMacroEval(
[],
eBlock(list{eLetStatement("z", eBlock(list{eBlock(list{exampleExpressionY})}))}),
"Ok(@{z: :y})",
)
// Empty block
testMacro([], eBlock(list{}), "Ok(:undefined block)") //TODO: should be an error
// :$$_block_$$ (:$$_block_$$ (:$_let_$ :y (:add :x 1)) :y)"
testMacro(
[],
eBlock(list{
eBlock(list{
eLetStatement("y", eFunction("add", list{eSymbol("x"), eNumber(1.)})),
eSymbol("y"),
}),
}),
"Ok((:$$_bindExpression_$$ {(:$_let_$ :y (:add :x 1)); :y}))",
)
testMacroEval(
[("x", IEvNumber(1.))],
eBlock(list{
eBlock(list{
eLetStatement("y", eFunction("add", list{eSymbol("x"), eNumber(1.)})),
eSymbol("y"),
}),
}),
"Ok(2)",
)
})
describe("lambda", () => {
// assign a lambda to a variable
let lambdaExpression = eFunction("$$_lambda_$$", list{eArrayString(["y"]), exampleExpressionY})
testMacro([], lambdaExpression, "Ok(lambda(y=>internal code))")
// call a lambda
let callLambdaExpression = list{lambdaExpression, eNumber(1.)}->ExpressionT.EList
testMacro([], callLambdaExpression, "Ok(((:$$_lambda_$$ [y] :y) 1))")
testMacroEval([], callLambdaExpression, "Ok(1)")
// Parameters shadow the outer scope
testMacroEval([("y", IEvNumber(666.))], callLambdaExpression, "Ok(1)")
// When not shadowed by the parameters, the outer scope variables are available
let lambdaExpression = eFunction(
"$$_lambda_$$",
list{eArrayString(["z"]), eFunction("add", list{eSymbol("y"), eSymbol("z")})},
)
let callLambdaExpression = eList(list{lambdaExpression, eNumber(1.)})
testMacroEval([("y", IEvNumber(666.))], callLambdaExpression, "Ok(667)")
})

View File

@ -0,0 +1,37 @@
module ExpressionValue = ReducerInterface.ExternalExpressionValue
open Jest
open Expect
let expectEvalToBe = (expr: string, answer: string) =>
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toBe(answer)
let testEval = (expr, answer) => test(expr, () => expectEvalToBe(expr, answer))
describe("builtin", () => {
// All MathJs operators and functions are available for string, number and boolean
// .e.g + - / * > >= < <= == /= not and or
// See https://mathjs.org/docs/expressions/syntax.html
// See https://mathjs.org/docs/reference/functions.html
testEval("-1", "Ok(-1)")
testEval("1-1", "Ok(0)")
testEval("2>1", "Ok(true)")
testEval("concat('a','b')", "Ok('ab')")
})
describe("builtin exception", () => {
//It's a pity that MathJs does not return error position
test("MathJs Exception", () =>
expectEvalToBe("testZadanga(1)", "Error(JS Exception: Error: Undefined function testZadanga)")
)
})
describe("error reporting from collection functions", () => {
testEval("arr=[1,2,3]; map(arr, {|x| x*2})", "Ok([2,4,6])")
testEval(
"arr = [normal(3,2)]; map(arr, zarathsuzaWasHere)",
"Error(zarathsuzaWasHere is not defined)",
)
// FIXME: returns "Error(Function not found: map(Array,Symbol))"
// Actually this error is correct but not informative
})

View File

@ -0,0 +1,22 @@
// Reducer_Helpers
module ErrorValue = Reducer_ErrorValue
module ExternalExpressionValue = ReducerInterface.ExternalExpressionValue
module InternalExpressionValue = ReducerInterface.InternalExpressionValue
module Bindings = Reducer_Bindings
let removeDefaultsInternal = (iev: InternalExpressionValue.t) => {
switch iev {
| InternalExpressionValue.IEvBindings(nameSpace) =>
Bindings.removeOther(
nameSpace,
ReducerInterface.StdLib.internalStdLib,
)->InternalExpressionValue.IEvBindings
| value => value
}
}
let removeDefaultsExternal = (ev: ExternalExpressionValue.t): ExternalExpressionValue.t =>
ev->InternalExpressionValue.toInternal->removeDefaultsInternal->InternalExpressionValue.toExternal
let rRemoveDefaultsInternal = r => Belt.Result.map(r, removeDefaultsInternal)
let rRemoveDefaultsExternal = r => Belt.Result.map(r, removeDefaultsExternal)

View File

@ -0,0 +1,31 @@
module MathJs = Reducer_MathJs
module ErrorValue = Reducer.ErrorValue
open Jest
open ExpectJs
describe("eval", () => {
test("Number", () => expect(MathJs.Eval.eval("1"))->toEqual(Ok(IEvNumber(1.))))
test("Number expr", () => expect(MathJs.Eval.eval("1-1"))->toEqual(Ok(IEvNumber(0.))))
test("String", () => expect(MathJs.Eval.eval("'hello'"))->toEqual(Ok(IEvString("hello"))))
test("String expr", () =>
expect(MathJs.Eval.eval("concat('hello ','world')"))->toEqual(Ok(IEvString("hello world")))
)
test("Boolean", () => expect(MathJs.Eval.eval("true"))->toEqual(Ok(IEvBool(true))))
test("Boolean expr", () => expect(MathJs.Eval.eval("2>1"))->toEqual(Ok(IEvBool(true))))
})
describe("errors", () => {
// All those errors propagete up and are returned by the resolver
test("unknown function", () =>
expect(MathJs.Eval.eval("testZadanga()"))->toEqual(
Error(ErrorValue.REJavaScriptExn(Some("Undefined function testZadanga"), Some("Error"))),
)
)
test("unknown answer type", () =>
expect(MathJs.Eval.eval("1+1i"))->toEqual(
Error(ErrorValue.RETodo("Unhandled MathJs literal type: object")),
)
)
})

View File

@ -1,62 +0,0 @@
@@warning("-44")
module Namespace = Reducer_Namespace
open Jest
open Expect
open Expect.Operators
let makeValue = (v: float) => v->Reducer_T.IEvNumber
describe("Namespace", () => {
let value = makeValue(5.)
let v2 = makeValue(2.)
let ns = Namespace.make()->Namespace.set("value", value)
test("get", () => {
expect(ns->Namespace.get("value")) == Some(value)
})
test("get nonexisting value", () => {
expect(ns->Namespace.get("nosuchvalue")) == None
})
test("set", () => {
let ns2 = ns->Namespace.set("v2", v2)
expect(ns2->Namespace.get("v2")) == Some(v2)
})
test("immutable", () => {
let _ = ns->Namespace.set("v2", Reducer_T.IEvNumber(2.))
expect(ns->Namespace.get("v2")) == None
})
describe("merge many", () => {
let x1 = makeValue(10.)
let x2 = makeValue(20.)
let x3 = makeValue(30.)
let x4 = makeValue(40.)
let ns1 = Namespace.make()->Namespace.set("x1", x1)->Namespace.set("x2", x2)
let ns2 = Namespace.make()->Namespace.set("x3", x3)->Namespace.set("x4", x4)
let nsMerged = Namespace.mergeMany([ns, ns1, ns2])
test(
"merge many 1",
() => {
expect(nsMerged->Namespace.get("x1")) == Some(x1)
},
)
test(
"merge many 2",
() => {
expect(nsMerged->Namespace.get("x4")) == Some(x4)
},
)
test(
"merge many 3",
() => {
expect(nsMerged->Namespace.get("value")) == Some(value)
},
)
})
})

View File

@ -14,46 +14,47 @@ describe("Peggy parse", () => {
}) })
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
testParse("1", "{1}") testParse("1", "{1}")
testParse("'hello'", "{'hello'}") testParse("'hello'", "{'hello'}")
testParse("true", "{true}") testParse("true", "{true}")
testParse("1+2", "{(:add 1 2)}") testParse("1+2", "{(::add 1 2)}")
testParse("add(1,2)", "{(:add 1 2)}") testParse("add(1,2)", "{(::add 1 2)}")
testParse("(1)", "{1}") testParse("(1)", "{1}")
testParse("(1+2)", "{(:add 1 2)}") testParse("(1+2)", "{(::add 1 2)}")
}) })
describe("unary", () => { describe("unary", () => {
testParse("-1", "{(:unaryMinus 1)}") testParse("-1", "{(::unaryMinus 1)}")
testParse("!true", "{(:not true)}") testParse("!true", "{(::not true)}")
testParse("1 + -1", "{(:add 1 (:unaryMinus 1))}") testParse("1 + -1", "{(::add 1 (::unaryMinus 1))}")
testParse("-a[0]", "{(:unaryMinus (:$_atIndex_$ :a 0))}") testParse("-a[0]", "{(::unaryMinus (::$_atIndex_$ :a 0))}")
testParse("!a[0]", "{(:not (:$_atIndex_$ :a 0))}") testParse("!a[0]", "{(::not (::$_atIndex_$ :a 0))}")
}) })
describe("multiplicative", () => { describe("multiplicative", () => {
testParse("1 * 2", "{(:multiply 1 2)}") testParse("1 * 2", "{(::multiply 1 2)}")
testParse("1 / 2", "{(:divide 1 2)}") testParse("1 / 2", "{(::divide 1 2)}")
testParse("1 * 2 * 3", "{(:multiply (:multiply 1 2) 3)}") testParse("1 * 2 * 3", "{(::multiply (::multiply 1 2) 3)}")
testParse("1 * 2 / 3", "{(:divide (:multiply 1 2) 3)}") testParse("1 * 2 / 3", "{(::divide (::multiply 1 2) 3)}")
testParse("1 / 2 * 3", "{(:multiply (:divide 1 2) 3)}") testParse("1 / 2 * 3", "{(::multiply (::divide 1 2) 3)}")
testParse("1 / 2 / 3", "{(:divide (:divide 1 2) 3)}") testParse("1 / 2 / 3", "{(::divide (::divide 1 2) 3)}")
testParse("1 * 2 + 3 * 4", "{(:add (:multiply 1 2) (:multiply 3 4))}") testParse("1 * 2 + 3 * 4", "{(::add (::multiply 1 2) (::multiply 3 4))}")
testParse("1 * 2 - 3 * 4", "{(:subtract (:multiply 1 2) (:multiply 3 4))}") testParse("1 * 2 - 3 * 4", "{(::subtract (::multiply 1 2) (::multiply 3 4))}")
testParse("1 * 2 .+ 3 * 4", "{(:dotAdd (:multiply 1 2) (:multiply 3 4))}") testParse("1 * 2 .+ 3 * 4", "{(::dotAdd (::multiply 1 2) (::multiply 3 4))}")
testParse("1 * 2 .- 3 * 4", "{(:dotSubtract (:multiply 1 2) (:multiply 3 4))}") testParse("1 * 2 .- 3 * 4", "{(::dotSubtract (::multiply 1 2) (::multiply 3 4))}")
testParse("1 * 2 + 3 .* 4", "{(:add (:multiply 1 2) (:dotMultiply 3 4))}") testParse("1 * 2 + 3 .* 4", "{(::add (::multiply 1 2) (::dotMultiply 3 4))}")
testParse("1 * 2 + 3 / 4", "{(:add (:multiply 1 2) (:divide 3 4))}") testParse("1 * 2 + 3 / 4", "{(::add (::multiply 1 2) (::divide 3 4))}")
testParse("1 * 2 + 3 ./ 4", "{(:add (:multiply 1 2) (:dotDivide 3 4))}") testParse("1 * 2 + 3 ./ 4", "{(::add (::multiply 1 2) (::dotDivide 3 4))}")
testParse("1 * 2 - 3 .* 4", "{(:subtract (:multiply 1 2) (:dotMultiply 3 4))}") testParse("1 * 2 - 3 .* 4", "{(::subtract (::multiply 1 2) (::dotMultiply 3 4))}")
testParse("1 * 2 - 3 / 4", "{(:subtract (:multiply 1 2) (:divide 3 4))}") testParse("1 * 2 - 3 / 4", "{(::subtract (::multiply 1 2) (::divide 3 4))}")
testParse("1 * 2 - 3 ./ 4", "{(:subtract (:multiply 1 2) (:dotDivide 3 4))}") testParse("1 * 2 - 3 ./ 4", "{(::subtract (::multiply 1 2) (::dotDivide 3 4))}")
testParse("1 * 2 - 3 * 4^5", "{(:subtract (:multiply 1 2) (:multiply 3 (:pow 4 5)))}") testParse("1 * 2 - 3 * 4^5", "{(::subtract (::multiply 1 2) (::multiply 3 (::pow 4 5)))}")
testParse( testParse(
"1 * 2 - 3 * 4^5^6", "1 * 2 - 3 * 4^5^6",
"{(:subtract (:multiply 1 2) (:multiply 3 (:pow (:pow 4 5) 6)))}", "{(::subtract (::multiply 1 2) (::multiply 3 (::pow (::pow 4 5) 6)))}",
) )
testParse("1 * -a[-2]", "{(:multiply 1 (:unaryMinus (:$_atIndex_$ :a (:unaryMinus 2))))}") testParse("1 * -a[-2]", "{(::multiply 1 (::unaryMinus (::$_atIndex_$ :a (::unaryMinus 2))))}")
}) })
describe("multi-line", () => { describe("multi-line", () => {
@ -69,27 +70,27 @@ describe("Peggy parse", () => {
describe("functions", () => { describe("functions", () => {
testParse("identity(x) = x", "{:identity = {|:x| {:x}}}") // Function definitions become lambda assignments testParse("identity(x) = x", "{:identity = {|:x| {:x}}}") // Function definitions become lambda assignments
testParse("identity(x)", "{(:identity :x)}") testParse("identity(x)", "{(::identity :x)}")
}) })
describe("arrays", () => { describe("arrays", () => {
testParse("[]", "{[]}") testParse("[]", "{(::$_constructArray_$ ())}")
testParse("[0, 1, 2]", "{[0; 1; 2]}") testParse("[0, 1, 2]", "{(::$_constructArray_$ (0 1 2))}")
testParse("['hello', 'world']", "{['hello'; 'world']}") testParse("['hello', 'world']", "{(::$_constructArray_$ ('hello' 'world'))}")
testParse("([0,1,2])[1]", "{(:$_atIndex_$ [0; 1; 2] 1)}") testParse("([0,1,2])[1]", "{(::$_atIndex_$ (::$_constructArray_$ (0 1 2)) 1)}")
}) })
describe("records", () => { describe("records", () => {
testParse("{a: 1, b: 2}", "{{'a': 1, 'b': 2}}") testParse("{a: 1, b: 2}", "{(::$_constructRecord_$ ('a': 1 'b': 2))}")
testParse("{1+0: 1, 2+0: 2}", "{{(:add 1 0): 1, (:add 2 0): 2}}") // key can be any expression testParse("{1+0: 1, 2+0: 2}", "{(::$_constructRecord_$ ((::add 1 0): 1 (::add 2 0): 2))}") // key can be any expression
testParse("record.property", "{(:$_atIndex_$ :record 'property')}") testParse("record.property", "{(::$_atIndex_$ :record 'property')}")
}) })
describe("post operators", () => { describe("post operators", () => {
//function call, array and record access are post operators with higher priority than unary operators //function call, array and record access are post operators with higher priority than unary operators
testParse("a==!b(1)", "{(:equal :a (:not (:b 1)))}") testParse("a==!b(1)", "{(::equal :a (::not (::b 1)))}")
testParse("a==!b[1]", "{(:equal :a (:not (:$_atIndex_$ :b 1)))}") testParse("a==!b[1]", "{(::equal :a (::not (::$_atIndex_$ :b 1)))}")
testParse("a==!b.one", "{(:equal :a (:not (:$_atIndex_$ :b 'one')))}") testParse("a==!b.one", "{(::equal :a (::not (::$_atIndex_$ :b 'one')))}")
}) })
describe("comments", () => { describe("comments", () => {
@ -99,10 +100,10 @@ describe("Peggy parse", () => {
testParse("/* This is a multi line comment */ 1", "{1}") testParse("/* This is a multi line comment */ 1", "{1}")
testParse( testParse(
` `
/* This is /* This is
a multi line a multi line
comment */ comment */
1`, 1`,
"{1}", "{1}",
) )
}) })
@ -125,67 +126,70 @@ describe("Peggy parse", () => {
}) })
describe("logical", () => { describe("logical", () => {
testParse("true || false", "{(:or true false)}") testParse("true || false", "{(::or true false)}")
testParse("true && false", "{(:and true false)}") testParse("true && false", "{(::and true false)}")
testParse("a * b + c", "{(:add (:multiply :a :b) :c)}") // for comparison testParse("a * b + c", "{(::add (::multiply :a :b) :c)}") // for comparison
testParse("a && b || c", "{(:or (:and :a :b) :c)}") testParse("a && b || c", "{(::or (::and :a :b) :c)}")
testParse("a && b || c && d", "{(:or (:and :a :b) (:and :c :d))}") testParse("a && b || c && d", "{(::or (::and :a :b) (::and :c :d))}")
testParse("a && !b || c", "{(:or (:and :a (:not :b)) :c)}") testParse("a && !b || c", "{(::or (::and :a (::not :b)) :c)}")
testParse("a && b==c || d", "{(:or (:and :a (:equal :b :c)) :d)}") testParse("a && b==c || d", "{(::or (::and :a (::equal :b :c)) :d)}")
testParse("a && b!=c || d", "{(:or (:and :a (:unequal :b :c)) :d)}") testParse("a && b!=c || d", "{(::or (::and :a (::unequal :b :c)) :d)}")
testParse("a && !(b==c) || d", "{(:or (:and :a (:not (:equal :b :c))) :d)}") testParse("a && !(b==c) || d", "{(::or (::and :a (::not (::equal :b :c))) :d)}")
testParse("a && b>=c || d", "{(:or (:and :a (:largerEq :b :c)) :d)}") testParse("a && b>=c || d", "{(::or (::and :a (::largerEq :b :c)) :d)}")
testParse("a && !(b>=c) || d", "{(:or (:and :a (:not (:largerEq :b :c))) :d)}") testParse("a && !(b>=c) || d", "{(::or (::and :a (::not (::largerEq :b :c))) :d)}")
testParse("a && b<=c || d", "{(:or (:and :a (:smallerEq :b :c)) :d)}") testParse("a && b<=c || d", "{(::or (::and :a (::smallerEq :b :c)) :d)}")
testParse("a && b>c || d", "{(:or (:and :a (:larger :b :c)) :d)}") testParse("a && b>c || d", "{(::or (::and :a (::larger :b :c)) :d)}")
testParse("a && b<c || d", "{(:or (:and :a (:smaller :b :c)) :d)}") testParse("a && b<c || d", "{(::or (::and :a (::smaller :b :c)) :d)}")
testParse("a && b<c[i] || d", "{(:or (:and :a (:smaller :b (:$_atIndex_$ :c :i))) :d)}") testParse("a && b<c[i] || d", "{(::or (::and :a (::smaller :b (::$_atIndex_$ :c :i))) :d)}")
testParse("a && b<c.i || d", "{(:or (:and :a (:smaller :b (:$_atIndex_$ :c 'i'))) :d)}") testParse("a && b<c.i || d", "{(::or (::and :a (::smaller :b (::$_atIndex_$ :c 'i'))) :d)}")
testParse("a && b<c(i) || d", "{(:or (:and :a (:smaller :b (:c :i))) :d)}") testParse("a && b<c(i) || d", "{(::or (::and :a (::smaller :b (::c :i))) :d)}")
testParse("a && b<1+2 || d", "{(:or (:and :a (:smaller :b (:add 1 2))) :d)}") testParse("a && b<1+2 || d", "{(::or (::and :a (::smaller :b (::add 1 2))) :d)}")
testParse("a && b<1+2*3 || d", "{(:or (:and :a (:smaller :b (:add 1 (:multiply 2 3)))) :d)}") testParse(
"a && b<1+2*3 || d",
"{(::or (::and :a (::smaller :b (::add 1 (::multiply 2 3)))) :d)}",
)
testParse( testParse(
"a && b<1+2*-3+4 || d", "a && b<1+2*-3+4 || d",
"{(:or (:and :a (:smaller :b (:add (:add 1 (:multiply 2 (:unaryMinus 3))) 4))) :d)}", "{(::or (::and :a (::smaller :b (::add (::add 1 (::multiply 2 (::unaryMinus 3))) 4))) :d)}",
) )
testParse( testParse(
"a && b<1+2*3 || d ? true : false", "a && b<1+2*3 || d ? true : false",
"{(::$$_ternary_$$ (:or (:and :a (:smaller :b (:add 1 (:multiply 2 3)))) :d) true false)}", "{(::$$_ternary_$$ (::or (::and :a (::smaller :b (::add 1 (::multiply 2 3)))) :d) true false)}",
) )
}) })
describe("pipe", () => { describe("pipe", () => {
testParse("1 -> add(2)", "{(:add 1 2)}") testParse("1 -> add(2)", "{(::add 1 2)}")
testParse("-1 -> add(2)", "{(:add (:unaryMinus 1) 2)}") testParse("-1 -> add(2)", "{(::add (::unaryMinus 1) 2)}")
testParse("-a[1] -> add(2)", "{(:add (:unaryMinus (:$_atIndex_$ :a 1)) 2)}") testParse("-a[1] -> add(2)", "{(::add (::unaryMinus (::$_atIndex_$ :a 1)) 2)}")
testParse("-f(1) -> add(2)", "{(:add (:unaryMinus (:f 1)) 2)}") testParse("-f(1) -> add(2)", "{(::add (::unaryMinus (::f 1)) 2)}")
testParse("1 + 2 -> add(3)", "{(:add 1 (:add 2 3))}") testParse("1 + 2 -> add(3)", "{(::add 1 (::add 2 3))}")
testParse("1 -> add(2) * 3", "{(:multiply (:add 1 2) 3)}") testParse("1 -> add(2) * 3", "{(::multiply (::add 1 2) 3)}")
testParse("1 -> subtract(2)", "{(:subtract 1 2)}") testParse("1 -> subtract(2)", "{(::subtract 1 2)}")
testParse("-1 -> subtract(2)", "{(:subtract (:unaryMinus 1) 2)}") testParse("-1 -> subtract(2)", "{(::subtract (::unaryMinus 1) 2)}")
testParse("1 -> subtract(2) * 3", "{(:multiply (:subtract 1 2) 3)}") testParse("1 -> subtract(2) * 3", "{(::multiply (::subtract 1 2) 3)}")
}) })
describe("elixir pipe", () => { describe("elixir pipe", () => {
//handled together with -> so there is no need for seperate tests //handled together with -> so there is no need for seperate tests
testParse("1 |> add(2)", "{(:add 1 2)}") testParse("1 |> add(2)", "{(::add 1 2)}")
}) })
describe("to", () => { describe("to", () => {
testParse("1 to 2", "{(:credibleIntervalToDistribution 1 2)}") testParse("1 to 2", "{(::credibleIntervalToDistribution 1 2)}")
testParse("-1 to -2", "{(:credibleIntervalToDistribution (:unaryMinus 1) (:unaryMinus 2))}") // lower than unary testParse("-1 to -2", "{(::credibleIntervalToDistribution (::unaryMinus 1) (::unaryMinus 2))}") // lower than unary
testParse( testParse(
"a[1] to a[2]", "a[1] to a[2]",
"{(:credibleIntervalToDistribution (:$_atIndex_$ :a 1) (:$_atIndex_$ :a 2))}", "{(::credibleIntervalToDistribution (::$_atIndex_$ :a 1) (::$_atIndex_$ :a 2))}",
) // lower than post ) // lower than post
testParse( testParse(
"a.p1 to a.p2", "a.p1 to a.p2",
"{(:credibleIntervalToDistribution (:$_atIndex_$ :a 'p1') (:$_atIndex_$ :a 'p2'))}", "{(::credibleIntervalToDistribution (::$_atIndex_$ :a 'p1') (::$_atIndex_$ :a 'p2'))}",
) // lower than post ) // lower than post
testParse("1 to 2 + 3", "{(:credibleIntervalToDistribution 1 (:add 2 3))}") testParse("1 to 2 + 3", "{(::add (::credibleIntervalToDistribution 1 2) 3)}") // higher than binary operators
testParse( testParse(
"1->add(2) to 3->add(4) -> add(4)", "1->add(2) to 3->add(4) -> add(4)",
"{(:credibleIntervalToDistribution (:add 1 2) (:add (:add 3 4) 4))}", "{(::credibleIntervalToDistribution (::add 1 2) (::add (::add 3 4) 4))}",
) // lower than chain ) // lower than chain
}) })
@ -196,8 +200,8 @@ describe("Peggy parse", () => {
}) })
describe("lambda", () => { describe("lambda", () => {
testParse("{|x| x}", "{{|:x| :x}}") testParse("{|x| x}", "{{|:x| {:x}}}")
testParse("f={|x| x}", "{:f = {|:x| :x}}") testParse("f={|x| x}", "{:f = {{|:x| {:x}}}}")
testParse("f(x)=x", "{:f = {|:x| {:x}}}") // Function definitions are lambda assignments testParse("f(x)=x", "{:f = {|:x| {:x}}}") // Function definitions are lambda assignments
testParse("f(x)=x ? 1 : 0", "{:f = {|:x| {(::$$_ternary_$$ :x 1 0)}}}") // Function definitions are lambda assignments testParse("f(x)=x ? 1 : 0", "{:f = {|:x| {(::$$_ternary_$$ :x 1 0)}}}") // Function definitions are lambda assignments
}) })
@ -205,25 +209,31 @@ describe("Peggy parse", () => {
describe("Using lambda as value", () => { describe("Using lambda as value", () => {
testParse( testParse(
"myadd(x,y)=x+y; z=myadd; z", "myadd(x,y)=x+y; z=myadd; z",
"{:myadd = {|:x,:y| {(:add :x :y)}}; :z = {:myadd}; :z}", "{:myadd = {|:x,:y| {(::add :x :y)}}; :z = {:myadd}; :z}",
) )
testParse( testParse(
"myadd(x,y)=x+y; z=[myadd]; z", "myadd(x,y)=x+y; z=[myadd]; z",
"{:myadd = {|:x,:y| {(:add :x :y)}}; :z = {[:myadd]}; :z}", "{:myadd = {|:x,:y| {(::add :x :y)}}; :z = {(::$_constructArray_$ (:myadd))}; :z}",
) )
testParse( testParse(
"myaddd(x,y)=x+y; z={x: myaddd}; z", "myaddd(x,y)=x+y; z={x: myaddd}; z",
"{:myaddd = {|:x,:y| {(:add :x :y)}}; :z = {{'x': :myaddd}}; :z}", "{:myaddd = {|:x,:y| {(::add :x :y)}}; :z = {(::$_constructRecord_$ ('x': :myaddd))}; :z}",
)
testParse("f({|x| x+1})", "{(::f {|:x| {(::add :x 1)}})}")
testParse("map(arr, {|x| x+1})", "{(::map :arr {|:x| {(::add :x 1)}})}")
testParse(
"map([1,2,3], {|x| x+1})",
"{(::map (::$_constructArray_$ (1 2 3)) {|:x| {(::add :x 1)}})}",
)
testParse(
"[1,2,3]->map({|x| x+1})",
"{(::map (::$_constructArray_$ (1 2 3)) {|:x| {(::add :x 1)}})}",
) )
testParse("f({|x| x+1})", "{(:f {|:x| (:add :x 1)})}")
testParse("map(arr, {|x| x+1})", "{(:map :arr {|:x| (:add :x 1)})}")
testParse("map([1,2,3], {|x| x+1})", "{(:map [1; 2; 3] {|:x| (:add :x 1)})}")
testParse("[1,2,3]->map({|x| x+1})", "{(:map [1; 2; 3] {|:x| (:add :x 1)})}")
}) })
describe("unit", () => { describe("unit", () => {
testParse("1m", "{(:fromUnit_m 1)}") testParse("1m", "{(::fromUnit_m 1)}")
testParse("1M", "{(:fromUnit_M 1)}") testParse("1M", "{(::fromUnit_M 1)}")
testParse("1m+2cm", "{(:add (:fromUnit_m 1) (:fromUnit_cm 2))}") testParse("1m+2cm", "{(::add (::fromUnit_m 1) (::fromUnit_cm 2))}")
}) })
describe("Module", () => { describe("Module", () => {
testParse("x", "{:x}") testParse("x", "{:x}")
@ -234,118 +244,118 @@ describe("Peggy parse", () => {
describe("parsing new line", () => { describe("parsing new line", () => {
testParse( testParse(
` `
a + a +
b`, b`,
"{(:add :a :b)}", "{(::add :a :b)}",
) )
testParse( testParse(
` `
x= x=
1`, 1`,
"{:x = {1}}", "{:x = {1}}",
) )
testParse( testParse(
` `
x=1 x=1
y=2`, y=2`,
"{:x = {1}; :y = {2}}", "{:x = {1}; :y = {2}}",
) )
testParse( testParse(
` `
x={ x={
y=2; y=2;
y } y }
x`, x`,
"{:x = {:y = {2}; :y}; :x}", "{:x = {:y = {2}; :y}; :x}",
) )
testParse( testParse(
` `
x={ x={
y=2
y }
x`,
"{:x = {:y = {2}; :y}; :x}",
)
testParse(
`
x={
y=2
y
}
x`,
"{:x = {:y = {2}; :y}; :x}",
)
testParse(
`
x=1
y=2 y=2
y } z=3
x`, `,
"{:x = {:y = {2}; :y}; :x}",
)
testParse(
`
x={
y=2
y
}
x`,
"{:x = {:y = {2}; :y}; :x}",
)
testParse(
`
x=1
y=2
z=3
`,
"{:x = {1}; :y = {2}; :z = {3}}", "{:x = {1}; :y = {2}; :z = {3}}",
) )
testParse( testParse(
` `
f={ f={
x=1 x=1
y=2 y=2
z=3 z=3
x+y+z x+y+z
}
`,
"{:f = {:x = {1}; :y = {2}; :z = {3}; (:add (:add :x :y) :z)}}",
)
testParse(
`
f={
x=1
y=2
z=3
x+y+z
}
g=f+4
g
`,
"{:f = {:x = {1}; :y = {2}; :z = {3}; (:add (:add :x :y) :z)}; :g = {(:add :f 4)}; :g}",
)
testParse(
`
f =
{
x=1; //x
y=2 //y
z=
3
x+
y+
z
} }
g = `,
f + "{:f = {:x = {1}; :y = {2}; :z = {3}; (::add (::add :x :y) :z)}}",
4
g ->
h ->
p ->
q
`,
"{:f = {:x = {1}; :y = {2}; :z = {3}; (:add (:add :x :y) :z)}; :g = {(:add :f 4)}; (:q (:p (:h :g)))}",
) )
testParse( testParse(
` `
a |> f={
b |> x=1
c |> y=2
d z=3
`, x+y+z
"{(:d (:c (:b :a)))}", }
g=f+4
g
`,
"{:f = {:x = {1}; :y = {2}; :z = {3}; (::add (::add :x :y) :z)}; :g = {(::add :f 4)}; :g}",
) )
testParse( testParse(
` `
a |> f =
b |> {
c |> x=1; //x
d + y=2 //y
e z=
`, 3
"{(:add (:d (:c (:b :a))) :e)}", x+
y+
z
}
g =
f +
4
g ->
h ->
p ->
q
`,
"{:f = {:x = {1}; :y = {2}; :z = {3}; (::add (::add :x :y) :z)}; :g = {(::add :f 4)}; (::q (::p (::h :g)))}",
)
testParse(
`
a |>
b |>
c |>
d
`,
"{(::d (::c (::b :a)))}",
)
testParse(
`
a |>
b |>
c |>
d +
e
`,
"{(::add (::d (::c (::b :a))) :e)}",
) )
}) })

View File

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

View File

@ -1,5 +1,6 @@
module Expression = Reducer_Expression module Expression = Reducer_Expression
module ExpressionT = Reducer_Expression_T module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface.InternalExpressionValue
module Parse = Reducer_Peggy_Parse module Parse = Reducer_Peggy_Parse
module Result = Belt.Result module Result = Belt.Result
module ToExpression = Reducer_Peggy_ToExpression module ToExpression = Reducer_Peggy_ToExpression
@ -9,12 +10,12 @@ open Jest
open Expect open Expect
let expectParseToBe = (expr, answer) => let expectParseToBe = (expr, answer) =>
Parse.parse(expr, "test")->Parse.toStringResult->expect->toBe(answer) Parse.parse(expr)->Parse.toStringResult->expect->toBe(answer)
let testParse = (expr, answer) => test(expr, () => expectParseToBe(expr, answer)) let testParse = (expr, answer) => test(expr, () => expectParseToBe(expr, answer))
let expectExpressionToBe = (expr, answer, ~v="_", ()) => { let expectToExpressionToBe = (expr, answer, ~v="_", ()) => {
let rExpr = Parse.parse(expr, "test")->Result.map(ToExpression.fromNode) let rExpr = Parse.parse(expr)->Result.map(ToExpression.fromNode)
let a1 = rExpr->ExpressionT.toStringResultOkless let a1 = rExpr->ExpressionT.toStringResultOkless
if v == "_" { if v == "_" {
@ -22,24 +23,30 @@ let expectExpressionToBe = (expr, answer, ~v="_", ()) => {
} else { } else {
let a2 = let a2 =
rExpr rExpr
->E.R2.errMap(e => e->SqError.fromParseError) ->Result.flatMap(expr =>
->Result.flatMap(expr => Expression.BackCompatible.evaluate(expr)) Expression.reduceExpression(
->Reducer_Value.toStringResultOkless expr,
ReducerInterface_StdLib.internalStdLib,
ExpressionValue.defaultEnvironment,
)
)
->Reducer_Helpers.rRemoveDefaultsInternal
->ExpressionValue.toStringResultOkless
(a1, a2)->expect->toEqual((answer, v)) (a1, a2)->expect->toEqual((answer, v))
} }
} }
let testToExpression = (expr, answer, ~v="_", ()) => let testToExpression = (expr, answer, ~v="_", ()) =>
test(expr, () => expectExpressionToBe(expr, answer, ~v, ())) test(expr, () => expectToExpressionToBe(expr, answer, ~v, ()))
module MyOnly = { module MyOnly = {
let testParse = (expr, answer) => Only.test(expr, () => expectParseToBe(expr, answer)) let testParse = (expr, answer) => Only.test(expr, () => expectParseToBe(expr, answer))
let testToExpression = (expr, answer, ~v="_", ()) => let testToExpression = (expr, answer, ~v="_", ()) =>
Only.test(expr, () => expectExpressionToBe(expr, answer, ~v, ())) Only.test(expr, () => expectToExpressionToBe(expr, answer, ~v, ()))
} }
module MySkip = { module MySkip = {
let testParse = (expr, answer) => Skip.test(expr, () => expectParseToBe(expr, answer)) let testParse = (expr, answer) => Skip.test(expr, () => expectParseToBe(expr, answer))
let testToExpression = (expr, answer, ~v="_", ()) => let testToExpression = (expr, answer, ~v="_", ()) =>
Skip.test(expr, () => expectExpressionToBe(expr, answer, ~v, ())) Skip.test(expr, () => expectToExpressionToBe(expr, answer, ~v, ()))
} }

View File

@ -1,12 +0,0 @@
open Jest
open Reducer_Peggy_TestHelpers
// Note: these tests aren't useful anymore since outer block macro got deleted.
// Probably can be removed or folded into other Peggy tests.
describe("Peggy Outer Block", () => {
testToExpression("1", "1", ~v="1", ())
testToExpression("x=1", "x = {1}", ~v="()", ())
testToExpression("x=1; y=2", "x = {1}; y = {2}", ~v="()", ())
testToExpression("x=1; 2", "x = {1}; 2", ~v="2", ())
testToExpression("x={a=1; a}; x", "x = {a = {1}; a}; x", ~v="1", ())
})

View File

@ -1,4 +1,5 @@
module Bindings = Reducer_Bindings module Bindings = Reducer_Bindings
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
open Jest open Jest
open Reducer_Peggy_TestHelpers open Reducer_Peggy_TestHelpers
@ -6,121 +7,147 @@ open Reducer_Peggy_TestHelpers
describe("Peggy to Expression", () => { describe("Peggy to Expression", () => {
describe("literals operators parenthesis", () => { describe("literals operators parenthesis", () => {
// Note that there is always an outer block. Otherwise, external bindings are ignored 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", ())
testToExpression("1+2", "(add)(1, 2)", ~v="3", ()) testToExpression("1+2", "{(:add 1 2)}", ~v="3", ())
testToExpression("add(1,2)", "(add)(1, 2)", ~v="3", ()) testToExpression("add(1,2)", "{(:add 1 2)}", ~v="3", ())
testToExpression("(1)", "1", ()) testToExpression("(1)", "{1}", ())
testToExpression("(1+2)", "(add)(1, 2)", ()) testToExpression("(1+2)", "{(:add 1 2)}", ())
}) })
describe("unary", () => { describe("unary", () => {
testToExpression("-1", "(unaryMinus)(1)", ~v="-1", ()) testToExpression("-1", "{(:unaryMinus 1)}", ~v="-1", ())
testToExpression("!true", "(not)(true)", ~v="false", ()) testToExpression("!true", "{(:not true)}", ~v="false", ())
testToExpression("1 + -1", "(add)(1, (unaryMinus)(1))", ~v="0", ()) testToExpression("1 + -1", "{(:add 1 (:unaryMinus 1))}", ~v="0", ())
testToExpression("-a[0]", "(unaryMinus)(($_atIndex_$)(a, 0))", ()) testToExpression("-a[0]", "{(:unaryMinus (:$_atIndex_$ :a 0))}", ())
}) })
describe("multi-line", () => { describe("multi-line", () => {
testToExpression("x=1; 2", "x = {1}; 2", ~v="2", ()) testToExpression("x=1; 2", "{(:$_let_$ :x {1}); 2}", ~v="2", ())
testToExpression("x=1; y=2", "x = {1}; y = {2}", ()) testToExpression("x=1; y=2", "{(:$_let_$ :x {1}); (:$_let_$ :y {2})}", ~v="@{x: 1,y: 2}", ())
}) })
describe("variables", () => { describe("variables", () => {
testToExpression("x = 1", "x = {1}", ()) testToExpression("x = 1", "{(:$_let_$ :x {1})}", ~v="@{x: 1}", ())
testToExpression("x", "x", ~v="Error(x is not defined)", ()) //TODO: value should return error testToExpression("x", "{:x}", ~v=":x", ()) //TODO: value should return error
testToExpression("x = 1; x", "x = {1}; x", ~v="1", ()) testToExpression("x = 1; x", "{(:$_let_$ :x {1}); :x}", ~v="1", ())
}) })
describe("functions", () => { describe("functions", () => {
testToExpression("identity(x) = x", "identity = {|x| {x}}", ()) // Function definitions become lambda assignments testToExpression(
testToExpression("identity(x)", "(identity)(x)", ()) // Note value returns error properly "identity(x) = x",
"{(:$_let_$ :identity (:$$_lambda_$$ [x] {:x}))}",
~v="@{identity: lambda(x=>internal code)}",
(),
) // Function definitions become lambda assignments
testToExpression("identity(x)", "{(:identity :x)}", ()) // Note value returns error properly
testToExpression( testToExpression(
"f(x) = x> 2 ? 0 : 1; f(3)", "f(x) = x> 2 ? 0 : 1; f(3)",
"f = {|x| {(larger)(x, 2) ? (0) : (1)}}; (f)(3)", "{(:$_let_$ :f (:$$_lambda_$$ [x] {(:$$_ternary_$$ (:larger :x 2) 0 1)})); (:f 3)}",
~v="0", ~v="0",
(), (),
) )
}) })
describe("arrays", () => { describe("arrays", () => {
testToExpression("[]", "[]", ~v="[]", ()) testToExpression("[]", "{(:$_constructArray_$ ())}", ~v="[]", ())
testToExpression("[0, 1, 2]", "[0, 1, 2]", ~v="[0,1,2]", ()) testToExpression("[0, 1, 2]", "{(:$_constructArray_$ (0 1 2))}", ~v="[0,1,2]", ())
testToExpression("['hello', 'world']", "['hello', 'world']", ~v="['hello','world']", ()) testToExpression(
testToExpression("([0,1,2])[1]", "($_atIndex_$)([0, 1, 2], 1)", ~v="1", ()) "['hello', 'world']",
"{(:$_constructArray_$ ('hello' 'world'))}",
~v="['hello','world']",
(),
)
testToExpression("([0,1,2])[1]", "{(:$_atIndex_$ (:$_constructArray_$ (0 1 2)) 1)}", ~v="1", ())
}) })
describe("records", () => { describe("records", () => {
testToExpression("{a: 1, b: 2}", "{'a': 1, 'b': 2}", ~v="{a: 1,b: 2}", ()) testToExpression(
testToExpression("{1+0: 1, 2+0: 2}", "{(add)(1, 0): 1, (add)(2, 0): 2}", ()) // key can be any expression "{a: 1, b: 2}",
testToExpression("record.property", "($_atIndex_$)(record, 'property')", ()) "{(:$_constructRecord_$ (('a' 1) ('b' 2)))}",
~v="{a: 1,b: 2}",
(),
)
testToExpression(
"{1+0: 1, 2+0: 2}",
"{(:$_constructRecord_$ (((:add 1 0) 1) ((:add 2 0) 2)))}",
(),
) // key can be any expression
testToExpression("record.property", "{(:$_atIndex_$ :record 'property')}", ())
testToExpression( testToExpression(
"record={property: 1}; record.property", "record={property: 1}; record.property",
"record = {{'property': 1}}; ($_atIndex_$)(record, 'property')", "{(:$_let_$ :record {(:$_constructRecord_$ (('property' 1)))}); (:$_atIndex_$ :record 'property')}",
~v="1", ~v="1",
(), (),
) )
}) })
describe("comments", () => { describe("comments", () => {
testToExpression("1 # This is a line comment", "1", ~v="1", ()) testToExpression("1 # This is a line comment", "{1}", ~v="1", ())
testToExpression("1 // This is a line comment", "1", ~v="1", ()) testToExpression("1 // This is a line comment", "{1}", ~v="1", ())
testToExpression("1 /* This is a multi line comment */", "1", ~v="1", ()) testToExpression("1 /* This is a multi line comment */", "{1}", ~v="1", ())
testToExpression("/* This is a multi line comment */ 1", "1", ~v="1", ()) testToExpression("/* This is a multi line comment */ 1", "{1}", ~v="1", ())
}) })
describe("ternary operator", () => { describe("ternary operator", () => {
testToExpression("true ? 1 : 0", "true ? (1) : (0)", ~v="1", ()) testToExpression("true ? 1 : 0", "{(:$$_ternary_$$ true 1 0)}", ~v="1", ())
testToExpression("false ? 1 : 0", "false ? (1) : (0)", ~v="0", ()) testToExpression("false ? 1 : 0", "{(:$$_ternary_$$ false 1 0)}", ~v="0", ())
testToExpression("true ? 1 : false ? 2 : 0", "true ? (1) : (false ? (2) : (0))", ~v="1", ()) // nested ternary testToExpression(
testToExpression("false ? 1 : false ? 2 : 0", "false ? (1) : (false ? (2) : (0))", ~v="0", ()) // nested ternary "true ? 1 : false ? 2 : 0",
describe( "{(:$$_ternary_$$ true 1 (:$$_ternary_$$ false 2 0))}",
"ternary bindings", ~v="1",
() => { (),
testToExpression( ) // nested ternary
// expression binding testToExpression(
"f(a) = a > 5 ? 1 : 0; f(6)", "false ? 1 : false ? 2 : 0",
"f = {|a| {(larger)(a, 5) ? (1) : (0)}}; (f)(6)", "{(:$$_ternary_$$ false 1 (:$$_ternary_$$ false 2 0))}",
~v="1", ~v="0",
(), (),
) ) // nested ternary
testToExpression( describe("ternary bindings", () => {
// when true binding testToExpression(
"f(a) = a > 5 ? a : 0; f(6)", // expression binding
"f = {|a| {(larger)(a, 5) ? (a) : (0)}}; (f)(6)", "f(a) = a > 5 ? 1 : 0; f(6)",
~v="6", "{(:$_let_$ :f (:$$_lambda_$$ [a] {(:$$_ternary_$$ (:larger :a 5) 1 0)})); (:f 6)}",
(), ~v="1",
) (),
testToExpression( )
// when false binding testToExpression(
"f(a) = a < 5 ? 1 : a; f(6)", // when true binding
"f = {|a| {(smaller)(a, 5) ? (1) : (a)}}; (f)(6)", "f(a) = a > 5 ? a : 0; f(6)",
~v="6", "{(:$_let_$ :f (:$$_lambda_$$ [a] {(:$$_ternary_$$ (:larger :a 5) :a 0)})); (:f 6)}",
(), ~v="6",
) (),
}, )
) testToExpression(
// when false binding
"f(a) = a < 5 ? 1 : a; f(6)",
"{(:$_let_$ :f (:$$_lambda_$$ [a] {(:$$_ternary_$$ (:smaller :a 5) 1 :a)})); (:f 6)}",
~v="6",
(),
)
})
}) })
describe("if then else", () => { describe("if then else", () => {
testToExpression("if true then 2 else 3", "true ? ({2}) : ({3})", ()) testToExpression("if true then 2 else 3", "{(:$$_ternary_$$ true {2} {3})}", ())
testToExpression("if true then {2} else {3}", "true ? ({2}) : ({3})", ()) testToExpression("if true then {2} else {3}", "{(:$$_ternary_$$ true {2} {3})}", ())
testToExpression( testToExpression(
"if false then {2} else if false then {4} else {5}", "if false then {2} else if false then {4} else {5}",
"false ? ({2}) : (false ? ({4}) : ({5}))", "{(:$$_ternary_$$ false {2} (:$$_ternary_$$ false {4} {5}))}",
(), (),
) //nested if ) //nested if
}) })
describe("pipe", () => { describe("pipe", () => {
testToExpression("1 -> add(2)", "(add)(1, 2)", ~v="3", ()) testToExpression("1 -> add(2)", "{(:add 1 2)}", ~v="3", ())
testToExpression("-1 -> add(2)", "(add)((unaryMinus)(1), 2)", ~v="1", ()) // note that unary has higher priority naturally testToExpression("-1 -> add(2)", "{(:add (:unaryMinus 1) 2)}", ~v="1", ()) // note that unary has higher priority naturally
testToExpression("1 -> add(2) * 3", "(multiply)((add)(1, 2), 3)", ~v="9", ()) testToExpression("1 -> add(2) * 3", "{(:multiply (:add 1 2) 3)}", ~v="9", ())
}) })
describe("elixir pipe", () => { describe("elixir pipe", () => {
testToExpression("1 |> add(2)", "(add)(1, 2)", ~v="3", ()) testToExpression("1 |> add(2)", "{(:add 1 2)}", ~v="3", ())
}) })
// see testParse for priorities of to and credibleIntervalToDistribution // see testParse for priorities of to and credibleIntervalToDistribution
@ -130,28 +157,43 @@ describe("Peggy to Expression", () => {
// Like lambdas they have a local scope. // Like lambdas they have a local scope.
testToExpression( testToExpression(
"y=99; x={y=1; y}", "y=99; x={y=1; y}",
"y = {99}; x = {y = {1}; y}", "{(:$_let_$ :y {99}); (:$_let_$ :x {(:$_let_$ :y {1}); :y})}",
// "{(:$_let_$ :y {99}); (:$_let_$ :x {(:$_let_$ :y {1}); :y}); (:$_endOfOuterBlock_$ () ())}", ~v="@{x: 1,y: 99}",
(), (),
) )
}) })
describe("lambda", () => { describe("lambda", () => {
testToExpression("{|x| x}", "{|x| x}", ~v="lambda(x=>internal code)", ()) testToExpression("{|x| x}", "{(:$$_lambda_$$ [x] {:x})}", ~v="lambda(x=>internal code)", ())
testToExpression("f={|x| x}", "f = {|x| x}", ()) testToExpression(
testToExpression("f(x)=x", "f = {|x| {x}}", ()) // Function definitions are lambda assignments "f={|x| x}",
testToExpression("f(x)=x ? 1 : 0", "f = {|x| {x ? (1) : (0)}}", ()) "{(:$_let_$ :f {(:$$_lambda_$$ [x] {:x})})}",
~v="@{f: lambda(x=>internal code)}",
(),
)
testToExpression(
"f(x)=x",
"{(:$_let_$ :f (:$$_lambda_$$ [x] {:x}))}",
~v="@{f: lambda(x=>internal code)}",
(),
) // Function definitions are lambda assignments
testToExpression(
"f(x)=x ? 1 : 0",
"{(:$_let_$ :f (:$$_lambda_$$ [x] {(:$$_ternary_$$ :x 1 0)}))}",
~v="@{f: lambda(x=>internal code)}",
(),
)
}) })
describe("module", () => { describe("module", () => {
// testToExpression("Math.pi", "{:Math.pi}", ~v="3.141592653589793", ()) // testToExpression("Math.pi", "{:Math.pi}", ~v="3.141592653589793", ())
// Only.test("stdlibrary", () => { // Only.test("stdlibrary", () => {
// SquiggleLibrary_StdLib.stdLib // ReducerInterface_StdLib.internalStdLib
// ->IEvBindings // ->IEvBindings
// ->Reducer_Value.toString // ->InternalExpressionValue.toString
// ->expect // ->expect
// ->toBe("") // ->toBe("")
// }) // })
testToExpression("Math.pi", "Math.pi", ~v="3.141592653589793", ()) testToExpression("Math.pi", "{:Math.pi}", ~v="3.141592653589793", ())
}) })
}) })

View File

@ -5,92 +5,92 @@ describe("Peggy Types to Expression", () => {
describe("type of", () => { describe("type of", () => {
testToExpression( testToExpression(
"p: number", "p: number",
"{(:$_typeOf_$ :p #number); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :p #number)}",
// ~v="@{_typeReferences_: {p: #number}}", ~v="@{_typeReferences_: {p: #number}}",
(), (),
) )
}) })
describe("type alias", () => { describe("type alias", () => {
testToExpression( testToExpression(
"type index=number", "type index=number",
"{(:$_typeAlias_$ #index #number); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeAlias_$ #index #number)}",
// ~v="@{_typeAliases_: {index: #number}}", ~v="@{_typeAliases_: {index: #number}}",
(), (),
) )
}) })
describe("type or", () => { describe("type or", () => {
testToExpression( testToExpression(
"answer: number|string|distribution", "answer: number|string|distribution",
"{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ #number #string #distribution))); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ (#number #string #distribution))))}",
// ~v="@{_typeReferences_: {answer: {typeOr: [#number,#string,#distribution],typeTag: 'typeOr'}}}", ~v="@{_typeReferences_: {answer: {typeOr: [#number,#string,#distribution],typeTag: 'typeOr'}}}",
(), (),
) )
}) })
describe("type function", () => { describe("type function", () => {
testToExpression( testToExpression(
"f: number=>number=>number", "f: number=>number=>number",
"{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ #number #number #number))); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ (#number #number #number))))}",
// ~v="@{_typeReferences_: {f: {inputs: [#number,#number],output: #number,typeTag: 'typeFunction'}}}", ~v="@{_typeReferences_: {f: {inputs: [#number,#number],output: #number,typeTag: 'typeFunction'}}}",
(), (),
) )
testToExpression( testToExpression(
"f: number=>number", "f: number=>number",
"{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ #number #number))); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ (#number #number))))}",
// ~v="@{_typeReferences_: {f: {inputs: [#number],output: #number,typeTag: 'typeFunction'}}}", ~v="@{_typeReferences_: {f: {inputs: [#number],output: #number,typeTag: 'typeFunction'}}}",
(), (),
) )
}) })
describe("high priority contract", () => { 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))); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ ((:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 100) #string))))}",
// ~v="@{_typeReferences_: {answer: {typeOr: [{max: 100,min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'},#string],typeTag: 'typeOr'}}}", ~v="@{_typeReferences_: {answer: {typeOr: [{max: 100,min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'},#string],typeTag: 'typeOr'}}}",
(), (),
) )
testToExpression( testToExpression(
"answer: number<-memberOf([1,3,5])", "answer: number<-memberOf([1,3,5])",
"{(:$_typeOf_$ :answer (:$_typeModifier_memberOf_$ #number (:$_constructArray_$ 1 3 5))); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :answer (:$_typeModifier_memberOf_$ #number (:$_constructArray_$ (1 3 5))))}",
// ~v="@{_typeReferences_: {answer: {memberOf: [1,3,5],typeIdentifier: #number,typeTag: 'typeIdentifier'}}}", ~v="@{_typeReferences_: {answer: {memberOf: [1,3,5],typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
(), (),
) )
testToExpression( testToExpression(
"answer: number<-min(1)", "answer: number<-min(1)",
"{(:$_typeOf_$ :answer (:$_typeModifier_min_$ #number 1)); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :answer (:$_typeModifier_min_$ #number 1))}",
// ~v="@{_typeReferences_: {answer: {min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}", ~v="@{_typeReferences_: {answer: {min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
(), (),
) )
testToExpression( testToExpression(
"answer: number<-max(10)", "answer: number<-max(10)",
"{(:$_typeOf_$ :answer (:$_typeModifier_max_$ #number 10)); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :answer (:$_typeModifier_max_$ #number 10))}",
// ~v="@{_typeReferences_: {answer: {max: 10,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}", ~v="@{_typeReferences_: {answer: {max: 10,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
(), (),
) )
testToExpression( testToExpression(
"answer: number<-min(1)<-max(10)", "answer: number<-min(1)<-max(10)",
"{(:$_typeOf_$ :answer (:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 10)); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :answer (:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 10))}",
// ~v="@{_typeReferences_: {answer: {max: 10,min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}", ~v="@{_typeReferences_: {answer: {max: 10,min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
(), (),
) )
testToExpression( testToExpression(
"answer: number<-max(10)<-min(1)", "answer: number<-max(10)<-min(1)",
"{(:$_typeOf_$ :answer (:$_typeModifier_min_$ (:$_typeModifier_max_$ #number 10) 1)); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :answer (:$_typeModifier_min_$ (:$_typeModifier_max_$ #number 10) 1))}",
// ~v="@{_typeReferences_: {answer: {max: 10,min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}", ~v="@{_typeReferences_: {answer: {max: 10,min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
(), (),
) )
}) })
describe("low priority contract", () => { describe("low priority contract", () => {
testToExpression( testToExpression(
"answer: number | string $ opaque", "answer: number | string $ opaque",
"{(:$_typeOf_$ :answer (:$_typeModifier_opaque_$ (:$_typeOr_$ (:$_constructArray_$ #number #string)))); (:$_endOfOuterBlock_$ () ())}", "{(:$_typeOf_$ :answer (:$_typeModifier_opaque_$ (:$_typeOr_$ (:$_constructArray_$ (#number #string)))))}",
// ~v="@{_typeReferences_: {answer: {opaque: true,typeOr: [#number,#string],typeTag: 'typeOr'}}}", ~v="@{_typeReferences_: {answer: {opaque: true,typeOr: [#number,#string],typeTag: 'typeOr'}}}",
(), (),
) )
}) })
describe("squiggle expressions in type contracts", () => { 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))); (:$_endOfOuterBlock_$ () ())}", "{(:$_let_$ :odds1 {(:$_constructArray_$ (1 3 5))}); (:$_let_$ :odds2 {(:$_constructArray_$ (7 9))}); (:$_typeAlias_$ #odds (:$_typeModifier_memberOf_$ #number (:concat :odds1 :odds2)))}",
// ~v="@{_typeAliases_: {odds: {memberOf: [1,3,5,7,9],typeIdentifier: #number,typeTag: 'typeIdentifier'}},odds1: [1,3,5],odds2: [7,9]}", ~v="@{_typeAliases_: {odds: {memberOf: [1,3,5,7,9],typeIdentifier: #number,typeTag: 'typeIdentifier'}},odds1: [1,3,5],odds2: [7,9]}",
(), (),
) )
}) })

View File

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

View File

@ -1,5 +1,6 @@
module Expression = Reducer_Expression
module ExpressionT = Reducer_Expression_T module ExpressionT = Reducer_Expression_T
module ExternalExpressionValue = ReducerInterface.ExternalExpressionValue
module ErrorValue = Reducer_ErrorValue
open Jest open Jest
open Expect open Expect
@ -7,22 +8,30 @@ open Expect
let unwrapRecord = rValue => let unwrapRecord = rValue =>
rValue->Belt.Result.flatMap(value => rValue->Belt.Result.flatMap(value =>
switch value { switch value {
| Reducer_T.IEvRecord(aRecord) => Ok(aRecord) | ExternalExpressionValue.EvRecord(aRecord) => Ok(aRecord)
| _ => SqError.Message.RETodo("TODO: Internal bindings must be returned")->Error | _ => ErrorValue.RETodo("TODO: External bindings must be returned")->Error
} }
) )
let expectParseToBe = (code: string, answer: string) => let expectParseToBe = (expr: string, answer: string) =>
Expression.BackCompatible.parse(code)->ExpressionT.toStringResult->expect->toBe(answer) Reducer.parse(expr)->ExpressionT.toStringResult->expect->toBe(answer)
let expectEvalToBe = (code: string, answer: string) => let expectEvalToBe = (expr: string, answer: string) =>
Expression.BackCompatible.evaluateString(code)->Reducer_Value.toStringResult->expect->toBe(answer) Reducer.evaluate(expr)
->Reducer_Helpers.rRemoveDefaultsExternal
let expectEvalError = (code: string) => ->ExternalExpressionValue.toStringResult
Expression.BackCompatible.evaluateString(code)
->Reducer_Value.toStringResult
->expect ->expect
->toMatch("Error\\(") ->toBe(answer)
let expectEvalError = (expr: string) =>
Reducer.evaluate(expr)->ExternalExpressionValue.toStringResult->expect->toMatch("Error\(")
let expectEvalBindingsToBe = (expr: string, bindings: Reducer.externalBindings, answer: string) =>
Reducer.evaluateUsingOptions(expr, ~externalBindings=Some(bindings), ~environment=None)
->Reducer_Helpers.rRemoveDefaultsExternal
->ExternalExpressionValue.toStringResult
->expect
->toBe(answer)
let testParseToBe = (expr, answer) => test(expr, () => expectParseToBe(expr, answer)) let testParseToBe = (expr, answer) => test(expr, () => expectParseToBe(expr, answer))
let testDescriptionParseToBe = (desc, expr, answer) => let testDescriptionParseToBe = (desc, expr, answer) =>
@ -31,12 +40,18 @@ let testDescriptionParseToBe = (desc, expr, answer) =>
let testEvalError = expr => test(expr, () => expectEvalError(expr)) let testEvalError = expr => test(expr, () => expectEvalError(expr))
let testEvalToBe = (expr, answer) => test(expr, () => expectEvalToBe(expr, answer)) let testEvalToBe = (expr, answer) => test(expr, () => expectEvalToBe(expr, answer))
let testDescriptionEvalToBe = (desc, expr, answer) => test(desc, () => expectEvalToBe(expr, answer)) let testDescriptionEvalToBe = (desc, expr, answer) => test(desc, () => expectEvalToBe(expr, answer))
let testEvalBindingsToBe = (expr, bindingsList, answer) =>
test(expr, () => expectEvalBindingsToBe(expr, bindingsList->Js.Dict.fromList, answer))
module MySkip = { module MySkip = {
let testParseToBe = (expr, answer) => Skip.test(expr, () => expectParseToBe(expr, answer)) let testParseToBe = (expr, answer) => Skip.test(expr, () => expectParseToBe(expr, answer))
let testEvalToBe = (expr, answer) => Skip.test(expr, () => expectEvalToBe(expr, answer)) let testEvalToBe = (expr, answer) => Skip.test(expr, () => expectEvalToBe(expr, answer))
let testEvalBindingsToBe = (expr, bindingsList, answer) =>
Skip.test(expr, () => expectEvalBindingsToBe(expr, bindingsList->Js.Dict.fromList, answer))
} }
module MyOnly = { module MyOnly = {
let testParseToBe = (expr, answer) => Only.test(expr, () => expectParseToBe(expr, answer)) let testParseToBe = (expr, answer) => Only.test(expr, () => expectParseToBe(expr, answer))
let testEvalToBe = (expr, answer) => Only.test(expr, () => expectEvalToBe(expr, answer)) let testEvalToBe = (expr, answer) => Only.test(expr, () => expectEvalToBe(expr, answer))
let testEvalBindingsToBe = (expr, bindingsList, answer) =>
Only.test(expr, () => expectEvalBindingsToBe(expr, bindingsList->Js.Dict.fromList, answer))
} }

View File

@ -0,0 +1,88 @@
open Jest
open Expect
module BindingsReplacer = Reducer_Expression_BindingsReplacer
module Expression = Reducer_Expression
// module ExpressionValue = ReducerInterface.ExpressionValue
module InternalExpressionValue = ReducerInterface.InternalExpressionValue
module ExpressionWithContext = Reducer_ExpressionWithContext
module Macro = Reducer_Expression_Macro
module T = Reducer_Expression_T
module Bindings = Reducer_Bindings
let testMacro_ = (
tester,
bindArray: array<(string, InternalExpressionValue.t)>,
expr: T.expression,
expectedCode: string,
) => {
let bindings = Bindings.fromArray(bindArray)
tester(expr->T.toString, () =>
expr
->Macro.expandMacroCall(
bindings,
InternalExpressionValue.defaultEnvironment,
Expression.reduceExpression,
)
->ExpressionWithContext.toStringResult
->expect
->toEqual(expectedCode)
)
}
let testMacroEval_ = (
tester,
bindArray: array<(string, InternalExpressionValue.t)>,
expr: T.expression,
expectedValue: string,
) => {
let bindings = Bindings.fromArray(bindArray)
tester(expr->T.toString, () =>
expr
->Macro.doMacroCall(
bindings,
InternalExpressionValue.defaultEnvironment,
Expression.reduceExpression,
)
->InternalExpressionValue.toStringResult
->expect
->toEqual(expectedValue)
)
}
let testMacro = (
bindArray: array<(string, InternalExpressionValue.t)>,
expr: T.expression,
expectedExpr: string,
) => testMacro_(test, bindArray, expr, expectedExpr)
let testMacroEval = (
bindArray: array<(string, InternalExpressionValue.t)>,
expr: T.expression,
expectedValue: string,
) => testMacroEval_(test, bindArray, expr, expectedValue)
module MySkip = {
let testMacro = (
bindArray: array<(string, InternalExpressionValue.t)>,
expr: T.expression,
expectedExpr: string,
) => testMacro_(Skip.test, bindArray, expr, expectedExpr)
let testMacroEval = (
bindArray: array<(string, InternalExpressionValue.t)>,
expr: T.expression,
expectedValue: string,
) => testMacroEval_(Skip.test, bindArray, expr, expectedValue)
}
module MyOnly = {
let testMacro = (
bindArray: array<(string, InternalExpressionValue.t)>,
expr: T.expression,
expectedExpr: string,
) => testMacro_(Only.test, bindArray, expr, expectedExpr)
let testMacroEval = (
bindArray: array<(string, InternalExpressionValue.t)>,
expr: T.expression,
expectedValue: string,
) => testMacroEval_(Only.test, bindArray, expr, expectedValue)
}

View File

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

View File

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

View File

@ -0,0 +1,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,14 +0,0 @@
open Jest
open Expect
describe("ExpressionValue", () => {
test("argsToString", () =>
expect([IEvNumber(1.), IEvString("a")]->Reducer_Value.argsToString)->toBe("1,'a'")
)
test("toStringFunctionCall", () =>
expect(("fn", [IEvNumber(1.), IEvString("a")])->Reducer_Value.toStringFunctionCall)->toBe(
"fn(1,'a')",
)
)
})

View File

@ -1,13 +0,0 @@
open Jest
open Reducer_Peggy_TestHelpers
describe("Construct Array", () => {
testToExpression("[1,2]", "[1, 2]", ~v="[1,2]", ())
testToExpression("[]", "[]", ~v="[]", ())
testToExpression(
"f(x)=x; g(x)=x; [f, g]",
"f = {|x| {x}}; g = {|x| {x}}; [f, g]",
~v="[lambda(x=>internal code),lambda(x=>internal code)]",
(),
)
})

View File

@ -0,0 +1,14 @@
open Jest
open Reducer_TestHelpers
describe("Eval with Bindings", () => {
testEvalBindingsToBe("x", list{("x", ExternalExpressionValue.EvNumber(1.))}, "Ok(1)")
testEvalBindingsToBe("x+1", list{("x", ExternalExpressionValue.EvNumber(1.))}, "Ok(2)")
testParseToBe("y = x+1; y", "Ok({(:$_let_$ :y {(:add :x 1)}); :y})")
testEvalBindingsToBe("y = x+1; y", list{("x", ExternalExpressionValue.EvNumber(1.))}, "Ok(2)")
testEvalBindingsToBe(
"y = x+1",
list{("x", ExternalExpressionValue.EvNumber(1.))},
"Ok(@{x: 1,y: 2})",
)
})

View File

@ -2,15 +2,11 @@ open Jest
open Reducer_TestHelpers open Reducer_TestHelpers
describe("Parse function assignment", () => { describe("Parse function assignment", () => {
testParseToBe("f(x)=x", "Ok(f = {|x| {x}})") testParseToBe("f(x)=x", "Ok({(:$_let_$ :f (:$$_lambda_$$ [x] {:x}))})")
testParseToBe("f(x)=2*x", "Ok(f = {|x| {(multiply)(2, x)}})") testParseToBe("f(x)=2*x", "Ok({(:$_let_$ :f (:$$_lambda_$$ [x] {(:multiply 2 :x)}))})")
//MathJs does not allow blocks in function definitions //MathJs does not allow blocks in function definitions
}) })
describe("Evaluate function assignment", () => { describe("Evaluate function assignment", () => {
testEvalToBe("f(x)=x; f(1)", "Ok(1)") testEvalToBe("f(x)=x; f(1)", "Ok(1)")
}) })
describe("Shadowing", () => {
testEvalToBe("x = 5; f(y) = x*y; x = 6; f(2)", "Ok(10)")
})

View File

@ -34,29 +34,38 @@ describe("symbol not defined", () => {
testEvalToBe("f(x)=x(y); f(f)", "Error(y is not defined)") testEvalToBe("f(x)=x(y); f(f)", "Error(y is not defined)")
testEvalToBe("f(x)=x; f(f)", "Ok(lambda(x=>internal code))") testEvalToBe("f(x)=x; f(f)", "Ok(lambda(x=>internal code))")
testEvalToBe("f(x)=x(y); f(z)", "Error(z is not defined)") testEvalToBe("f(x)=x(y); f(z)", "Error(z is not defined)")
testEvalToBe("f(x)=x(y); f(2)", "Error(y is not defined)") testEvalToBe("f(x)=x(y); f(2)", "Error(2 is not a function)")
testEvalToBe("f(x)=x(1); f(2)", "Error(2 is not a function)") testEvalToBe("f(x)=x(1); f(2)", "Error(2 is not a function)")
}) })
describe("call and bindings", () => { describe("call and bindings", () => {
testEvalToBe("f(x)=x+1; f(0)", "Ok(1)") testEvalToBe("f(x)=x+1", "Ok(@{f: lambda(x=>internal code)})")
testEvalToBe("f(x)=x+1; f(1)", "Ok(2)") testEvalToBe("f(x)=x+1; f(1)", "Ok(2)")
testEvalToBe("f=1;y=2", "Ok(())") testEvalToBe("f=1;y=2", "Ok(@{f: 1,y: 2})")
testEvalToBe("f(x)=x+1; y=f(1); y", "Ok(2)") testEvalToBe("f(x)=x+1; y=f(1)", "Ok(@{f: lambda(x=>internal code),y: 2})")
testEvalToBe("f(x)=x+1; y=f(1); f(1)", "Ok(2)") testEvalToBe("f(x)=x+1; y=f(1); f(1)", "Ok(2)")
testEvalToBe("f(x)=x+1; y=f(1); z=f(1); z", "Ok(2)") testEvalToBe("f(x)=x+1; y=f(1); z=f(1)", "Ok(@{f: lambda(x=>internal code),y: 2,z: 2})")
testEvalToBe("f(x)=x+1; g(x)=f(x)+1; g(0)", "Ok(2)") testEvalToBe(
testParseToBe("f=99; g(x)=f; g(2)", "Ok(f = {99}; g = {|x| {f}}; (g)(2))") "f(x)=x+1; g(x)=f(x)+1",
"Ok(@{f: lambda(x=>internal code),g: lambda(x=>internal code)})",
)
testParseToBe(
"f=99; g(x)=f; g(2)",
"Ok({(:$_let_$ :f {99}); (:$_let_$ :g (:$$_lambda_$$ [x] {:f})); (:g 2)})",
)
testEvalToBe("f=99; g(x)=f; g(2)", "Ok(99)") testEvalToBe("f=99; g(x)=f; g(2)", "Ok(99)")
testEvalToBe("f(x)=x; g(x)=f(x); g(2)", "Ok(2)") testEvalToBe("f(x)=x; g(x)=f(x); g(2)", "Ok(2)")
testEvalToBe("f(x)=x+1; g(x)=f(x)+1; y=g(2); y", "Ok(4)") testEvalToBe(
"f(x)=x+1; g(x)=f(x)+1; y=g(2)",
"Ok(@{f: lambda(x=>internal code),g: lambda(x=>internal code),y: 4})",
)
testEvalToBe("f(x)=x+1; g(x)=f(x)+1; g(2)", "Ok(4)") testEvalToBe("f(x)=x+1; g(x)=f(x)+1; g(2)", "Ok(4)")
}) })
describe("function tricks", () => { describe("function tricks", () => {
testEvalError("f(x)=f(y)=2; f(2)") //Error because chain assignment is not allowed testEvalError("f(x)=f(y)=2; f(2)") //Error because chain assignment is not allowed
testEvalToBe("y=2;g(x)=y+1;g(2)", "Ok(3)") testEvalToBe("y=2;g(x)=y+1;g(2)", "Ok(3)")
testEvalToBe("y=2;g(x)=inspect(y)+1;y", "Ok(2)") testEvalToBe("y=2;g(x)=inspect(y)+1", "Ok(@{g: lambda(x=>internal code),y: 2})")
MySkip.testEvalToBe("f(x) = x(x); f(f)", "????") // TODO: Infinite loop. Any solution? Catching proper exception or timeout? MySkip.testEvalToBe("f(x) = x(x); f(f)", "????") // TODO: Infinite loop. Any solution? Catching proper exception or timeout?
MySkip.testEvalToBe("f(x, x)=x+x; f(1,2)", "????") // TODO: Duplicate parameters MySkip.testEvalToBe("f(x, x)=x+x; f(1,2)", "????") // TODO: Duplicate parameters
testEvalToBe("myadd(x,y)=x+y; z=myadd; z", "Ok(lambda(x,y=>internal code))") testEvalToBe("myadd(x,y)=x+y; z=myadd; z", "Ok(lambda(x,y=>internal code))")
@ -64,7 +73,10 @@ describe("function tricks", () => {
}) })
describe("lambda in structures", () => { describe("lambda in structures", () => {
testEvalToBe("myadd(x,y)=x+y; z=[myadd]", "Ok(())") testEvalToBe(
"myadd(x,y)=x+y; z=[myadd]",
"Ok(@{myadd: lambda(x,y=>internal code),z: [lambda(x,y=>internal code)]})",
)
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0]", "Ok(lambda(x,y=>internal code))") testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0]", "Ok(lambda(x,y=>internal code))")
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0](3,2)", "Ok(5)") testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0](3,2)", "Ok(5)")
testEvalToBe("myaddd(x,y)=x+y; z={x: myaddd}; z", "Ok({x: lambda(x,y=>internal code)})") testEvalToBe("myaddd(x,y)=x+y; z={x: myaddd}; z", "Ok({x: lambda(x,y=>internal code)})")

View File

@ -5,7 +5,3 @@ 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???")
}) })
describe("map", () => {
testEvalToBe("arr=[1,2,3]; map(arr, {|x| x*2})", "Ok([2,4,6])")
})

View File

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

View File

@ -2,29 +2,19 @@ open Jest
open Reducer_TestHelpers open Reducer_TestHelpers
describe("eval", () => { describe("eval", () => {
// All MathJs operators and functions are builtin for string, float and boolean
// .e.g + - / * > >= < <= == /= not and or
// See https://mathjs.org/docs/reference/functions.html
describe("expressions", () => { describe("expressions", () => {
testEvalToBe("1", "Ok(1)") testEvalToBe("1", "Ok(1)")
testEvalToBe("-1", "Ok(-1)")
testEvalToBe("1-1", "Ok(0)")
testEvalToBe("1+2", "Ok(3)") testEvalToBe("1+2", "Ok(3)")
testEvalToBe("(1+2)*3", "Ok(9)") testEvalToBe("(1+2)*3", "Ok(9)")
testEvalToBe("2>1", "Ok(true)") testEvalToBe("2>1", "Ok(true)")
testEvalToBe("concat('a ', 'b')", "Ok('a b')") testEvalToBe("concat('a ', 'b')", "Ok('a b')")
testEvalToBe("concat([3,4], [5,6,7])", "Ok([3,4,5,6,7])")
testEvalToBe("log(10)", "Ok(2.302585092994046)") testEvalToBe("log(10)", "Ok(2.302585092994046)")
testEvalToBe("Math.cos(10)", "Ok(-0.8390715290764524)") testEvalToBe("cos(10)", "Ok(-0.8390715290764524)")
// TODO more built ins // TODO more built ins
}) })
describe("missing function", () => {
testEvalToBe("testZadanga(1)", "Error(testZadanga is not defined)")
testEvalToBe(
"arr = [normal(3,2)]; map(arr, zarathsuzaWasHere)",
"Error(zarathsuzaWasHere is not defined)",
)
})
describe("arrays", () => { describe("arrays", () => {
test("empty array", () => expectEvalToBe("[]", "Ok([])")) test("empty array", () => expectEvalToBe("[]", "Ok([])"))
testEvalToBe("[1, 2, 3]", "Ok([1,2,3])") testEvalToBe("[1, 2, 3]", "Ok([1,2,3])")
@ -37,16 +27,14 @@ 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( test("always the same property ending", () =>
"always the same property ending", expectEvalToBe(
() => `{
expectEvalToBe(
`{
a: 1, a: 1,
b: 2, b: 2,
}`, }`,
"Ok({a: 1,b: 2})", "Ok({a: 1,b: 2})",
), )
) )
}) })
@ -60,11 +48,7 @@ describe("eval", () => {
testEvalToBe("x=1; y=x+1; y+1", "Ok(3)") testEvalToBe("x=1; y=x+1; y+1", "Ok(3)")
testEvalError("1; x=1") testEvalError("1; x=1")
testEvalError("1; 1") testEvalError("1; 1")
testEvalToBe("x=1; x=1; x", "Ok(1)") testEvalToBe("x=1; x=1", "Ok(@{x: 1})")
})
describe("blocks", () => {
testEvalToBe("x = { y = { z = 5; z * 2 }; y + 3 }; x", "Ok(13)")
}) })
}) })
@ -80,33 +64,3 @@ describe("test exceptions", () => {
// "Error(TODO: unhandled rescript exception)", // "Error(TODO: unhandled rescript exception)",
// ) // )
}) })
describe("stacktraces", () => {
test("nested calls", () => {
open Expect
let error =
Expression.BackCompatible.evaluateString(`
f(x) = {
y = "a"
x + y
}
g = {|x| f(x)}
h(x) = g(x)
h(5)
`)
->E.R.getError
->E.O2.toExn("oops")
->SqError.toStringWithStackTrace
expect(
error,
)->toBe(`Error: There are function matches for add(), but with different arguments: [add(number, number)]; [add(distribution, number)]; [add(number, distribution)]; [add(distribution, distribution)]; [add(date, duration)]; [add(duration, duration)]
Stack trace:
f at line 4, column 5
g at line 6, column 12
h at line 7, column 10
<top> at line 8, column 3
`)
})
})

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