Compare commits

..

3 Commits

Author SHA1 Message Date
Sam Nolan
b44bf0cc38 Fix failing tests due to environment change 2022-07-12 17:25:11 +10:00
Sam Nolan
ee14a47bd6 Fix failing floating point test 2022-07-12 15:29:50 +10:00
Sam Nolan
fe5a42353e Add percentile graphed as environment option 2022-07-12 15:16:13 +10:00
374 changed files with 12750 additions and 19252 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

@ -11,14 +11,3 @@ updates:
interval: "weekly" interval: "weekly"
commit-message: commit-message:
prefix: "⬆️" prefix: "⬆️"
open-pull-requests-limit: 100
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
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: steps:
- uses: actions/checkout@v3 - id: skip_lang_check
- name: Setup Node.js environment name: Check if the changes are about squiggle-lang src files
uses: actions/setup-node@v3 uses: fkirc/skip-duplicate-actions@v3.4.1
with: with:
node-version: 16 paths: '["packages/squiggle-lang/**"]'
cache: 'yarn' - id: skip_components_check
- name: Install dependencies name: Check if the changes are about components src files
run: yarn --frozen-lockfile uses: fkirc/skip-duplicate-actions@v3.4.1
- name: Turbo run with:
run: npx turbo run build test lint bundle paths: '["packages/components/**"]'
- id: skip_website_check
name: Check if the changes are about website src files
uses: fkirc/skip-duplicate-actions@v3.4.1
with:
paths: '["packages/website/**"]'
- id: skip_vscodeext_check
name: Check if the changes are about vscode extension src files
uses: fkirc/skip-duplicate-actions@v3.4.1
with:
paths: '["packages/vscode-ext/**"]'
- id: skip_cli_check
name: Check if the changes are about cli src files
uses: fkirc/skip-duplicate-actions@v3.4.1
with:
paths: '["packages/cli/**"]'
coverage: lang-lint:
name: Coverage name: Language lint
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@v2
- name: Install Dependencies
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:
dry: true
prettier_options: --check packages/squiggle-lang
lang-build-test-bundle:
name: Language build, test, and bundle
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:
- uses: actions/checkout@v2
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@v2
- 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@v2
- 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@v2
- 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@v2
- 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@v2
- name: Install dependencies from monorepo level
run: cd ../../ && yarn
- name: Lint the VSCode Extension source code
run: yarn lint
vscode-ext-build:
name: VS Code extension build
runs-on: ubuntu-latest
needs: pre_check
if: ${{ (needs.pre_check.outputs.should_skip_components != 'true') || (needs.pre_check.outputs.should_skip_lang != 'true') }} || (needs.pre_check.outputs.should_skip_vscodeext != 'true') }}
defaults:
run:
shell: bash
working-directory: packages/vscode-ext
steps:
- uses: actions/checkout@v2
- name: Install dependencies from monorepo level
run: cd ../../ && yarn
- name: Build
run: yarn compile
cli-lint:
name: CLI lint
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_cli != 'true' }}
defaults:
run:
shell: bash
working-directory: packages/cli
steps:
- uses: actions/checkout@v2
- name: Check javascript, typescript, and markdown lint
uses: creyD/prettier_action@v4.2
with:
dry: true
prettier_options: --check packages/cli

View File

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

