Compare commits
	
		
			2 Commits
		
	
	
		
			develop
			...
			no-thin-lo
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 05ef366a9d | |||
| 2944b0f79f | 
							
								
								
									
										20
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
								
							|  | @ -9,22 +9,22 @@ | |||
| # This also holds true for GitHub teams. | ||||
| 
 | ||||
| # Rescript  | ||||
| *.res @berekuk @OAGr | ||||
| *.resi @berekuk @OAGr | ||||
| *.res @OAGr @quinn-dougherty | ||||
| *.resi @OAGr @quinn-dougherty | ||||
| 
 | ||||
| # Typescript  | ||||
| *.tsx @Hazelfire @berekuk @OAGr  | ||||
| *.ts @Hazelfire @berekuk @OAGr | ||||
| *.tsx @Hazelfire @OAGr | ||||
| *.ts @Hazelfire @OAGr | ||||
| 
 | ||||
| # Javascript | ||||
| *.js @Hazelfire @berekuk @OAGr | ||||
| *.js @Hazelfire @OAGr | ||||
| 
 | ||||
| # Any opsy files | ||||
| .github/** @quinn-dougherty @berekuk @OAGr | ||||
| *.json @quinn-dougherty @Hazelfire @berekuk @OAGr | ||||
| *.y*ml @quinn-dougherty @berekuk @OAGr | ||||
| *.config.js @Hazelfire @berekuk @OAGr | ||||
| vercel.json @OAGr @berekuk @Hazelfire | ||||
| .github/** @quinn-dougherty @OAGr | ||||
| *.json @quinn-dougherty @Hazelfire @OAGr | ||||
| *.y*ml @quinn-dougherty @OAGr | ||||
| *.config.js @Hazelfire @OAGr | ||||
| netlify.toml @quinn-dougherty @OAGr @Hazelfire  | ||||
| 
 | ||||
| # Documentation | ||||
| *.md @quinn-dougherty @OAGr @Hazelfire | ||||
|  |  | |||
							
								
								
									
										11
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -11,14 +11,3 @@ updates: | |||
|       interval: "weekly" | ||||
|     commit-message: | ||||
|       prefix: "⬆️" | ||||
|     open-pull-requests-limit: 100 | ||||
|     labels: | ||||
|       - "dependencies" | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: "weekly" | ||||
|     commit-message: | ||||
|       prefix: "⬆️" | ||||
|     labels: | ||||
|       - "dependencies" | ||||
|  |  | |||
							
								
								
									
										87
									
								
								.github/workflows/ci-cachix.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										87
									
								
								.github/workflows/ci-cachix.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -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 | ||||
							
								
								
									
										227
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										227
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -1,4 +1,4 @@ | |||
| name: Squiggle packages checks | ||||
| name: Squiggle packages check | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|  | @ -9,40 +9,213 @@ on: | |||
|     branches: | ||||
|       - master | ||||
|       - develop | ||||
| 
 | ||||
| env: | ||||
|   TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} | ||||
|   TURBO_TEAM: quantified-uncertainty | ||||
|       - reducer-dev | ||||
| 
 | ||||
| jobs: | ||||
|   build-test-lint: | ||||
|     name: Build, test, lint | ||||
|   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: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Setup Node.js environment | ||||
|         uses: actions/setup-node@v3 | ||||
|       - id: skip_lang_check | ||||
|         name: Check if the changes are about squiggle-lang src files | ||||
|         uses: fkirc/skip-duplicate-actions@v3.4.1 | ||||
|         with: | ||||
|           node-version: 16 | ||||
|           cache: 'yarn' | ||||
|       - name: Install dependencies | ||||
|         run: yarn --frozen-lockfile | ||||
|       - name: Turbo run | ||||
|         run: npx turbo run build test lint bundle | ||||
|           paths: '["packages/squiggle-lang/**"]' | ||||
|       - id: skip_components_check | ||||
|         name: Check if the changes are about components src files | ||||
|         uses: fkirc/skip-duplicate-actions@v3.4.1 | ||||
|         with: | ||||
|           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: | ||||
|     name: Coverage | ||||
|   lang-lint: | ||||
|     name: Language lint | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: pre_check | ||||
|     if: ${{ needs.pre_check.outputs.should_skip_lang != 'true' }} | ||||
|     defaults: | ||||
|       run: | ||||
|         shell: bash | ||||
|         working-directory: packages/squiggle-lang | ||||
|     steps: | ||||
|       - 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: | ||||
|           fetch-depth: 2 | ||||
|       - name: Setup Node.js environment | ||||
|         uses: actions/setup-node@v2 | ||||
|       - name: Install dependencies from monorepo level | ||||
|         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: | ||||
|           node-version: 16 | ||||
|           cache: 'yarn' | ||||
|       - name: Install dependencies | ||||
|         run: yarn | ||||
|       - name: Coverage | ||||
|         run: npx turbo run coverage | ||||
|           dry: true | ||||
|           prettier_options: --check packages/components --ignore-path packages/components/.prettierignore | ||||
| 
 | ||||
|   components-bundle-build: | ||||
|     name: Components bundle and 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') }} | ||||
|     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 | ||||
|  |  | |||
							
								
								
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -33,11 +33,11 @@ jobs: | |||
| 
 | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|         uses: actions/checkout@v2 | ||||
| 
 | ||||
|       # Initializes the CodeQL tools for scanning. | ||||
|       - name: Initialize CodeQL | ||||
|         uses: github/codeql-action/init@v2 | ||||
|         uses: github/codeql-action/init@v1 | ||||
|         with: | ||||
|           languages: ${{ matrix.language }} | ||||
|           # 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). | ||||
|       # If this step fails, then you should remove it and run the build manually (see below) | ||||
|       - name: Autobuild | ||||
|         uses: github/codeql-action/autobuild@v2 | ||||
|         uses: github/codeql-action/autobuild@v1 | ||||
|       - name: Install dependencies | ||||
|         run: yarn | ||||
|       - name: Build rescript | ||||
|  | @ -65,4 +65,4 @@ jobs: | |||
|       #   make release | ||||
| 
 | ||||
|       - name: Perform CodeQL Analysis | ||||
|         uses: github/codeql-action/analyze@v2 | ||||
|         uses: github/codeql-action/analyze@v1 | ||||
|  |  | |||
							
								
								
									
										141
									
								
								.github/workflows/release-please.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										141
									
								
								.github/workflows/release-please.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -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
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -7,9 +7,3 @@ yarn-error.log | |||
| **/.sync.ffs_db | ||||
| .direnv | ||||
| .log | ||||
| 
 | ||||
| .vscode | ||||
| todo.txt | ||||
| result | ||||
| shell.nix | ||||
| .turbo | ||||
|  |  | |||
|  | @ -1,16 +1,15 @@ | |||
| .direnv | ||||
| *.bs.js | ||||
| *.gen.tsx | ||||
| packages/*/dist | ||||
| packages/components/storybook-static | ||||
| node_modules | ||||
| packages/*/node_modules | ||||
| packages/website/.docusaurus | ||||
| packages/squiggle-lang/lib | ||||
| packages/squiggle-lang/.nyc_output/ | ||||
| packages/squiggle-lang/coverage/ | ||||
| packages/squiggle-lang/.cache/ | ||||
| packages/website/build/ | ||||
| packages/squiggle-lang/src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js | ||||
| packages/vscode-ext/media/vendor/ | ||||
| packages/squiggle-lang/.nyc_output/ | ||||
| packages/*/dist | ||||
| result | ||||
|  |  | |||
|  | @ -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" | ||||
| } | ||||
|  | @ -1 +0,0 @@ | |||
| See the [Changelog.mdx page](./packages/website/docs/Changelog.mdx) for the changelog. | ||||
|  | @ -16,7 +16,7 @@ Squiggle is currently pre-alpha. | |||
| 
 | ||||
| # 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 | ||||
| 
 | ||||
|  | @ -28,7 +28,7 @@ Squiggle is a **monorepo** with three **packages**. | |||
| 
 | ||||
| # 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 | ||||
| 
 | ||||
|  | @ -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`. | ||||
| 
 | ||||
| - 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 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. | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										32
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								README.md
									
									
									
									
									
								
							|  | @ -12,8 +12,8 @@ _An estimation language_. | |||
| 
 | ||||
| - [Gallery](https://www.squiggle-language.com/docs/Discussions/Gallery) | ||||
| - [Squiggle playground](https://squiggle-language.com/playground) | ||||
| - [Language basics](https://www.squiggle-language.com/docs/Guides/Language) | ||||
| - [Squiggle functions source of truth](https://www.squiggle-language.com/docs/Guides/Functions) | ||||
| - [Language basics](https://www.squiggle-language.com/docs/Features/Language) | ||||
| - [Squiggle functions source of truth](https://www.squiggle-language.com/docs/Features/Functions) | ||||
| - [Known bugs](https://www.squiggle-language.com/docs/Discussions/Bugs) | ||||
| - [Original lesswrong sequence](https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3) | ||||
| - [Author your squiggle models as Observable notebooks](https://observablehq.com/@hazelfire/squiggle) | ||||
|  | @ -21,10 +21,10 @@ _An estimation language_. | |||
| 
 | ||||
| ## Our deployments | ||||
| 
 | ||||
| - **website/docs prod**: https://squiggle-language.com | ||||
| - **website/docs staging**: https://preview.squiggle-language.com | ||||
| - **components storybook prod**: https://components.squiggle-language.com | ||||
| - **components storybook staging**: https://preview-components.squiggle-language.com | ||||
| - **website/docs prod**: https://squiggle-language.com [](https://app.netlify.com/sites/squiggle-documentation/deploys) | ||||
| - **website/docs staging**: https://develop--squiggle-documentation.netlify.app/ | ||||
| - **components storybook prod**: https://squiggle-components.netlify.app/ [](https://app.netlify.com/sites/squiggle-components/deploys) | ||||
| - **components storybook staging**: https://develop--squiggle-components.netlify.app/ | ||||
| - **legacy (2020) playground**: https://playground.squiggle-language.com | ||||
| 
 | ||||
| ## Packages | ||||
|  | @ -51,25 +51,7 @@ For any project in the repo, begin by running `yarn` in the top level | |||
| yarn | ||||
| ``` | ||||
| 
 | ||||
| Then use `turbo` to build the specific packages or the entire monorepo: | ||||
| 
 | ||||
| ```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). | ||||
| See `packages/*/README.md` to work with whatever project you're interested in. | ||||
| 
 | ||||
| # Contributing | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										79
									
								
								flake.lock
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								flake.lock
									
									
									
									
									
								
							|  | @ -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 | ||||
| } | ||||
							
								
								
									
										99
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										99
									
								
								flake.nix
									
									
									
									
									
								
							|  | @ -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); | ||||
| } | ||||
|  | @ -1 +0,0 @@ | |||
| Visit `quantified-uncertainty.cachix.org` for information about how to add our binary cache to your local dev environment. | ||||
|  | @ -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 ]; | ||||
|   }; | ||||
| } | ||||
|  | @ -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"; | ||||
|   }; | ||||
| } | ||||
|  | @ -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 | ||||
|     ''; | ||||
|   }; | ||||
| } | ||||
|  | @ -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 | ||||
|     ''; | ||||
|   }; | ||||
| 
 | ||||
| } | ||||
|  | @ -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"; | ||||
|   }; | ||||
| } | ||||
|  | @ -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
									
								
							
							
						
						
									
										18
									
								
								nixos.sh
									
									
									
									
									
										Executable 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 | ||||
|  | @ -2,11 +2,12 @@ | |||
|   "private": true, | ||||
|   "name": "squiggle", | ||||
|   "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": { | ||||
|     "prettier": "^2.7.1", | ||||
|     "turbo": "^1.5.5" | ||||
|     "prettier": "^2.7.1" | ||||
|   }, | ||||
|   "workspaces": [ | ||||
|     "packages/*" | ||||
|  |  | |||
|  | @ -20,30 +20,3 @@ Runs compilation in the current directory and all of its subdirectories. | |||
| ### `npx squiggle-cli-experimental watch` | ||||
| 
 | ||||
| Watches `.squiggleU` files in the current directory (and subdirectories) and rebuilds them when they are saved. Note that this will _not_ rebuild files when their dependencies are changed, just when they are changed directly. | ||||
| 
 | ||||
| ## 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. | ||||
|  |  | |||
|  | @ -7,15 +7,13 @@ | |||
|   "bin": "index.js", | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "start": "node .", | ||||
|     "lint": "prettier --check .", | ||||
|     "format": "prettier --write ." | ||||
|     "start": "node ." | ||||
|   }, | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "chalk": "^5.1.0", | ||||
|     "chalk": "^5.0.1", | ||||
|     "chokidar": "^3.5.3", | ||||
|     "commander": "^9.4.1", | ||||
|     "commander": "^9.3.0", | ||||
|     "fs": "^0.0.1-security", | ||||
|     "glob": "^8.0.3", | ||||
|     "indent-string": "^5.0.0" | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ Add to `App.js`: | |||
| ```jsx | ||||
| import { SquiggleEditor } from "@quri/squiggle-components"; | ||||
| <SquiggleEditor | ||||
|   defaultCode="x = beta($alpha, 10); x + $shift" | ||||
|   initialSquiggleString="x = beta($alpha, 10); x + $shift" | ||||
|   jsImports={{ alpha: 3, shift: 20 }} | ||||
| />; | ||||
| ``` | ||||
|  | @ -50,10 +50,11 @@ export function DynamicSquiggleChart({ squiggleString }) { | |||
|   } else { | ||||
|     return ( | ||||
|         <SquiggleChart | ||||
|           defaultCode={squiggleString} | ||||
|           squiggleString={squiggleString} | ||||
|           width={445} | ||||
|           height={200} | ||||
|           showSummary={true} | ||||
|           showTypes={true} | ||||
|         /> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -1,6 +0,0 @@ | |||
| /** @type {import('ts-jest').JestConfigWithTsJest} */ | ||||
| module.exports = { | ||||
|   preset: "ts-jest", | ||||
|   setupFilesAfterEnv: ["<rootDir>/test/setup.js"], | ||||
|   testEnvironment: "jsdom", | ||||
| }; | ||||
							
								
								
									
										8
									
								
								packages/components/netlify.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								packages/components/netlify.toml
									
									
									
									
									
										Normal 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" | ||||
|  | @ -1,73 +1,62 @@ | |||
| { | ||||
|   "name": "@quri/squiggle-components", | ||||
|   "version": "0.5.0", | ||||
|   "version": "0.2.20", | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "@floating-ui/react-dom": "^1.0.0", | ||||
|     "@floating-ui/react-dom-interactions": "^0.10.1", | ||||
|     "@headlessui/react": "^1.7.3", | ||||
|     "@headlessui/react": "^1.6.5", | ||||
|     "@heroicons/react": "^1.0.6", | ||||
|     "@hookform/resolvers": "^2.9.8", | ||||
|     "@quri/squiggle-lang": "^0.5.0", | ||||
|     "@hookform/resolvers": "^2.9.1", | ||||
|     "@quri/squiggle-lang": "^0.2.8", | ||||
|     "@react-hook/size": "^2.1.2", | ||||
|     "@types/uuid": "^8.3.4", | ||||
|     "clsx": "^1.2.1", | ||||
|     "framer-motion": "^7.5.3", | ||||
|     "clsx": "^1.1.1", | ||||
|     "lodash": "^4.17.21", | ||||
|     "react": "^18.1.0", | ||||
|     "react-ace": "^10.1.0", | ||||
|     "react-hook-form": "^7.37.0", | ||||
|     "react-hook-form": "^7.32.2", | ||||
|     "react-use": "^17.4.0", | ||||
|     "react-vega": "^7.6.0", | ||||
|     "uuid": "^9.0.0", | ||||
|     "react-vega": "^7.5.1", | ||||
|     "vega": "^5.22.1", | ||||
|     "vega-embed": "^6.21.0", | ||||
|     "vega-lite": "^5.5.0", | ||||
|     "vscode-uri": "^3.0.6", | ||||
|     "vega-lite": "^5.2.0", | ||||
|     "yup": "^0.32.11" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/plugin-proposal-private-property-in-object": "^7.18.6", | ||||
|     "@storybook/addon-actions": "^6.5.12", | ||||
|     "@storybook/addon-essentials": "^6.5.12", | ||||
|     "@storybook/addon-links": "^6.5.12", | ||||
|     "@storybook/builder-webpack5": "^6.5.12", | ||||
|     "@storybook/manager-webpack5": "^6.5.12", | ||||
|     "@babel/plugin-proposal-private-property-in-object": "^7.17.12", | ||||
|     "@storybook/addon-actions": "^6.5.9", | ||||
|     "@storybook/addon-essentials": "^6.5.9", | ||||
|     "@storybook/addon-links": "^6.5.9", | ||||
|     "@storybook/builder-webpack5": "^6.5.9", | ||||
|     "@storybook/manager-webpack5": "^6.5.9", | ||||
|     "@storybook/node-logger": "^6.5.9", | ||||
|     "@storybook/preset-create-react-app": "^4.1.2", | ||||
|     "@storybook/react": "^6.5.12", | ||||
|     "@testing-library/jest-dom": "^5.16.5", | ||||
|     "@testing-library/react": "^13.4.0", | ||||
|     "@testing-library/user-event": "^14.4.3", | ||||
|     "@storybook/react": "^6.5.9", | ||||
|     "@testing-library/jest-dom": "^5.16.4", | ||||
|     "@testing-library/react": "^13.3.0", | ||||
|     "@testing-library/user-event": "^14.2.1", | ||||
|     "@types/jest": "^27.5.0", | ||||
|     "@types/lodash": "^4.14.186", | ||||
|     "@types/node": "^18.8.3", | ||||
|     "@types/react": "^18.0.21", | ||||
|     "@types/styled-components": "^5.1.26", | ||||
|     "@types/uuid": "^8.3.4", | ||||
|     "@types/lodash": "^4.14.182", | ||||
|     "@types/node": "^18.0.0", | ||||
|     "@types/react": "^18.0.9", | ||||
|     "@types/react-dom": "^18.0.5", | ||||
|     "@types/styled-components": "^5.1.24", | ||||
|     "@types/webpack": "^5.28.0", | ||||
|     "canvas": "^2.10.1", | ||||
|     "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", | ||||
|     "postcss-cli": "^10.0.0", | ||||
|     "postcss-import": "^15.0.0", | ||||
|     "postcss-loader": "^7.0.1", | ||||
|     "postcss-nesting": "^10.2.0", | ||||
|     "postcss-cli": "^9.1.0", | ||||
|     "postcss-import": "^14.1.0", | ||||
|     "postcss-loader": "^7.0.0", | ||||
|     "react": "^18.1.0", | ||||
|     "react-dom": "^18.2.0", | ||||
|     "react-scripts": "^5.0.1", | ||||
|     "style-loader": "^3.3.1", | ||||
|     "tailwindcss": "^3.1.8", | ||||
|     "ts-jest": "^29.0.3", | ||||
|     "ts-loader": "^9.4.1", | ||||
|     "tsconfig-paths-webpack-plugin": "^4.0.0", | ||||
|     "typescript": "^4.8.4", | ||||
|     "web-vitals": "^3.0.3", | ||||
|     "webpack": "^5.74.0", | ||||
|     "tailwindcss": "^3.1.3", | ||||
|     "ts-loader": "^9.3.0", | ||||
|     "tsconfig-paths-webpack-plugin": "^3.5.2", | ||||
|     "typescript": "^4.7.3", | ||||
|     "web-vitals": "^2.1.4", | ||||
|     "webpack": "^5.73.0", | ||||
|     "webpack-cli": "^4.10.0", | ||||
|     "webpack-dev-server": "^4.11.1" | ||||
|     "webpack-dev-server": "^4.9.2" | ||||
|   }, | ||||
|   "peerDependencies": { | ||||
|     "react": "^16.8.0 || ^17 || ^18", | ||||
|  | @ -75,7 +64,7 @@ | |||
|   }, | ||||
|   "scripts": { | ||||
|     "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:storybook": "build-storybook -s public", | ||||
|     "build": "yarn run build:cjs && yarn run build:css && yarn run build:storybook", | ||||
|  | @ -83,10 +72,7 @@ | |||
|     "all": "yarn bundle && yarn build", | ||||
|     "lint": "prettier --check .", | ||||
|     "format": "prettier --write .", | ||||
|     "prepack": "yarn run build:cjs && yarn run bundle", | ||||
|     "test": "jest", | ||||
|     "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand", | ||||
|     "test:profile": "node --cpu-prof node_modules/.bin/jest --runInBand" | ||||
|     "prepack": "yarn bundle && tsc -b" | ||||
|   }, | ||||
|   "eslintConfig": { | ||||
|     "extends": [ | ||||
|  |  | |||
|  | @ -24,13 +24,13 @@ export const Alert: React.FC<{ | |||
|   children, | ||||
| }) => { | ||||
|   return ( | ||||
|     <div className={clsx("rounded-md p-4", backgroundColor)} role="status"> | ||||
|     <div className={clsx("rounded-md p-4", backgroundColor)}> | ||||
|       <div className="flex"> | ||||
|         <Icon | ||||
|           className={clsx("h-5 w-5 flex-shrink-0", iconColor)} | ||||
|           aria-hidden="true" | ||||
|         /> | ||||
|         <div className="ml-3 grow"> | ||||
|         <div className="ml-3"> | ||||
|           <header className={clsx("text-sm font-medium", headingColor)}> | ||||
|             {heading} | ||||
|           </header> | ||||
|  |  | |||
|  | @ -1,44 +1,31 @@ | |||
| import _ from "lodash"; | ||||
| import React, { FC, useMemo, useRef } from "react"; | ||||
| import React, { FC, useMemo } from "react"; | ||||
| import AceEditor from "react-ace"; | ||||
| 
 | ||||
| import "ace-builds/src-noconflict/mode-golang"; | ||||
| import "ace-builds/src-noconflict/theme-github"; | ||||
| 
 | ||||
| import { SqLocation } from "@quri/squiggle-lang"; | ||||
| 
 | ||||
| interface CodeEditorProps { | ||||
|   value: string; | ||||
|   onChange: (value: string) => void; | ||||
|   onSubmit?: () => void; | ||||
|   oneLine?: boolean; | ||||
|   width?: number; | ||||
|   height: number; | ||||
|   showGutter?: boolean; | ||||
|   errorLocations?: SqLocation[]; | ||||
| } | ||||
| 
 | ||||
| export const CodeEditor: FC<CodeEditorProps> = ({ | ||||
|   value, | ||||
|   onChange, | ||||
|   onSubmit, | ||||
|   height, | ||||
|   oneLine = false, | ||||
|   showGutter = false, | ||||
|   errorLocations = [], | ||||
|   height, | ||||
| }) => { | ||||
|   const lineCount = value.split("\n").length; | ||||
|   const id = useMemo(() => _.uniqueId(), []); | ||||
| 
 | ||||
|   // this is necessary because AceEditor binds commands on mount, see https://github.com/securingsincity/react-ace/issues/684
 | ||||
|   const onSubmitRef = useRef<typeof onSubmit | null>(null); | ||||
|   onSubmitRef.current = onSubmit; | ||||
| 
 | ||||
|   const editorEl = useRef<AceEditor | null>(null); | ||||
| 
 | ||||
|   return ( | ||||
|     <AceEditor | ||||
|       ref={editorEl} | ||||
|       value={value} | ||||
|       mode="golang" | ||||
|       theme="github" | ||||
|  | @ -55,22 +42,10 @@ export const CodeEditor: FC<CodeEditorProps> = ({ | |||
|       editorProps={{ | ||||
|         $blockScrolling: true, | ||||
|       }} | ||||
|       setOptions={{}} | ||||
|       commands={[ | ||||
|         { | ||||
|           name: "submit", | ||||
|           bindKey: { mac: "Cmd-Enter", win: "Ctrl-Enter" }, | ||||
|           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", | ||||
|       }))} | ||||
|       setOptions={{ | ||||
|         enableBasicAutocompletion: false, | ||||
|         enableLiveAutocompletion: false, | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -1,106 +1,83 @@ | |||
| import * as React from "react"; | ||||
| import { | ||||
|   SqDistribution, | ||||
|   Distribution, | ||||
|   result, | ||||
|   SqDistributionError, | ||||
|   resultMap, | ||||
|   SqRecord, | ||||
|   environment, | ||||
|   SqDistributionTag, | ||||
|   distributionError, | ||||
|   distributionErrorToString, | ||||
| } from "@quri/squiggle-lang"; | ||||
| import { Vega } from "react-vega"; | ||||
| import { Vega, VisualizationSpec } from "react-vega"; | ||||
| import * as chartSpecification from "../vega-specs/spec-distributions.json"; | ||||
| import { ErrorAlert } from "./Alert"; | ||||
| import { useSize } from "react-use"; | ||||
| import clsx from "clsx"; | ||||
| 
 | ||||
| import { | ||||
|   buildVegaSpec, | ||||
|   DistributionChartSpecOptions, | ||||
| } from "../lib/distributionSpecBuilder"; | ||||
|   linearXScale, | ||||
|   logXScale, | ||||
|   linearYScale, | ||||
|   expYScale, | ||||
| } from "./DistributionVegaScales"; | ||||
| import { NumberShower } from "./NumberShower"; | ||||
| import { Plot, parsePlot } from "../lib/plotParser"; | ||||
| import { flattenResult } from "../lib/utility"; | ||||
| import { hasMassBelowZero } from "../lib/distributionUtils"; | ||||
| 
 | ||||
| export type DistributionPlottingSettings = { | ||||
|   /** Whether to show a summary of means, stdev, percentiles etc */ | ||||
|   showSummary: boolean; | ||||
|   actions?: boolean; | ||||
| } & DistributionChartSpecOptions; | ||||
| 
 | ||||
| export type DistributionChartProps = { | ||||
|   plot: Plot; | ||||
|   environment: environment; | ||||
| type DistributionChartProps = { | ||||
|   distribution: Distribution; | ||||
|   width?: number; | ||||
|   height: number; | ||||
|   xAxisType?: "number" | "dateTime"; | ||||
| } & DistributionPlottingSettings; | ||||
|   /** Whether to show a summary of means, stdev, percentiles etc */ | ||||
|   showSummary: boolean; | ||||
|   /** Whether to show the user graph controls (scale etc) */ | ||||
|   showControls?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export function defaultPlot(distribution: SqDistribution): Plot { | ||||
|   return { distributions: [{ name: "default", distribution }] }; | ||||
| } | ||||
| export const DistributionChart: React.FC<DistributionChartProps> = ({ | ||||
|   distribution, | ||||
|   height, | ||||
|   showSummary, | ||||
|   width, | ||||
|   showControls = false, | ||||
| }) => { | ||||
|   const [isLogX, setLogX] = React.useState(false); | ||||
|   const [isExpY, setExpY] = React.useState(false); | ||||
| 
 | ||||
| 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) => { | ||||
|   const { | ||||
|     plot, | ||||
|     environment, | ||||
|     height, | ||||
|     showSummary, | ||||
|     width, | ||||
|     logX, | ||||
|     actions = false, | ||||
|   } = props; | ||||
|   const [sized] = useSize((size) => { | ||||
|     const shapes = flattenResult( | ||||
|       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") { | ||||
|     const p3wrapped = distribution.inv(0.03); | ||||
|     const p97wrapped = distribution.inv(0.97); | ||||
|     if (p3wrapped.tag == "Error") { | ||||
|       return <ErrorAlert heading="Distribution Calculation Error"> | ||||
|         {distributionErrorToString(p3wrapped.value)} | ||||
|       </ErrorAlert> | ||||
|     } else if (p97wrapped.tag == "Error") { | ||||
|       return <ErrorAlert heading="Distribution Calculation Error"> | ||||
|         {distributionErrorToString(p97wrapped.value)} | ||||
|       </ErrorAlert> | ||||
|     } | ||||
|     const p3 = p3wrapped.value | ||||
|     const p97 = p97wrapped.value | ||||
| 
 | ||||
|     const truncatedDistributionWrapper = distribution.truncate(p3, p97) | ||||
|     if (truncatedDistributionWrapper.tag == "Error") { | ||||
|       return <ErrorAlert heading="Distribution Truncation For Display Error"> | ||||
|         {distributionErrorToString(truncatedDistributionWrapper.value)} | ||||
|       </ErrorAlert> | ||||
|     } | ||||
|     const truncatedDistribution = truncatedDistributionWrapper.value | ||||
| 
 | ||||
|     const shape = truncatedDistribution.pointSet(); //distribution.pointSet();
 | ||||
|     if (shape.tag === "Error") { | ||||
|       return ( | ||||
|         <ErrorAlert heading="Distribution Error"> | ||||
|           {shapes.value.toString()} | ||||
|           {distributionErrorToString(shape.value)} | ||||
|         </ErrorAlert> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // if this is a sample set, include the samples
 | ||||
|     const samples: number[] = []; | ||||
|     for (const { distribution } of plot?.distributions) { | ||||
|       if (distribution.tag === SqDistributionTag.SampleSet) { | ||||
|         samples.push(...distribution.value()); | ||||
|       } | ||||
|     } | ||||
|     const massBelow0 = | ||||
|       shape.value.continuous.some((x) => x.x <= 0) || | ||||
|       shape.value.discrete.some((x) => x.x <= 0); | ||||
|     const spec = buildVegaSpec(isLogX, isExpY); | ||||
| 
 | ||||
|     const domain = shapes.value.flatMap((shape) => | ||||
|       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; | ||||
|     let widthProp = width ? width : size.width; | ||||
|     if (widthProp < 20) { | ||||
|       console.warn( | ||||
|         `Width of Distribution is set to ${widthProp}, which is too small` | ||||
|  | @ -108,37 +85,81 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => { | |||
|       widthProp = 20; | ||||
|     } | ||||
| 
 | ||||
|     const vegaData = { data: shapes.value, samples }; | ||||
| 
 | ||||
|     return ( | ||||
|       <div style={{ width: widthProp }}> | ||||
|         {logX && shapes.value.some(hasMassBelowZero) ? ( | ||||
|           <ErrorAlert heading="Log Domain Error"> | ||||
|             Cannot graph distribution with negative values on logarithmic scale. | ||||
|           </ErrorAlert> | ||||
|         ) : ( | ||||
|           <Vega | ||||
|             spec={spec} | ||||
|             data={vegaData} | ||||
|             width={widthProp - 10} | ||||
|             height={height} | ||||
|             actions={actions} | ||||
|           /> | ||||
|         )} | ||||
|         <Vega | ||||
|           spec={spec} | ||||
|           data={{ con: shape.value.continuous, dis: shape.value.discrete }} | ||||
|           width={widthProp - 10} | ||||
|           height={height} | ||||
|           actions={false} | ||||
|         /> | ||||
|         <div className="flex justify-center"> | ||||
|           {showSummary && plot.distributions.length === 1 && ( | ||||
|             <SummaryTable | ||||
|               distribution={plot.distributions[0].distribution} | ||||
|               environment={environment} | ||||
|             /> | ||||
|           )} | ||||
|           {showSummary && <SummaryTable distribution={distribution} />} | ||||
|         </div> | ||||
|         {showControls && ( | ||||
|           <div> | ||||
|             <CheckBox | ||||
|               label="Log X scale" | ||||
|               value={isLogX} | ||||
|               onChange={setLogX} | ||||
|               // Check whether we should disable the checkbox
 | ||||
|               {...(massBelow0 | ||||
|                 ? { | ||||
|                   disabled: true, | ||||
|                   tooltip: | ||||
|                     "Your distribution has mass lower than or equal to 0. Log only works on strictly positive values.", | ||||
|                 } | ||||
|                 : {})} | ||||
|             /> | ||||
|             <CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} /> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|     ); | ||||
|   }); | ||||
|   return sized; | ||||
| }; | ||||
| 
 | ||||
| function buildVegaSpec(isLogX: boolean, isExpY: boolean): VisualizationSpec { | ||||
|   return { | ||||
|     ...chartSpecification, | ||||
|     scales: [ | ||||
|       isLogX ? logXScale : linearXScale, | ||||
|       isExpY ? expYScale : linearYScale, | ||||
|     ], | ||||
|   } as VisualizationSpec; | ||||
| } | ||||
| 
 | ||||
| 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" | ||||
|         value={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 }> = ({ | ||||
|   children, | ||||
| }) => ( | ||||
|  | @ -154,36 +175,32 @@ const Cell: React.FC<{ children: React.ReactNode }> = ({ children }) => ( | |||
| ); | ||||
| 
 | ||||
| type SummaryTableProps = { | ||||
|   distribution: SqDistribution; | ||||
|   environment: environment; | ||||
|   distribution: Distribution; | ||||
| }; | ||||
| 
 | ||||
| const SummaryTable: React.FC<SummaryTableProps> = ({ | ||||
|   distribution, | ||||
|   environment, | ||||
| }) => { | ||||
|   const mean = distribution.mean(environment); | ||||
|   const stdev = distribution.stdev(environment); | ||||
|   const p5 = distribution.inv(environment, 0.05); | ||||
|   const p10 = distribution.inv(environment, 0.1); | ||||
|   const p25 = distribution.inv(environment, 0.25); | ||||
|   const p50 = distribution.inv(environment, 0.5); | ||||
|   const p75 = distribution.inv(environment, 0.75); | ||||
|   const p90 = distribution.inv(environment, 0.9); | ||||
|   const p95 = distribution.inv(environment, 0.95); | ||||
| const SummaryTable: React.FC<SummaryTableProps> = ({ distribution }) => { | ||||
|   const mean = distribution.mean(); | ||||
|   const stdev = distribution.stdev(); | ||||
|   const p5 = distribution.inv(0.05); | ||||
|   const p30 = distribution.inv(0.1); | ||||
|   const p25 = distribution.inv(0.25); | ||||
|   const p50 = distribution.inv(0.5); | ||||
|   const p75 = distribution.inv(0.75); | ||||
|   const p90 = distribution.inv(0.9); | ||||
|   const p97 = distribution.inv(0.95); | ||||
| 
 | ||||
|   const hasResult = (x: result<number, SqDistributionError>): boolean => | ||||
|   const hasResult = (x: result<number, distributionError>): boolean => | ||||
|     x.tag === "Ok"; | ||||
| 
 | ||||
|   const unwrapResult = ( | ||||
|     x: result<number, SqDistributionError> | ||||
|     x: result<number, distributionError> | ||||
|   ): React.ReactNode => { | ||||
|     if (x.tag === "Ok") { | ||||
|       return <NumberShower number={x.value} />; | ||||
|     } else { | ||||
|       return ( | ||||
|         <ErrorAlert heading="Distribution Error"> | ||||
|           {x.value.toString()} | ||||
|           {distributionErrorToString(x.value)} | ||||
|         </ErrorAlert> | ||||
|       ); | ||||
|     } | ||||
|  | @ -209,12 +226,12 @@ const SummaryTable: React.FC<SummaryTableProps> = ({ | |||
|           <Cell>{unwrapResult(mean)}</Cell> | ||||
|           {hasResult(stdev) && <Cell>{unwrapResult(stdev)}</Cell>} | ||||
|           <Cell>{unwrapResult(p5)}</Cell> | ||||
|           <Cell>{unwrapResult(p10)}</Cell> | ||||
|           <Cell>{unwrapResult(p30)}</Cell> | ||||
|           <Cell>{unwrapResult(p25)}</Cell> | ||||
|           <Cell>{unwrapResult(p50)}</Cell> | ||||
|           <Cell>{unwrapResult(p75)}</Cell> | ||||
|           <Cell>{unwrapResult(p90)}</Cell> | ||||
|           <Cell>{unwrapResult(p95)}</Cell> | ||||
|           <Cell>{unwrapResult(p97)}</Cell> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|     </table> | ||||
|  |  | |||
|  | @ -1,15 +1,8 @@ | |||
| import * as React from "react"; | ||||
| import { | ||||
|   SqLambda, | ||||
|   environment, | ||||
|   SqValueTag, | ||||
|   SqError, | ||||
| } from "@quri/squiggle-lang"; | ||||
| import { lambdaValue, environment, runForeign } from "@quri/squiggle-lang"; | ||||
| import { FunctionChart1Dist } from "./FunctionChart1Dist"; | ||||
| import { FunctionChart1Number } from "./FunctionChart1Number"; | ||||
| import { DistributionPlottingSettings } from "./DistributionChart"; | ||||
| import { MessageAlert } from "./Alert"; | ||||
| import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; | ||||
| import { ErrorAlert, MessageAlert } from "./Alert"; | ||||
| 
 | ||||
| export type FunctionChartSettings = { | ||||
|   start: number; | ||||
|  | @ -18,49 +11,27 @@ export type FunctionChartSettings = { | |||
| }; | ||||
| 
 | ||||
| interface FunctionChartProps { | ||||
|   fn: SqLambda; | ||||
|   fn: lambdaValue; | ||||
|   chartSettings: FunctionChartSettings; | ||||
|   distributionPlotSettings: DistributionPlottingSettings; | ||||
|   environment: environment; | ||||
|   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> = ({ | ||||
|   fn, | ||||
|   chartSettings, | ||||
|   environment, | ||||
|   distributionPlotSettings, | ||||
|   height, | ||||
| }) => { | ||||
|   console.log(fn.parameters().length); | ||||
|   if (fn.parameters().length !== 1) { | ||||
|   if (fn.parameters.length > 1) { | ||||
|     return ( | ||||
|       <MessageAlert heading="Function Display Not Supported"> | ||||
|         Only functions with one parameter are displayed. | ||||
|       </MessageAlert> | ||||
|     ); | ||||
|   } | ||||
|   const result1 = fn.call([chartSettings.start]); | ||||
|   const result2 = fn.call([chartSettings.stop]); | ||||
|   const result1 = runForeign(fn, [chartSettings.start], environment); | ||||
|   const result2 = runForeign(fn, [chartSettings.stop], environment); | ||||
|   const getValidResult = () => { | ||||
|     if (result1.tag === "Ok") { | ||||
|       return result1; | ||||
|  | @ -71,23 +42,20 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({ | |||
|     } | ||||
|   }; | ||||
|   const validResult = getValidResult(); | ||||
|   const resultType = | ||||
|     validResult.tag === "Ok" ? validResult.value.tag : ("Error" as const); | ||||
| 
 | ||||
|   if (validResult.tag === "Error") { | ||||
|     return <FunctionCallErrorAlert error={validResult.value} />; | ||||
|   } | ||||
| 
 | ||||
|   switch (validResult.value.tag) { | ||||
|     case SqValueTag.Distribution: | ||||
|   switch (resultType) { | ||||
|     case "distribution": | ||||
|       return ( | ||||
|         <FunctionChart1Dist | ||||
|           fn={fn} | ||||
|           chartSettings={chartSettings} | ||||
|           environment={environment} | ||||
|           height={height} | ||||
|           distributionPlotSettings={distributionPlotSettings} | ||||
|         /> | ||||
|       ); | ||||
|     case SqValueTag.Number: | ||||
|     case "number": | ||||
|       return ( | ||||
|         <FunctionChart1Number | ||||
|           fn={fn} | ||||
|  | @ -96,11 +64,15 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({ | |||
|           height={height} | ||||
|         /> | ||||
|       ); | ||||
|     case "Error": | ||||
|       return ( | ||||
|         <ErrorAlert heading="Error">The function failed to be run</ErrorAlert> | ||||
|       ); | ||||
|     default: | ||||
|       return ( | ||||
|         <MessageAlert heading="Function Display Not Supported"> | ||||
|           There is no function visualization for this type of output:{" "} | ||||
|           <span className="font-bold">{validResult.value.tag}</span> | ||||
|           <span className="font-bold">{resultType}</span> | ||||
|         </MessageAlert> | ||||
|       ); | ||||
|   } | ||||
|  |  | |||
|  | @ -2,21 +2,18 @@ import * as React from "react"; | |||
| import _ from "lodash"; | ||||
| import type { Spec } from "vega"; | ||||
| import { | ||||
|   SqDistribution, | ||||
|   Distribution, | ||||
|   result, | ||||
|   SqLambda, | ||||
|   lambdaValue, | ||||
|   environment, | ||||
|   SqError, | ||||
|   SqValue, | ||||
|   SqValueTag, | ||||
|   runForeign, | ||||
|   squiggleExpression, | ||||
|   errorValue, | ||||
|   errorValueToString, | ||||
| } from "@quri/squiggle-lang"; | ||||
| import { createClassFromSpec } from "react-vega"; | ||||
| import * as percentilesSpec from "../vega-specs/spec-percentiles.json"; | ||||
| import { | ||||
|   DistributionChart, | ||||
|   DistributionPlottingSettings, | ||||
|   defaultPlot, | ||||
| } from "./DistributionChart"; | ||||
| import { DistributionChart } from "./DistributionChart"; | ||||
| import { NumberShower } from "./NumberShower"; | ||||
| import { ErrorAlert } from "./Alert"; | ||||
| 
 | ||||
|  | @ -45,9 +42,8 @@ export type FunctionChartSettings = { | |||
| }; | ||||
| 
 | ||||
| interface FunctionChart1DistProps { | ||||
|   fn: SqLambda; | ||||
|   fn: lambdaValue; | ||||
|   chartSettings: FunctionChartSettings; | ||||
|   distributionPlotSettings: DistributionPlottingSettings; | ||||
|   environment: environment; | ||||
|   height: number; | ||||
| } | ||||
|  | @ -76,17 +72,9 @@ type errors = _.Dictionary< | |||
|   }[] | ||||
| >; | ||||
| 
 | ||||
| type point = { x: number; value: result<SqDistribution, string> }; | ||||
| type point = { x: number; value: result<Distribution, string> }; | ||||
| 
 | ||||
| let getPercentiles = ({ | ||||
|   chartSettings, | ||||
|   fn, | ||||
|   environment, | ||||
| }: { | ||||
|   chartSettings: FunctionChartSettings; | ||||
|   fn: SqLambda; | ||||
|   environment: environment; | ||||
| }) => { | ||||
| let getPercentiles = ({ chartSettings, fn, environment }) => { | ||||
|   let chartPointsToRender = _rangeByCount( | ||||
|     chartSettings.start, | ||||
|     chartSettings.stop, | ||||
|  | @ -94,9 +82,9 @@ let getPercentiles = ({ | |||
|   ); | ||||
| 
 | ||||
|   let chartPointsData: point[] = chartPointsToRender.map((x) => { | ||||
|     let result = fn.call([x]); | ||||
|     let result = runForeign(fn, [x], environment); | ||||
|     if (result.tag === "Ok") { | ||||
|       if (result.value.tag === SqValueTag.Distribution) { | ||||
|       if (result.value.tag == "distribution") { | ||||
|         return { x, value: { tag: "Ok", value: result.value.value } }; | ||||
|       } else { | ||||
|         return { | ||||
|  | @ -111,13 +99,13 @@ let getPercentiles = ({ | |||
|     } else { | ||||
|       return { | ||||
|         x, | ||||
|         value: { tag: "Error", value: result.value.toString() }, | ||||
|         value: { tag: "Error", value: errorValueToString(result.value) }, | ||||
|       }; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   let initialPartition: [ | ||||
|     { x: number; value: SqDistribution }[], | ||||
|     { x: number; value: Distribution }[], | ||||
|     { x: number; value: string }[] | ||||
|   ] = [[], []]; | ||||
| 
 | ||||
|  | @ -133,23 +121,26 @@ let getPercentiles = ({ | |||
|   let groupedErrors: errors = _.groupBy(errors, (x) => 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, | ||||
|       p1: unwrap(value.inv(environment, 0.01)), | ||||
|       p5: unwrap(value.inv(environment, 0.05)), | ||||
|       p10: unwrap(value.inv(environment, 0.1)), | ||||
|       p20: unwrap(value.inv(environment, 0.2)), | ||||
|       p30: unwrap(value.inv(environment, 0.3)), | ||||
|       p40: unwrap(value.inv(environment, 0.4)), | ||||
|       p50: unwrap(value.inv(environment, 0.5)), | ||||
|       p60: unwrap(value.inv(environment, 0.6)), | ||||
|       p70: unwrap(value.inv(environment, 0.7)), | ||||
|       p80: unwrap(value.inv(environment, 0.8)), | ||||
|       p90: unwrap(value.inv(environment, 0.9)), | ||||
|       p95: unwrap(value.inv(environment, 0.95)), | ||||
|       p99: unwrap(value.inv(environment, 0.99)), | ||||
|       p1: unwrap(toPointSet.inv(0.01)), | ||||
|       p5: unwrap(toPointSet.inv(0.05)), | ||||
|       p10: unwrap(toPointSet.inv(0.1)), | ||||
|       p20: unwrap(toPointSet.inv(0.2)), | ||||
|       p30: unwrap(toPointSet.inv(0.3)), | ||||
|       p40: unwrap(toPointSet.inv(0.4)), | ||||
|       p50: unwrap(toPointSet.inv(0.5)), | ||||
|       p60: unwrap(toPointSet.inv(0.6)), | ||||
|       p70: unwrap(toPointSet.inv(0.7)), | ||||
|       p80: unwrap(toPointSet.inv(0.8)), | ||||
|       p90: unwrap(toPointSet.inv(0.9)), | ||||
|       p95: unwrap(toPointSet.inv(0.95)), | ||||
|       p99: unwrap(toPointSet.inv(0.99)), | ||||
|     }; | ||||
|     return res; | ||||
|   }); | ||||
| 
 | ||||
|   return { percentiles, errors: groupedErrors }; | ||||
|  | @ -159,7 +150,6 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({ | |||
|   fn, | ||||
|   chartSettings, | ||||
|   environment, | ||||
|   distributionPlotSettings, | ||||
|   height, | ||||
| }) => { | ||||
|   let [mouseOverlay, setMouseOverlay] = React.useState(0); | ||||
|  | @ -170,25 +160,22 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({ | |||
|     setMouseOverlay(NaN); | ||||
|   } | ||||
|   const signalListeners = { mousemove: handleHover, mouseout: handleOut }; | ||||
| 
 | ||||
|   //TODO: This custom error handling is a bit hacky and should be improved.
 | ||||
|   let mouseItem: result<SqValue, SqError> = !!mouseOverlay | ||||
|     ? fn.call([mouseOverlay]) | ||||
|   let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay | ||||
|     ? runForeign(fn, [mouseOverlay], environment) | ||||
|     : { | ||||
|         tag: "Error", | ||||
|         value: SqError.createOtherError( | ||||
|           "Hover x-coordinate returned NaN. Expected a number." | ||||
|         ), | ||||
|         value: { | ||||
|           tag: "REExpectedType", | ||||
|           value: "Hover x-coordinate returned NaN. Expected a number.", | ||||
|         }, | ||||
|       }; | ||||
|   let showChart = | ||||
|     mouseItem.tag === "Ok" && | ||||
|     mouseItem.value.tag === SqValueTag.Distribution ? ( | ||||
|     mouseItem.tag === "Ok" && mouseItem.value.tag === "distribution" ? ( | ||||
|       <DistributionChart | ||||
|         plot={defaultPlot(mouseItem.value.value)} | ||||
|         environment={environment} | ||||
|         distribution={mouseItem.value.value} | ||||
|         width={400} | ||||
|         height={50} | ||||
|         {...distributionPlotSettings} | ||||
|         showSummary={false} | ||||
|       /> | ||||
|     ) : null; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,16 @@ | |||
| import * as React from "react"; | ||||
| import _ from "lodash"; | ||||
| 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 * as lineChartSpec from "../vega-specs/spec-line-chart.json"; | ||||
| import { ErrorAlert } from "./Alert"; | ||||
| import { squiggleValueTag } from "@quri/squiggle-lang/src/rescript/ForTS/ForTS_SquiggleValue/ForTS_SquiggleValue_tag"; | ||||
| 
 | ||||
| let SquiggleLineChart = createClassFromSpec({ | ||||
|   spec: lineChartSpec as Spec, | ||||
|  | @ -25,7 +30,7 @@ export type FunctionChartSettings = { | |||
| }; | ||||
| 
 | ||||
| interface FunctionChart1NumberProps { | ||||
|   fn: SqLambda; | ||||
|   fn: lambdaValue; | ||||
|   chartSettings: FunctionChartSettings; | ||||
|   environment: environment; | ||||
|   height: number; | ||||
|  | @ -33,25 +38,20 @@ interface FunctionChart1NumberProps { | |||
| 
 | ||||
| type point = { x: number; value: result<number, string> }; | ||||
| 
 | ||||
| let getFunctionImage = ({ | ||||
|   chartSettings, | ||||
|   fn, | ||||
|   environment, | ||||
| }: { | ||||
|   chartSettings: FunctionChartSettings; | ||||
|   fn: SqLambda; | ||||
|   environment: environment; | ||||
| }) => { | ||||
| let getFunctionImage = ({ chartSettings, fn, environment }) => { | ||||
|   //We adjust the count, because the count is made for distributions, which are much more expensive to estimate
 | ||||
|   let adjustedCount = chartSettings.count * 20; | ||||
| 
 | ||||
|   let chartPointsToRender = _rangeByCount( | ||||
|     chartSettings.start, | ||||
|     chartSettings.stop, | ||||
|     chartSettings.count | ||||
|     adjustedCount | ||||
|   ); | ||||
| 
 | ||||
|   let chartPointsData: point[] = chartPointsToRender.map((x) => { | ||||
|     let result = fn.call([x]); | ||||
|     let result = runForeign(fn, [x], environment); | ||||
|     if (result.tag === "Ok") { | ||||
|       if (result.value.tag === SqValueTag.Number) { | ||||
|       if (result.value.tag == "number") { | ||||
|         return { x, value: { tag: "Ok", value: result.value.value } }; | ||||
|       } else { | ||||
|         return { | ||||
|  | @ -65,7 +65,7 @@ let getFunctionImage = ({ | |||
|     } else { | ||||
|       return { | ||||
|         x, | ||||
|         value: { tag: "Error", value: result.value.toString() }, | ||||
|         value: { tag: "Error", value: errorValueToString(result.value) }, | ||||
|       }; | ||||
|     } | ||||
|   }); | ||||
|  |  | |||
|  | @ -1,158 +1,82 @@ | |||
| import * as React from "react"; | ||||
| import { | ||||
|   SqValue, | ||||
|   squiggleExpression, | ||||
|   bindings, | ||||
|   environment, | ||||
|   SqProject, | ||||
|   jsImports, | ||||
|   defaultImports, | ||||
|   defaultBindings, | ||||
|   defaultEnvironment, | ||||
| } from "@quri/squiggle-lang"; | ||||
| import { FunctionChartSettings } from "./FunctionChart"; | ||||
| import { useSquiggle } from "../lib/hooks"; | ||||
| import { SquiggleViewer } from "./SquiggleViewer"; | ||||
| import { JsImports } from "../lib/jsImports"; | ||||
| import { getValueToRender } from "../lib/utility"; | ||||
| import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; | ||||
| import { SquiggleItem } from "./SquiggleItem"; | ||||
| 
 | ||||
| export type SquiggleChartProps = { | ||||
| export interface SquiggleChartProps { | ||||
|   /** The input string for squiggle */ | ||||
|   code: string; | ||||
|   /** Allows to re-run the code if code hasn't changed */ | ||||
|   executionId?: number; | ||||
|   squiggleString?: string; | ||||
|   /** If the output requires monte carlo sampling, the amount of samples */ | ||||
|   sampleCount?: number; | ||||
|   /** If the result is a function, where the function domain starts */ | ||||
|   diagramStart?: number; | ||||
|   /** If the result is a function, where the function domain ends */ | ||||
|   diagramStop?: number; | ||||
|   /** If the result is a function, the amount of stops sampled */ | ||||
|   diagramCount?: number; | ||||
|   /** The amount of points returned to draw the distribution */ | ||||
|   environment?: environment; | ||||
|   /** If the result is a function, where the function starts, ends and the amount of stops */ | ||||
|   chartSettings?: FunctionChartSettings; | ||||
|   /** When the squiggle code gets reevaluated */ | ||||
|   onChange?(expr: SqValue | undefined, sourceName: string): void; | ||||
|   onChange?(expr: squiggleExpression | undefined): void; | ||||
|   /** CSS width of the element */ | ||||
|   width?: number; | ||||
|   height?: number; | ||||
|   /** Bindings of previous variables declared */ | ||||
|   bindings?: bindings; | ||||
|   /** JS imported parameters */ | ||||
|   jsImports?: JsImports; | ||||
|   jsImports?: jsImports; | ||||
|   /** Whether to show a summary of the distribution */ | ||||
|   showSummary?: boolean; | ||||
|   /** Set the x scale to be logarithmic by deault */ | ||||
|   logX?: boolean; | ||||
|   /** Set the y scale to be exponential by deault */ | ||||
|   expY?: boolean; | ||||
|   /** How to format numbers on the x axis */ | ||||
|   tickFormat?: string; | ||||
|   /** Title of the graphed distribution */ | ||||
|   title?: string; | ||||
|   /** Color of the graphed distribution */ | ||||
|   color?: string; | ||||
|   /** Specify the lower bound of the x scale */ | ||||
|   minX?: number; | ||||
|   /** Specify the upper bound of the x scale */ | ||||
|   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 */ | ||||
|   distributionChartActions?: boolean; | ||||
|   enableLocalSettings?: boolean; | ||||
| } & (StandaloneExecutionProps | ProjectExecutionProps); | ||||
|   /** Whether to show type information about returns, default false */ | ||||
|   showTypes?: boolean; | ||||
|   /** Whether to show graph controls (scale etc)*/ | ||||
|   showControls?: boolean; | ||||
| } | ||||
| 
 | ||||
| // 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 defaultImports: JsImports = {}; | ||||
| const defaultChartSettings = { start: 0, stop: 10, count: 20 }; | ||||
| 
 | ||||
| export const splitSquiggleChartSettings = (props: SquiggleChartProps) => { | ||||
|   const { | ||||
|     showSummary = false, | ||||
|     logX = false, | ||||
|     expY = false, | ||||
|     diagramStart = 0, | ||||
|     diagramStop = 10, | ||||
|     diagramCount = 20, | ||||
|     tickFormat, | ||||
|     minX, | ||||
|     maxX, | ||||
|     color, | ||||
|     title, | ||||
|     xAxisType = "number", | ||||
|     distributionChartActions, | ||||
|   } = props; | ||||
| export const SquiggleChart: React.FC<SquiggleChartProps> = ({ | ||||
|   squiggleString = "", | ||||
|   environment, | ||||
|   onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
 | ||||
|   height = 200, | ||||
|   bindings = defaultBindings, | ||||
|   jsImports = defaultImports, | ||||
|   showSummary = false, | ||||
|   width, | ||||
|   showTypes = false, | ||||
|   showControls = false, | ||||
|   chartSettings = defaultChartSettings, | ||||
| }) => { | ||||
|   const { result } = useSquiggle({ | ||||
|     code: squiggleString, | ||||
|     bindings, | ||||
|     environment, | ||||
|     jsImports, | ||||
|     onChange, | ||||
|   }); | ||||
| 
 | ||||
|   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, | ||||
|       jsImports = defaultImports, | ||||
|       onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
 | ||||
|       executionId = 0, | ||||
|       width, | ||||
|       height = 200, | ||||
|       enableLocalSettings = false, | ||||
|       continues, | ||||
|       project, | ||||
|       environment, | ||||
|     } = props; | ||||
| 
 | ||||
|     const resultAndBindings = useSquiggle({ | ||||
|       environment, | ||||
|       continues, | ||||
|       project, | ||||
|       code, | ||||
|       jsImports, | ||||
|       onChange, | ||||
|       executionId, | ||||
|     }); | ||||
| 
 | ||||
|     const valueToRender = getValueToRender(resultAndBindings); | ||||
| 
 | ||||
|     return ( | ||||
|       <SquiggleViewer | ||||
|         result={valueToRender} | ||||
|         width={width} | ||||
|         height={height} | ||||
|         distributionPlotSettings={distributionPlotSettings} | ||||
|         chartSettings={chartSettings} | ||||
|         environment={ | ||||
|           project ? project.getEnvironment() : environment ?? defaultEnvironment | ||||
|         } | ||||
|         enableLocalSettings={enableLocalSettings} | ||||
|       /> | ||||
|     ); | ||||
|   if (result.tag !== "Ok") { | ||||
|     return <SquiggleErrorAlert error={result.value} />; | ||||
|   } | ||||
| ); | ||||
| 
 | ||||
|   return ( | ||||
|     <SquiggleItem | ||||
|       expression={result.value} | ||||
|       width={width} | ||||
|       height={height} | ||||
|       showSummary={showSummary} | ||||
|       showTypes={showTypes} | ||||
|       showControls={showControls} | ||||
|       chartSettings={chartSettings} | ||||
|       environment={environment ?? defaultEnvironment} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -13,7 +13,6 @@ const SquiggleContext = React.createContext<SquiggleContextShape>({ | |||
| 
 | ||||
| export const SquiggleContainer: React.FC<Props> = ({ children }) => { | ||||
|   const context = useContext(SquiggleContext); | ||||
| 
 | ||||
|   if (context.containerized) { | ||||
|     return <>{children}</>; | ||||
|   } else { | ||||
|  |  | |||
|  | @ -1,92 +1,164 @@ | |||
| import React from "react"; | ||||
| import React, { useState } from "react"; | ||||
| import * as ReactDOM from "react-dom"; | ||||
| import { CodeEditor } from "./CodeEditor"; | ||||
| import { SquiggleContainer } from "./SquiggleContainer"; | ||||
| import { | ||||
|   splitSquiggleChartSettings, | ||||
|   SquiggleChartProps, | ||||
| } 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"; | ||||
|   squiggleExpression, | ||||
|   environment, | ||||
|   bindings, | ||||
|   jsImports, | ||||
|   defaultEnvironment, | ||||
| } from "@quri/squiggle-lang"; | ||||
| import { defaultImports, defaultBindings } from "@quri/squiggle-lang"; | ||||
| import { SquiggleContainer } from "./SquiggleContainer"; | ||||
| import { useSquiggle, useSquigglePartial } from "../lib/hooks"; | ||||
| import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; | ||||
| import { SquiggleItem } from "./SquiggleItem"; | ||||
| 
 | ||||
| const WrappedCodeEditor: React.FC<{ | ||||
|   code: string; | ||||
|   setCode: (code: string) => void; | ||||
|   errorLocations?: SqLocation[]; | ||||
| }> = ({ code, setCode, errorLocations }) => ( | ||||
|   <div className="border border-grey-200 p-2 m-4" data-testid="squiggle-editor"> | ||||
| }> = ({ code, setCode }) => ( | ||||
|   <div className="border border-grey-200 p-2 m-4"> | ||||
|     <CodeEditor | ||||
|       value={code} | ||||
|       onChange={setCode} | ||||
|       oneLine={true} | ||||
|       showGutter={false} | ||||
|       height={20} | ||||
|       errorLocations={errorLocations} | ||||
|     /> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| export type SquiggleEditorProps = SquiggleChartProps & { | ||||
|   defaultCode?: string; | ||||
|   onCodeChange?: (code: string) => void; | ||||
| }; | ||||
| export interface SquiggleEditorProps { | ||||
|   /** The input string for squiggle */ | ||||
|   initialSquiggleString?: string; | ||||
|   /** The width of the element */ | ||||
|   width?: number; | ||||
|   /** If the result is a function, where the function starts */ | ||||
|   diagramStart?: number; | ||||
|   /** If the result is a function, where the function ends */ | ||||
|   diagramStop?: number; | ||||
|   /** If the result is a function, how many points along the function it samples */ | ||||
|   diagramCount?: number; | ||||
|   /** When the environment changes. Used again for notebook magic */ | ||||
|   onChange?(expr: squiggleExpression | undefined): void; | ||||
|   /** Previous variable declarations */ | ||||
|   bindings?: bindings; | ||||
|   /** If the output requires monte carlo sampling, the amount of samples */ | ||||
|   environment?: environment; | ||||
|   /** JS Imports */ | ||||
|   jsImports?: jsImports; | ||||
|   /** Whether to show detail about types of the returns, default false */ | ||||
|   showTypes?: boolean; | ||||
|   /** Whether to give users access to graph controls */ | ||||
|   showControls?: boolean; | ||||
|   /** Whether to show a summary table */ | ||||
|   showSummary?: boolean; | ||||
| } | ||||
| 
 | ||||
| const defaultOnChange = () => {}; | ||||
| const defaultImports: JsImports = {}; | ||||
| export const SquiggleEditor: React.FC<SquiggleEditorProps> = ({ | ||||
|   initialSquiggleString = "", | ||||
|   width, | ||||
|   diagramStart = 0, | ||||
|   diagramStop = 10, | ||||
|   diagramCount = 20, | ||||
|   onChange, | ||||
|   bindings = defaultBindings, | ||||
|   environment, | ||||
|   jsImports = defaultImports, | ||||
|   showTypes = false, | ||||
|   showControls = false, | ||||
|   showSummary = false, | ||||
| }: SquiggleEditorProps) => { | ||||
|   const [code, setCode] = useState(initialSquiggleString); | ||||
| 
 | ||||
| export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => { | ||||
|   const [code, setCode] = useMaybeControlledValue({ | ||||
|     value: props.code, | ||||
|     defaultValue: props.defaultCode ?? "", | ||||
|     onChange: props.onCodeChange, | ||||
|   }); | ||||
| 
 | ||||
|   const { distributionPlotSettings, chartSettings } = | ||||
|     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, | ||||
|   const { result, observableRef } = useSquiggle({ | ||||
|     code, | ||||
|     project, | ||||
|     bindings, | ||||
|     environment, | ||||
|     jsImports, | ||||
|     onChange, | ||||
|     executionId, | ||||
|   }); | ||||
| 
 | ||||
|   const valueToRender = getValueToRender(resultAndBindings); | ||||
|   const errorLocations = getErrorLocations(resultAndBindings.result); | ||||
|   const chartSettings = { | ||||
|     start: diagramStart, | ||||
|     stop: diagramStop, | ||||
|     count: diagramCount, | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <SquiggleContainer> | ||||
|       <WrappedCodeEditor | ||||
|         code={code} | ||||
|         setCode={setCode} | ||||
|         errorLocations={errorLocations} | ||||
|       /> | ||||
|       <SquiggleViewer | ||||
|         result={valueToRender} | ||||
|         width={width} | ||||
|         height={height} | ||||
|         distributionPlotSettings={distributionPlotSettings} | ||||
|         chartSettings={chartSettings} | ||||
|         environment={environment ?? defaultEnvironment} | ||||
|         enableLocalSettings={enableLocalSettings} | ||||
|       /> | ||||
|     </SquiggleContainer> | ||||
|     <div ref={observableRef}> | ||||
|       <SquiggleContainer> | ||||
|         <WrappedCodeEditor code={code} setCode={setCode} /> | ||||
|         {result.tag === "Ok" ? ( | ||||
|           <SquiggleItem | ||||
|             expression={result.value} | ||||
|             width={width} | ||||
|             height={200} | ||||
|             showSummary={showSummary} | ||||
|             showTypes={showTypes} | ||||
|             showControls={showControls} | ||||
|             chartSettings={chartSettings} | ||||
|             environment={environment ?? defaultEnvironment} | ||||
|           /> | ||||
|         ) : ( | ||||
|           <SquiggleErrorAlert error={result.value} /> | ||||
|         )} | ||||
|       </SquiggleContainer> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export function renderSquiggleEditorToDom(props: SquiggleEditorProps) { | ||||
|   const parent = document.createElement("div"); | ||||
|   ReactDOM.render(<SquiggleEditor {...props} />, parent); | ||||
|   return parent; | ||||
| } | ||||
| 
 | ||||
| export interface SquigglePartialProps { | ||||
|   /** The input string for squiggle */ | ||||
|   initialSquiggleString?: string; | ||||
|   /** when the environment changes. Used again for notebook magic*/ | ||||
|   onChange?(expr: bindings | undefined): 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> = ({ | ||||
|   initialSquiggleString = "", | ||||
|   onChange, | ||||
|   bindings = defaultBindings, | ||||
|   environment, | ||||
|   jsImports = defaultImports, | ||||
| }: SquigglePartialProps) => { | ||||
|   const [code, setCode] = useState(initialSquiggleString); | ||||
| 
 | ||||
|   const { result, observableRef } = useSquigglePartial({ | ||||
|     code, | ||||
|     bindings, | ||||
|     environment, | ||||
|     jsImports, | ||||
|     onChange, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <div ref={observableRef}> | ||||
|       <SquiggleContainer> | ||||
|         <WrappedCodeEditor code={code} setCode={setCode} /> | ||||
|         {result.tag !== "Ok" ? ( | ||||
|           <SquiggleErrorAlert error={result.value} /> | ||||
|         ) : null} | ||||
|       </SquiggleContainer> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export function renderSquigglePartialToDom(props: SquigglePartialProps) { | ||||
|   const parent = document.createElement("div"); | ||||
|   ReactDOM.render(<SquigglePartial {...props} />, parent); | ||||
|   return parent; | ||||
| } | ||||
|  |  | |||
|  | @ -1,44 +1,11 @@ | |||
| import { SqError, SqFrame } from "@quri/squiggle-lang"; | ||||
| import { errorValue, errorValueToString } from "@quri/squiggle-lang"; | ||||
| import React from "react"; | ||||
| import { ErrorAlert } from "./Alert"; | ||||
| 
 | ||||
| type Props = { | ||||
|   error: SqError; | ||||
| }; | ||||
| 
 | ||||
| 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; | ||||
|   error: errorValue; | ||||
| }; | ||||
| 
 | ||||
| export const SquiggleErrorAlert: React.FC<Props> = ({ error }) => { | ||||
|   return ( | ||||
|     <ErrorAlert heading="Error"> | ||||
|       <div className="space-y-4"> | ||||
|         <div>{error.toString()}</div> | ||||
|         <StackTrace error={error} /> | ||||
|       </div> | ||||
|     </ErrorAlert> | ||||
|   ); | ||||
|   return <ErrorAlert heading="Error">{errorValueToString(error)}</ErrorAlert>; | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										262
									
								
								packages/components/src/components/SquiggleItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								packages/components/src/components/SquiggleItem.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,262 @@ | |||
| import * as React from "react"; | ||||
| import { | ||||
|   squiggleExpression, | ||||
|   environment, | ||||
|   declaration, | ||||
| } from "@quri/squiggle-lang"; | ||||
| import { NumberShower } from "./NumberShower"; | ||||
| import { DistributionChart } from "./DistributionChart"; | ||||
| import { FunctionChart, FunctionChartSettings } from "./FunctionChart"; | ||||
| 
 | ||||
| function getRange<a>(x: declaration<a>) { | ||||
|   const first = x.args[0]; | ||||
|   switch (first.tag) { | ||||
|     case "Float": { | ||||
|       return { floats: { min: first.value.min, max: first.value.max } }; | ||||
|     } | ||||
|     case "Date": { | ||||
|       return { time: { min: first.value.min, max: first.value.max } }; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function getChartSettings<a>(x: declaration<a>): FunctionChartSettings { | ||||
|   const range = getRange(x); | ||||
|   const min = range.floats ? range.floats.min : 0; | ||||
|   const max = range.floats ? range.floats.max : 10; | ||||
|   return { | ||||
|     start: min, | ||||
|     stop: max, | ||||
|     count: 20, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| interface VariableBoxProps { | ||||
|   heading: string; | ||||
|   children: React.ReactNode; | ||||
|   showTypes: boolean; | ||||
| } | ||||
| 
 | ||||
| export const VariableBox: React.FC<VariableBoxProps> = ({ | ||||
|   heading = "Error", | ||||
|   children, | ||||
|   showTypes = false, | ||||
| }) => { | ||||
|   if (showTypes) { | ||||
|     return ( | ||||
|       <div className="bg-white border border-grey-200 m-2"> | ||||
|         <div className="border-b border-grey-200 p-3"> | ||||
|           <header className="font-mono">{heading}</header> | ||||
|         </div> | ||||
|         <div className="p-3">{children}</div> | ||||
|       </div> | ||||
|     ); | ||||
|   } else { | ||||
|     return <div>{children}</div>; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export interface SquiggleItemProps { | ||||
|   /** The input string for squiggle */ | ||||
|   expression: squiggleExpression; | ||||
|   width?: number; | ||||
|   height: number; | ||||
|   /** Whether to show a summary of statistics for distributions */ | ||||
|   showSummary: boolean; | ||||
|   /** Whether to show type information */ | ||||
|   showTypes: boolean; | ||||
|   /** Whether to show users graph controls (scale etc) */ | ||||
|   showControls: boolean; | ||||
|   /** Settings for displaying functions */ | ||||
|   chartSettings: FunctionChartSettings; | ||||
|   /** Environment for further function executions */ | ||||
|   environment: environment; | ||||
| } | ||||
| 
 | ||||
| export const SquiggleItem: React.FC<SquiggleItemProps> = ({ | ||||
|   expression, | ||||
|   width, | ||||
|   height, | ||||
|   showSummary, | ||||
|   showTypes = false, | ||||
|   showControls = false, | ||||
|   chartSettings, | ||||
|   environment, | ||||
| }) => { | ||||
|   switch (expression.tag) { | ||||
|     case "number": | ||||
|       return ( | ||||
|         <VariableBox heading="Number" showTypes={showTypes}> | ||||
|           <div className="font-semibold text-slate-600"> | ||||
|             <NumberShower precision={3} number={expression.value} /> | ||||
|           </div> | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "distribution": { | ||||
|       const distType = expression.value.type(); | ||||
|       return ( | ||||
|         <VariableBox | ||||
|           heading={`Distribution (${distType})`} | ||||
|           showTypes={showTypes} | ||||
|         > | ||||
|           {distType === "Symbolic" && showTypes ? ( | ||||
|             <div>{expression.value.toString()}</div> | ||||
|           ) : null} | ||||
|           <DistributionChart | ||||
|             distribution={expression.value} | ||||
|             height={height} | ||||
|             width={width} | ||||
|             showSummary={showSummary} | ||||
|             showControls={showControls} | ||||
|           /> | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     } | ||||
|     case "string": | ||||
|       return ( | ||||
|         <VariableBox heading="String" showTypes={showTypes}> | ||||
|           <span className="text-slate-400">"</span> | ||||
|           <span className="text-slate-600 font-semibold"> | ||||
|             {expression.value} | ||||
|           </span> | ||||
|           <span className="text-slate-400">"</span> | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "boolean": | ||||
|       return ( | ||||
|         <VariableBox heading="Boolean" showTypes={showTypes}> | ||||
|           {expression.value.toString()} | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "symbol": | ||||
|       return ( | ||||
|         <VariableBox heading="Symbol" showTypes={showTypes}> | ||||
|           <span className="text-slate-500 mr-2">Undefined Symbol:</span> | ||||
|           <span className="text-slate-600">{expression.value}</span> | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "call": | ||||
|       return ( | ||||
|         <VariableBox heading="Call" showTypes={showTypes}> | ||||
|           {expression.value} | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "array": | ||||
|       return ( | ||||
|         <VariableBox heading="Array" showTypes={showTypes}> | ||||
|           {expression.value.map((r, i) => ( | ||||
|             <div key={i} className="flex pt-1"> | ||||
|               <div className="flex-none bg-slate-100 rounded-sm px-1"> | ||||
|                 <header className="text-slate-400 font-mono">{i}</header> | ||||
|               </div> | ||||
|               <div className="px-2 mb-2 grow"> | ||||
|                 <SquiggleItem | ||||
|                   key={i} | ||||
|                   expression={r} | ||||
|                   width={width !== undefined ? width - 20 : width} | ||||
|                   height={50} | ||||
|                   showTypes={showTypes} | ||||
|                   showControls={showControls} | ||||
|                   chartSettings={chartSettings} | ||||
|                   environment={environment} | ||||
|                   showSummary={showSummary} | ||||
|                 /> | ||||
|               </div> | ||||
|             </div> | ||||
|           ))} | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "record": | ||||
|       return ( | ||||
|         <VariableBox heading="Record" showTypes={showTypes}> | ||||
|           <div className="space-y-3"> | ||||
|             {Object.entries(expression.value).map(([key, r]) => ( | ||||
|               <div key={key} className="flex space-x-2"> | ||||
|                 <div className="flex-none"> | ||||
|                   <header className="text-slate-500 font-mono">{key}:</header> | ||||
|                 </div> | ||||
|                 <div className="px-2 grow bg-gray-50 border border-gray-100 rounded-sm"> | ||||
|                   <SquiggleItem | ||||
|                     expression={r} | ||||
|                     width={width !== undefined ? width - 20 : width} | ||||
|                     height={height / 3} | ||||
|                     showTypes={showTypes} | ||||
|                     showSummary={showSummary} | ||||
|                     showControls={showControls} | ||||
|                     chartSettings={chartSettings} | ||||
|                     environment={environment} | ||||
|                   /> | ||||
|                 </div> | ||||
|               </div> | ||||
|             ))} | ||||
|           </div> | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "arraystring": | ||||
|       return ( | ||||
|         <VariableBox heading="Array String" showTypes={showTypes}> | ||||
|           {expression.value.map((r) => `"${r}"`).join(", ")} | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "date": | ||||
|       return ( | ||||
|         <VariableBox heading="Date" showTypes={showTypes}> | ||||
|           {expression.value.toDateString()} | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "timeDuration": { | ||||
|       return ( | ||||
|         <VariableBox heading="Time Duration" showTypes={showTypes}> | ||||
|           <NumberShower precision={3} number={expression.value} /> | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     } | ||||
|     case "lambda": | ||||
|       return ( | ||||
|         <VariableBox heading="Function" showTypes={showTypes}> | ||||
|           <div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join( | ||||
|             "," | ||||
|           )})`}</div>
 | ||||