View File

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

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 +0,0 @@
{
"packages/cli": "0.0.3",
"packages/components": "0.4.1",
"packages/squiggle-lang": "0.4.1",
"packages/vscode-ext": "0.4.1",
"packages/website": "0.0.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

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

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

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,62 @@
{ {
"name": "@quri/squiggle-components", "name": "@quri/squiggle-components",
"version": "0.5.0", "version": "0.2.23",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/react-dom": "^1.0.0", "@headlessui/react": "^1.6.6",
"@floating-ui/react-dom-interactions": "^0.10.1",
"@headlessui/react": "^1.7.3",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@hookform/resolvers": "^2.9.8", "@hookform/resolvers": "^2.9.5",
"@quri/squiggle-lang": "^0.5.0", "@quri/squiggle-lang": "^0.2.8",
"@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": "^6.4.3",
"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.33.1",
"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.3.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.9",
"@storybook/addon-links": "^6.5.12", "@storybook/addon-links": "^6.5.9",
"@storybook/builder-webpack5": "^6.5.12", "@storybook/builder-webpack5": "^6.5.9",
"@storybook/manager-webpack5": "^6.5.12", "@storybook/manager-webpack5": "^6.5.9",
"@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.9",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.2.1",
"@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.0.3",
"@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.5",
"ts-jest": "^29.0.3", "ts-loader": "^9.3.0",
"ts-loader": "^9.4.1", "tsconfig-paths-webpack-plugin": "^3.5.2",
"tsconfig-paths-webpack-plugin": "^4.0.0", "typescript": "^4.7.4",
"typescript": "^4.8.4", "web-vitals": "^2.1.4",
"web-vitals": "^3.0.3", "webpack": "^5.73.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 +64,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 +72,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,106 +1,68 @@
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";
import { useSize } from "react-use"; import { useSize } from "react-use";
import clsx from "clsx";
import { import {
buildVegaSpec, buildVegaSpec,
DistributionChartSpecOptions, DistributionChartSpecOptions,
} from "../lib/distributionSpecBuilder"; } from "../lib/distributionSpecBuilder";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "./NumberShower";
import { Plot, parsePlot } from "../lib/plotParser";
import { flattenResult } from "../lib/utility";
import { hasMassBelowZero } from "../lib/distributionUtils";
export type DistributionPlottingSettings = { export type DistributionPlottingSettings = {
/** Whether to show a summary of means, stdev, percentiles etc */ /** Whether to show a summary of means, stdev, percentiles etc */
showSummary: boolean; showSummary: boolean;
actions?: boolean; /** Whether to show the user graph controls (scale etc) */
showControls: boolean;
} & 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"; actions?: boolean;
} & 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,
showControls,
logX, logX,
expY,
actions = false, actions = false,
} = props; } = props;
const [sized] = useSize((size) => { const [isLogX, setLogX] = React.useState(logX);
const shapes = flattenResult( const [isExpY, setExpY] = React.useState(expY);
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") { React.useEffect(() => setLogX(logX), [logX]);
React.useEffect(() => setExpY(expY), [expY]);
const shape = distribution.pointSet();
const [sized] = useSize((size) => {
if (shape.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 massBelow0 =
const samples: number[] = []; shape.value.continuous.some((x) => x.x <= 0) ||
for (const { distribution } of plot?.distributions) { shape.value.discrete.some((x) => x.x <= 0);
if (distribution.tag === SqDistributionTag.SampleSet) { const spec = buildVegaSpec(props);
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,37 +70,77 @@ 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) ? ( {!(isLogX && massBelow0) ? (
<ErrorAlert heading="Log Domain Error">
Cannot graph distribution with negative values on logarithmic scale.
</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}
/> />
) : (
<ErrorAlert heading="Log Domain Error">
Cannot graph distribution with negative values on logarithmic scale.
</ErrorAlert>
)} )}
<div className="flex justify-center"> <div className="flex justify-center">
{showSummary && plot.distributions.length === 1 && ( {showSummary && <SummaryTable distribution={distribution} />}
<SummaryTable
distribution={plot.distributions[0].distribution}
environment={environment}
/>
)}
</div> </div>
{showControls && (
<div>
<CheckBox
label="Log X scale"
value={isLogX}
onChange={setLogX}
// Check whether we should disable the checkbox
{...(massBelow0
? {
disabled: true,
tooltip:
"Your distribution has mass lower than or equal to 0. Log only works on strictly positive values.",
}
: {})}
/>
<CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} />
</div>
)}
</div> </div>
); );
}); });
return sized; return sized;
}; };
interface CheckBoxProps {
label: string;
onChange: (x: boolean) => void;
value: boolean;
disabled?: boolean;
tooltip?: string;
}
export const CheckBox: React.FC<CheckBoxProps> = ({
label,
onChange,
value,
disabled = false,
tooltip,
}) => {
return (
<span title={tooltip}>
<input
type="checkbox"
checked={value}
onChange={() => onChange(!value)}
disabled={disabled}
className="form-checkbox"
/>
<label className={clsx(disabled && "text-slate-400")}> {label}</label>
</span>
);
};
const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({ const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({
children, children,
}) => ( }) => (
@ -154,36 +156,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,9 @@
import * as React from "react"; import * as React from "react";
import { import { lambdaValue, environment, runForeign } from "@quri/squiggle-lang";
SqLambda,
environment,
SqValueTag,
SqError,
} 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 +12,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 +26,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;
@ -71,13 +45,11 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
} }
}; };
const validResult = getValidResult(); const validResult = getValidResult();
const resultType =
validResult.tag === "Ok" ? validResult.value.tag : ("Error" as const);
if (validResult.tag === "Error") { switch (resultType) {
return <FunctionCallErrorAlert error={validResult.value} />; case "distribution":
}
switch (validResult.value.tag) {
case SqValueTag.Distribution:
return ( return (
<FunctionChart1Dist <FunctionChart1Dist
fn={fn} fn={fn}
@ -87,7 +59,7 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
distributionPlotSettings={distributionPlotSettings} distributionPlotSettings={distributionPlotSettings}
/> />
); );
case SqValueTag.Number: case "number":
return ( return (
<FunctionChart1Number <FunctionChart1Number
fn={fn} fn={fn}
@ -96,11 +68,15 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
height={height} height={height}
/> />
); );
case "Error":
return (
<ErrorAlert heading="Error">The function failed to be run</ErrorAlert>
);
default: default:
return ( return (
<MessageAlert heading="Function Display Not Supported"> <MessageAlert heading="Function Display Not Supported">
There is no function visualization for this type of output:{" "} There is no function visualization for this type of output:{" "}
<span className="font-bold">{validResult.value.tag}</span> <span className="font-bold">{resultType}</span>
</MessageAlert> </MessageAlert>
); );
} }

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 };
@ -170,22 +165,19 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
setMouseOverlay(NaN); setMouseOverlay(NaN);
} }
const signalListeners = { mousemove: handleHover, mouseout: handleOut }; const signalListeners = { mousemove: handleHover, mouseout: handleOut };
let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
//TODO: This custom error handling is a bit hacky and should be improved. ? runForeign(fn, [mouseOverlay], environment)
let mouseItem: result<SqValue, SqError> = !!mouseOverlay
? fn.call([mouseOverlay])
: { : {
tag: "Error", tag: "Error",
value: SqError.createOtherError( value: {
"Hover x-coordinate returned NaN. Expected a number." tag: "REExpectedType",
), 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,25 +38,20 @@ 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, //We adjust the count, because the count is made for distributions, which are much more expensive to estimate
fn, let adjustedCount = chartSettings.count * 20;
environment,
}: {
chartSettings: FunctionChartSettings;
fn: SqLambda;
environment: environment;
}) => {
let chartPointsToRender = _rangeByCount( let chartPointsToRender = _rangeByCount(
chartSettings.start, chartSettings.start,
chartSettings.stop, chartSettings.stop,
chartSettings.count adjustedCount
); );
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 +65,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,24 @@
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 { SquiggleErrorAlert } from "./SquiggleErrorAlert";
import { JsImports } from "../lib/jsImports"; import { SquiggleItem } from "./SquiggleItem";
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 */
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,14 +26,20 @@ 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;
/** Whether to show type information about returns, default false */
showTypes?: boolean;
/** Whether to show graph controls (scale etc)*/
showControls?: boolean;
/** Set the x scale to be logarithmic by deault */ /** Set the x scale to be logarithmic by deault */
logX?: boolean; logX?: boolean;
/** Set the y scale to be exponential by deault */ /** Set the y scale to be exponential by deault */
@ -46,112 +54,76 @@ 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; }
} & (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 = "",
environment,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
bindings = defaultBindings,
jsImports = defaultImports,
showSummary = false, showSummary = false,
width,
showTypes = false,
showControls = false,
logX = false, logX = false,
expY = false, expY = false,
diagramStart = 0, diagramStart = 0,
diagramStop = 10, diagramStop = 10,
diagramCount = 20, diagramCount = 100,
tickFormat, tickFormat,
minX, minX,
maxX, maxX,
color, color,
title, title,
xAxisType = "number",
distributionChartActions, distributionChartActions,
} = props; }) => {
const result = useSquiggle({
const distributionPlotSettings = {
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,
}); });
const valueToRender = getValueToRender(resultAndBindings); if (result.tag !== "Ok") {
return <SquiggleErrorAlert error={result.value} />;
}
let distributionPlotSettings = {
showControls,
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
color,
title,
actions: distributionChartActions,
};
let chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return ( return (
<SquiggleViewer <SquiggleItem
result={valueToRender} expression={result.value}
width={width} width={width}
height={height} height={height}
distributionPlotSettings={distributionPlotSettings} distributionPlotSettings={distributionPlotSettings}
showTypes={showTypes}
chartSettings={chartSettings} chartSettings={chartSettings}
environment={ environment={environment ?? defaultEnvironment}
project ? project.getEnvironment() : environment ?? defaultEnvironment
}
enableLocalSettings={enableLocalSettings}
/> />
); );
} }

View File

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

View File

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

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

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

View File