|           <FunctionChart | ||||
|             fn={expression.value} | ||||
|             chartSettings={chartSettings} | ||||
|             height={height} | ||||
|             environment={{ | ||||
|               sampleCount: environment.sampleCount / 10, | ||||
|               xyPointLength: environment.xyPointLength / 10, | ||||
|             }} | ||||
|           /> | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     case "lambdaDeclaration": { | ||||
|       return ( | ||||
|         <VariableBox heading="Function Declaration" showTypes={showTypes}> | ||||
|           <FunctionChart | ||||
|             fn={expression.value.fn} | ||||
|             chartSettings={getChartSettings(expression.value)} | ||||
|             height={height} | ||||
|             environment={{ | ||||
|               sampleCount: environment.sampleCount / 10, | ||||
|               xyPointLength: environment.xyPointLength / 10, | ||||
|             }} | ||||
|           /> | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     } | ||||
|     case "module": { | ||||
|       return ( | ||||
|         <VariableBox heading="Module" showTypes={showTypes}> | ||||
|           <span className="text-slate-600 font-semibold">Internal Module</span> | ||||
|         </VariableBox> | ||||
|       ); | ||||
|     } | ||||
|     default: { | ||||
|       return ( | ||||
|         <div> | ||||
|           <span>No display for type: </span>{" "} | ||||
|           <span className="font-semibold text-slate-600">{expression.tag}</span> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | @ -1,66 +1,46 @@ | |||
| import React, { | ||||
|   FC, | ||||
|   useState, | ||||
|   useEffect, | ||||
|   useMemo, | ||||
|   useRef, | ||||
|   useCallback, | ||||
| } from "react"; | ||||
| import { useForm, UseFormRegister, useWatch } from "react-hook-form"; | ||||
| import React, { FC, Fragment, useState } from "react"; | ||||
| import ReactDOM from "react-dom"; | ||||
| import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; | ||||
| import * as yup from "yup"; | ||||
| import { | ||||
|   useMaybeControlledValue, | ||||
|   useRunnerState, | ||||
|   useSquiggle, | ||||
| } from "../lib/hooks"; | ||||
| import { yupResolver } from "@hookform/resolvers/yup"; | ||||
| import { Tab } from "@headlessui/react"; | ||||
| import { | ||||
|   ChartSquareBarIcon, | ||||
|   CheckCircleIcon, | ||||
|   ClipboardCopyIcon, | ||||
|   CodeIcon, | ||||
|   CogIcon, | ||||
|   CurrencyDollarIcon, | ||||
|   EyeIcon, | ||||
|   PauseIcon, | ||||
|   PlayIcon, | ||||
|   RefreshIcon, | ||||
| } from "@heroicons/react/solid"; | ||||
| import clsx from "clsx"; | ||||
| 
 | ||||
| import { environment, SqProject } from "@quri/squiggle-lang"; | ||||
| import { defaultBindings, environment } from "@quri/squiggle-lang"; | ||||
| 
 | ||||
| import { SquiggleChartProps } from "./SquiggleChart"; | ||||
| import { SquiggleChart } from "./SquiggleChart"; | ||||
| import { CodeEditor } from "./CodeEditor"; | ||||
| import { JsonEditor } from "./JsonEditor"; | ||||
| import { ErrorAlert, SuccessAlert } from "./Alert"; | ||||
| import { SquiggleContainer } from "./SquiggleContainer"; | ||||
| import { Toggle } from "./ui/Toggle"; | ||||
| 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 & { | ||||
| interface PlaygroundProps { | ||||
|   /** The initial squiggle string to put in the playground */ | ||||
|   defaultCode?: string; | ||||
|   initialSquiggleString?: string; | ||||
|   /** How many pixels high is the playground */ | ||||
|   height?: number; | ||||
|   /** Whether to show the types of outputs in the playground */ | ||||
|   showTypes?: boolean; | ||||
|   /** Whether to show the log scale controls in the playground */ | ||||
|   showControls?: boolean; | ||||
|   /** Whether to show the summary table in the playground */ | ||||
|   showSummary?: boolean; | ||||
|   /** If code is set, component becomes controlled */ | ||||
|   code?: string; | ||||
|   onCodeChange?(expr: string): void; | ||||
|   /* When settings change */ | ||||
|   onSettingsChange?(settings: any): void; | ||||
|   /** Should we show the editor? */ | ||||
|   showEditor?: boolean; | ||||
|   /** Useful for playground on squiggle website, where we update the anchor link based on current code and settings */ | ||||
|   showShareButton?: boolean; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| const schema = yup | ||||
|   .object({}) | ||||
|   .object() | ||||
|   .shape({ | ||||
|     sampleCount: yup | ||||
|       .number() | ||||
|  | @ -78,68 +58,311 @@ const schema = yup | |||
|       .default(1000) | ||||
|       .min(10) | ||||
|       .max(10000), | ||||
|     chartHeight: yup.number().required().positive().integer().default(350), | ||||
|     leftSizePercent: yup | ||||
|       .number() | ||||
|       .required() | ||||
|       .positive() | ||||
|       .integer() | ||||
|       .min(10) | ||||
|       .max(100) | ||||
|       .default(50), | ||||
|     showTypes: yup.boolean(), | ||||
|     showControls: yup.boolean(), | ||||
|     showSummary: yup.boolean(), | ||||
|     showEditor: yup.boolean(), | ||||
|     showSettingsPage: yup.boolean().default(false), | ||||
|     diagramStart: yup | ||||
|       .number() | ||||
|       .required() | ||||
|       .positive() | ||||
|       .integer() | ||||
|       .default(0) | ||||
|       .min(0), | ||||
|     diagramStop: yup | ||||
|       .number() | ||||
|       .required() | ||||
|       .positive() | ||||
|       .integer() | ||||
|       .default(10) | ||||
|       .min(0), | ||||
|     diagramCount: yup | ||||
|       .number() | ||||
|       .required() | ||||
|       .positive() | ||||
|       .integer() | ||||
|       .default(20) | ||||
|       .min(2), | ||||
|   }) | ||||
|   .concat(viewSettingsSchema); | ||||
|   .required(); | ||||
| 
 | ||||
| type FormFields = yup.InferType<typeof schema>; | ||||
| type StyledTabProps = { | ||||
|   name: string; | ||||
|   icon: (props: React.ComponentProps<"svg">) => JSX.Element; | ||||
| }; | ||||
| 
 | ||||
| const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({ | ||||
|   register, | ||||
| const StyledTab: React.FC<StyledTabProps> = ({ name, icon: Icon }) => { | ||||
|   return ( | ||||
|     <Tab key={name} as={Fragment}> | ||||
|       {({ selected }) => ( | ||||
|         <button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100"> | ||||
|           <span | ||||
|             className={clsx( | ||||
|               "p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium", | ||||
|               selected && "bg-white shadow-sm ring-1 ring-black ring-opacity-5" | ||||
|             )} | ||||
|           > | ||||
|             <Icon | ||||
|               className={clsx( | ||||
|                 "-ml-0.5 mr-2 h-4 w-4", | ||||
|                 selected | ||||
|                   ? "text-slate-500" | ||||
|                   : "text-gray-400 group-hover:text-gray-900" | ||||
|               )} | ||||
|             /> | ||||
|             <span | ||||
|               className={clsx( | ||||
|                 selected | ||||
|                   ? "text-gray-900" | ||||
|                   : "text-gray-600 group-hover:text-gray-900" | ||||
|               )} | ||||
|             > | ||||
|               {name} | ||||
|             </span> | ||||
|           </span> | ||||
|         </button> | ||||
|       )} | ||||
|     </Tab> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({ | ||||
|   title, | ||||
|   children, | ||||
| }) => ( | ||||
|   <div className="space-y-6 p-3 max-w-xl"> | ||||
|     <div> | ||||
|       <InputItem | ||||
|         name="sampleCount" | ||||
|         type="number" | ||||
|         label="Sample Count" | ||||
|         register={register} | ||||
|       /> | ||||
|       <div className="mt-2"> | ||||
|         <Text> | ||||
|           How many samples to use for Monte Carlo simulations. This can | ||||
|           occasionally be overridden by specific Squiggle programs. | ||||
|         </Text> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div> | ||||
|       <InputItem | ||||
|         name="xyPointLength" | ||||
|         type="number" | ||||
|         register={register} | ||||
|         label="Coordinate Count (For PointSet Shapes)" | ||||
|       /> | ||||
|       <div className="mt-2"> | ||||
|         <Text> | ||||
|           When distributions are converted into PointSet shapes, we need to know | ||||
|           how many coordinates to use. | ||||
|         </Text> | ||||
|       </div> | ||||
|     </div> | ||||
|   <div> | ||||
|     <header className="text-lg leading-6 font-medium text-gray-900"> | ||||
|       {title} | ||||
|     </header> | ||||
|     <div className="mt-4">{children}</div> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| const InputVariablesSettings: React.FC<{ | ||||
|   initialImports: JsImports; | ||||
|   setImports: (imports: JsImports) => void; | ||||
| }> = ({ initialImports, setImports }) => { | ||||
|   const [importString, setImportString] = useState(() => | ||||
|     JSON.stringify(initialImports) | ||||
|   ); | ||||
|   const [importsAreValid, setImportsAreValid] = useState(true); | ||||
| const Text: FC<{ children: React.ReactNode }> = ({ children }) => ( | ||||
|   <p className="text-sm text-gray-500">{children}</p> | ||||
| ); | ||||
| 
 | ||||
|   const onChange = (value: string) => { | ||||
|     setImportString(value); | ||||
|     let imports = {}; | ||||
| function InputItem<T>({ | ||||
|   name, | ||||
|   label, | ||||
|   type, | ||||
|   register, | ||||
| }: { | ||||
|   name: Path<T>; | ||||
|   label: string; | ||||
|   type: "number"; | ||||
|   register: UseFormRegister<T>; | ||||
| }) { | ||||
|   return ( | ||||
|     <label className="block"> | ||||
|       <div className="text-sm font-medium text-gray-600 mb-1">{label}</div> | ||||
|       <input | ||||
|         type={type} | ||||
|         {...register(name)} | ||||
|         className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" | ||||
|       /> | ||||
|     </label> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function Checkbox<T>({ | ||||
|   name, | ||||
|   label, | ||||
|   register, | ||||
| }: { | ||||
|   name: Path<T>; | ||||
|   label: string; | ||||
|   register: UseFormRegister<T>; | ||||
| }) { | ||||
|   return ( | ||||
|     <label className="flex items-center"> | ||||
|       <input | ||||
|         type="checkbox" | ||||
|         {...register(name)} | ||||
|         className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" | ||||
|       /> | ||||
|       {/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */} | ||||
|       <div className="ml-3 text-sm font-medium text-gray-700">{label}</div> | ||||
|     </label> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export const SquigglePlayground: FC<PlaygroundProps> = ({ | ||||
|   initialSquiggleString = "", | ||||
|   height = 500, | ||||
|   showTypes = false, | ||||
|   showControls = false, | ||||
|   showSummary = false, | ||||
|   code: controlledCode, | ||||
|   onCodeChange, | ||||
|   showEditor = true, | ||||
| }) => { | ||||
|   const [uncontrolledCode, setUncontrolledCode] = useState( | ||||
|     initialSquiggleString | ||||
|   ); | ||||
|   const [importString, setImportString] = useState("{}"); | ||||
|   const [imports, setImports] = useState({}); | ||||
|   const [importsAreValid, setImportsAreValid] = useState(true); | ||||
|   const { register, control } = useForm({ | ||||
|     resolver: yupResolver(schema), | ||||
|     defaultValues: { | ||||
|       sampleCount: 1000, | ||||
|       xyPointLength: 1000, | ||||
|       chartHeight: 150, | ||||
|       showTypes: showTypes, | ||||
|       showControls: showControls, | ||||
|       showSummary: showSummary, | ||||
|       showEditor: showEditor, | ||||
|       leftSizePercent: 50, | ||||
|       showSettingsPage: false, | ||||
|       diagramStart: 0, | ||||
|       diagramStop: 10, | ||||
|       diagramCount: 20, | ||||
|     }, | ||||
|   }); | ||||
|   const vars = useWatch({ | ||||
|     control, | ||||
|   }); | ||||
|   const chartSettings = { | ||||
|     start: Number(vars.diagramStart), | ||||
|     stop: Number(vars.diagramStop), | ||||
|     count: Number(vars.diagramCount), | ||||
|   }; | ||||
|   const env: environment = { | ||||
|     sampleCount: Number(vars.sampleCount), | ||||
|     xyPointLength: Number(vars.xyPointLength), | ||||
|   }; | ||||
|   const getChangeJson = (r: string) => { | ||||
|     setImportString(r); | ||||
|     try { | ||||
|       imports = JSON.parse(value); | ||||
|       setImports(JSON.parse(r)); | ||||
|       setImportsAreValid(true); | ||||
|     } catch (e) { | ||||
|       setImportsAreValid(false); | ||||
|     } | ||||
|     setImports(imports); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|   const code = controlledCode ?? uncontrolledCode; | ||||
| 
 | ||||
|   const samplingSettings = ( | ||||
|     <div className="space-y-6 p-3 max-w-xl"> | ||||
|       <div> | ||||
|         <InputItem | ||||
|           name="sampleCount" | ||||
|           type="number" | ||||
|           label="Sample Count" | ||||
|           register={register} | ||||
|         /> | ||||
|         <div className="mt-2"> | ||||
|           <Text> | ||||
|             How many samples to use for Monte Carlo simulations. This can | ||||
|             occasionally be overridden by specific Squiggle programs. | ||||
|           </Text> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div> | ||||
|         <InputItem | ||||
|           name="xyPointLength" | ||||
|           type="number" | ||||
|           register={register} | ||||
|           label="Coordinate Count (For PointSet Shapes)" | ||||
|         /> | ||||
|         <div className="mt-2"> | ||||
|           <Text> | ||||
|             When distributions are converted into PointSet shapes, we need to | ||||
|             know how many coordinates to use. | ||||
|           </Text> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| 
 | ||||
|   const viewSettings = ( | ||||
|     <div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl"> | ||||
|       <HeadedSection title="General Display Settings"> | ||||
|         <div className="space-y-4"> | ||||
|           <Checkbox | ||||
|             name="showEditor" | ||||
|             register={register} | ||||
|             label="Show code editor on left" | ||||
|           /> | ||||
|           <InputItem | ||||
|             name="chartHeight" | ||||
|             type="number" | ||||
|             register={register} | ||||
|             label="Chart Height (in pixels)" | ||||
|           /> | ||||
|           <Checkbox | ||||
|             name="showTypes" | ||||
|             register={register} | ||||
|             label="Show information about displayed types" | ||||
|           /> | ||||
|         </div> | ||||
|       </HeadedSection> | ||||
| 
 | ||||
|       <div className="pt-8"> | ||||
|         <HeadedSection title="Distribution Display Settings"> | ||||
|           <div className="space-y-2"> | ||||
|             <Checkbox | ||||
|               register={register} | ||||
|               name="showControls" | ||||
|               label="Show toggles to adjust scale of x and y axes" | ||||
|             /> | ||||
|             <Checkbox | ||||
|               register={register} | ||||
|               name="showSummary" | ||||
|               label="Show summary statistics" | ||||
|             /> | ||||
|           </div> | ||||
|         </HeadedSection> | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="pt-8"> | ||||
|         <HeadedSection title="Function Display Settings"> | ||||
|           <div className="space-y-6"> | ||||
|             <Text> | ||||
|               When displaying functions of single variables that return numbers | ||||
|               or distributions, we need to use defaults for the x-axis. We need | ||||
|               to select a minimum and maximum value of x to sample, and a number | ||||
|               n of the number of points to sample. | ||||
|             </Text> | ||||
|             <div className="space-y-4"> | ||||
|               <InputItem | ||||
|                 type="number" | ||||
|                 name="diagramStart" | ||||
|                 register={register} | ||||
|                 label="Min X Value" | ||||
|               /> | ||||
|               <InputItem | ||||
|                 type="number" | ||||
|                 name="diagramStop" | ||||
|                 register={register} | ||||
|                 label="Max X Value" | ||||
|               /> | ||||
|               <InputItem | ||||
|                 type="number" | ||||
|                 name="diagramCount" | ||||
|                 register={register} | ||||
|                 label="Points between X min and X max to sample" | ||||
|               /> | ||||
|             </div> | ||||
|           </div> | ||||
|         </HeadedSection> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| 
 | ||||
|   const inputVariableSettings = ( | ||||
|     <div className="p-3 max-w-3xl"> | ||||
|       <HeadedSection title="Import Variables from JSON"> | ||||
|         <div className="space-y-6"> | ||||
|  | @ -151,7 +374,7 @@ const InputVariablesSettings: React.FC<{ | |||
|           <div className="border border-slate-200 mt-6 mb-2"> | ||||
|             <JsonEditor | ||||
|               value={importString} | ||||
|               onChange={onChange} | ||||
|               onChange={getChangeJson} | ||||
|               oneLine={false} | ||||
|               showGutter={true} | ||||
|               height={150} | ||||
|  | @ -170,193 +393,32 @@ const InputVariablesSettings: React.FC<{ | |||
|       </HeadedSection> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const RunControls: React.FC<{ | ||||
|   autorunMode: boolean; | ||||
|   isRunning: boolean; | ||||
|   isStale: boolean; | ||||
|   onAutorunModeChange: (value: boolean) => void; | ||||
|   run: () => void; | ||||
| }> = ({ autorunMode, isRunning, isStale, onAutorunModeChange, run }) => { | ||||
|   const CurrentPlayIcon = isRunning ? RefreshIcon : PlayIcon; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex space-x-1 items-center" data-testid="autorun-controls"> | ||||
|       {autorunMode ? null : ( | ||||
|         <button onClick={run}> | ||||
|           <CurrentPlayIcon | ||||
|             className={clsx( | ||||
|               "w-8 h-8", | ||||
|               isRunning && "animate-spin", | ||||
|               isStale ? "text-indigo-500" : "text-gray-400" | ||||
|             )} | ||||
|           /> | ||||
|         </button> | ||||
|       )} | ||||
|       <Toggle | ||||
|         texts={["Autorun", "Paused"]} | ||||
|         icons={[CheckCircleIcon, PauseIcon]} | ||||
|         status={autorunMode} | ||||
|         onChange={onAutorunModeChange} | ||||
|         spinIcon={autorunMode && isRunning} | ||||
|       /> | ||||
|     </div> | ||||
|   const squiggleChart = ( | ||||
|     <SquiggleChart | ||||
|       squiggleString={code} | ||||
|       environment={env} | ||||
|       chartSettings={chartSettings} | ||||
|       height={vars.chartHeight} | ||||
|       showTypes={vars.showTypes} | ||||
|       showControls={vars.showControls} | ||||
|       showSummary={vars.showSummary} | ||||
|       bindings={defaultBindings} | ||||
|       jsImports={imports} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const ShareButton: React.FC = () => { | ||||
|   const [isCopied, setIsCopied] = useState(false); | ||||
|   const copy = () => { | ||||
|     navigator.clipboard.writeText((window.top || window).location.href); | ||||
|     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 = { | ||||
|   getLeftPanelElement: () => HTMLDivElement | undefined; | ||||
| }; | ||||
| export const PlaygroundContext = React.createContext<PlaygroundContextShape>({ | ||||
|   getLeftPanelElement: () => undefined, | ||||
| }); | ||||
| 
 | ||||
| export const SquigglePlayground: FC<PlaygroundProps> = ({ | ||||
|   defaultCode = "", | ||||
|   height = 500, | ||||
|   showSummary = true, | ||||
|   logX = false, | ||||
|   expY = false, | ||||
|   title, | ||||
|   minX, | ||||
|   maxX, | ||||
|   tickFormat = defaultTickFormat, | ||||
|   distributionChartActions, | ||||
|   code: controlledCode, | ||||
|   onCodeChange, | ||||
|   onSettingsChange, | ||||
|   showEditor = true, | ||||
|   showShareButton = false, | ||||
|   continues, | ||||
|   project, | ||||
| }) => { | ||||
|   const [code, setCode] = useMaybeControlledValue({ | ||||
|     value: controlledCode, | ||||
|     defaultValue: defaultCode, | ||||
|     onChange: onCodeChange, | ||||
|   }); | ||||
| 
 | ||||
|   const [imports, setImports] = useState<JsImports>({}); | ||||
| 
 | ||||
|   const { register, control } = useForm({ | ||||
|     resolver: yupResolver(schema), | ||||
|     defaultValues: { | ||||
|       sampleCount: 1000, | ||||
|       xyPointLength: 1000, | ||||
|       chartHeight: 150, | ||||
|       logX, | ||||
|       expY, | ||||
|       title, | ||||
|       minX, | ||||
|       maxX, | ||||
|       tickFormat, | ||||
|       distributionChartActions, | ||||
|       showSummary, | ||||
|       showEditor, | ||||
|       diagramStart: 0, | ||||
|       diagramStop: 10, | ||||
|       diagramCount: 20, | ||||
|     }, | ||||
|   }); | ||||
|   const vars = useWatch({ | ||||
|     control, | ||||
|   }); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     onSettingsChange?.(vars); | ||||
|   }, [vars, onSettingsChange]); | ||||
| 
 | ||||
|   const environment: environment = useMemo( | ||||
|     () => ({ | ||||
|       sampleCount: Number(vars.sampleCount), | ||||
|       xyPointLength: Number(vars.xyPointLength), | ||||
|     }), | ||||
|     [vars.sampleCount, vars.xyPointLength] | ||||
|   ); | ||||
| 
 | ||||
|   const { | ||||
|     run, | ||||
|     autorunMode, | ||||
|     setAutorunMode, | ||||
|     isRunning, | ||||
|     renderedCode, | ||||
|     executionId, | ||||
|   } = useRunnerState(code); | ||||
| 
 | ||||
|   const resultAndBindings = useSquiggle({ | ||||
|     environment, | ||||
|     continues, | ||||
|     code: renderedCode, | ||||
|     project, | ||||
|     jsImports: imports, | ||||
|     executionId, | ||||
|   }); | ||||
| 
 | ||||
|   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 ? ( | ||||
|     <div className="border border-slate-200" data-testid="squiggle-editor"> | ||||
|     <div className="border border-slate-200"> | ||||
|       <CodeEditor | ||||
|         errorLocations={errorLocations} | ||||
|         value={code} | ||||
|         onChange={setCode} | ||||
|         onSubmit={run} | ||||
|         onChange={(newCode) => { | ||||
|           if (controlledCode === undefined) { | ||||
|             // uncontrolled mode
 | ||||
|             setUncontrolledCode(newCode); | ||||
|           } | ||||
|           onCodeChange?.(newCode); | ||||
|         }} | ||||
|         oneLine={false} | ||||
|         showGutter={true} | ||||
|         height={height - 1} | ||||
|  | @ -367,84 +429,45 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({ | |||
|   ); | ||||
| 
 | ||||
|   const tabs = ( | ||||
|     <StyledTab.Panels> | ||||
|       <StyledTab.Panel>{firstTab}</StyledTab.Panel> | ||||
|       <StyledTab.Panel> | ||||
|         <SamplingSettings register={register} /> | ||||
|       </StyledTab.Panel> | ||||
|       <StyledTab.Panel> | ||||
|         <ViewSettings | ||||
|           register={ | ||||
|             // This is dangerous, but doesn't cause any problems.
 | ||||
|             // I tried to make `ViewSettings` generic (to allow it to accept any extension of a settings schema), but it didn't work.
 | ||||
|             register as unknown as UseFormRegister< | ||||
|               yup.InferType<typeof viewSettingsSchema> | ||||
|             > | ||||
|           } | ||||
|         /> | ||||
|       </StyledTab.Panel> | ||||
|       <StyledTab.Panel> | ||||
|         <InputVariablesSettings | ||||
|           initialImports={imports} | ||||
|           setImports={setImports} | ||||
|         /> | ||||
|       </StyledTab.Panel> | ||||
|     </StyledTab.Panels> | ||||
|     <Tab.Panels> | ||||
|       <Tab.Panel>{firstTab}</Tab.Panel> | ||||
|       <Tab.Panel>{samplingSettings}</Tab.Panel> | ||||
|       <Tab.Panel>{viewSettings}</Tab.Panel> | ||||
|       <Tab.Panel>{inputVariableSettings}</Tab.Panel> | ||||
|     </Tab.Panels> | ||||
|   ); | ||||
| 
 | ||||
|   const leftPanelRef = useRef<HTMLDivElement | null>(null); | ||||
| 
 | ||||
|   const withEditor = ( | ||||
|     <div className="flex mt-2"> | ||||
|       <div | ||||
|         className="w-1/2 relative" | ||||
|         style={{ minHeight: height }} | ||||
|         ref={leftPanelRef} | ||||
|       > | ||||
|         {tabs} | ||||
|       </div> | ||||
|       <div className="w-1/2 p-2 pl-4" data-testid="playground-result"> | ||||
|         {squiggleChart} | ||||
|       </div> | ||||
|     <div className="flex mt-1"> | ||||
|       <div className="w-1/2">{tabs}</div> | ||||
|       <div className="w-1/2 p-2 pl-4">{squiggleChart}</div> | ||||
|     </div> | ||||
|   ); | ||||
| 
 | ||||
|   const withoutEditor = <div className="mt-3">{tabs}</div>; | ||||
| 
 | ||||
|   const getLeftPanelElement = useCallback(() => { | ||||
|     return leftPanelRef.current ?? undefined; | ||||
|   }, []); | ||||
| 
 | ||||
|   return ( | ||||
|     <SquiggleContainer> | ||||
|       <PlaygroundContext.Provider value={{ getLeftPanelElement }}> | ||||
|         <StyledTab.Group> | ||||
|           <div className="pb-4"> | ||||
|             <div className="flex justify-between items-center"> | ||||
|               <StyledTab.List> | ||||
|                 <StyledTab | ||||
|                   name={vars.showEditor ? "Code" : "Display"} | ||||
|                   icon={vars.showEditor ? CodeIcon : EyeIcon} | ||||
|                 /> | ||||
|                 <StyledTab name="Sampling Settings" icon={CogIcon} /> | ||||
|                 <StyledTab name="View Settings" icon={ChartSquareBarIcon} /> | ||||
|                 <StyledTab name="Input Variables" icon={CurrencyDollarIcon} /> | ||||
|               </StyledTab.List> | ||||
|               <div className="flex space-x-2 items-center"> | ||||
|                 <RunControls | ||||
|                   autorunMode={autorunMode} | ||||
|                   isStale={renderedCode !== code} | ||||
|                   run={run} | ||||
|                   isRunning={isRunning} | ||||
|                   onAutorunModeChange={setAutorunMode} | ||||
|                 /> | ||||
|                 {showShareButton && <ShareButton />} | ||||
|               </div> | ||||
|             </div> | ||||
|             {vars.showEditor ? withEditor : withoutEditor} | ||||
|           </div> | ||||
|         </StyledTab.Group> | ||||
|       </PlaygroundContext.Provider> | ||||
|       <Tab.Group> | ||||
|         <div className="pb-4"> | ||||
|           <Tab.List className="flex w-fit p-0.5 mt-2 rounded-md bg-slate-100 hover:bg-slate-200"> | ||||
|             <StyledTab | ||||
|               name={vars.showEditor ? "Code" : "Display"} | ||||
|               icon={vars.showEditor ? CodeIcon : EyeIcon} | ||||
|             /> | ||||
|             <StyledTab name="Sampling Settings" icon={CogIcon} /> | ||||
|             <StyledTab name="View Settings" icon={ChartSquareBarIcon} /> | ||||
|             <StyledTab name="Input Variables" icon={CurrencyDollarIcon} /> | ||||
|           </Tab.List> | ||||
|           {vars.showEditor ? withEditor : withoutEditor} | ||||
|         </div> | ||||
|       </Tab.Group> | ||||
|     </SquiggleContainer> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export function renderSquigglePlaygroundToDom(props: PlaygroundProps) { | ||||
|   const parent = document.createElement("div"); | ||||
|   ReactDOM.render(<SquigglePlayground {...props} />, parent); | ||||
|   return parent; | ||||
| } | ||||
|  |  | |||
|  | @ -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> | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | @ -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> | ||||
|   ); | ||||
| }; | ||||
|  | @ -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> | ||||
|   ); | ||||
| }; | ||||
|  | @ -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, | ||||
| }); | ||||
|  | @ -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> | ||||
|   ); | ||||
| }; | ||||
|  | @ -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("."); | ||||
|  | @ -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> | ||||
|   ); | ||||
| }; | ||||
|  | @ -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> | ||||
|   ); | ||||
| }; | ||||
|  | @ -1,37 +0,0 @@ | |||
| import clsx from "clsx"; | ||||
| import React from "react"; | ||||
| import { Path, UseFormRegister, FieldValues } from "react-hook-form"; | ||||
| 
 | ||||
| export function Checkbox<T extends FieldValues>({ | ||||
|   name, | ||||
|   label, | ||||
|   register, | ||||
|   disabled, | ||||
|   tooltip, | ||||
| }: { | ||||
|   name: Path<T>; | ||||
|   label: string; | ||||
|   register: UseFormRegister<T>; | ||||
|   disabled?: boolean; | ||||
|   tooltip?: string; | ||||
| }) { | ||||
|   return ( | ||||
|     <label className="flex items-center" title={tooltip}> | ||||
|       <input | ||||
|         type="checkbox" | ||||
|         disabled={disabled} | ||||
|         {...register(name)} | ||||
|         className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" | ||||
|       /> | ||||
|       {/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */} | ||||
|       <div | ||||
|         className={clsx( | ||||
|           "ml-3 text-sm font-medium", | ||||
|           disabled ? "text-gray-400" : "text-gray-700" | ||||
|         )} | ||||
|       > | ||||
|         {label} | ||||
|       </div> | ||||
|     </label> | ||||
|   ); | ||||
| } | ||||
|  | @ -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> | ||||
| ); | ||||
|  | @ -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> | ||||
|   ); | ||||
| } | ||||
|  | @ -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; | ||||
|  | @ -1,60 +0,0 @@ | |||
| import React, { Fragment } from "react"; | ||||
| import { Tab } from "@headlessui/react"; | ||||
| import clsx from "clsx"; | ||||
| 
 | ||||
| type StyledTabProps = { | ||||
|   name: string; | ||||
|   icon: (props: React.ComponentProps<"svg">) => JSX.Element; | ||||
| }; | ||||
| 
 | ||||
| type StyledTabType = React.FC<StyledTabProps> & { | ||||
|   List: React.FC<{ children: React.ReactNode }>; | ||||
|   Group: typeof Tab.Group; | ||||
|   Panels: typeof Tab.Panels; | ||||
|   Panel: typeof Tab.Panel; | ||||
| }; | ||||
| 
 | ||||
| export const StyledTab: StyledTabType = ({ name, icon: Icon }) => { | ||||
|   return ( | ||||
|     <Tab as={Fragment}> | ||||
|       {({ selected }) => ( | ||||
|         <button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100"> | ||||
|           <span | ||||
|             className={clsx( | ||||
|               "p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium", | ||||
|               selected && "bg-white shadow-sm ring-1 ring-black ring-opacity-5" | ||||
|             )} | ||||
|           > | ||||
|             <Icon | ||||
|               className={clsx( | ||||
|                 "-ml-0.5 mr-2 h-4 w-4", | ||||
|                 selected | ||||
|                   ? "text-slate-500" | ||||
|                   : "text-gray-400 group-hover:text-gray-900" | ||||
|               )} | ||||
|             /> | ||||
|             <span | ||||
|               className={clsx( | ||||
|                 selected | ||||
|                   ? "text-gray-900" | ||||
|                   : "text-gray-600 group-hover:text-gray-900" | ||||
|               )} | ||||
|             > | ||||
|               {name} | ||||
|             </span> | ||||
|           </span> | ||||
|         </button> | ||||
|       )} | ||||
|     </Tab> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| StyledTab.List = ({ children }) => ( | ||||
|   <Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200"> | ||||
|     {children} | ||||
|   </Tab.List> | ||||
| ); | ||||
| 
 | ||||
| StyledTab.Group = Tab.Group; | ||||
| StyledTab.Panels = Tab.Panels; | ||||
| StyledTab.Panel = Tab.Panel; | ||||
|  | @ -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> | ||||
| ); | ||||
|  | @ -1,47 +0,0 @@ | |||
| import { RefreshIcon } from "@heroicons/react/solid"; | ||||
| import clsx from "clsx"; | ||||
| import React from "react"; | ||||
| 
 | ||||
| type IconType = (props: React.ComponentProps<"svg">) => JSX.Element; | ||||
| 
 | ||||
| type Props = { | ||||
|   status: boolean; | ||||
|   onChange: (status: boolean) => void; | ||||
|   texts: [string, string]; | ||||
|   icons: [IconType, IconType]; | ||||
|   spinIcon?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export const Toggle: React.FC<Props> = ({ | ||||
|   status, | ||||
|   onChange, | ||||
|   texts: [onText, offText], | ||||
|   icons: [OnIcon, OffIcon], | ||||
|   spinIcon, | ||||
| }) => { | ||||
|   const CurrentIcon = status ? OnIcon : OffIcon; | ||||
|   return ( | ||||
|     <button | ||||
|       className={clsx( | ||||
|         "rounded-md py-0.5 bg-slate-500 text-white text-xs font-semibold flex items-center space-x-1", | ||||
|         status ? "bg-slate-500" : "bg-gray-400", | ||||
|         status ? "pl-1 pr-3" : "pl-3 pr-1", | ||||
|         !status && "flex-row-reverse space-x-reverse" | ||||
|       )} | ||||
|       onClick={() => onChange(!status)} | ||||
|     > | ||||
|       <div className="relative w-6 h-6" key={String(spinIcon)}> | ||||
|         <CurrentIcon | ||||
|           className={clsx( | ||||
|             "w-6 h-6 absolute opacity-100", | ||||
|             spinIcon && "animate-hide" | ||||
|           )} | ||||
|         /> | ||||
|         {spinIcon && ( | ||||
|           <RefreshIcon className="w-6 h-6 absolute opacity-0 animate-appear-and-spin" /> | ||||
|         )} | ||||
|       </div> | ||||
|       <span>{status ? onText : offText}</span> | ||||
|     </button> | ||||
|   ); | ||||
| }; | ||||
|  | @ -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> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | @ -1,5 +1,14 @@ | |||
| export { SqProject } from "@quri/squiggle-lang/"; | ||||
| export { SquiggleChart } from "./components/SquiggleChart"; | ||||
| export { SquiggleEditor } from "./components/SquiggleEditor"; | ||||
| export { SquigglePlayground } from "./components/SquigglePlayground"; | ||||
| export { | ||||
|   SquiggleEditor, | ||||
|   SquigglePartial, | ||||
|   renderSquiggleEditorToDom, | ||||
|   renderSquigglePartialToDom, | ||||
| } from "./components/SquiggleEditor"; | ||||
| export { | ||||
|   SquigglePlayground, | ||||
|   renderSquigglePlaygroundToDom, | ||||
| } from "./components/SquigglePlayground"; | ||||
| export { SquiggleContainer } from "./components/SquiggleContainer"; | ||||
| 
 | ||||
| export { mergeBindings } from "@quri/squiggle-lang"; | ||||
|  |  | |||
|  | @ -1,398 +0,0 @@ | |||
| import { VisualizationSpec } from "react-vega"; | ||||
| import type { LogScale, LinearScale, PowScale, TimeScale } from "vega"; | ||||
| 
 | ||||
| export type DistributionChartSpecOptions = { | ||||
|   /** Set the x scale to be logarithmic by deault */ | ||||
|   logX: boolean; | ||||
|   /** Set the y scale to be exponential by deault */ | ||||
|   expY: boolean; | ||||
|   /** The minimum x coordinate shown on the chart */ | ||||
|   minX?: number; | ||||
|   /** The maximum x coordinate shown on the chart */ | ||||
|   maxX?: number; | ||||
|   /** The title of the chart */ | ||||
|   title?: string; | ||||
|   /** The formatting of the ticks */ | ||||
|   format?: string; | ||||
|   /** Whether the x-axis should be dates or numbers */ | ||||
|   xAxisType?: "number" | "dateTime"; | ||||
| }; | ||||
| 
 | ||||
| /** X Scales */ | ||||
| export const linearXScale: LinearScale = { | ||||
|   name: "xscale", | ||||
|   clamp: true, | ||||
|   type: "linear", | ||||
|   range: "width", | ||||
|   zero: false, | ||||
|   nice: false, | ||||
| }; | ||||
| 
 | ||||
| export const logXScale: LogScale = { | ||||
|   name: "xscale", | ||||
|   type: "log", | ||||
|   range: "width", | ||||
|   zero: false, | ||||
|   base: 10, | ||||
|   nice: false, | ||||
|   clamp: true, | ||||
| }; | ||||
| 
 | ||||
| export const timeXScale: TimeScale = { | ||||
|   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", | ||||
|   type: "pow", | ||||
|   exponent: 0.1, | ||||
|   range: "height", | ||||
|   zero: true, | ||||
|   nice: false, | ||||
| }; | ||||
| 
 | ||||
| export const defaultTickFormat = ".9~s"; | ||||
| export const timeTickFormat = "%b %d, %Y %H:%M"; | ||||
| const width = 500; | ||||
| 
 | ||||
| export function buildVegaSpec( | ||||
|   specOptions: DistributionChartSpecOptions & { maxY: number } | ||||
| ): VisualizationSpec { | ||||
|   const { | ||||
|     title, | ||||
|     minX, | ||||
|     maxX, | ||||
|     logX, | ||||
|     expY, | ||||
|     xAxisType = "number", | ||||
|     maxY, | ||||
|   } = specOptions; | ||||
| 
 | ||||
|   const dateTime = xAxisType === "dateTime"; | ||||
| 
 | ||||
|   // some fallbacks
 | ||||
|   const format = specOptions?.format | ||||
|     ? specOptions.format | ||||
|     : dateTime | ||||
|     ? timeTickFormat | ||||
|     : defaultTickFormat; | ||||
| 
 | ||||
|   let xScale = dateTime ? timeXScale : logX ? logXScale : linearXScale; | ||||
| 
 | ||||
|   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", | ||||
|     description: "Squiggle plot chart", | ||||
|     width: width, | ||||
|     height: 100, | ||||
|     padding: 5, | ||||
|     data: [{ name: "data" }, { name: "domain" }, { name: "samples" }], | ||||
|     signals: [ | ||||
|       { | ||||
|         name: "hover", | ||||
|         value: null, | ||||
|         on: [ | ||||
|           { events: "mouseover", update: "datum" }, | ||||
|           { events: "mouseout", update: "null" }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         name: "position", | ||||
|         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" }, | ||||
|       }, | ||||
|     ], | ||||
|     axes: [ | ||||
|       { | ||||
|         orient: "bottom", | ||||
|         scale: "xscale", | ||||
|         labelColor: "#727d93", | ||||
|         tickColor: "#fff", | ||||
|         tickOpacity: 0.0, | ||||
|         domainColor: "#fff", | ||||
|         domainOpacity: 0.0, | ||||
|         format: format, | ||||
|         tickCount: dateTime ? 3 : 10, | ||||
|         labelOverlap: "greedy", | ||||
|       }, | ||||
|     ], | ||||
|     marks: [ | ||||
|       { | ||||
|         name: "all_distributions", | ||||
|         type: "group", | ||||
|         from: { | ||||
|           facet: { | ||||
|             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: { | ||||
|           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: { | ||||
|             text: { | ||||
|               signal: dateTime | ||||
|                 ? "position_scaled ? utcyear(position_scaled) + '-' + utcmonth(position_scaled) + '-' + utcdate(position_scaled) + 'T' + utchours(position_scaled)+':' +utcminutes(position_scaled) : ''" | ||||
|                 : "position_scaled ? format(position_scaled, ',.4r')  : ''", | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         type: "rule", | ||||
|         interactive: false, | ||||
|         encode: { | ||||
|           enter: { | ||||
|             x: { value: 0 }, | ||||
|             y: { scale: "yscale", value: 0 }, | ||||
| 
 | ||||
|             y2: { | ||||
|               signal: "height", | ||||
|               offset: 2, | ||||
|             }, | ||||
|             strokeDash: { value: [5, 5] }, | ||||
|           }, | ||||
| 
 | ||||
|           update: { | ||||
|             x: { | ||||
|               signal: | ||||
|                 "position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null", | ||||
|             }, | ||||
| 
 | ||||
|             opacity: { | ||||
|               signal: | ||||
|                 "position ? position[0] < 0 ? 0 : position[0] > width ? 0 : 1 : 0", | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|     legends: [ | ||||
|       { | ||||
|         fill: "color", | ||||
|         orient: "top", | ||||
|         labelFontSize: 12, | ||||
|         encode: { | ||||
|           symbols: { | ||||
|             update: { | ||||
|               fill: [ | ||||
|                 { test: "length(domain('color')) == 1", value: "transparent" }, | ||||
|                 { scale: "color", field: "value" }, | ||||
|               ], | ||||
|             }, | ||||
|           }, | ||||
|           labels: { | ||||
|             interactive: true, | ||||
|             update: { | ||||
|               fill: [ | ||||
|                 { test: "length(domain('color')) == 1", value: "transparent" }, | ||||
|                 { value: "black" }, | ||||
|               ], | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|     ...(title && { | ||||
|       title: { | ||||
|         text: title, | ||||
|       }, | ||||
|     }), | ||||
|   }; | ||||
| 
 | ||||
|   return spec; | ||||
| } | ||||
|  | @ -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); | ||||
							
								
								
									
										63
									
								
								packages/components/src/lib/hooks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								packages/components/src/lib/hooks.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| import { | ||||
|   bindings, | ||||
|   environment, | ||||
|   jsImports, | ||||
|   run, | ||||
|   runPartial, | ||||
| } from "@quri/squiggle-lang"; | ||||
| import { useEffect, useMemo, useRef } from "react"; | ||||
| 
 | ||||
| type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = { | ||||
|   code: string; | ||||
|   bindings?: bindings; | ||||
|   jsImports?: jsImports; | ||||
|   environment?: environment; | ||||
|   onChange?: (expr: Extract<T, { tag: "Ok" }>["value"] | undefined) => void; | ||||
| }; | ||||
| 
 | ||||
| const useSquiggleAny = <T extends ReturnType<typeof run | typeof runPartial>>( | ||||
|   args: SquiggleArgs<T>, | ||||
|   f: (...args: Parameters<typeof run>) => T | ||||
| ) => { | ||||
|   //  We're using observable, where div elements can have a `value` property:
 | ||||
|   // https://observablehq.com/@observablehq/introduction-to-views
 | ||||
|   //
 | ||||
|   //  This is here to get the 'viewof' part of:
 | ||||
|   //  viewof env = cell('normal(0,1)')
 | ||||
|   //  to work
 | ||||
|   const ref = useRef< | ||||
|     HTMLDivElement & { value?: Extract<T, { tag: "Ok" }>["value"] } | ||||
|   >(null); | ||||
|   const result: T = useMemo<T>( | ||||
|     () => f(args.code, args.bindings, args.environment, args.jsImports), | ||||
|     [f, args.code, args.bindings, args.environment, args.jsImports] | ||||
|   ); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!ref.current) return; | ||||
|     ref.current.value = result.tag === "Ok" ? result.value : undefined; | ||||
| 
 | ||||
|     ref.current.dispatchEvent(new CustomEvent("input")); | ||||
|   }, [result]); | ||||
| 
 | ||||
|   const { onChange } = args; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     onChange?.(result.tag === "Ok" ? result.value : undefined); | ||||
|   }, [result, onChange]); | ||||
| 
 | ||||
|   return { | ||||
|     result, // squiggleExpression or externalBindings
 | ||||
|     observableRef: ref, // can be passed to outermost <div> if you want to use your component as an observablehq's view
 | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export const useSquigglePartial = ( | ||||
|   args: SquiggleArgs<ReturnType<typeof runPartial>> | ||||
| ) => { | ||||
|   return useSquiggleAny(args, runPartial); | ||||
| }; | ||||
| 
 | ||||
| export const useSquiggle = (args: SquiggleArgs<ReturnType<typeof run>>) => { | ||||
|   return useSquiggleAny(args, run); | ||||
| }; | ||||
|  | @ -1,3 +0,0 @@ | |||
| export { useMaybeControlledValue } from "./useMaybeControlledValue"; | ||||
| export { useSquiggle } from "./useSquiggle"; | ||||
| export { useRunnerState } from "./useRunnerState"; | ||||
|  | @ -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]; | ||||
| } | ||||
|  | @ -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 }); | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
|  | @ -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; | ||||
| }; | ||||
|  | @ -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; | ||||
| }; | ||||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  | @ -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 []; | ||||
|   } | ||||
| } | ||||
|  | @ -3,7 +3,7 @@ import { Canvas, Meta, Story, Props } from "@storybook/addon-docs"; | |||
| 
 | ||||
| <Meta title="Squiggle/SquiggleChart" component={SquiggleChart} /> | ||||
| 
 | ||||
| export const Template = (props) => <SquiggleChart {...props} />; | ||||
| export const Template = SquiggleChart; | ||||
| /*  | ||||
|   We have to hardcode a width here, because otherwise some interaction with | ||||
|   Storybook creates an infinite loop with the internal width | ||||
|  | @ -29,7 +29,7 @@ could be continuous, discrete or mixed. | |||
|   <Story | ||||
|     name="Continuous Symbolic" | ||||
|     args={{ | ||||
|       code: "normal(5,2)", | ||||
|       squiggleString: "normal(5,2)", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -43,7 +43,7 @@ could be continuous, discrete or mixed. | |||
|   <Story | ||||
|     name="Continuous Pointset" | ||||
|     args={{ | ||||
|       code: "PointSet.fromDist(normal(5,2))", | ||||
|       squiggleString: "toPointSet(normal(5,2))", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -57,7 +57,7 @@ could be continuous, discrete or mixed. | |||
|   <Story | ||||
|     name="Continuous SampleSet" | ||||
|     args={{ | ||||
|       code: "SampleSet.fromDist(normal(5,2))", | ||||
|       squiggleString: "toSampleSet(normal(5,2), 1000)", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -71,23 +71,7 @@ could be continuous, discrete or mixed. | |||
|   <Story | ||||
|     name="Discrete" | ||||
|     args={{ | ||||
|       code: "mx(0, 1, 3, 5, 8, 10, [0.1, 0.8, 0.5, 0.3, 0.2, 0.1])", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|     {Template.bind({})} | ||||
|   </Story> | ||||
| </Canvas> | ||||
| 
 | ||||
| ### Date Distribution | ||||
| 
 | ||||
| <Canvas> | ||||
|   <Story | ||||
|     name="Date Distribution" | ||||
|     args={{ | ||||
|       code: "mx(1661819770311, 1661829770311, 1661839770311)", | ||||
|       width, | ||||
|       xAxisType: "dateTime", | ||||
|       squiggleString: "mx(0, 1, 3, 5, 8, 10, [0.1, 0.8, 0.5, 0.3, 0.2, 0.1])", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -101,34 +85,8 @@ could be continuous, discrete or mixed. | |||
|   <Story | ||||
|     name="Mixed" | ||||
|     args={{ | ||||
|       code: "mx(0, 1, 3, 5, 8, normal(8, 1), [0.1, 0.3, 0.4, 0.35, 0.2, 0.8])", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|     {Template.bind({})} | ||||
|   </Story> | ||||
| </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)), | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| `, | ||||
|       squiggleString: | ||||
|         "mx(0, 1, 3, 5, 8, normal(8, 1), [0.1, 0.3, 0.4, 0.35, 0.2, 0.8])", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -145,7 +103,7 @@ to allow large and small numbers being printed cleanly. | |||
|   <Story | ||||
|     name="Constant" | ||||
|     args={{ | ||||
|       code: "500000000", | ||||
|       squiggleString: "500000000", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -159,7 +117,7 @@ to allow large and small numbers being printed cleanly. | |||
|   <Story | ||||
|     name="Array" | ||||
|     args={{ | ||||
|       code: "[normal(5,2), normal(10,1), normal(40,2), 400000]", | ||||
|       squiggleString: "[normal(5,2), normal(10,1), normal(40,2), 400000]", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -173,7 +131,7 @@ to allow large and small numbers being printed cleanly. | |||
|   <Story | ||||
|     name="Error" | ||||
|     args={{ | ||||
|       code: "f(x) = normal(", | ||||
|       squiggleString: "f(x) = normal(", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -187,7 +145,7 @@ to allow large and small numbers being printed cleanly. | |||
|   <Story | ||||
|     name="Boolean" | ||||
|     args={{ | ||||
|       code: "3 == 3", | ||||
|       squiggleString: "3 == 3", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -201,7 +159,7 @@ to allow large and small numbers being printed cleanly. | |||
|   <Story | ||||
|     name="Function to Distribution" | ||||
|     args={{ | ||||
|       code: "foo(t) = normal(t,2)*normal(5,3); foo", | ||||
|       squiggleString: "foo(t) = normal(t,2)*normal(5,3); foo", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -215,7 +173,7 @@ to allow large and small numbers being printed cleanly. | |||
|   <Story | ||||
|     name="Function to Number" | ||||
|     args={{ | ||||
|       code: "foo(t) = t^2; foo", | ||||
|       squiggleString: "foo(t) = t^2; foo", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -229,7 +187,7 @@ to allow large and small numbers being printed cleanly. | |||
|   <Story | ||||
|     name="Record" | ||||
|     args={{ | ||||
|       code: "{foo: 35 to 50, bar: [1,2,3]}", | ||||
|       squiggleString: "{foo: 35 to 50, bar: [1,2,3]}", | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  | @ -243,7 +201,7 @@ to allow large and small numbers being printed cleanly. | |||
|   <Story | ||||
|     name="String" | ||||
|     args={{ | ||||
|       code: '"Lucky day!"', | ||||
|       squiggleString: '"Lucky day!"', | ||||
|       width, | ||||
|     }} | ||||
|   > | ||||
|  |  | |||
|  | @ -14,20 +14,7 @@ the distribution. | |||
|   <Story | ||||
|     name="Normal" | ||||
|     args={{ | ||||
|       defaultCode: "normal(5,2)", | ||||
|     }} | ||||
|   > | ||||
|     {Template.bind({})} | ||||
|   </Story> | ||||
| </Canvas> | ||||
| 
 | ||||
| It's also possible to create a controlled version of the same component | ||||
| 
 | ||||
| <Canvas> | ||||
|   <Story | ||||
|     name="Controlled" | ||||
|     args={{ | ||||
|       code: "normal(5,2)", | ||||
|       initialSquiggleString: "normal(5,2)", | ||||
|     }} | ||||
|   > | ||||
|     {Template.bind({})} | ||||
|  | @ -40,7 +27,7 @@ You can also name variables like so: | |||
|   <Story | ||||
|     name="Variables" | ||||
|     args={{ | ||||
|       defaultCode: "x = 2\nnormal(x,2)", | ||||
|       initialSquiggleString: "x = 2\nnormal(x,2)", | ||||
|     }} | ||||
|   > | ||||
|     {Template.bind({})} | ||||
|  |  | |||
							
								
								
									
										51
									
								
								packages/components/src/stories/SquigglePartial.stories.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								packages/components/src/stories/SquigglePartial.stories.mdx
									
									
									
									
									
										Normal 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={{ | ||||
|       initialSquiggleString: "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} | ||||
|             initialSquiggleString={props.initialPartialString} | ||||
|             onChange={setBindings} | ||||
|           /> | ||||
|           <SquiggleEditor | ||||
|             {...props} | ||||
|             initialSquiggleString={props.initialEditorString} | ||||
|             bindings={bindings} | ||||
|           /> | ||||
|         </> | ||||
|       ); | ||||
|     }} | ||||
|   </Story> | ||||
| </Canvas> | ||||
|  | @ -14,23 +14,10 @@ including sampling settings, in squiggle. | |||
|   <Story | ||||
|     name="Normal" | ||||
|     args={{ | ||||
|       defaultCode: "normal(5,2)", | ||||
|       initialSquiggleString: "normal(5,2)", | ||||
|       height: 800, | ||||
|     }} | ||||
|   > | ||||
|     {Template.bind({})} | ||||
|   </Story> | ||||
| </Canvas> | ||||
| 
 | ||||
| <Canvas> | ||||
|   <Story | ||||
|     name="With share button" | ||||
|     args={{ | ||||
|       defaultCode: "normal(5,2)", | ||||
|       height: 800, | ||||
|       showShareButton: true, | ||||
|     }} | ||||
|   > | ||||
|     {Template.bind({})} | ||||
|   </Story> | ||||
| </Canvas> | ||||
|  |  | |||
|  | @ -22,8 +22,3 @@ but this line is still necessary for proper initialization of `--tw-*` variables | |||
| .ace_cursor { | ||||
|   border-left: 2px solid !important; | ||||
| } | ||||
| 
 | ||||
| .ace-error-marker { | ||||
|   position: absolute; | ||||
|   border-bottom: 1px solid red; | ||||
| } | ||||
|  |  | |||
|  | @ -5,27 +5,6 @@ module.exports = { | |||
|   }, | ||||
|   important: ".squiggle", | ||||
|   theme: { | ||||
|     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 }, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     extend: {}, | ||||
|   }, | ||||
| }; | ||||
|  |  | |||
|  | @ -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") | ||||
|   ); | ||||
| */ | ||||
| }); | ||||
|  | @ -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"); | ||||
| }); | ||||
|  | @ -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([]); | ||||
| }); | ||||
|  | @ -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), | ||||
| }; | ||||
|  | @ -1,4 +0,0 @@ | |||
| { | ||||
|   "buildCommand": "cd ../.. && npx turbo run build --filter=@quri/squiggle-components", | ||||
|   "outputDirectory": "storybook-static" | ||||
| } | ||||
							
								
								
									
										2
									
								
								packages/squiggle-lang/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								packages/squiggle-lang/.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -22,5 +22,3 @@ _coverage | |||
| coverage | ||||
| .nyc_output/ | ||||
| src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js | ||||
| src/rescript/Reducer/Reducer_Peggy/helpers.js | ||||
| src/rescript/ReducerProject/ReducerProject_IncludeParser.js | ||||
|  |  | |||
|  | @ -3,8 +3,6 @@ lib | |||
| *.bs.js | ||||
| *.gen.tsx | ||||
| .nyc_output/ | ||||
| coverage/ | ||||
| _coverage/ | ||||
| .cache/ | ||||
| Reducer_Peggy_GeneratedParser.js | ||||
| ReducerProject_IncludeParser.js | ||||
| src/rescript/Reducer/Reducer_Peggy/helpers.js | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ environment created from the squiggle code. | |||
| ```js | ||||
| import { run } from "@quri/squiggle-lang"; | ||||
| 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; | ||||
| ``` | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,16 +14,4 @@ describe("Combining Continuous and Discrete Distributions", () => { | |||
|     ), // Multiply distribution by -1 | ||||
|     true, | ||||
|   ) | ||||
|   makeTest( | ||||
|     "keep order of xs when first number is discrete and adding", | ||||
|     AlgebraicShapeCombination.isOrdered( | ||||
|       AlgebraicShapeCombination.combineShapesContinuousDiscrete( | ||||
|         #Add, | ||||
|         {xs: [0., 1.], ys: [1., 1.]}, | ||||
|         {xs: [1.], ys: [1.]}, | ||||
|         ~discretePosition=First, | ||||
|       ), | ||||
|     ), // 1 + distribution | ||||
|     true, | ||||
|   ) | ||||
| }) | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| open Jest | ||||
| open Expect | ||||
| 
 | ||||
| let env: GenericDist.env = { | ||||
| let env: DistributionOperation.env = { | ||||
|   sampleCount: 100, | ||||
|   xyPointLength: 100, | ||||
| } | ||||
|  | @ -34,7 +34,7 @@ describe("sparkline", () => { | |||
|     expected: DistributionOperation.outputType, | ||||
|   ) => { | ||||
|     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) | ||||
|     }) | ||||
|   } | ||||
|  | @ -81,8 +81,8 @@ describe("sparkline", () => { | |||
| describe("toPointSet", () => { | ||||
|   test("on symbolic normal distribution", () => { | ||||
|     let result = | ||||
|       run(FromDist(#ToDist(ToPointSet), normalDist5)) | ||||
|       ->outputMap(FromDist(#ToFloat(#Mean))) | ||||
|       run(FromDist(ToDist(ToPointSet), normalDist5)) | ||||
|       ->outputMap(FromDist(ToFloat(#Mean))) | ||||
|       ->toFloat | ||||
|       ->toExt | ||||
|     expect(result)->toBeSoCloseTo(5.0, ~digits=0) | ||||
|  | @ -90,10 +90,10 @@ describe("toPointSet", () => { | |||
| 
 | ||||
|   test("on sample set", () => { | ||||
|     let result = | ||||
|       run(FromDist(#ToDist(ToPointSet), normalDist5)) | ||||
|       ->outputMap(FromDist(#ToDist(ToSampleSet(1000)))) | ||||
|       ->outputMap(FromDist(#ToDist(ToPointSet))) | ||||
|       ->outputMap(FromDist(#ToFloat(#Mean))) | ||||
|       run(FromDist(ToDist(ToPointSet), normalDist5)) | ||||
|       ->outputMap(FromDist(ToDist(ToSampleSet(1000)))) | ||||
|       ->outputMap(FromDist(ToDist(ToPointSet))) | ||||
|       ->outputMap(FromDist(ToFloat(#Mean))) | ||||
|       ->toFloat | ||||
|       ->toExt | ||||
|     expect(result)->toBeSoCloseTo(5.0, ~digits=-1) | ||||
|  |  | |||
|  | @ -32,29 +32,25 @@ describe("dotSubtract", () => { | |||
|  */ | ||||
|   Skip.test("mean of normal minus exponential (property)", () => { | ||||
|     assert_( | ||||
|       property2( | ||||
|         float_(), | ||||
|         floatRange(1e-5, 1e5), | ||||
|         (mean, rate) => { | ||||
|           // We limit ourselves to stdev=1 so that the integral is trivial | ||||
|           let dotDifference = DistributionOperation.Constructors.pointwiseSubtract( | ||||
|             ~env, | ||||
|             mkNormal(mean, 1.0), | ||||
|             mkExponential(rate), | ||||
|       property2(float_(), floatRange(1e-5, 1e5), (mean, rate) => { | ||||
|         // We limit ourselves to stdev=1 so that the integral is trivial | ||||
|         let dotDifference = DistributionOperation.Constructors.pointwiseSubtract( | ||||
|           ~env, | ||||
|           mkNormal(mean, 1.0), | ||||
|           mkExponential(rate), | ||||
|         ) | ||||
|         let meanResult = E.R2.bind(DistributionOperation.Constructors.mean(~env), dotDifference) | ||||
|         // according to algebra or random variables, | ||||
|         let meanAnalytical = | ||||
|           mean -. | ||||
|           SymbolicDist.Exponential.mean({rate: rate})->E.R2.toExn( | ||||
|             "On trusted input this should never happen", | ||||
|           ) | ||||
|           let meanResult = E.R2.bind(DistributionOperation.Constructors.mean(~env), dotDifference) | ||||
|           // according to algebra or random variables, | ||||
|           let meanAnalytical = | ||||
|             mean -. | ||||
|             SymbolicDist.Exponential.mean({rate: rate})->E.R2.toExn( | ||||
|               "On trusted input this should never happen", | ||||
|             ) | ||||
|           switch meanResult { | ||||
|           | Ok(meanValue) => abs_float(meanValue -. meanAnalytical) /. abs_float(meanValue) < 1e-2 // 1% relative error | ||||
|           | Error(err) => err === DistributionTypes.OperationError(DivisionByZeroError) | ||||
|           } | ||||
|         }, | ||||
|       ), | ||||
|         switch meanResult { | ||||
|         | Ok(meanValue) => abs_float(meanValue -. meanAnalytical) /. abs_float(meanValue) < 1e-2 // 1% relative error | ||||
|         | Error(err) => err === DistributionTypes.OperationError(DivisionByZeroError) | ||||
|         } | ||||
|       }), | ||||
|     ) | ||||
|     pass | ||||
|   }) | ||||
|  |  | |||
|  | @ -40,60 +40,51 @@ let algebraicPower = algebraicPower(~env) | |||
| 
 | ||||
| describe("(Algebraic) addition of distributions", () => { | ||||
|   describe("mean", () => { | ||||
|     test( | ||||
|       "normal(mean=5) + normal(mean=20)", | ||||
|       () => { | ||||
|         normalDist5 | ||||
|         ->algebraicAdd(normalDist20) | ||||
|     test("normal(mean=5) + normal(mean=20)", () => { | ||||
|       normalDist5 | ||||
|       ->algebraicAdd(normalDist20) | ||||
|       ->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean) | ||||
|       ->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(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(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.786831807237022, ~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) | ||||
|         } | ||||
|       }, | ||||
|     ) | ||||
|       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.786831807237022, ~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", () => { | ||||
|     // TEST IS WRONG. SEE STDEV ADDITION EXPRESSION. | ||||
|  | @ -131,282 +122,247 @@ describe("(Algebraic) addition of distributions", () => { | |||
|         } | ||||
|       }, | ||||
|     ) | ||||
|     test( | ||||
|       "(normal(mean=10) + normal(mean=10)).pdf(1.9e1)", | ||||
|       () => { | ||||
|         let received = | ||||
|           normalDist20 | ||||
|           ->Ok | ||||
|           ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1.9e1)) | ||||
|           ->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.pdf(d, 1.9e1)) | ||||
|           ->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)).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 { | ||||
|     test("(normal(mean=10) + normal(mean=10)).pdf(1.9e1)", () => { | ||||
|       let received = | ||||
|         normalDist20 | ||||
|         ->Ok | ||||
|         ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1.9e1)) | ||||
|         ->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.pdf(d, 1.9e1)) | ||||
|         ->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") | ||||
|         // 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=4. | ||||
|         // This value was calculated by a python script | ||||
|         | Some(x) => x->expect->toBeSoCloseTo(0.979023, ~digits=0) | ||||
|         | Some(y) => x->expect->toBeSoCloseTo(y, ~digits=1) | ||||
|         } | ||||
|       }, | ||||
|     ) | ||||
|     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) | ||||
|         } | ||||
|       }, | ||||
|     ) | ||||
|       } | ||||
|     }) | ||||
|     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") | ||||
|       // 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=4. | ||||
|       // This value was calculated by a python script | ||||
|       | 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", () => { | ||||
|     testAll( | ||||
|       "(normal(mean=5) + normal(mean=5)).cdf (imprecise)", | ||||
|       list{6e0, 8e0, 1e1, 1.2e1}, | ||||
|       x => { | ||||
|         let received = | ||||
|           normalDist10 | ||||
|           ->Ok | ||||
|           ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x)) | ||||
|           ->E.R2.fmap(run) | ||||
|           ->E.R2.fmap(toFloat) | ||||
|           ->E.R.toOption | ||||
|           ->E.O.flatten | ||||
|         let calculated = | ||||
|           normalDist5 | ||||
|           ->algebraicAdd(normalDist5) | ||||
|           ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x)) | ||||
|           ->E.R2.fmap(run) | ||||
|           ->E.R2.fmap(toFloat) | ||||
|           ->E.R.toOption | ||||
|           ->E.O.flatten | ||||
|     testAll("(normal(mean=5) + normal(mean=5)).cdf (imprecise)", list{6e0, 8e0, 1e1, 1.2e1}, x => { | ||||
|       let received = | ||||
|         normalDist10 | ||||
|         ->Ok | ||||
|         ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x)) | ||||
|         ->E.R2.fmap(run) | ||||
|         ->E.R2.fmap(toFloat) | ||||
|         ->E.R.toOption | ||||
|         ->E.O.flatten | ||||
|       let calculated = | ||||
|         normalDist5 | ||||
|         ->algebraicAdd(normalDist5) | ||||
|         ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x)) | ||||
|         ->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=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 { | ||||
|       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") | ||||
|         // 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) | ||||
|         | Some(y) => x->expect->toBeSoCloseTo(y, ~digits=0) | ||||
|         } | ||||
|       }, | ||||
|     ) | ||||
|     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 { | ||||
|       } | ||||
|     }) | ||||
|     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") | ||||
|         // 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) | ||||
|         | 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") | ||||
|       // 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", () => { | ||||
|     testAll( | ||||
|       "(normal(mean=5) + normal(mean=5)).inv (imprecise)", | ||||
|       list{5e-2, 4.2e-3, 9e-3}, | ||||
|       x => { | ||||
|         let received = | ||||
|           normalDist10 | ||||
|           ->Ok | ||||
|           ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x)) | ||||
|           ->E.R2.fmap(run) | ||||
|           ->E.R2.fmap(toFloat) | ||||
|           ->E.R.toOption | ||||
|           ->E.O.flatten | ||||
|         let calculated = | ||||
|           normalDist5 | ||||
|           ->algebraicAdd(normalDist5) | ||||
|           ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x)) | ||||
|           ->E.R2.fmap(run) | ||||
|           ->E.R2.fmap(toFloat) | ||||
|           ->E.R.toOption | ||||
|           ->E.O.flatten | ||||
|     testAll("(normal(mean=5) + normal(mean=5)).inv (imprecise)", list{5e-2, 4.2e-3, 9e-3}, x => { | ||||
|       let received = | ||||
|         normalDist10 | ||||
|         ->Ok | ||||
|         ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x)) | ||||
|         ->E.R2.fmap(run) | ||||
|         ->E.R2.fmap(toFloat) | ||||
|         ->E.R.toOption | ||||
|         ->E.O.flatten | ||||
|       let calculated = | ||||
|         normalDist5 | ||||
|         ->algebraicAdd(normalDist5) | ||||
|         ->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x)) | ||||
|         ->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( | ||||
|       "(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 { | ||||
|       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") | ||||
|         // 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) | ||||
|         | Some(y) => x->expect->toBeSoCloseTo(y, ~digits=-1) | ||||
|         } | ||||
|       }, | ||||
|     ) | ||||
|     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 { | ||||
|       } | ||||
|     }) | ||||
|     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") | ||||
|         // 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) | ||||
|         | 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") | ||||
|       // 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) | ||||
|       } | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  |  | |||
|  | @ -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.  | ||||
| 
 | ||||
| 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.  | ||||
| */ | ||||
|  | @ -87,22 +87,14 @@ describe("Means are invariant", () => { | |||
|     let testAddInvariant = (t1, t2) => | ||||
|       E.R.liftM2(testAdditionMean, t1, t2)->E.R.toExn("Means were not invariant", _) | ||||
| 
 | ||||
|     testAll( | ||||
|       "with two of the same distribution", | ||||
|       distributions, | ||||
|       dist => { | ||||
|         testAddInvariant(dist, dist) | ||||
|       }, | ||||
|     ) | ||||
|     testAll("with two of the same distribution", distributions, dist => { | ||||
|       testAddInvariant(dist, dist) | ||||
|     }) | ||||
| 
 | ||||
|     testAll( | ||||
|       "with two different distributions", | ||||
|       pairsOfDifferentDistributions, | ||||
|       dists => { | ||||
|         let (dist1, dist2) = dists | ||||
|         testAddInvariant(dist1, dist2) | ||||
|       }, | ||||
|     ) | ||||
|     testAll("with two different distributions", pairsOfDifferentDistributions, dists => { | ||||
|       let (dist1, dist2) = dists | ||||
|       testAddInvariant(dist1, dist2) | ||||
|     }) | ||||
| 
 | ||||
|     testAll( | ||||
|       "with two different distributions in swapped order", | ||||
|  | @ -124,22 +116,14 @@ describe("Means are invariant", () => { | |||
|     let testSubtractInvariant = (t1, t2) => | ||||
|       E.R.liftM2(testSubtractionMean, t1, t2)->E.R.toExn("Means were not invariant", _) | ||||
| 
 | ||||
|     testAll( | ||||
|       "with two of the same distribution", | ||||
|       distributions, | ||||
|       dist => { | ||||
|         testSubtractInvariant(dist, dist) | ||||
|       }, | ||||
|     ) | ||||
|     testAll("with two of the same distribution", distributions, dist => { | ||||
|       testSubtractInvariant(dist, dist) | ||||
|     }) | ||||
| 
 | ||||
|     testAll( | ||||
|       "with two different distributions", | ||||
|       pairsOfDifferentDistributions, | ||||
|       dists => { | ||||
|         let (dist1, dist2) = dists | ||||
|         testSubtractInvariant(dist1, dist2) | ||||
|       }, | ||||
|     ) | ||||
|     testAll("with two different distributions", pairsOfDifferentDistributions, dists => { | ||||
|       let (dist1, dist2) = dists | ||||
|       testSubtractInvariant(dist1, dist2) | ||||
|     }) | ||||
| 
 | ||||
|     testAll( | ||||
|       "with two different distributions in swapped order", | ||||
|  | @ -161,22 +145,14 @@ describe("Means are invariant", () => { | |||
|     let testMultiplicationInvariant = (t1, t2) => | ||||
|       E.R.liftM2(testMultiplicationMean, t1, t2)->E.R.toExn("Means were not invariant", _) | ||||
| 
 | ||||
|     testAll( | ||||
|       "with two of the same distribution", | ||||
|       distributions, | ||||
|       dist => { | ||||
|         testMultiplicationInvariant(dist, dist) | ||||
|       }, | ||||
|     ) | ||||
|     testAll("with two of the same distribution", distributions, dist => { | ||||
|       testMultiplicationInvariant(dist, dist) | ||||
|     }) | ||||
| 
 | ||||
|     testAll( | ||||
|       "with two different distributions", | ||||
|       pairsOfDifferentDistributions, | ||||
|       dists => { | ||||
|         let (dist1, dist2) = dists | ||||
|         testMultiplicationInvariant(dist1, dist2) | ||||
|       }, | ||||
|     ) | ||||
|     testAll("with two different distributions", pairsOfDifferentDistributions, dists => { | ||||
|       let (dist1, dist2) = dists | ||||
|       testMultiplicationInvariant(dist1, dist2) | ||||
|     }) | ||||
| 
 | ||||
|     testAll( | ||||
|       "with two different distributions in swapped order", | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ describe("mixture", () => { | |||
|       let (mean1, mean2) = tup | ||||
|       let meanValue = { | ||||
|         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) | ||||
|  | @ -28,7 +28,7 @@ describe("mixture", () => { | |||
|       let meanValue = { | ||||
|         run( | ||||
|           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 exponentialMean = 1.0 /. rate | ||||
|  | @ -52,7 +52,7 @@ describe("mixture", () => { | |||
|             (mkUniform(low, high), uniformWeight), | ||||
|             (mkLognormal(mu, sigma), lognormalWeight), | ||||
|           ]), | ||||
|         )->outputMap(FromDist(#ToFloat(#Mean))) | ||||
|         )->outputMap(FromDist(ToFloat(#Mean))) | ||||
|       } | ||||
|       let uniformMean = (low +. high) /. 2.0 | ||||
|       let lognormalMean = mu +. sigma ** 2.0 /. 2.0 | ||||
|  |  | |||
|  | @ -17,9 +17,10 @@ describe("klDivergence: continuous -> continuous -> float", () => { | |||
|       let answer = | ||||
|         uniformMakeR(lowAnswer, highAnswer)->E.R2.errMap(s => DistributionTypes.ArgumentError(s)) | ||||
|       let prediction = | ||||
|         uniformMakeR(lowPrediction, highPrediction)->E.R2.errMap( | ||||
|           s => DistributionTypes.ArgumentError(s), | ||||
|         ) | ||||
|         uniformMakeR( | ||||
|           lowPrediction, | ||||
|           highPrediction, | ||||
|         )->E.R2.errMap(s => DistributionTypes.ArgumentError(s)) | ||||
|       // integral along the support of the answer of answer.pdf(x) times log of prediction.pdf(x) divided by answer.pdf(x) dx | ||||
|       let analyticalKl = Js.Math.log((highPrediction -. lowPrediction) /. (highAnswer -. lowAnswer)) | ||||
|       let kl = E.R.liftJoin2(klDivergence, prediction, answer) | ||||
|  | @ -182,11 +183,11 @@ describe("combineAlongSupportOfSecondArgument0", () => { | |||
|     let answer = | ||||
|       uniformMakeR(lowAnswer, highAnswer)->E.R2.errMap(s => DistributionTypes.ArgumentError(s)) | ||||
|     let prediction = | ||||
|       uniformMakeR(lowPrediction, highPrediction)->E.R2.errMap( | ||||
|         s => DistributionTypes.ArgumentError(s), | ||||
|       ) | ||||
|     let answerWrapped = E.R.fmap(a => run(FromDist(#ToDist(ToPointSet), a)), answer) | ||||
|     let predictionWrapped = E.R.fmap(a => run(FromDist(#ToDist(ToPointSet), a)), prediction) | ||||
|       uniformMakeR(lowPrediction, highPrediction)->E.R2.errMap(s => DistributionTypes.ArgumentError( | ||||
|         s, | ||||
|       )) | ||||
|     let answerWrapped = E.R.fmap(a => run(FromDist(ToDist(ToPointSet), a)), answer) | ||||
|     let predictionWrapped = E.R.fmap(a => run(FromDist(ToDist(ToPointSet), a)), prediction) | ||||
| 
 | ||||
|     let interpolator = XYShape.XtoY.continuousInterpolator(#Stepwise, #UseZero) | ||||
|     let integrand = PointSetDist_Scoring.WithDistAnswer.integrand | ||||
|  |  | |||
|  | @ -0,0 +1,90 @@ | |||
| open Jest | ||||
| open Expect | ||||
| open GenericDist_Fixtures | ||||
| exception ScoreFailed | ||||
| 
 | ||||
| describe("TwoScalars: scalar -> scalar -> score", () => { | ||||
|   test("score: infinity", () => { | ||||
|     let scalar1 = 1.0 // 100% of probability mass 1.0 | ||||
|     let scalar2 = 2.0 // 100% of probability mass to 2.0 | ||||
|     let score = PointSetDist_Scoring.TwoScalars.score(~estimate=scalar1, ~answer=scalar2) | ||||
|     switch score { | ||||
|     | Ok(x) => x->expect->toEqual(infinity) | ||||
|     | _ => raise(MixtureFailed) | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   test("score: 0.0", () => { | ||||
|     let scalar1 = 1.5 // 100% of probability mass 1.5 | ||||
|     let scalar2 = 1.5 // 100% of probability mass to 1.5 | ||||
|     let score = PointSetDist_Scoring.TwoScalars.score(~estimate=scalar1, ~answer=scalar2) | ||||
|     switch score { | ||||
|     | Ok(x) => x->expect->toEqual(0.0) | ||||
|     | _ => raise(MixtureFailed) | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   test("scoreWithPrior: minus infinity", () => { | ||||
|     let scalar1 = 1.5 // 100% of probability mass 1.5 | ||||
|     let scalar2 = 1.5 // 100% of probability mass to 1.5 | ||||
|     let scalar3 = 1.0 // 100% of probability mass to 1.0 | ||||
| 
 | ||||
|     let score = PointSetDist_Scoring.TwoScalars.scoreWithPrior( | ||||
|       ~estimate=scalar1, | ||||
|       ~answer=scalar2, | ||||
|       ~prior=scalar3, | ||||
|     ) | ||||
|     switch score { | ||||
|     | Ok(x) => x->expect->toEqual(-.infinity) | ||||
|     | _ => raise(MixtureFailed) | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   test("scoreWithPrior: 0.0", () => { | ||||
|     let scalar1 = 1.5 // 100% of probability mass 1.5 | ||||
|     let scalar2 = 1.5 // 100% of probability mass to 1.5 | ||||
|     let scalar3 = 1.5 // 100% of probability mass to 1.5 | ||||
| 
 | ||||
|     let score = PointSetDist_Scoring.TwoScalars.scoreWithPrior( | ||||
|       ~estimate=scalar1, | ||||
|       ~answer=scalar2, | ||||
|       ~prior=scalar3, | ||||
|     ) | ||||
|     switch score { | ||||
|     | Ok(x) => x->expect->toEqual(0.0) | ||||
|     | _ => raise(ScoreFailed) | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   test("scoreWithPrior: really dumb forecasters", () => { | ||||
|     let scalar1 = 1.0 // 100% of probability mass 1.0 | ||||
|     let scalar2 = 1.5 // 100% of probability mass to 1.5 | ||||
|     let scalar3 = 1.0 // 100% of probability mass to 1.0 | ||||
| 
 | ||||
|     let score = PointSetDist_Scoring.TwoScalars.scoreWithPrior( | ||||
|       ~estimate=scalar1, | ||||
|       ~answer=scalar2, | ||||
|       ~prior=scalar3, | ||||
|     ) | ||||
|     switch score { | ||||
|     | Ok(x) => x->expect->toEqual(infinity -. infinity) // "Error: Really dumb forecasters" | ||||
|     | _ => raise(ScoreFailed) | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   test("scoreWithPrior: 0.0", () => { | ||||
|     let scalar1 = 1.0 // 100% of probability mass 1.0 | ||||
|     let scalar2 = 1.0 // 100% of probability mass to 1.0 | ||||
|     let scalar3 = 1.0 // 100% of probability mass to 1.0 | ||||
| 
 | ||||
|     let score = PointSetDist_Scoring.TwoScalars.scoreWithPrior( | ||||
|       ~estimate=scalar1, | ||||
|       ~answer=scalar2, | ||||
|       ~prior=scalar3, | ||||
|     ) | ||||
|     switch score { | ||||
|     | Ok(x) => x->expect->toEqual(0.0) | ||||
|     | _ => raise(ScoreFailed) | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
|  | @ -3,39 +3,39 @@ open Expect | |||
| open TestHelpers | ||||
| 
 | ||||
| // 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", () => { | ||||
|   testAll("has no impact on normal distributions", list{-1e8, -1e-2, 0.0, 1e-4, 1e16}, mean => { | ||||
|     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) | ||||
|   }) | ||||
| }) | ||||
| 
 | ||||
| describe("(Symbolic) 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)", () => { | ||||
|     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)", () => { | ||||
|     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 => { | ||||
|     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 | ||||
|   }) | ||||
| 
 | ||||
|   test("of a cauchy distribution", () => { | ||||
|     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) | ||||
|     //-> toBe(GenDistError(Other("Cauchy distributions may have no mean value."))) | ||||
|  | @ -47,7 +47,10 @@ describe("(Symbolic) mean", () => { | |||
|     tup => { | ||||
|       let (low, medium, high) = tup | ||||
|       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/ | ||||
|     }, | ||||
|  | @ -60,7 +63,7 @@ describe("(Symbolic) mean", () => { | |||
|     tup => { | ||||
|       let (alpha, beta) = tup | ||||
|       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 | ||||
|     }, | ||||
|  | @ -69,35 +72,18 @@ describe("(Symbolic) mean", () => { | |||
|   // 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)", () => { | ||||
|     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 | ||||
|   }) | ||||
| 
 | ||||
|   testAll( | ||||
|     "of beta distributions from mean and standard dev", | ||||
|     list{(0.39, 0.1), (0.08, 0.1), (0.8, 0.3)}, | ||||
|     tup => { | ||||
|       let (mean, stdev) = tup | ||||
|       let betaDistribution = SymbolicDist.Beta.fromMeanAndStdev(mean, stdev) | ||||
|       let meanValue = | ||||
|         betaDistribution->E.R2.fmap( | ||||
|           d => run(FromDist(#ToFloat(#Mean), d->DistributionTypes.Symbolic)), | ||||
|         ) | ||||
|       switch meanValue { | ||||
|       | Ok(value) => value->unpackFloat->expect->toBeCloseTo(mean) | ||||
|       | Error(err) => err->expect->toBe("shouldn't happen") | ||||
|       } | ||||
|     }, | ||||
|   ) | ||||
| 
 | ||||
|   testAll( | ||||
|     "of lognormal distributions", | ||||
|     list{(2.0, 4.0), (1e-7, 1e-2), (-1e6, 10.0), (1e3, -1e2), (-1e8, -1e4), (1e2, 1e-5)}, | ||||
|     tup => { | ||||
|       let (mu, sigma) = tup | ||||
|       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/ | ||||
|     }, | ||||
|  | @ -109,14 +95,14 @@ describe("(Symbolic) mean", () => { | |||
|     tup => { | ||||
|       let (low, high) = tup | ||||
|       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 | ||||
|     }, | ||||
|   ) | ||||
| 
 | ||||
|   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) | ||||
|   }) | ||||
| }) | ||||
|  |  | |||
|  | @ -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), | ||||
|   ) | ||||
| }) | ||||
|  | @ -9,28 +9,22 @@ let prepareInputs = (ar, minWeight) => | |||
| describe("Continuous and discrete splits", () => { | ||||
|   makeTest( | ||||
|     "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], []), | ||||
|   ) | ||||
| 
 | ||||
|   makeTest( | ||||
|     "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)]), | ||||
|   ) | ||||
| 
 | ||||
|   makeTest( | ||||
|     "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], []), | ||||
|   ) | ||||
| 
 | ||||
|   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 arr = Belt.Array.range(1, count) |> E.A.fmap(float_of_int) | ||||
|     let sorted = arr |> Belt.SortArray.stableSortBy(_, compare) | ||||
|  |  | |||
							
								
								
									
										20
									
								
								packages/squiggle-lang/__tests__/Lodash_test.res
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/squiggle-lang/__tests__/Lodash_test.res
									
									
									
									
									
										Normal 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)]), | ||||
|     ) | ||||
|   }) | ||||
| ) | ||||
|  | @ -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) | ||||
|       }, | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
|  | @ -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", EvNumber(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", EvNumber(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", EvNumber(2.))]), eSymbol("x")), | ||||
|     "Ok(2 context: {x: 2})", | ||||
|   ) | ||||
|   // When an let statement is the end expression then bindings are returned | ||||
|   testMacro( | ||||
|     [], | ||||
|     eBindExpression(eBindings([("x", EvNumber(2.))]), exampleStatementY), | ||||
|     "Ok((:$_exportBindings_$ (:$_setBindings_$ {x: 2} :y 1)) context: {x: 2})", | ||||
|   ) | ||||
|   // Now let's reduce that expression | ||||
|   testMacroEval( | ||||
|     [], | ||||
|     eBindExpression(eBindings([("x", EvNumber(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", EvNumber(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", EvNumber(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", EvNumber(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", EvNumber(666.))], callLambdaExpression, "Ok(667)") | ||||
| }) | ||||
|  | @ -0,0 +1,41 @@ | |||
| module ExpressionValue = ReducerInterface.ExpressionValue | ||||
| 
 | ||||
| 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 | ||||
| }) | ||||
							
								
								
									
										19
									
								
								packages/squiggle-lang/__tests__/Reducer/Reducer_Helpers.res
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/squiggle-lang/__tests__/Reducer/Reducer_Helpers.res
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| module ExpressionT = Reducer_Expression_T | ||||
| module ExpressionValue = ReducerInterface.ExpressionValue | ||||
| module ErrorValue = Reducer_ErrorValue | ||||
| module Bindings = Reducer_Category_Bindings | ||||
| 
 | ||||
| let removeDefaults = (ev: ExpressionT.expressionValue): ExpressionT.expressionValue => | ||||
|   switch ev { | ||||
|   | EvRecord(extbindings) => { | ||||
|       let bindings: Bindings.t = Bindings.fromRecord(extbindings) | ||||
|       let keys = Js.Dict.keys(Reducer.defaultExternalBindings) | ||||
|       Belt.Map.String.keep(bindings, (key, _value) => { | ||||
|         let removeThis = Js.Array2.includes(keys, key) | ||||
|         !removeThis | ||||
|       })->Bindings.toExpressionValue | ||||
|     } | ||||
|   | value => value | ||||
|   } | ||||
| 
 | ||||
| let rRemoveDefaults = r => Belt.Result.map(r, ev => removeDefaults(ev)) | ||||
|  | @ -0,0 +1,32 @@ | |||
| open ReducerInterface.ExpressionValue | ||||
| module MathJs = Reducer_MathJs | ||||
| module ErrorValue = Reducer.ErrorValue | ||||
| 
 | ||||
| open Jest | ||||
| open ExpectJs | ||||
| 
 | ||||
| describe("eval", () => { | ||||
|   test("Number", () => expect(MathJs.Eval.eval("1"))->toEqual(Ok(EvNumber(1.)))) | ||||
|   test("Number expr", () => expect(MathJs.Eval.eval("1-1"))->toEqual(Ok(EvNumber(0.)))) | ||||
|   test("String", () => expect(MathJs.Eval.eval("'hello'"))->toEqual(Ok(EvString("hello")))) | ||||
|   test("String expr", () => | ||||
|     expect(MathJs.Eval.eval("concat('hello ','world')"))->toEqual(Ok(EvString("hello world"))) | ||||
|   ) | ||||
|   test("Boolean", () => expect(MathJs.Eval.eval("true"))->toEqual(Ok(EvBool(true)))) | ||||
|   test("Boolean expr", () => expect(MathJs.Eval.eval("2>1"))->toEqual(Ok(EvBool(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")), | ||||
|     ) | ||||
|   ) | ||||
| }) | ||||
|  | @ -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
		Loading…
	
		Reference in New Issue
	
	Block a user