@ -1,23 +1,11 @@
import React, { import React, { FC, useState, useEffect, useMemo } from "react";
FC, import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form";
useState,
useEffect,
useMemo,
useRef,
useCallback,
} from "react";
import { useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup"; import * as yup from "yup";
import { import { useMaybeControlledValue } 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,
CheckCircleIcon, CheckCircleIcon,
ClipboardCopyIcon,
CodeIcon, CodeIcon,
CogIcon, CogIcon,
CurrencyDollarIcon, CurrencyDollarIcon,
@ -28,61 +16,114 @@ 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";
import { SquiggleContainer } from "./SquiggleContainer"; import { SquiggleContainer } from "./SquiggleContainer";
import { Toggle } from "./ui/Toggle"; import { Toggle } from "./ui/Toggle";
import { Checkbox } from "./ui/Checkbox";
import { StyledTab } from "./ui/StyledTab"; import { StyledTab } from "./ui/StyledTab";
import { InputItem } from "./ui/InputItem";
import { Text } from "./ui/Text";
import { ViewSettings, viewSettingsSchema } from "./ViewSettings";
import { HeadedSection } from "./ui/HeadedSection";
import { defaultTickFormat } from "../lib/distributionSpecBuilder";
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 */
defaultCode?: string; defaultCode?: string;
/** How many pixels high is the playground */
onCodeChange?(expr: string): void; onCodeChange?(expr: string): void;
/* When settings change */ /* When settings change */
onSettingsChange?(settings: any): void; onSettingsChange?(settings: any): void;
/** Should we show the editor? */ /** Should we show the editor? */
showEditor?: boolean; showEditor?: boolean;
/** Useful for playground on squiggle website, where we update the anchor link based on current code and settings */
showShareButton?: boolean;
}; };
const schema = yup const schema = yup.object({}).shape({
.object({}) sampleCount: yup
.shape({ .number()
sampleCount: yup .required()
.number() .positive()
.required() .integer()
.positive() .default(1000)
.integer() .min(10)
.default(1000) .max(1000000),
.min(10) xyPointLength: yup
.max(1000000), .number()
xyPointLength: yup .required()
.number() .positive()
.required() .integer()
.positive() .default(1000)
.integer() .min(10)
.default(1000) .max(10000),
.min(10) percentile: yup.number().required().positive().default(0.9998).min(0).max(1),
.max(10000), chartHeight: yup.number().required().positive().integer().default(350),
}) leftSizePercent: yup
.concat(viewSettingsSchema); .number()
.required()
.positive()
.integer()
.min(10)
.max(100)
.default(50),
showTypes: yup.boolean().required(),
showControls: yup.boolean().required(),
showSummary: yup.boolean().required(),
showEditor: yup.boolean().required(),
logX: yup.boolean().required(),
expY: yup.boolean().required(),
tickFormat: yup.string().default(".9~s"),
title: yup.string(),
color: yup.string().default("#739ECC").required(),
minX: yup.number(),
maxX: yup.number(),
distributionChartActions: yup.boolean(),
showSettingsPage: yup.boolean().default(false),
diagramStart: yup.number().required().positive().integer().default(0).min(0),
diagramStop: yup.number().required().positive().integer().default(10).min(0),
diagramCount: yup.number().required().positive().integer().default(20).min(2),
});
type FormFields = yup.InferType<typeof schema>; type FormFields = yup.InferType<typeof schema>;
const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({
title,
children,
}) => (
<div>
<header className="text-lg leading-6 font-medium text-gray-900">
{title}
</header>
<div className="mt-4">{children}</div>
</div>
);
const Text: FC<{ children: React.ReactNode }> = ({ children }) => (
<p className="text-sm text-gray-500">{children}</p>
);
function InputItem<T>({
name,
label,
type,
register,
}: {
name: Path<T>;
label: string;
type: "number" | "text" | "color";
register: UseFormRegister<T>;
}) {
return (
<label className="block">
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
<input
type={type}
{...register(name, { valueAsNumber: type === "number" })}
className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</label>
);
}
const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({ const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
register, register,
}) => ( }) => (
@ -115,12 +156,148 @@ const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
</Text> </Text>
</div> </div>
</div> </div>
<div>
<InputItem
name="percentile"
type="number"
label="Symbolic Distribution Percentile"
register={register}
/>
<div className="mt-2">
<Text>
When converting symbolic distributions to PointSet distributions, what
percentile to sample the points within.
</Text>
</div>
</div>
</div>
);
const ViewSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
register,
}) => (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings">
<div className="space-y-4">
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
<InputItem
name="chartHeight"
type="number"
register={register}
label="Chart Height (in pixels)"
/>
<Checkbox
name="showTypes"
register={register}
label="Show information about displayed types"
/>
</div>
</HeadedSection>
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="logX"
label="Show x scale logarithmically"
/>
<Checkbox
register={register}
name="expY"
label="Show y scale exponentially"
/>
<Checkbox
register={register}
name="distributionChartActions"
label="Show vega chart controls"
/>
<Checkbox
register={register}
name="showControls"
label="Show toggles to adjust scale of x and y axes"
/>
<Checkbox
register={register}
name="showSummary"
label="Show summary statistics"
/>
<InputItem
name="minX"
type="number"
register={register}
label="The minimum of the charted distribution domain"
/>
<InputItem
name="maxX"
type="number"
register={register}
label="The maximum of the charted distribution domain"
/>
<InputItem
name="title"
type="text"
register={register}
label="The title shown on the distribution"
/>
<InputItem
name="tickFormat"
type="text"
register={register}
label="The format that the ticks are rendered"
/>
<InputItem
name="color"
type="color"
register={register}
label="The color of the charted distribution"
/>
</div>
</HeadedSection>
</div>
<div className="pt-8">
<HeadedSection title="Function Display Settings">
<div className="space-y-6">
<Text>
When displaying functions of single variables that return numbers or
distributions, we need to use defaults for the x-axis. We need to
select a minimum and maximum value of x to sample, and a number n of
the number of points to sample.
</Text>
<div className="space-y-4">
<InputItem
type="number"
name="diagramStart"
register={register}
label="Min X Value"
/>
<InputItem
type="number"
name="diagramStop"
register={register}
label="Max X Value"
/>
<InputItem
type="number"
name="diagramCount"
register={register}
label="Points between X min and X max to sample"
/>
</div>
</div>
</HeadedSection>
</div>
</div> </div>
); );
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 +306,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 +359,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
@ -199,60 +376,69 @@ const RunControls: React.FC<{
icons={[CheckCircleIcon, PauseIcon]} icons={[CheckCircleIcon, PauseIcon]}
status={autorunMode} status={autorunMode}
onChange={onAutorunModeChange} onChange={onAutorunModeChange}
spinIcon={autorunMode && isRunning}
/> />
</div> </div>
); );
}; };
const ShareButton: React.FC = () => { const useRunnerState = (code: string) => {
const [isCopied, setIsCopied] = useState(false); const [autorunMode, setAutorunMode] = useState(true);
const copy = () => { const [renderedCode, setRenderedCode] = useState(code); // used in manual run mode only
navigator.clipboard.writeText((window.top || window).location.href); const [isRunning, setIsRunning] = useState(false); // used in manual run mode only
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1000);
};
return (
<div className="w-36">
<Button onClick={copy} wide>
{isCopied ? (
"Copied to clipboard!"
) : (
<div className="flex items-center space-x-1">
<ClipboardCopyIcon className="w-4 h-4" />
<span>Copy share link</span>
</div>
)}
</Button>
</div>
);
};
type PlaygroundContextShape = { // This part is tricky and fragile; we need to re-render first to make sure that the icon is spinning,
getLeftPanelElement: () => HTMLDivElement | undefined; // and only then evaluate the squiggle code (which freezes the UI).
// Also note that `useEffect` execution order matters here.
// Hopefully it'll all go away after we make squiggle code evaluation async.
useEffect(() => {
if (renderedCode === code && isRunning) {
// It's not possible to put this after `setRenderedCode(code)` below because React would apply
// `setIsRunning` and `setRenderedCode` together and spinning icon will disappear immediately.
setIsRunning(false);
}
}, [renderedCode, code, isRunning]);
useEffect(() => {
if (!autorunMode && isRunning) {
setRenderedCode(code); // TODO - force run even if code hasn't changed
}
}, [autorunMode, code, isRunning]);
const run = () => {
// The rest will be handled by useEffects above, but we need to update the spinner first.
setIsRunning(true);
};
return {
run,
renderedCode: autorunMode ? code : renderedCode,
isRunning,
autorunMode,
setAutorunMode: (newValue: boolean) => {
if (!newValue) setRenderedCode(code);
setAutorunMode(newValue);
},
};
}; };
export const PlaygroundContext = React.createContext<PlaygroundContextShape>({
getLeftPanelElement: () => undefined,
});
export const SquigglePlayground: FC<PlaygroundProps> = ({ export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultCode = "", defaultCode = "",
height = 500, height = 500,
showSummary = true, showTypes = false,
showControls = false,
showSummary = false,
logX = false, logX = false,
expY = false, expY = false,
title, title,
minX, minX,
maxX, maxX,
tickFormat = defaultTickFormat, color = "#739ECC",
tickFormat = ".9~s",
distributionChartActions, distributionChartActions,
code: controlledCode, code: controlledCode,
onCodeChange, onCodeChange,
onSettingsChange, onSettingsChange,
showEditor = true, showEditor = true,
showShareButton = false,
continues,
project,
}) => { }) => {
const [code, setCode] = useMaybeControlledValue({ const [code, setCode] = useMaybeControlledValue({
value: controlledCode, value: controlledCode,
@ -260,23 +446,29 @@ 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),
defaultValues: { defaultValues: {
percentile: 0.9998,
sampleCount: 1000, sampleCount: 1000,
xyPointLength: 1000, xyPointLength: 1000,
chartHeight: 150, chartHeight: 150,
showTypes,
showControls,
logX, logX,
expY, expY,
title, title,
minX, minX,
maxX, maxX,
color,
tickFormat, tickFormat,
distributionChartActions, distributionChartActions,
showSummary, showSummary,
showEditor, showEditor,
leftSizePercent: 50,
showSettingsPage: false,
diagramStart: 0, diagramStart: 0,
diagramStop: 10, diagramStop: 10,
diagramCount: 20, diagramCount: 20,
@ -290,70 +482,31 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
onSettingsChange?.(vars); onSettingsChange?.(vars);
}, [vars, onSettingsChange]); }, [vars, onSettingsChange]);
const environment: environment = useMemo( const env: environment = useMemo(
() => ({ () => ({
percentile: Number(vars.percentile),
sampleCount: Number(vars.sampleCount), sampleCount: Number(vars.sampleCount),
xyPointLength: Number(vars.xyPointLength), xyPointLength: Number(vars.xyPointLength),
}), }),
[vars.sampleCount, vars.xyPointLength] [vars.sampleCount, vars.xyPointLength]
); );
const { const { run, autorunMode, setAutorunMode, isRunning, renderedCode } =
run, useRunnerState(code);
autorunMode,
setAutorunMode,
isRunning,
renderedCode,
executionId,
} = useRunnerState(code);
const resultAndBindings = useSquiggle({ const squiggleChart = (
environment, <SquiggleChart
continues, code={renderedCode}
code: renderedCode, environment={env}
project, {...vars}
jsImports: imports, bindings={defaultBindings}
executionId, jsImports={imports}
}); />
);
const valueToRender = getValueToRender(resultAndBindings);
const squiggleChart =
renderedCode === "" ? null : (
<div className="relative">
{isRunning ? (
<div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" />
) : null}
<SquiggleViewer
result={valueToRender}
environment={environment}
height={vars.chartHeight || 150}
distributionPlotSettings={{
showSummary: vars.showSummary ?? false,
logX: vars.logX ?? false,
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}
/>
</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}
@ -373,15 +526,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
<SamplingSettings register={register} /> <SamplingSettings register={register} />
</StyledTab.Panel> </StyledTab.Panel>
<StyledTab.Panel> <StyledTab.Panel>
<ViewSettings <ViewSettings register={register} />
register={
// This is dangerous, but doesn't cause any problems.
// I tried to make `ViewSettings` generic (to allow it to accept any extension of a settings schema), but it didn't work.
register as unknown as UseFormRegister<
yup.InferType<typeof viewSettingsSchema>
>
}
/>
</StyledTab.Panel> </StyledTab.Panel>
<StyledTab.Panel> <StyledTab.Panel>
<InputVariablesSettings <InputVariablesSettings
@ -392,59 +537,41 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
</StyledTab.Panels> </StyledTab.Panels>
); );
const leftPanelRef = useRef<HTMLDivElement | null>(null);
const withEditor = ( const withEditor = (
<div className="flex mt-2"> <div className="flex mt-1">
<div <div className="w-1/2">{tabs}</div>
className="w-1/2 relative" <div className="w-1/2 p-2 pl-4">{squiggleChart}</div>
style={{ minHeight: height }}
ref={leftPanelRef}
>
{tabs}
</div>
<div className="w-1/2 p-2 pl-4" data-testid="playground-result">
{squiggleChart}
</div>
</div> </div>
); );
const withoutEditor = <div className="mt-3">{tabs}</div>; const withoutEditor = <div className="mt-3">{tabs}</div>;
const getLeftPanelElement = useCallback(() => { console.log(vars);
return leftPanelRef.current ?? undefined;
}, []);
return ( return (
<SquiggleContainer> <SquiggleContainer>
<PlaygroundContext.Provider value={{ getLeftPanelElement }}> <StyledTab.Group>
<StyledTab.Group> <div className="pb-4">
<div className="pb-4"> <div className="flex justify-between items-center mt-2">
<div className="flex justify-between items-center"> <StyledTab.List>
<StyledTab.List> <StyledTab
<StyledTab name={vars.showEditor ? "Code" : "Display"}
name={vars.showEditor ? "Code" : "Display"} icon={vars.showEditor ? CodeIcon : EyeIcon}
icon={vars.showEditor ? CodeIcon : EyeIcon} />
/> <StyledTab name="Sampling Settings" icon={CogIcon} />
<StyledTab name="Sampling Settings" icon={CogIcon} /> <StyledTab name="View Settings" icon={ChartSquareBarIcon} />
<StyledTab name="View Settings" icon={ChartSquareBarIcon} /> <StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} /> </StyledTab.List>
</StyledTab.List> <RunControls
<div className="flex space-x-2 items-center"> autorunMode={autorunMode}
<RunControls isStale={renderedCode !== code}
autorunMode={autorunMode} run={run}
isStale={renderedCode !== code} isRunning={isRunning}
run={run} onAutorunModeChange={setAutorunMode}
isRunning={isRunning} />
onAutorunModeChange={setAutorunMode}
/>
{showShareButton && <ShareButton />}
</div>
</div>
{vars.showEditor ? withEditor : withoutEditor}
</div> </div>
</StyledTab.Group> {vars.showEditor ? withEditor : withoutEditor}
</PlaygroundContext.Provider> </div>
</StyledTab.Group>
</SquiggleContainer> </SquiggleContainer>
); );
}; };

View File

@ -1,310 +0,0 @@
import React, { useContext } from "react";
import { SqDistributionTag, SqValue, SqValueTag } from "@quri/squiggle-lang";
import { NumberShower } from "../NumberShower";
import { DistributionChart, defaultPlot, makePlot } from "../DistributionChart";
import { FunctionChart } from "../FunctionChart";
import clsx from "clsx";
import { VariableBox } from "./VariableBox";
import { ItemSettingsMenu } from "./ItemSettingsMenu";
import { hasMassBelowZero } from "../../lib/distributionUtils";
import { MergedItemSettings } from "./utils";
import { ViewerContext } from "./ViewerContext";
/*
// DISABLED FOR 0.4 branch, for now
function getRange<a>(x: declaration<a>) {
const first = x.args[0];
switch (first.tag) {
case "Float": {
return { floats: { min: first.value.min, max: first.value.max } };
}
case "Date": {
return { time: { min: first.value.min, max: first.value.max } };
}
}
}
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
const range = getRange(x);
const min = range.floats ? range.floats.min : 0;
const max = range.floats ? range.floats.max : 10;
return {
start: min,
stop: max,
count: 20,
};
}
*/
const VariableList: React.FC<{
value: SqValue;
heading: string;
children: (settings: MergedItemSettings) => React.ReactNode;
}> = ({ value, heading, children }) => (
<VariableBox value={value} heading={heading}>
{(settings) => (
<div
className={clsx(
"space-y-3",
value.location.path.items.length ? "pt-1 mt-1" : null
)}
>
{children(settings)}
</div>
)}
</VariableBox>
);
export interface Props {
/** The output of squiggle's run */
value: SqValue;
width?: number;
}
export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
const { getMergedSettings } = useContext(ViewerContext);
switch (value.tag) {
case SqValueTag.Number:
return (
<VariableBox value={value} heading="Number">
{() => (
<div className="font-semibold text-slate-600">
<NumberShower precision={3} number={value.value} />
</div>
)}
</VariableBox>
);
case SqValueTag.Distribution: {
const distType = value.value.tag;
return (
<VariableBox
value={value}
heading={`Distribution (${distType})\n${
distType === SqDistributionTag.Symbolic
? value.value.toString()
: ""
}`}
renderSettingsMenu={({ onChange }) => {
const shape = value.value.pointSet(
getMergedSettings(value.location).environment
);
return (
<ItemSettingsMenu
value={value}
onChange={onChange}
disableLogX={
shape.tag === "Ok" && hasMassBelowZero(shape.value.asShape())
}
withFunctionSettings={false}
/>
);
}}
>
{(settings) => {
return (
<DistributionChart
plot={defaultPlot(value.value)}
environment={settings.environment}
{...settings.distributionPlotSettings}
height={settings.height}
width={width}
/>
);
}}
</VariableBox>
);
}
case SqValueTag.String:
return (
<VariableBox value={value} heading="String">
{() => (
<>
<span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold font-mono">
{value.value}
</span>
<span className="text-slate-400">"</span>
</>
)}
</VariableBox>
);
case SqValueTag.Bool:
return (
<VariableBox value={value} heading="Boolean">
{() => value.value.toString()}
</VariableBox>
);
case SqValueTag.Date:
return (
<VariableBox value={value} heading="Date">
{() => value.value.toDateString()}
</VariableBox>
);
case SqValueTag.Void:
return (
<VariableBox value={value} heading="Void">
{() => "Void"}
</VariableBox>
);
case SqValueTag.TimeDuration: {
return (
<VariableBox value={value} heading="Time Duration">
{() => <NumberShower precision={3} number={value.value} />}
</VariableBox>
);
}
case SqValueTag.Lambda:
return (
<VariableBox
value={value}
heading="Function"
renderSettingsMenu={({ onChange }) => {
return (
<ItemSettingsMenu
value={value}
onChange={onChange}
withFunctionSettings={true}
/>
);
}}
>
{(settings) => (
<>
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${value.value
.parameters()
.join(",")})`}</div>
<FunctionChart
fn={value.value}
chartSettings={settings.chartSettings}
distributionPlotSettings={settings.distributionPlotSettings}
height={settings.height}
environment={{
sampleCount: settings.environment.sampleCount / 10,
xyPointLength: settings.environment.xyPointLength / 10,
}}
/>
</>
)}
</VariableBox>
);
case SqValueTag.Declaration: {
return (
<VariableBox
value={value}
heading="Function Declaration"
renderSettingsMenu={({ onChange }) => {
return (
<ItemSettingsMenu
onChange={onChange}
value={value}
withFunctionSettings={true}
/>
);
}}
>
{(settings) => (
<div>NOT IMPLEMENTED IN 0.4 YET</div>
// <FunctionChart
// fn={expression.value.fn}
// chartSettings={getChartSettings(expression.value)}
// distributionPlotSettings={settings.distributionPlotSettings}
// height={settings.height}
// environment={{
// sampleCount: settings.environment.sampleCount / 10,
// xyPointLength: settings.environment.xyPointLength / 10,
// }}
// />
)}
</VariableBox>
);
}
case SqValueTag.Record:
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 (
<VariableList value={value} heading="Array">
{(_) =>
value.value
.getValues()
.map((r, i) => (
<ExpressionViewer
key={i}
value={r}
width={width !== undefined ? width - 20 : width}
/>
))
}
</VariableList>
);
default: {
return (
<VariableList value={value} heading="Error">
{() => (
<div>
<span>No display for type: </span>{" "}
<span className="font-semibold text-slate-600">
{(value as { tag: string }).tag}
</span>
</div>
)}
</VariableList>
);
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
import { DistributionPlottingSettings } from "../DistributionChart";
import { FunctionChartSettings } from "../FunctionChart";
import { environment, SqValueLocation } from "@quri/squiggle-lang";
export type LocalItemSettings = {
collapsed: boolean;
distributionPlotSettings?: Partial<DistributionPlottingSettings>;
chartSettings?: Partial<FunctionChartSettings>;
height?: number;
environment?: Partial<environment>;
};
export type MergedItemSettings = {
distributionPlotSettings: DistributionPlottingSettings;
chartSettings: FunctionChartSettings;
height: number;
environment: environment;
};
export const locationAsString = (location: SqValueLocation) =>
location.path.items.join(".");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
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 { 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,79 @@ 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 timeTickFormat = "%b %d, %Y %H:%M";
const width = 500;
export function buildVegaSpec( export function buildVegaSpec(
specOptions: DistributionChartSpecOptions & { maxY: number } specOptions: DistributionChartSpecOptions
): VisualizationSpec { ): VisualizationSpec {
const { let {
format = ".9~s",
color = "#739ECC",
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 +148,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: "datum.y",
}, },
}, },
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 +0,0 @@
import { SqShape } from "@quri/squiggle-lang";
export const hasMassBelowZero = (shape: SqShape) =>
shape.continuous.some((x) => x.x <= 0) ||
shape.discrete.some((x) => x.x <= 0);

View File

@ -0,0 +1,64 @@
import {
bindings,
environment,
jsImports,
run,
runPartial,
} from "@quri/squiggle-lang";
import { useEffect, useMemo, useState } from "react";
type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = {
code: string;
bindings?: bindings;
jsImports?: jsImports;
environment?: environment;
onChange?: (expr: Extract<T, { tag: "Ok" }>["value"] | undefined) => void;
};
const useSquiggleAny = <T extends ReturnType<typeof run | typeof runPartial>>(
args: SquiggleArgs<T>,
f: (...args: Parameters<typeof run>) => T
) => {
const result: T = useMemo<T>(
() => f(args.code, args.bindings, args.environment, args.jsImports),
[f, args.code, args.bindings, args.environment, args.jsImports]
);
const { onChange } = args;
useEffect(() => {
onChange?.(result.tag === "Ok" ? result.value : undefined);
}, [result, onChange]);
return result;
};
export const useSquigglePartial = (
args: SquiggleArgs<ReturnType<typeof runPartial>>
) => {
return useSquiggleAny(args, runPartial);
};
export const useSquiggle = (args: SquiggleArgs<ReturnType<typeof run>>) => {
return useSquiggleAny(args, run);
};
type ControlledValueArgs<T> = {
value?: T;
defaultValue: T;
onChange?: (x: T) => void;
};
export function useMaybeControlledValue<T>(
args: ControlledValueArgs<T>
): [T, (x: T) => void] {
let [uncontrolledValue, setUncontrolledValue] = useState(args.defaultValue);
let value = args.value ?? uncontrolledValue;
let onChange = (newValue: T) => {
if (args.value === undefined) {
// uncontrolled mode
setUncontrolledValue(newValue);
}
args.onChange?.(newValue);
};
return [value, onChange];
}

View File

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

View File

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

View File

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

View File

@ -1,97 +0,0 @@
import {
result,
SqError,
SqProject,
SqRecord,
SqValue,
environment,
} from "@quri/squiggle-lang";
import { useEffect, useMemo } from "react";
import { JsImports, jsImportsToSquiggleCode } from "../jsImports";
import * as uuid from "uuid";
type SquiggleArgs = {
environment?: environment;
code: string;
executionId?: number;
jsImports?: JsImports;
project?: SqProject;
continues?: string[];
onChange?: (expr: SqValue | undefined, sourceName: string) => void;
};
export type ResultAndBindings = {
result: result<SqValue, SqError>;
bindings: SqRecord;
};
const importSourceName = (sourceName: string) => "imports-" + sourceName;
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
[
args.code,
args.jsImports,
args.executionId,
sourceName,
continues,
project,
env,
]
);
const { onChange } = args;
useEffect(() => {
onChange?.(
result.result.tag === "Ok" ? result.result.value : undefined,
sourceName
);
}, [result, onChange, sourceName]);
useEffect(() => {
return () => {
project.removeSource(sourceName);
if (project.getSource(importSourceName(sourceName)))
project.removeSource(importSourceName(sourceName));
};
}, [project, sourceName]);
return result;
};

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

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

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

View File

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

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

View File

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

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

View File

@ -1,10 +1,6 @@
open Jest open Jest
open Expect open Expect
open TestHelpers
let env: GenericDist.env = {
sampleCount: 100,
xyPointLength: 100,
}
let { let {
normalDist5, normalDist5,
@ -34,7 +30,7 @@ describe("sparkline", () => {
expected: DistributionOperation.outputType, expected: DistributionOperation.outputType,
) => { ) => {
test(name, () => { test(name, () => {
let result = DistributionOperation.run(~env, FromDist(#ToString(ToSparkline(20)), dist)) let result = DistributionOperation.run(~env, FromDist(ToString(ToSparkline(20)), dist))
expect(result)->toEqual(expected) expect(result)->toEqual(expected)
}) })
} }
@ -81,8 +77,8 @@ describe("sparkline", () => {
describe("toPointSet", () => { describe("toPointSet", () => {
test("on symbolic normal distribution", () => { test("on symbolic normal distribution", () => {
let result = let result =
run(FromDist(#ToDist(ToPointSet), normalDist5)) run(FromDist(ToDist(ToPointSet), normalDist5))
->outputMap(FromDist(#ToFloat(#Mean))) ->outputMap(FromDist(ToFloat(#Mean)))
->toFloat ->toFloat
->toExt ->toExt
expect(result)->toBeSoCloseTo(5.0, ~digits=0) expect(result)->toBeSoCloseTo(5.0, ~digits=0)
@ -90,10 +86,10 @@ describe("toPointSet", () => {
test("on sample set", () => { test("on sample set", () => {
let result = let result =
run(FromDist(#ToDist(ToPointSet), normalDist5)) run(FromDist(ToDist(ToPointSet), normalDist5))
->outputMap(FromDist(#ToDist(ToSampleSet(1000)))) ->outputMap(FromDist(ToDist(ToSampleSet(1000))))
->outputMap(FromDist(#ToDist(ToPointSet))) ->outputMap(FromDist(ToDist(ToPointSet)))
->outputMap(FromDist(#ToFloat(#Mean))) ->outputMap(FromDist(ToFloat(#Mean)))
->toFloat ->toFloat
->toExt ->toExt
expect(result)->toBeSoCloseTo(5.0, ~digits=-1) expect(result)->toBeSoCloseTo(5.0, ~digits=-1)

View File

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

@ -19,6 +19,7 @@ exception MixtureFailed
let float1 = 1.0 let float1 = 1.0
let float2 = 2.0 let float2 = 2.0
let float3 = 3.0 let float3 = 3.0
let point1 = TestHelpers.mkDelta(float1) let {mkDelta} = module(TestHelpers)
let point2 = TestHelpers.mkDelta(float2) let point1 = mkDelta(float1)
let point3 = TestHelpers.mkDelta(float3) let point2 = mkDelta(float2)
let point3 = mkDelta(float3)

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

@ -3,7 +3,6 @@ open Expect
open TestHelpers open TestHelpers
open GenericDist_Fixtures open GenericDist_Fixtures
let klDivergence = DistributionOperation.Constructors.LogScore.distEstimateDistAnswer(~env)
// integral from low to high of 1 / (high - low) log(normal(mean, stdev)(x) / (1 / (high - low))) dx // integral from low to high of 1 / (high - low) log(normal(mean, stdev)(x) / (1 / (high - low))) dx
let klNormalUniform = (mean, stdev, low, high): float => let klNormalUniform = (mean, stdev, low, high): float =>
-.Js.Math.log((high -. low) /. Js.Math.sqrt(2.0 *. MagicNumbers.Math.pi *. stdev ** 2.0)) +. -.Js.Math.log((high -. low) /. Js.Math.sqrt(2.0 *. MagicNumbers.Math.pi *. stdev ** 2.0)) +.
@ -12,14 +11,17 @@ let klNormalUniform = (mean, stdev, low, high): float =>
(mean ** 2.0 -. (high +. low) *. mean +. (low ** 2.0 +. high *. low +. high ** 2.0) /. 3.0) (mean ** 2.0 -. (high +. low) *. mean +. (low ** 2.0 +. high *. low +. high ** 2.0) /. 3.0)
describe("klDivergence: continuous -> continuous -> float", () => { describe("klDivergence: continuous -> continuous -> float", () => {
let klDivergence = DistributionOperation.Constructors.klDivergence(~env)
let testUniform = (lowAnswer, highAnswer, lowPrediction, highPrediction) => { let testUniform = (lowAnswer, highAnswer, lowPrediction, highPrediction) => {
test("of two uniforms is equal to the analytic expression", () => { test("of two uniforms is equal to the analytic expression", () => {
let answer = let answer =
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)
@ -56,7 +58,7 @@ describe("klDivergence: continuous -> continuous -> float", () => {
let kl = E.R.liftJoin2(klDivergence, prediction, answer) let kl = E.R.liftJoin2(klDivergence, prediction, answer)
switch kl { switch kl {
| Ok(kl') => kl'->expect->toBeSoCloseTo(analyticalKl, ~digits=2) | Ok(kl') => kl'->expect->toBeSoCloseTo(analyticalKl, ~digits=3)
| Error(err) => { | Error(err) => {
Js.Console.log(DistributionTypes.Error.toString(err)) Js.Console.log(DistributionTypes.Error.toString(err))
raise(KlFailed) raise(KlFailed)
@ -80,6 +82,7 @@ describe("klDivergence: continuous -> continuous -> float", () => {
}) })
describe("klDivergence: discrete -> discrete -> float", () => { describe("klDivergence: discrete -> discrete -> float", () => {
let klDivergence = DistributionOperation.Constructors.klDivergence(~env)
let mixture = a => DistributionTypes.DistributionOperation.Mixture(a) let mixture = a => DistributionTypes.DistributionOperation.Mixture(a)
let a' = [(point1, 1e0), (point2, 1e0)]->mixture->run let a' = [(point1, 1e0), (point2, 1e0)]->mixture->run
let b' = [(point1, 1e0), (point2, 1e0), (point3, 1e0)]->mixture->run let b' = [(point1, 1e0), (point2, 1e0), (point3, 1e0)]->mixture->run
@ -114,6 +117,7 @@ describe("klDivergence: discrete -> discrete -> float", () => {
}) })
describe("klDivergence: mixed -> mixed -> float", () => { describe("klDivergence: mixed -> mixed -> float", () => {
let klDivergence = DistributionOperation.Constructors.klDivergence(~env)
let mixture' = a => DistributionTypes.DistributionOperation.Mixture(a) let mixture' = a => DistributionTypes.DistributionOperation.Mixture(a)
let mixture = a => { let mixture = a => {
let dist' = a->mixture'->run let dist' = a->mixture'->run
@ -182,18 +186,18 @@ 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)
let interpolator = XYShape.XtoY.continuousInterpolator(#Stepwise, #UseZero) let interpolator = XYShape.XtoY.continuousInterpolator(#Stepwise, #UseZero)
let integrand = PointSetDist_Scoring.WithDistAnswer.integrand let integrand = PointSetDist_Scoring.KLDivergence.integrand
let result = switch (answerWrapped, predictionWrapped) { let result = switch (answerWrapped, predictionWrapped) {
| (Ok(Dist(PointSet(Continuous(a)))), Ok(Dist(PointSet(Continuous(b))))) => | (Ok(Dist(PointSet(Continuous(a)))), Ok(Dist(PointSet(Continuous(b))))) =>
Some(combineAlongSupportOfSecondArgument(interpolator, integrand, a.xyShape, b.xyShape)) Some(combineAlongSupportOfSecondArgument(integrand, interpolator, a.xyShape, b.xyShape))
| _ => None | _ => None
} }
result result

View File

@ -11,7 +11,7 @@ describe("mixture", () => {
let (mean1, mean2) = tup let (mean1, mean2) = tup
let meanValue = { let meanValue = {
run(Mixture([(mkNormal(mean1, 9e-1), 0.5), (mkNormal(mean2, 9e-1), 0.5)]))->outputMap( run(Mixture([(mkNormal(mean1, 9e-1), 0.5), (mkNormal(mean2, 9e-1), 0.5)]))->outputMap(
FromDist(#ToFloat(#Mean)), FromDist(ToFloat(#Mean)),
) )
} }
meanValue->unpackFloat->expect->toBeSoCloseTo((mean1 +. mean2) /. 2.0, ~digits=-1) meanValue->unpackFloat->expect->toBeSoCloseTo((mean1 +. mean2) /. 2.0, ~digits=-1)
@ -28,7 +28,7 @@ describe("mixture", () => {
let meanValue = { let meanValue = {
run( run(
Mixture([(mkBeta(alpha, beta), betaWeight), (mkExponential(rate), exponentialWeight)]), Mixture([(mkBeta(alpha, beta), betaWeight), (mkExponential(rate), exponentialWeight)]),
)->outputMap(FromDist(#ToFloat(#Mean))) )->outputMap(FromDist(ToFloat(#Mean)))
} }
let betaMean = 1.0 /. (1.0 +. beta /. alpha) let betaMean = 1.0 /. (1.0 +. beta /. alpha)
let exponentialMean = 1.0 /. rate let exponentialMean = 1.0 /. rate
@ -52,7 +52,7 @@ describe("mixture", () => {
(mkUniform(low, high), uniformWeight), (mkUniform(low, high), uniformWeight),
(mkLognormal(mu, sigma), lognormalWeight), (mkLognormal(mu, sigma), lognormalWeight),
]), ]),
)->outputMap(FromDist(#ToFloat(#Mean))) )->outputMap(FromDist(ToFloat(#Mean)))
} }
let uniformMean = (low +. high) /. 2.0 let uniformMean = (low +. high) /. 2.0
let lognormalMean = mu +. sigma ** 2.0 /. 2.0 let lognormalMean = mu +. sigma ** 2.0 /. 2.0

View File

@ -1,68 +0,0 @@
open Jest
open Expect
open TestHelpers
open GenericDist_Fixtures
exception ScoreFailed
describe("WithScalarAnswer: discrete -> scalar -> score", () => {
let mixture = a => DistributionTypes.DistributionOperation.Mixture(a)
let pointA = mkDelta(3.0)
let pointB = mkDelta(2.0)
let pointC = mkDelta(1.0)
let pointD = mkDelta(0.0)
test("score: agrees with analytical answer when finite", () => {
let prediction' = [(pointA, 0.25), (pointB, 0.25), (pointC, 0.25), (pointD, 0.25)]->mixture->run
let prediction = switch prediction' {
| Dist(PointSet(p)) => p
| _ => raise(MixtureFailed)
}
let answer = 2.0 // So this is: assigning 100% probability to 2.0
let result = PointSetDist_Scoring.WithScalarAnswer.score(~estimate=prediction, ~answer)
switch result {
| Ok(x) => x->expect->toEqual(-.Js.Math.log(0.25 /. 1.0))
| _ => raise(ScoreFailed)
}
})
test("score: agrees with analytical answer when finite", () => {
let prediction' = [(pointA, 0.75), (pointB, 0.25)]->mixture->run
let prediction = switch prediction' {
| Dist(PointSet(p)) => p
| _ => raise(MixtureFailed)
}
let answer = 3.0 // So this is: assigning 100% probability to 2.0
let result = PointSetDist_Scoring.WithScalarAnswer.score(~estimate=prediction, ~answer)
switch result {
| Ok(x) => x->expect->toEqual(-.Js.Math.log(0.75 /. 1.0))
| _ => raise(ScoreFailed)
}
})
test("scoreWithPrior: agrees with analytical answer when finite", () => {
let prior' = [(pointA, 0.5), (pointB, 0.5)]->mixture->run
let prediction' = [(pointA, 0.75), (pointB, 0.25)]->mixture->run
let prediction = switch prediction' {
| Dist(PointSet(p)) => p
| _ => raise(MixtureFailed)
}
let prior = switch prior' {
| Dist(PointSet(p)) => p
| _ => raise(MixtureFailed)
}
let answer = 3.0 // So this is: assigning 100% probability to 2.0
let result = PointSetDist_Scoring.WithScalarAnswer.scoreWithPrior(
~estimate=prediction,
~answer,
~prior,
)
switch result {
| Ok(x) => x->expect->toEqual(-.Js.Math.log(0.75 /. 1.0) -. -.Js.Math.log(0.5 /. 1.0))
| _ => raise(ScoreFailed)
}
})
})

View File

@ -3,39 +3,39 @@ 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 => {
let normalValue = mkNormal(mean, 2.0) let normalValue = mkNormal(mean, 2.0)
let normalizedValue = run(FromDist(#ToDist(Normalize), normalValue)) let normalizedValue = run(FromDist(ToDist(Normalize), normalValue))
normalizedValue->unpackDist->expect->toEqual(normalValue) normalizedValue->unpackDist->expect->toEqual(normalValue)
}) })
}) })
describe("(Symbolic) mean", () => { describe("(Symbolic) mean", () => {
testAll("of normal distributions", list{-1e8, -16.0, -1e-2, 0.0, 1e-4, 32.0, 1e16}, mean => { testAll("of normal distributions", list{-1e8, -16.0, -1e-2, 0.0, 1e-4, 32.0, 1e16}, mean => {
run(FromDist(#ToFloat(#Mean), mkNormal(mean, 4.0)))->unpackFloat->expect->toBeCloseTo(mean) run(FromDist(ToFloat(#Mean), mkNormal(mean, 4.0)))->unpackFloat->expect->toBeCloseTo(mean)
}) })
Skip.test("of normal(0, -1) (it NaNs out)", () => { Skip.test("of normal(0, -1) (it NaNs out)", () => {
run(FromDist(#ToFloat(#Mean), mkNormal(1e1, -1e0)))->unpackFloat->expect->ExpectJs.toBeFalsy run(FromDist(ToFloat(#Mean), mkNormal(1e1, -1e0)))->unpackFloat->expect->ExpectJs.toBeFalsy
}) })
test("of normal(0, 1e-8) (it doesn't freak out at tiny stdev)", () => { test("of normal(0, 1e-8) (it doesn't freak out at tiny stdev)", () => {
run(FromDist(#ToFloat(#Mean), mkNormal(0.0, 1e-8)))->unpackFloat->expect->toBeCloseTo(0.0) run(FromDist(ToFloat(#Mean), mkNormal(0.0, 1e-8)))->unpackFloat->expect->toBeCloseTo(0.0)
}) })
testAll("of exponential distributions", list{1e-7, 2.0, 10.0, 100.0}, rate => { testAll("of exponential distributions", list{1e-7, 2.0, 10.0, 100.0}, rate => {
let meanValue = run( let meanValue = run(
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Exponential({rate: rate}))), FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Exponential({rate: rate}))),
) )
meanValue->unpackFloat->expect->toBeCloseTo(1.0 /. rate) // https://en.wikipedia.org/wiki/Exponential_distribution#Mean,_variance,_moments,_and_median meanValue->unpackFloat->expect->toBeCloseTo(1.0 /. rate) // https://en.wikipedia.org/wiki/Exponential_distribution#Mean,_variance,_moments,_and_median
}) })
test("of a cauchy distribution", () => { test("of a cauchy distribution", () => {
let meanValue = run( let meanValue = run(
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Cauchy({local: 1.0, scale: 1.0}))), FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Cauchy({local: 1.0, scale: 1.0}))),
) )
meanValue->unpackFloat->expect->toBeSoCloseTo(1.0098094001641797, ~digits=5) meanValue->unpackFloat->expect->toBeSoCloseTo(1.0098094001641797, ~digits=5)
//-> toBe(GenDistError(Other("Cauchy distributions may have no mean value."))) //-> toBe(GenDistError(Other("Cauchy distributions may have no mean value.")))
@ -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
}, },
@ -69,7 +72,7 @@ describe("(Symbolic) mean", () => {
// TODO: When we have our theory of validators we won't want this to be NaN but to be an error. // TODO: When we have our theory of validators we won't want this to be NaN but to be an error.
test("of beta(0, 0)", () => { test("of beta(0, 0)", () => {
let meanValue = run( let meanValue = run(
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Beta({alpha: 0.0, beta: 0.0}))), FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Beta({alpha: 0.0, beta: 0.0}))),
) )
meanValue->unpackFloat->expect->ExpectJs.toBeFalsy meanValue->unpackFloat->expect->ExpectJs.toBeFalsy
}) })
@ -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,14 +112,14 @@ describe("(Symbolic) mean", () => {
tup => { tup => {
let (low, high) = tup let (low, high) = tup
let meanValue = run( let meanValue = run(
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Uniform({low, high}))), FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Uniform({low: low, high: high}))),
) )
meanValue->unpackFloat->expect->toBeCloseTo((low +. high) /. 2.0) // https://en.wikipedia.org/wiki/Continuous_uniform_distribution#Moments meanValue->unpackFloat->expect->toBeCloseTo((low +. high) /. 2.0) // https://en.wikipedia.org/wiki/Continuous_uniform_distribution#Moments
}, },
) )
test("of a float", () => { test("of a float", () => {
let meanValue = run(FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Float(7.7)))) let meanValue = run(FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Float(7.7))))
meanValue->unpackFloat->expect->toBeCloseTo(7.7) meanValue->unpackFloat->expect->toBeCloseTo(7.7)
}) })
}) })

View File

@ -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,4 @@
open Jest
open Expect
test("todo", () => expect("1")->toBe("1"))

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,41 @@
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')")
testEval(
"addOne(t)=t+1; toList(mapSamples(fromSamples([1,2,3,4,5,6]), addOne))",
"Ok([2,3,4,5,6,7])",
)
})
describe("builtin exception", () => {
//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 Module = Reducer_Module
let removeDefaultsInternal = (iev: InternalExpressionValue.t) => {
switch iev {
| InternalExpressionValue.IEvModule(nameSpace) =>
Module.removeOther(
nameSpace,
ReducerInterface.StdLib.internalStdLib,
)->InternalExpressionValue.IEvModule
| 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

@ -0,0 +1,119 @@
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
module ExpressionT = Reducer_Expression_T
module Module = Reducer_Module
module Bindings = Reducer_Module
module ErrorValue = Reducer_ErrorValue
open Jest
open Expect
// ----------------------
// --- Start of Module File
// ----------------------
module FooImplementation = {
// As this is a Rescript module, functions can use other functions in this module
// and in other stdLib modules implemented this way.
// Embedding function definitions in to switch statements is a bad practice
// - to reduce line count or to
let fooNumber = 0.0
let fooString = "Foo String"
let fooBool = true
let makeFoo = (a: string, b: string, _environment): string => `I am ${a}-foo and I am ${b}-foo`
let makeBar = (a: float, b: float, _environment): string =>
`I am ${a->Js.Float.toString}-bar and I am ${b->Js.Float.toString}-bar`
// You can also define functions that has their internal errors
let makeReturningError = (_a: float, _b: float, _environment): result<float, ErrorValue.t> =>
if false {
0.->Ok
} else {
ErrorValue.RETodo("test error")->Error
}
}
// There is a potential for type modules to define lift functions
// for their own type to get rid of switch statements.
module FooFFI = {
let makeFoo: ExpressionT.optionFfiFn = (args: array<InternalExpressionValue.t>, environment) => {
switch args {
| [IEvString(a), IEvString(b)] => FooImplementation.makeFoo(a, b, environment)->IEvString->Some
| _ => None
}
}
let makeBar: ExpressionT.optionFfiFn = (args: array<InternalExpressionValue.t>, environment) =>
switch args {
| [IEvNumber(a), IEvNumber(b)] => FooImplementation.makeBar(a, b, environment)->IEvString->Some
| _ => None
}
let makeReturningError: ExpressionT.optionFfiFnReturningResult = (
args: array<InternalExpressionValue.t>,
environment,
) =>
switch args {
| [IEvNumber(a), IEvNumber(b)] =>
FooImplementation.makeReturningError(a, b, environment)
->Belt.Result.map(v => v->InternalExpressionValue.IEvNumber)
->Some
| _ => None
}
}
let fooModule: Module.t =
Module.emptyStdLib
->Module.defineNumber("fooNumber", FooImplementation.fooNumber)
->Module.defineString("fooString", FooImplementation.fooString)
->Module.defineBool("fooBool", FooImplementation.fooBool)
->Module.defineFunction("makeFoo", FooFFI.makeFoo)
->Module.defineFunction("makeBar", FooFFI.makeBar)
->Module.defineFunctionReturningResult("makeReturningError", FooFFI.makeReturningError)
let makeBindings = (prevBindings: Bindings.t): Bindings.t =>
prevBindings->Module.defineModule("Foo", fooModule)
// ----------------------
// --- End of Module File
// ----------------------
let stdLibWithFoo = Bindings.emptyBindings->makeBindings
let evalWithFoo = sourceCode =>
Reducer_Expression.parse(sourceCode)->Belt.Result.flatMap(expr =>
Reducer_Expression.reduceExpression(
expr,
stdLibWithFoo,
InternalExpressionValue.defaultEnvironment,
)
)
let evalToStringResultWithFoo = sourceCode =>
evalWithFoo(sourceCode)->InternalExpressionValue.toStringResult
describe("Module", () => {
test("fooNumber", () => {
let result = evalToStringResultWithFoo("Foo.fooNumber")
expect(result)->toEqual("Ok(0)")
})
test("fooString", () => {
let result = evalToStringResultWithFoo("Foo.fooString")
expect(result)->toEqual("Ok('Foo String')")
})
test("fooBool", () => {
let result = evalToStringResultWithFoo("Foo.fooBool")
expect(result)->toEqual("Ok(true)")
})
test("fooBool", () => {
let result = evalToStringResultWithFoo("Foo.fooBool")
expect(result)->toEqual("Ok(true)")
})
test("makeFoo", () => {
let result = evalToStringResultWithFoo("Foo.makeFoo('a', 'b')")
expect(result)->toEqual("Ok('I am a-foo and I am b-foo')")
})
test("makeFoo wrong arguments", () => {
let result = evalToStringResultWithFoo("Foo.makeFoo(1, 2)")
// Notice the error with types
expect(result)->toEqual("Error(Function not found: makeFoo(Number,Number))")
})
test("makeBar", () => {
let result = evalToStringResultWithFoo("Foo.makeBar(1, 2)")
expect(result)->toEqual("Ok('I am 1-bar and I am 2-bar')")
})
})

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

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