diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6c453be0d1..3131c8ed0d 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "7.0.5", + "version": "8.1.1", "commands": [ "fantomas" ], @@ -17,14 +17,14 @@ "rollForward": false }, "fsharp-analyzers": { - "version": "0.37.2", + "version": "0.41.0", "commands": [ "fsharp-analyzers" ], "rollForward": false }, "easybuild.shipit": { - "version": "3.0.1", + "version": "3.3.0", "commands": [ "shipit" ], diff --git a/.github/actions/setup-dotnet/action.yml b/.github/actions/setup-dotnet/action.yml new file mode 100644 index 0000000000..923cdc6ccd --- /dev/null +++ b/.github/actions/setup-dotnet/action.yml @@ -0,0 +1,18 @@ +name: Setup .NET +description: Install the .NET SDK pinned by global.json with a cached NuGet package directory. + +runs: + using: composite + steps: + - uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + cache: true + # Package versions also come from .props, which .fsproj files interpolate. + # restore writes *.nuget.g.props into obj/. + cache-dependency-path: | + **/*.fsproj + **/*.props + **/*.targets + .config/dotnet-tools.json + !**/obj/** diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml new file mode 100644 index 0000000000..85be3be357 --- /dev/null +++ b/.github/actions/setup-node/action.yml @@ -0,0 +1,14 @@ +name: Setup Node.js +description: Install the Node.js version pinned by package.json with a cached npm download directory. + +runs: + using: composite + steps: + - uses: actions/setup-node@v7 + with: + node-version-file: package.json + cache: npm + # build.sh also installs in src/fable-compiler-js, src/fable-standalone and tests/React. + cache-dependency-path: | + **/package-lock.json + !**/node_modules/** diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f5690d2a79..4e896361fe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,6 +2,8 @@ version: 2 updates: - package-ecosystem: "npm" directory: "/" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -18,6 +20,8 @@ updates: - package-ecosystem: "nuget" directory: "/" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -34,6 +38,8 @@ updates: - package-ecosystem: "github-actions" directory: "/" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 10 @@ -46,6 +52,8 @@ updates: - package-ecosystem: "uv" directory: "/" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -58,6 +66,8 @@ updates: - package-ecosystem: "uv" directory: "/src/fable-library-py" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -70,6 +80,8 @@ updates: - package-ecosystem: "npm" directory: "/tests/React" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -94,6 +106,8 @@ updates: - package-ecosystem: "cargo" directory: "/src/fable-library-rust" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -106,6 +120,8 @@ updates: - package-ecosystem: "cargo" directory: "/src/fable-library-py" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5aae1394f5..3c65091a58 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,9 +25,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Restore tools run: dotnet tool restore @@ -35,6 +33,22 @@ jobs: - name: Check F# code run: dotnet fantomas . --check --verbosity d + - name: Set up Python 3.13 + uses: actions/setup-python@v7 + with: + python-version: "3.13" + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Check Python imports and exports + run: uv run --frozen ruff check --select I,RUF022,RUF068 src/fable-library-py + + - name: Check Python formatting + run: uv run --frozen ruff format --check src/fable-library-py + # Verify that Fable.sln can be restored # See: https://github.com/fable-compiler/Fable/issues/3687 restore-project: @@ -46,9 +60,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Restore project run: dotnet restore Fable.sln @@ -68,14 +80,10 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Fable Tests - JavaScript (linux) if: matrix.platform == 'ubuntu-latest' @@ -94,14 +102,10 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Fable Tests - TypeScript run: ./build.sh test typescript @@ -118,14 +122,10 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Fable Tests run: ./build.sh test integration @@ -139,14 +139,10 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Fable Tests run: ./build.sh test standalone @@ -157,7 +153,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, windows-latest] - python-version: [3.12, 3.13, 3.14] + python-version: ["3.13", "3.14", "3.15"] runs-on: ${{ matrix.platform }} env: UV_LINK_MODE: copy @@ -166,9 +162,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v7 @@ -219,7 +213,7 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.12", "3.13", "3.14"] + python-version: ["3.13", "3.14", "3.15"] runs-on: ${{ matrix.platform }} env: UV_LINK_MODE: copy @@ -272,20 +266,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - test: ["default", "no_std", "threaded"] + features: [",", "lrc_ptr", "no_std", "reflection", "threaded"] steps: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -296,20 +286,22 @@ jobs: run: ./build.sh fable-library --rust - name: Fable Tests - Rust - run: ./build.sh test rust --${{ matrix.test }} + run: ./build.sh test rust --features "${{ matrix.features }}" # Separate build job for Dart build-dart: runs-on: ubuntu-latest timeout-minutes: 45 + # Without id-token, setup-dart skips the pub.dev token, so packages are + # fetched anonymously. + permissions: + contents: read steps: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Dart SDK uses: dart-lang/setup-dart@v1 @@ -326,9 +318,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Erlang/OTP uses: erlef/setup-beam@v1 @@ -358,9 +348,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Restore tools run: dotnet tool restore diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 6b2f2be0c8..7ef7a068ca 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,15 +19,17 @@ jobs: name: Claude runs-on: ubuntu-latest - # Only trigger on @claude mentions from trusted contributors - # (NONE = first-time contributors with no prior contributions) + # Only trigger on @claude mentions from maintainers; a review carries its author on `review`, not `comment` if: | - ( - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) - ) && - github.event.comment.author_association != 'NONE' + (github.event_name == 'issue_comment' + && contains(github.event.comment.body, '@claude') + && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' + && contains(github.event.comment.body, '@claude') + && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' + && contains(github.event.review.body, '@claude') + && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) steps: - name: Checkout repository diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml deleted file mode 100644 index 04a69b8915..0000000000 --- a/.github/workflows/commands.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Commands on PR -on: - issue_comment: - types: [created] - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - run_command: - if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/run') - runs-on: ubuntu-20.04 - steps: - - name: Extract command to run - uses: actions/github-script@v9.0.0 - id: command-extractor - with: - result-encoding: string - script: | - if (context.eventName !== "issue_comment") throw "Error: This action only works on issue_comment events."; - - // extract the command to run, allowed characters: a-z, A-Z, digits, hyphen, underscore - const regex = /^\/run ([a-zA-Z\d\-\_]+)/; - command = regex.exec(context.payload.comment.body); - if (command == null) throw "Error: No command found in the trigger phrase."; - - return command[1]; - - name: Get github ref - uses: actions/github-script@v9.0.0 - id: get-pr - with: - script: | - const result = await github.pulls.get({ - pull_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - }); - return { "ref": result.data.head.ref, "repository": result.data.head.repo.full_name}; - - name: Checkout repo - uses: actions/checkout@v7.0.1 - with: - repository: ${{ fromJson(steps.get-pr.outputs.result).repository }} - ref: ${{ fromJson(steps.get-pr.outputs.result).ref }} - fetch-depth: 0 - - name: Install dotnet - uses: actions/setup-dotnet@v6 - - name: Install dotnet tools - run: dotnet tool restore - - name: Process fantomas command - if: steps.command-extractor.outputs.result == 'fantomas' - id: fantomas - run: dotnet fantomas . -r - - name: Commit and push changes - if: steps.fantomas.outcome == 'success' || steps.xlf.outcome == 'success' - run: | - git config --local user.name "github-actions[bot]" - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -a -m 'Automated command ran: ${{ steps.command-extractor.outputs.result }} - - Co-authored-by: ${{ github.event.comment.user.login }} <${{ github.event.comment.user.id }}+${{ github.event.comment.user.login }}@users.noreply.github.com>' - git push - - name: Post command comment - if: steps.fantomas.outcome == 'success' - uses: actions/github-script@v9.0.0 - with: - script: | - // Probably, there's more universal way of getting outputs, but my gh-actions-fu is not that good. - var output = "" - if ("${{steps.command-extractor.outputs.result}}" == 'fantomas') { - output = "${{steps.fantomas.outputs.result}}" - } else if("${{steps.command-extractor.outputs.result}}" == 'xlf') { - output = "${{steps.xlf.outputs.result}}" - } - const body = `Ran ${{ steps.command-extractor.outputs.result }}: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}\n${output}`; - await github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body - }); - - name: Post command failed comment - if: failure() - uses: actions/github-script@v9.0.0 - with: - script: | - const body = `Failed to run ${{ steps.command-extractor.outputs.result }}: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; - await github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body - }); diff --git a/.github/workflows/conventional-pr-title.yml b/.github/workflows/conventional-pr-title.yml index ecd81d4aee..5b649294d9 100644 --- a/.github/workflows/conventional-pr-title.yml +++ b/.github/workflows/conventional-pr-title.yml @@ -21,4 +21,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: scopes: | - (?:all|(?:js|ts|dart|python|rust|beam|standalone)(?:/(?:js|ts|dart|python|rust|beam|standalone))*) + (?:all|(?:js|ts|dart|python|rust|beam|standalone|php)(?:/(?:js|ts|dart|python|rust|beam|standalone|php))*) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index d1fbc0eade..02b5901c07 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup-cli@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: version: v0.71.5 diff --git a/.github/workflows/easybuild-shipit.yml b/.github/workflows/easybuild-shipit.yml index c3bf9922ea..eb35850e3f 100644 --- a/.github/workflows/easybuild-shipit.yml +++ b/.github/workflows/easybuild-shipit.yml @@ -19,8 +19,13 @@ jobs: shipit-pr: name: ShipIt - Pull Request runs-on: ubuntu-latest - # Only run if the build workflow succeeded - if: ${{ github.event.workflow_run.conclusion == 'success' }} + # Only run on the upstream repository, and only if the build workflow succeeded + # The branches filter matches the head branch name, which a fork PR also controls + if: >- + !github.event.repository.fork + && github.event.workflow_run.event == 'push' + && github.event.workflow_run.head_repository.full_name == github.repository + && github.event.workflow_run.conclusion == 'success' steps: - uses: actions/checkout@v7.0.1 with: @@ -28,9 +33,7 @@ jobs: # Adapt this value based on your needs. fetch-depth: 2000 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet # Run your tests before generating the changelog to make sure everything is working fine before creating a pull request. - name: Prepare for release run: ./build.sh prepare-release @@ -43,21 +46,20 @@ jobs: release: name: Release runs-on: ubuntu-latest - if: "github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_commit.message, 'chore: release ')" + if: >- + !github.event.repository.fork + && github.event.workflow_run.event == 'push' + && github.event.workflow_run.head_repository.full_name == github.repository + && github.event.workflow_run.conclusion == 'success' + && startsWith(github.event.workflow_run.head_commit.message, 'chore: release ') steps: - uses: actions/checkout@v7.0.1 - # Configure how you want to publish your package, e.g. using the NuGet CLI, dotnet CLI, NPM, etc. - # It is recommanded to use Trusted Publishing to avoid having to manage API keys in your repository. + - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '24' - registry-url: 'https://registry.npmjs.org' + - name: Setup Node.js environment + uses: ./.github/actions/setup-node - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 546bfa670a..39f4d30d07 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v7 with: - python-version: '3.12' + python-version: '3.13' - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Install uv @@ -51,7 +51,7 @@ jobs: mkdir -p fable_library echo "__version__ = \"$VERSION\"" > fable_library/_version.py - name: Upload fable-library-py - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: fable-library-py path: ./temp/fable-library-py @@ -100,7 +100,7 @@ jobs: manylinux: auto working-directory: ./temp/fable-library-py - name: Upload wheels - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-linux-${{ matrix.platform.target }} path: ./temp/fable-library-py/dist @@ -139,7 +139,7 @@ jobs: manylinux: musllinux_1_2 working-directory: ./temp/fable-library-py - name: Upload wheels - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-musllinux-${{ matrix.platform.target }} path: ./temp/fable-library-py/dist @@ -174,7 +174,7 @@ jobs: sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} working-directory: ./temp/fable-library-py - name: Upload wheels - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-windows-${{ matrix.platform.target }} path: ./temp/fable-library-py/dist @@ -209,7 +209,7 @@ jobs: sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} working-directory: ./temp/fable-library-py - name: Upload wheels - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-macos-${{ matrix.platform.target }} path: ./temp/fable-library-py/dist @@ -232,7 +232,7 @@ jobs: args: --out dist working-directory: ./temp/fable-library-py - name: Upload sdist - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-sdist path: ./temp/fable-library-py/dist diff --git a/.github/workflows/python-type-checking.yml b/.github/workflows/python-type-checking.yml index 5b2e67cde2..f524ab542c 100644 --- a/.github/workflows/python-type-checking.yml +++ b/.github/workflows/python-type-checking.yml @@ -31,10 +31,10 @@ jobs: - name: Setup dotnet tools run: dotnet tool restore - - name: Set up Python 3.12 + - name: Set up Python 3.13 uses: actions/setup-python@v7 with: - python-version: "3.12" + python-version: "3.13" - name: Install uv run: | diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index 8a2253d099..35ffbd8ea0 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -51,7 +51,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 +# - github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -419,7 +419,7 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - name: Upload activation artifact if: success() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: activation include-hidden-files: true @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1299,7 +1299,7 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/sanitize_repo_memory_filenames.sh" - name: Upload repo-memory artifact (default) if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: repo-memory-default path: /tmp/gh-aw/repo-memory/default @@ -1308,7 +1308,7 @@ jobs: - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: agent path: | @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1371,7 +1371,7 @@ jobs: GH_AW_INFO_VERSION: "1.0.40" - name: Generate GitHub App token id: safe-outputs-app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 with: client-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1648,7 +1648,7 @@ jobs: mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '24' package-manager-cache: false @@ -1697,7 +1697,7 @@ jobs: XDG_CONFIG_HOME: /home/runner - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: detection path: /tmp/gh-aw/threat-detection/detection.log @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1928,7 +1928,7 @@ jobs: path: /tmp/gh-aw/ - name: Generate GitHub App token id: safe-outputs-app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 with: client-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} @@ -2004,7 +2004,7 @@ jobs: echo "Token invalidation step complete." - name: Upload Safe Outputs Items if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: safe-outputs-items path: | diff --git a/.vscode/launch.json b/.vscode/launch.json index 2ab2609f8b..45401b5196 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -253,6 +253,24 @@ "--noParallelTypeCheck" ] }, + { + "name": "Fable.Cli on ../quicktest-rust", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/src/Fable.Cli/bin/Debug/net10.0/fable.dll", + "args": [ + "--outDir", "${workspaceRoot}/src/quicktest-rust", + "--fableLib", "${workspaceRoot}/temp/fable-library-rust", + "--exclude", "Fable.Core", + "--lang", "Rust", + "--noCache", + "--noParallelTypeCheck" + ], + "cwd": "${workspaceRoot}/src/quicktest-rust", + "stopAtEntry": false, + "console": "internalConsole" + }, { "name": "Fable.Cli on ../fable-test", "type": "coreclr", diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 0000000000..c46b6fa9a0 --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,667 @@ +{ + "title": "Fable: F# to JavaScript, TypeScript, Python, Rust, Erlang and Dart compiler", + "description": "Fable compiles F# to JavaScript, TypeScript, Python, Rust, Dart and Erlang/BEAM. It is built on the F# Compiler Services and ships runtime libraries for each target, so F# code can run natively in those ecosystems.", + "license": "MIT", + "upload_type": "software", + "access_right": "open", + "keywords": [ + "F#", + "compiler", + "transpiler", + "JavaScript", + "TypeScript", + "Python", + "Rust", + "Dart", + "Erlang", + "BEAM" + ], + "creators": [ + { + "name": "García-Caro, Alfonso" + }, + { + "name": "Brattli, Dag" + }, + { + "name": "ncave" + }, + { + "name": "Mangel, Maxime" + } + ], + "contributors": [ + { + "name": "Forkmann, Steffen", + "type": "Other" + }, + { + "name": "Verdonck, Florian", + "type": "Other" + }, + { + "name": "Ajaj, Zaid", + "type": "Other" + }, + { + "name": "Swan, Alex", + "type": "Other" + }, + { + "name": "Podhola, David", + "type": "Other" + }, + { + "name": "Nosik, Ilja", + "type": "Other" + }, + { + "name": "Petricek, Tomas", + "type": "Other" + }, + { + "name": "fdcastel", + "type": "Other" + }, + { + "name": "Grund, Joe", + "type": "Other" + }, + { + "name": "Hietanen, Tuomas", + "type": "Other" + }, + { + "name": "Teasdale, David", + "type": "Other" + }, + { + "name": "kerams", + "type": "Other" + }, + { + "name": "Corrado, Alex", + "type": "Other" + }, + { + "name": "Claes, Jens", + "type": "Other" + }, + { + "name": "Cieślak, Krzysztof", + "type": "Other" + }, + { + "name": "Laïly, Melvyn", + "type": "Other" + }, + { + "name": "Booksbaum", + "type": "Other" + }, + { + "name": "Heintz, Aleksander", + "type": "Other" + }, + { + "name": "Jansson, Tomas", + "type": "Other" + }, + { + "name": "goswinr", + "type": "Other" + }, + { + "name": "Johnson, Cody", + "type": "Other" + }, + { + "name": "shayanhabibi", + "type": "Other" + }, + { + "name": "zach-interstates", + "type": "Other" + }, + { + "name": "Young, Zack", + "type": "Other" + }, + { + "name": "K, Mark", + "type": "Other" + }, + { + "name": "Zhao, Taine", + "type": "Other" + }, + { + "name": "Vitko, Valery", + "type": "Other" + }, + { + "name": "Dummann, Kolja", + "type": "Other" + }, + { + "name": "stroborobo", + "type": "Other" + }, + { + "name": "Shaw, Dave", + "type": "Other" + }, + { + "name": "Ohlrich, Jonathan", + "type": "Other" + }, + { + "name": "Trelford, Phillip", + "type": "Other" + }, + { + "name": "njlr", + "type": "Other" + }, + { + "name": "Chassaing, Jérémie", + "type": "Other" + }, + { + "name": "Thomas, Dave", + "type": "Other" + }, + { + "name": "Pauan", + "type": "Other" + }, + { + "name": "chadunit", + "type": "Other" + }, + { + "name": "Scites, Scott", + "type": "Other" + }, + { + "name": "Rivaille, PY", + "type": "Other" + }, + { + "name": "Clarke, Tom", + "type": "Other" + }, + { + "name": "Rieger, Lukas", + "type": "Other" + }, + { + "name": "Wostenberg, John", + "type": "Other" + }, + { + "name": "Soikin, Fyodor", + "type": "Other" + }, + { + "name": "Conforti, Leo", + "type": "Other" + }, + { + "name": "Thornton, Matt", + "type": "Other" + }, + { + "name": "piaste", + "type": "Other" + }, + { + "name": "Carter, Phillip", + "type": "Other" + }, + { + "name": "gusty", + "type": "Other" + }, + { + "name": "Brodeur, Francois", + "type": "Other" + }, + { + "name": "Esmerio, Diego", + "type": "Other" + }, + { + "name": "Dawkins, David", + "type": "Other" + }, + { + "name": "cannorin", + "type": "Other" + }, + { + "name": "i-p", + "type": "Other" + }, + { + "name": "Ott, Caroline", + "type": "Other" + }, + { + "name": "oopbase", + "type": "Other" + }, + { + "name": "Frèrebeau, Rémy", + "type": "Other" + }, + { + "name": "Leko, Tomas", + "type": "Other" + }, + { + "name": "McLellan, Michael", + "type": "Other" + }, + { + "name": "Harding, Eric", + "type": "Other" + }, + { + "name": "roboz0r", + "type": "Other" + }, + { + "name": "Sousa, Henrique", + "type": "Other" + }, + { + "name": "anchann", + "type": "Other" + }, + { + "name": "Smith, Stephen", + "type": "Other" + }, + { + "name": "Treanor, Simon", + "type": "Other" + }, + { + "name": "Hester, Jared", + "type": "Other" + }, + { + "name": "Sada, Enrico", + "type": "Other" + }, + { + "name": "Grebeniuk, Evgenii", + "type": "Other" + }, + { + "name": "Zafeiris, Orfeas", + "type": "Other" + }, + { + "name": "human-bean-555", + "type": "Other" + }, + { + "name": "Blair, Paul", + "type": "Other" + }, + { + "name": "Keše, Peter", + "type": "Other" + }, + { + "name": "Cullmann, Stefan", + "type": "Other" + }, + { + "name": "Sauleau, Sven", + "type": "Other" + }, + { + "name": "Bouquain, Thierry", + "type": "Other" + }, + { + "name": "do-wa", + "type": "Other" + }, + { + "name": "halfabench", + "type": "Other" + }, + { + "name": "irium", + "type": "Other" + }, + { + "name": "Verschatse, Gien", + "type": "Other" + }, + { + "name": "zanaptak", + "type": "Other" + }, + { + "name": "Segay, Gauthier", + "type": "Other" + }, + { + "name": "mizzle-mo", + "type": "Other" + }, + { + "name": "Rizzotto, Alessandro", + "type": "Other" + }, + { + "name": "Ågren, Andreas", + "type": "Other" + }, + { + "name": "Kurdiumov, Andrii", + "type": "Other" + }, + { + "name": "Hally, Bill", + "type": "Other" + }, + { + "name": "Churchill, Dave", + "type": "Other" + }, + { + "name": "Manske, Ian", + "type": "Other" + }, + { + "name": "Chang, Ji", + "type": "Other" + }, + { + "name": "Price, Joseph", + "type": "Other" + }, + { + "name": "Roncaglia, Julien", + "type": "Other" + }, + { + "name": "Tseing", + "type": "Other" + }, + { + "name": "Dittrich, Matthias", + "type": "Other" + }, + { + "name": "Malook, Max", + "type": "Other" + }, + { + "name": "Wilson, Max", + "type": "Other" + }, + { + "name": "Gumus, Onur", + "type": "Other" + }, + { + "name": "Frèrebeau, Rémy", + "type": "Other" + }, + { + "name": "Sim, Robert", + "type": "Other" + }, + { + "name": "ritcoder", + "type": "Other" + }, + { + "name": "Joux, Phil de", + "type": "Other" + }, + { + "name": "Pelech, Ondra", + "type": "Other" + }, + { + "name": "Forral, Oliver", + "type": "Other" + }, + { + "name": "Dunets, Nick", + "type": "Other" + }, + { + "name": "Darvey, Nick", + "type": "Other" + }, + { + "name": "Samudro, Naser Anjum", + "type": "Other" + }, + { + "name": "Janger, Mike", + "type": "Other" + }, + { + "name": "Stangarlin, Luiz Felipe", + "type": "Other" + }, + { + "name": "Luis, Leo Lorenzo", + "type": "Other" + }, + { + "name": "Battermann, Leif", + "type": "Other" + }, + { + "name": "Nischal, Kurren", + "type": "Other" + }, + { + "name": "Gerasimenko, Kirill", + "type": "Other" + }, + { + "name": "rbauduin", + "type": "Other" + }, + { + "name": "olivercoad", + "type": "Other" + }, + { + "name": "jonasdw", + "type": "Other" + }, + { + "name": "cr3wdayt5p", + "type": "Other" + }, + { + "name": "Podlovics, Zoltan", + "type": "Other" + }, + { + "name": "Shchur, Vladimir", + "type": "Other" + }, + { + "name": "Heimowski, Tomasz", + "type": "Other" + }, + { + "name": "Forkmann, Tim", + "type": "Other" + }, + { + "name": "Badger, The Gitter", + "type": "Other" + }, + { + "name": "Skar, Tengel", + "type": "Other" + }, + { + "name": "drk-mtr", + "type": "Other" + }, + { + "name": "TeaDrivenDev", + "type": "Other" + }, + { + "name": "Officer, Steve", + "type": "Other" + }, + { + "name": "Kirkvik, Simen L.", + "type": "Other" + }, + { + "name": "Tihon, Sergey", + "type": "Other" + }, + { + "name": "Melnikov, Roman", + "type": "Other" + }, + { + "name": "Boudereau, Clément", + "type": "Other" + }, + { + "name": "Queiróz, Adelar da Silva", + "type": "Other" + }, + { + "name": "Ball, Alan", + "type": "Other" + }, + { + "name": "Gorchakov, Artyom V.", + "type": "Other" + }, + { + "name": "bklop", + "type": "Other" + }, + { + "name": "Taylor, Ben", + "type": "Other" + }, + { + "name": "Taggart, Cameron", + "type": "Other" + }, + { + "name": "Meeren, Christer van der", + "type": "Other" + }, + { + "name": "Bouillier, Clément", + "type": "Other" + }, + { + "name": "McMinn, Damon", + "type": "Other" + }, + { + "name": "DashieTM", + "type": "Other" + }, + { + "name": "weebs", + "type": "Other" + }, + { + "name": "Programmerino", + "type": "Other" + }, + { + "name": "Gusarov, Eugene", + "type": "Other" + }, + { + "name": "Gabasova, Evelina", + "type": "Other" + }, + { + "name": "Romain, Flechner", + "type": "Other" + }, + { + "name": "fbehrens", + "type": "Other" + }, + { + "name": "Bilkus, Gary", + "type": "Other" + }, + { + "name": "Bouquain, Gaël", + "type": "Other" + }, + { + "name": "Tang, Hadrian", + "type": "Other" + }, + { + "name": "iyegoroff", + "type": "Other" + }, + { + "name": "Titkov, Ilya", + "type": "Other" + }, + { + "name": "Fajfr, Jan", + "type": "Other" + }, + { + "name": "Beekmann, Joerg", + "type": "Other" + }, + { + "name": "Egger, Johannes", + "type": "Other" + }, + { + "name": "Mols, Johannes", + "type": "Other" + }, + { + "name": "josselinauguste", + "type": "Other" + }, + { + "name": "Skripsky, Juraj", + "type": "Other" + }, + { + "name": "jstarkadf", + "type": "Other" + }, + { + "name": "Sacks, Justin", + "type": "Other" + }, + { + "name": "Frey, Kevin", + "type": "Other" + }, + { + "name": "Lambert, Kevin", + "type": "Other" + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index acc6bdd173..53a4b0ac4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,7 +98,7 @@ Be careful when modifying shared files like `FableTransforms.fs`, `Transforms.Ut `src/fable-library-py/` is distributed on PyPI and must be backward-compatible within minor versions. Do not change the semantics of existing functions — instead, create a new function with a new name and update `Replacements.fs` to call it. -All code in `fable-library-py` must be fully type-annotated using Python 3.12+ syntax (PEP 695). Avoid `Any`, `cast()`, and `# type: ignore` — these require justification. Keep functions small and simple: extract helpers and use early returns. The Rust extension modules (`fable-library-core`) exist to provide correct .NET semantics for numerics, arrays, etc. — not for performance (the FFI crossing is expensive). +All code in `fable-library-py` must be fully type-annotated using Python 3.13-compatible syntax, including PEP 695 type parameters. Avoid `Any`, `cast()`, and `# type: ignore` — these require justification. Keep functions small and simple: extract helpers and use early returns. The Rust extension modules (`fable-library-core`) exist to provide correct .NET semantics for numerics, arrays, etc. — not for performance (the FFI crossing is expensive). All transpiled Python code is type-checked with Pyright at standard settings (`./build.sh test python --type-check`). Do not introduce type-checking regressions without justification. @@ -125,7 +125,7 @@ DO NOT update the changelog files. These files are automatically generated based - .NET SDK 10+ - Node.js with npm -- Python 3.12+ with [uv](https://docs.astral.sh/uv/) (3.12+ required for PEP 695 type parameter syntax) +- Python 3.13+ with [uv](https://docs.astral.sh/uv/) (CI tests 3.13, 3.14, and 3.15; type checking and publish preparation use 3.13) - Rust toolchain (for Rust target) - Dart SDK (for Dart target) - Erlang/OTP with rebar3 (for BEAM target) diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..0efea4b0ae --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,27 @@ +cff-version: 1.2.0 +message: If you use Fable in your work, please cite it using this metadata. +type: software +title: "Fable: F# to JavaScript, TypeScript, Python, Rust, Erlang and Dart compiler" +abstract: Fable compiles F# to JavaScript, TypeScript, Python, Rust, Dart and Erlang/BEAM. It is built on the F# Compiler Services and ships runtime libraries for each target, so F# code can run natively in those ecosystems. +authors: + - family-names: García-Caro + given-names: Alfonso + - family-names: Brattli + given-names: Dag + - name: ncave + - family-names: Mangel + given-names: Maxime +repository-code: https://github.com/fable-compiler/Fable +url: https://fable.io +license: MIT +keywords: + - F# + - compiler + - transpiler + - JavaScript + - TypeScript + - Python + - Rust + - Dart + - Erlang + - BEAM diff --git a/package-lock.json b/package-lock.json index 82a40edb54..b840752c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,16 @@ "name": "fable-compiler", "version": "0.0.0", "dependencies": { - "@types/node": "^26.2.0", - "concurrently": "^10.0.5", + "@types/node": "^26.6.6", + "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.62.4", - "terser": "^5.50.0", + "rollup": "^4.67.3", + "terser": "^5.51.2", "typescript": "^7.0.2" }, "devDependencies": { - "markdownlint-cli": "^0.49.1" + "markdownlint-cli": "^0.50.0" }, "engines": { "node": ">=26" @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", - "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.67.3.tgz", + "integrity": "sha512-o4kGHKu2YRn/jegh1XMs6+1/3SuX6AfWaACGEWavFvN5D6YZd9OrMEBqcIMj0uTeKZr6nTXPhlpkc0IXC6DMXQ==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", - "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.67.3.tgz", + "integrity": "sha512-vfEEFajQ0K5crrtAAyownI2ZFthoY+BTwGnjoMgd4+l3YPuAcweykaMqcMYXT1wJKCnMiSIpNnAroAWix2fJ3w==", "cpu": [ "arm64" ], @@ -113,10 +113,23 @@ "android" ] }, + "node_modules/@rollup/rollup-android-x64": { + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-x64/-/rollup-android-x64-4.67.3.tgz", + "integrity": "sha512-/oNPEUU5nVDZmp1CYGDMIGdUBzON5z5RQufJbtjnOD4+CZYt//2ECHRvvmJL3EXLHaqQ7Zjr1dSTlhLCxJJbGQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", - "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.67.3.tgz", + "integrity": "sha512-Otbw33hEDwrM/jUn3nib5JLJL0vyTE0ZlmU8L44sCS+AJm0JiZs3ZlNUYj141L4k7XxYmOWtf+9cN2Kqog7OzA==", "cpu": [ "arm64" ], @@ -127,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", - "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.67.3.tgz", + "integrity": "sha512-3CNtrc+6X9HL+fzSkKF0o3WkBdO6YBm5PhbLnR9PeQNKLLL7ir4MDtlx6gvP9ZM3Uxjue3xVysAX7+VjWX66Ng==", "cpu": [ "x64" ], @@ -140,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", - "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.67.3.tgz", + "integrity": "sha512-KfHwLGjSfgOKHoyzhqqKNhohxNErfYRR2dNWXN6kn5iWMrDNldZopob+Ft/q6aurweYHRz+7M9HuFZ7nvYj5Xg==", "cpu": [ "arm64" ], @@ -153,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", - "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.67.3.tgz", + "integrity": "sha512-fXWMWNshsQrNi9pq8FJIpTZfLjhLUqDCabW1hq2Qv1kxzz7I63fRjFRNAC0C49xK9CyUngL/tWbSYjpKo2+GRg==", "cpu": [ "x64" ], @@ -166,9 +179,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", - "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.67.3.tgz", + "integrity": "sha512-22gepfGL1b0Aay36BYddC4mqvQoUgQwGU/5zrUK2BdJucWgWytbhxY4o3iSIbtZsSsFiiRlNT2xfAjrCsDQGQw==", "cpu": [ "arm" ], @@ -182,9 +195,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", - "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.67.3.tgz", + "integrity": "sha512-l04417gOo3y7xaQu52PeXGgy17dSPAa8qhs4iqxMDHKyAUpldNFsasy0kR+Z7CFifnTgwFGmyukDWLIZYEnyQQ==", "cpu": [ "arm" ], @@ -198,9 +211,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", - "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.67.3.tgz", + "integrity": "sha512-0SEhbnWkUv29OhSANlnnOKQSWs5KFoJ0FTQ3ZhxZunUjgBagQzzUmLgxpulYSpldXAGOCK5XLvvspoPngjEWpQ==", "cpu": [ "arm64" ], @@ -214,9 +227,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", - "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.67.3.tgz", + "integrity": "sha512-2wLIbgSqKUcEc4DHrQDHOg9LSfgfSr+rAoBj9wromzLMLSclupvEKHTEugGSuGuklrF7wF0dzXo9R5VymKXgcg==", "cpu": [ "arm64" ], @@ -230,9 +243,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", - "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.67.3.tgz", + "integrity": "sha512-cV3cQixj3ILfdOmvJeXu6GedaH/Xlm4/qtNAUxvVK0tQnzICits97mwuvWY/DZs0lEZTkbCqZmpO/+ne6hs5iQ==", "cpu": [ "loong64" ], @@ -246,9 +259,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", - "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.67.3.tgz", + "integrity": "sha512-DTnriHdEmvrGgPWHVoV8LfwkLsmG2oK9GYvlfV6+beMcdVWVYnNC3R6+jL9DhTVOIpv+Ys9A+1i9rM8aeayOZw==", "cpu": [ "loong64" ], @@ -262,9 +275,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", - "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.67.3.tgz", + "integrity": "sha512-2+v1frx+8t084SIinkBBURODj1lpTqLZO53INfozQa0+bnCNEGtRFag8Gi6OMV9kEK67zIRIqncfyKe1eIYGHA==", "cpu": [ "ppc64" ], @@ -278,9 +291,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", - "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.67.3.tgz", + "integrity": "sha512-vEi902pAjMHXPXOpU4Jr58jMMiZxTf8kkhis1xiLBtLtlm6oRaX+mgFjOsqMdbGt80lS+bfnTCEsAofbK9isLA==", "cpu": [ "ppc64" ], @@ -294,9 +307,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", - "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.67.3.tgz", + "integrity": "sha512-1Nm/kYmkKDcc9gJ9cRVsRPBrNUqLEaY5b2zDk2OsQMcWd2nJiFUpQf3q4T1IEVKgyUQSMuiVNDemLXcuKh/f9Q==", "cpu": [ "riscv64" ], @@ -310,9 +323,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", - "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.67.3.tgz", + "integrity": "sha512-bx93yN/wit1aY9lja1lRh2jfRPFDBfeI9kHoytXDi1Jw6dWz2P2zd6B3oKnQOHa5fTtpHmX9/swWOA2+jWLjKw==", "cpu": [ "riscv64" ], @@ -326,9 +339,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", - "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.67.3.tgz", + "integrity": "sha512-Nt1/uFKauyn2bwGcYaIcF9DA3TORW45XrtdhThm5QPu0TNDpDC5onMNSh8fzvB+klrnLKTCISCw8lR9xpxrEFA==", "cpu": [ "s390x" ], @@ -342,9 +355,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", - "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.67.3.tgz", + "integrity": "sha512-K8HqIGH9CD57XmZFo/0T3pafDAzAoB9Iof02FEFvOXg7lBn7vlkLEjHgtZkYp+srWANrlxTSf8BolQM/27kiCw==", "cpu": [ "x64" ], @@ -358,9 +371,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", - "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.67.3.tgz", + "integrity": "sha512-6vTihCOamPjOpgcjkXOFB+Lh+LM4Eb95EPYNHjcovwxsnQrFRIJ7M664fLnGklKFOZutrhR9/o+NfgQLE4Zmdg==", "cpu": [ "x64" ], @@ -374,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", - "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.67.3.tgz", + "integrity": "sha512-k5XxCKgPGASSGyXOhXVQQJM6VIRHteqwHBsbwhW55EoimmI2jfkPebwgg/lwcjezpvgyLu8YTTCRsdSrvHKA7A==", "cpu": [ "x64" ], @@ -387,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", - "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.67.3.tgz", + "integrity": "sha512-WYqJUhonki85lB0iQ/Ro9BNJiGd7rG7GBZBiB/h4EUBWiUKyI3vq6EZ1aTyy5Y8qVuSIx4ZdqDyAXYiR59Vtvw==", "cpu": [ "arm64" ], @@ -400,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", - "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.67.3.tgz", + "integrity": "sha512-WJgIOxBAnyZy2nXZdSk1xtDP0nmU41qfB47jCZ5VCxCipt2/aoImPtXMyPYB1fzXcpmNsAtYxBCY2d2WFYo00g==", "cpu": [ "arm64" ], @@ -413,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", - "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.67.3.tgz", + "integrity": "sha512-C0vJ/s69Fll/puogTouGiFkn7kFI7nIAQc8/PTVawth9VNpNYIpv9xrZr6syRaFzSxVhTUt5zoXUFLELGs6vVA==", "cpu": [ "ia32" ], @@ -426,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", - "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.67.3.tgz", + "integrity": "sha512-UFCIgrIBg2jU6Ajkrx+AwGz0EWpYCRTjr6wh+jBCY+A6r8pCDeeejTtMbbBszRdnmtUCzlXwwA4h/f4f3zPRSA==", "cpu": [ "x64" ], @@ -439,9 +452,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", - "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.67.3.tgz", + "integrity": "sha512-tlynfm3LIQtOCjFdT7kjyMnGrl0PbhyldgCfj7b1VxafD6uYiJFjd2ATzmYb+YnzNIzNezpK7XzFzDtPu37IeA==", "cpu": [ "x64" ], @@ -462,9 +475,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", "license": "MIT" }, "node_modules/@types/katex": { @@ -482,12 +495,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", - "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/unist": { @@ -873,11 +886,21 @@ } }, "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz", + "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==", "dev": true, - "license": "Python-2.0" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" }, "node_modules/balanced-match": { "version": "4.0.4", @@ -910,9 +933,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -1050,14 +1073,14 @@ } }, "node_modules/concurrently": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz", - "integrity": "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==", + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.6.tgz", + "integrity": "sha512-qgICXXFp2/3nAebRLmxcINKEXM1mLK3ij/y31P02VTs/KIkddIeC05n73DxOXsXMB9LFSqNU1/na3lDlOhcbIw==", "license": "MIT", "dependencies": { "chalk": "5.6.2", "rxjs": "7.8.2", - "shell-quote": "1.9.0", + "shell-quote": "1.12.0", "supports-color": "10.2.2", "tree-kill": "1.2.2", "yargs": "18.0.0" @@ -1178,13 +1201,13 @@ "license": "MIT" }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -1306,9 +1329,9 @@ } }, "node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", "dev": true, "license": "MIT", "engines": { @@ -1434,9 +1457,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz", - "integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", + "integrity": "sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==", "dev": true, "funding": [ { @@ -1450,7 +1473,7 @@ ], "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "argparse": "^3.0.3" }, "bin": { "js-yaml": "bin/js-yaml.mjs" @@ -1519,9 +1542,9 @@ } }, "node_modules/linkify-it": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", - "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", "dev": true, "funding": [ { @@ -1535,7 +1558,7 @@ ], "license": "MIT", "dependencies": { - "uc.micro": "^2.0.0" + "uc.micro": "^3.0.0" } }, "node_modules/lodash.uniq": { @@ -1545,9 +1568,9 @@ "license": "MIT" }, "node_modules/markdown-it": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", - "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz", + "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==", "dev": true, "funding": [ { @@ -1561,33 +1584,34 @@ ], "license": "MIT", "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.5.0", - "linkify-it": "^5.0.2", - "mdurl": "^2.0.0", + "argparse": "^3.0.3", + "entities": "^8.1.0", + "linkify-it": "^6.1.0", + "mdurl": "^2.1.0", "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" + "uc.micro": "^3.0.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/markdownlint": { - "version": "0.41.1", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.41.1.tgz", - "integrity": "sha512-qHKeU2E1bdyNAT077go2FVTNXvYcktN5IHtF6XyeD1l0PClxzSp2tUApAV14ORI8DGX4H9bNKZEzelZp4qn8IA==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.42.0.tgz", + "integrity": "sha512-kzsCV/rPWRFn3jUhcB919YF4Qgeh0g9tcogoXFadGSo6Zxj6662YOqWXv0jDn8nv5KCCxDfMKJZv6N4HkNkvTg==", "dev": true, "license": "MIT", "dependencies": { - "micromark": "4.0.2", - "micromark-core-commonmark": "2.0.3", + "micromark": "4.0.3", + "micromark-core-commonmark": "2.0.4", "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", - "micromark-extension-gfm-table": "2.1.1", + "micromark-extension-gfm-table": "2.1.2", "micromark-extension-math": "3.1.0", - "micromark-util-types": "2.0.2", - "string-width": "8.2.1" + "micromark-util-types": "2.0.3", + "re2js": "2.8.6", + "string-width": "8.3.0" }, "engines": { "node": ">=22" @@ -1597,23 +1621,23 @@ } }, "node_modules/markdownlint-cli": { - "version": "0.49.1", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.49.1.tgz", - "integrity": "sha512-qpYqJbSYf3jv57bdnFmCaZ/Wlu6IYHp2b6SOKrKBJ7OnPrDHIKmx4NERWH49QH9viTI6yO6raVDDn5nrf60VQQ==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.50.0.tgz", + "integrity": "sha512-4GqP/CqpF74ihrlLTOPc3I5b/tZabiiKnIVQ27xTzsS4hjU3oIHKg1YBzWP7fsa9AH48LPF0KcmCqDccpSeMEg==", "dev": true, "license": "MIT", "dependencies": { "commander": "~15.0.0", "deep-extend": "~0.6.0", - "ignore": "~7.0.6", - "js-yaml": "~5.2.1", + "ignore": "~7.0.13", + "js-yaml": "~5.5.0", "jsonc-parser": "~3.3.1", "jsonpointer": "~5.0.1", - "markdown-it": "~14.3.0", - "markdownlint": "~0.41.1", - "minimatch": "~10.2.5", + "markdown-it": "~15.0.3", + "markdownlint": "~0.42.0", + "minimatch": "~10.2.6", "run-con": "~1.3.3", - "smol-toml": "~1.7.0", + "smol-toml": "~1.9.1", "tinyglobby": "~0.2.17" }, "bin": { @@ -1624,16 +1648,16 @@ } }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", "dev": true, "license": "MIT" }, "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", "dev": true, "funding": [ { @@ -1657,6 +1681,7 @@ "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", @@ -1667,9 +1692,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", "dev": true, "funding": [ { @@ -1687,18 +1712,19 @@ "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", + "micromark-factory-space": "^2.1.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "micromark-util-types": "^2.0.3" } }, "node_modules/micromark-extension-directive": { @@ -1760,9 +1786,9 @@ } }, "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", "dev": true, "license": "MIT", "dependencies": { @@ -1843,9 +1869,9 @@ } }, "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", "dev": true, "funding": [ { @@ -2013,6 +2039,26 @@ "micromark-util-symbol": "^2.0.0" } }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, "node_modules/micromark-util-encode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", @@ -2150,9 +2196,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", "dev": true, "funding": [ { @@ -2167,12 +2213,12 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -2303,6 +2349,16 @@ "node": ">=6" } }, + "node_modules/re2js": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-2.8.6.tgz", + "integrity": "sha512-xLgQil4kIUCrAzVk9fRSkxkFNwmygLFjVxXrLc65aE1F0+Zsb8rxumFBy4XKyvgMCTL6kilDq3EZ0piE2dP/Dg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -2330,12 +2386,12 @@ } }, "node_modules/rollup": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", - "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.67.3.tgz", + "integrity": "sha512-HJXOntSbeXzBfjUQ+YfgcOR+NV+8queObyCqWWOy2YL6bXDFE7vNRgVXlOUyrQqfJnxdXcL27PXlkC0O3ze6jQ==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@types/estree": "^1.0.10" }, "bin": { "rollup": "dist/bin/rollup" @@ -2346,31 +2402,32 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.62.4", - "@rollup/rollup-android-arm64": "4.62.4", - "@rollup/rollup-darwin-arm64": "4.62.4", - "@rollup/rollup-darwin-x64": "4.62.4", - "@rollup/rollup-freebsd-arm64": "4.62.4", - "@rollup/rollup-freebsd-x64": "4.62.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", - "@rollup/rollup-linux-arm-musleabihf": "4.62.4", - "@rollup/rollup-linux-arm64-gnu": "4.62.4", - "@rollup/rollup-linux-arm64-musl": "4.62.4", - "@rollup/rollup-linux-loong64-gnu": "4.62.4", - "@rollup/rollup-linux-loong64-musl": "4.62.4", - "@rollup/rollup-linux-ppc64-gnu": "4.62.4", - "@rollup/rollup-linux-ppc64-musl": "4.62.4", - "@rollup/rollup-linux-riscv64-gnu": "4.62.4", - "@rollup/rollup-linux-riscv64-musl": "4.62.4", - "@rollup/rollup-linux-s390x-gnu": "4.62.4", - "@rollup/rollup-linux-x64-gnu": "4.62.4", - "@rollup/rollup-linux-x64-musl": "4.62.4", - "@rollup/rollup-openbsd-x64": "4.62.4", - "@rollup/rollup-openharmony-arm64": "4.62.4", - "@rollup/rollup-win32-arm64-msvc": "4.62.4", - "@rollup/rollup-win32-ia32-msvc": "4.62.4", - "@rollup/rollup-win32-x64-gnu": "4.62.4", - "@rollup/rollup-win32-x64-msvc": "4.62.4", + "@rollup/rollup-android-arm-eabi": "4.67.3", + "@rollup/rollup-android-arm64": "4.67.3", + "@rollup/rollup-android-x64": "4.67.3", + "@rollup/rollup-darwin-arm64": "4.67.3", + "@rollup/rollup-darwin-x64": "4.67.3", + "@rollup/rollup-freebsd-arm64": "4.67.3", + "@rollup/rollup-freebsd-x64": "4.67.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.67.3", + "@rollup/rollup-linux-arm-musleabihf": "4.67.3", + "@rollup/rollup-linux-arm64-gnu": "4.67.3", + "@rollup/rollup-linux-arm64-musl": "4.67.3", + "@rollup/rollup-linux-loong64-gnu": "4.67.3", + "@rollup/rollup-linux-loong64-musl": "4.67.3", + "@rollup/rollup-linux-ppc64-gnu": "4.67.3", + "@rollup/rollup-linux-ppc64-musl": "4.67.3", + "@rollup/rollup-linux-riscv64-gnu": "4.67.3", + "@rollup/rollup-linux-riscv64-musl": "4.67.3", + "@rollup/rollup-linux-s390x-gnu": "4.67.3", + "@rollup/rollup-linux-x64-gnu": "4.67.3", + "@rollup/rollup-linux-x64-musl": "4.67.3", + "@rollup/rollup-openbsd-x64": "4.67.3", + "@rollup/rollup-openharmony-arm64": "4.67.3", + "@rollup/rollup-win32-arm64-msvc": "4.67.3", + "@rollup/rollup-win32-ia32-msvc": "4.67.3", + "@rollup/rollup-win32-x64-gnu": "4.67.3", + "@rollup/rollup-win32-x64-msvc": "4.67.3", "fsevents": "~2.3.2" } }, @@ -2432,9 +2489,9 @@ } }, "node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2464,9 +2521,9 @@ } }, "node_modules/smol-toml": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", - "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2511,9 +2568,9 @@ } }, "node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2568,9 +2625,9 @@ } }, "node_modules/terser": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", - "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -2738,9 +2795,9 @@ } }, "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", "dev": true, "license": "MIT" }, @@ -2751,9 +2808,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "license": "MIT" }, "node_modules/url-template": { diff --git a/package.json b/package.json index a572ab555f..c7c9d1d2af 100644 --- a/package.json +++ b/package.json @@ -23,15 +23,15 @@ "posttests-compiler-js": "node --test-reporter spec --test-timeout 20000 --test temp/tests-compiler-js/Main.js" }, "dependencies": { - "@types/node": "^26.2.0", - "concurrently": "^10.0.5", + "@types/node": "^26.6.6", + "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.62.4", - "terser": "^5.50.0", + "rollup": "^4.67.3", + "terser": "^5.51.2", "typescript": "^7.0.2" }, "devDependencies": { - "markdownlint-cli": "^0.49.1" + "markdownlint-cli": "^0.50.0" } } diff --git a/pyproject.toml b/pyproject.toml index 0ab80c76ab..2432421a4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,14 +16,14 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ - "pytest>=9.1.1,<10", - "ruff>=0.16.3,<0.17", - "maturin>=1.14.1,<2", - "dunamai>=1.26.2,<2", - "pydantic>=2.13.4", + "pytest>=9.2.0,<10", + "ruff>=0.17.3,<0.18", + "maturin>=1.16.0,<2", + "dunamai>=1.26.3,<2", + "pydantic>=2.14.2", "attrs>=26.1.0", - "pyright>=1.1.411", - "ty>=0.0.72", + "pyright>=1.1.414", + "ty>=0.0.95", ] [tool.hatch.build.targets.sdist] diff --git a/src/Directory.Build.props b/src/Directory.Build.props index fb33a570e5..11646642b7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -23,16 +23,16 @@ - + all build - + all analyzers - + all analyzers diff --git a/src/Fable.AST/CHANGELOG.md b/src/Fable.AST/CHANGELOG.md index b572bfe0ff..cebcd3f4b8 100644 --- a/src/Fable.AST/CHANGELOG.md +++ b/src/Fable.AST/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: b471dc16fc3b5132af77b5974d1669c9b8220cca +last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 --- # Changelog @@ -9,6 +9,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.1.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b471dc16fc3b5132af77b5974d1669c9b8220cca..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + ## 5.0.0 - 2026-04-21 * Stable release diff --git a/src/Fable.AST/Plugins.fs b/src/Fable.AST/Plugins.fs index 930e0e8aa3..a39cee1354 100644 --- a/src/Fable.AST/Plugins.fs +++ b/src/Fable.AST/Plugins.fs @@ -40,6 +40,7 @@ type CompilerOptions = FileExtension: string TriggeredByDependency: bool NoReflection: bool + JsTemporal: bool } type PluginHelper = diff --git a/src/Fable.Analyzers/Fable.Analyzers.fsproj b/src/Fable.Analyzers/Fable.Analyzers.fsproj index 7e7ae1360d..1eb602f578 100644 --- a/src/Fable.Analyzers/Fable.Analyzers.fsproj +++ b/src/Fable.Analyzers/Fable.Analyzers.fsproj @@ -10,7 +10,7 @@ - + diff --git a/src/Fable.Build/AutomatedRelease.fs b/src/Fable.Build/AutomatedRelease.fs index a49637e4e8..e1a6afb371 100644 --- a/src/Fable.Build/AutomatedRelease.fs +++ b/src/Fable.Build/AutomatedRelease.fs @@ -52,10 +52,7 @@ let handle (args: string list) = // to the new version before that version is on npm. Standalone.handle [] Publish.publishNpm ProjectDir.fable_standalone - - // Give npm some time to make the new fable-standalone version available - // before CompilerJs.handle tries to install it. - System.Threading.Thread.Sleep(System.TimeSpan.FromSeconds(15.)) + Publish.waitForNpmAvailability ProjectDir.fable_standalone CompilerJs.handle [ "--skip-fable-standalone" ] diff --git a/src/Fable.Build/Fable.Build.fsproj b/src/Fable.Build/Fable.Build.fsproj index eff3e2622f..0999bbae27 100644 --- a/src/Fable.Build/Fable.Build.fsproj +++ b/src/Fable.Build/Fable.Build.fsproj @@ -46,11 +46,11 @@ - - + + - - + + diff --git a/src/Fable.Build/FableLibrary/TypeScript.fs b/src/Fable.Build/FableLibrary/TypeScript.fs index 6275e9b4a7..9d35b3527b 100644 --- a/src/Fable.Build/FableLibrary/TypeScript.fs +++ b/src/Fable.Build/FableLibrary/TypeScript.fs @@ -16,6 +16,7 @@ type BuildFableLibraryTypeScript() = [ Path.Combine("src", "fable-library-ts", "**", "*.ts") Path.Combine("src", "fable-library-ts", "**", "*.fs") + Path.Combine("src", "fable-library-ts", "**", "*.js") ], Path.Combine(".", "temp", "fable-library-ts") ) diff --git a/src/Fable.Build/Main.fs b/src/Fable.Build/Main.fs index d2df0287da..36f0b62085 100644 --- a/src/Fable.Build/Main.fs +++ b/src/Fable.Build/Main.fs @@ -59,12 +59,15 @@ Available commands: (can be run in watch mode) --adaptive-only Run only the tests for the adaptive version of Fable (can be run in watch mode) + --temporal-only Run only the main tests, compiled with the Temporal + date/time representation (can be run in watch mode) Options for Rust: --ast-only Run only the tests for the AST (can be run in watch mode) - --no_std Compile and run the tests without the standard library - --threaded Compile and run the tests with the threaded runtime + --no_std Legacy alias for --features no_std + --threaded Legacy alias for --features threaded + --features Compile and run with comma-separated features; omit or use "," for default features Options for Python: --type-check Run type checking on the generated code with Pyright diff --git a/src/Fable.Build/Publish.fs b/src/Fable.Build/Publish.fs index c1a81ec7f3..dcbfad026d 100644 --- a/src/Fable.Build/Publish.fs +++ b/src/Fable.Build/Publish.fs @@ -10,6 +10,7 @@ open EasyBuild.Tools.DotNet open EasyBuild.Tools.Changelog open EasyBuild.Tools.PackageJson open EasyBuild.Tools.Npm +open Semver let updateLibraryVersionInFableTransforms (compilerVersion: string) @@ -79,12 +80,36 @@ let publishNpm (projectDir: string) = PackageJson.replaceVersion (packageJsonFile, lastChangelogVersion) + let version = SemVersion.Parse(lastChangelogVersion, SemVersionStyles.Strict) + if PackageJson.needPublishing packageJsonFile then - Npm.publish (projectDir) + if version.IsPrerelease then + Npm.publish (projectDir, tag = version.PrereleaseIdentifiers.[0].Value) + else + Npm.publish (projectDir) + printfn $"Published!" else printfn $"Already up-to-date, skipping..." +let waitForNpmAvailability (projectDir: string) = + let packageJsonFile = Path.Combine(projectDir, "package.json") |> FileInfo + + let packageSpec = + PackageJson.getName packageJsonFile + + "@" + + PackageJson.getVersion packageJsonFile + + let timeout = TimeSpan.FromMinutes 10. + let stopwatch = Diagnostics.Stopwatch.StartNew() + + while PackageJson.needPublishing packageJsonFile do + if stopwatch.Elapsed > timeout then + failwith $"%s{packageSpec} is still not available on npm after %.0f{timeout.TotalMinutes} minutes" + + printfn $"Waiting for %s{packageSpec} to be available on npm..." + Threading.Thread.Sleep(TimeSpan.FromSeconds 10.) + let handle (args: string list) = // Build all the fable-libraries // Force rebuld of fable-libraries to make sure they are generated with the latest @@ -136,6 +161,7 @@ let handle (args: string list) = // to the new version before that version is on npm. Standalone.handle [] publishNpm ProjectDir.fable_standalone + waitForNpmAvailability ProjectDir.fable_standalone CompilerJs.handle [ "--skip-fable-standalone" ] diff --git a/src/Fable.Build/Quicktest/Rust.fs b/src/Fable.Build/Quicktest/Rust.fs index 91fe780de6..d08715b24a 100644 --- a/src/Fable.Build/Quicktest/Rust.fs +++ b/src/Fable.Build/Quicktest/Rust.fs @@ -2,13 +2,18 @@ module Build.Quicktest.Rust open Build.FableLibrary open Build.Quicktest.Core +open SimpleExec let handle (args: string list) = + let projectDir = "src/quicktest-rust" + + Command.Run("cargo", "clean", workingDirectory = projectDir) + genericQuicktest { Language = "rust" FableLibBuilder = BuildFableLibraryRust() - ProjectDir = "src/quicktest-rust" + ProjectDir = projectDir Extension = ".rs" RunMode = RunScript } diff --git a/src/Fable.Build/Test/Beam.fs b/src/Fable.Build/Test/Beam.fs index 13709cb2a1..bb2126743a 100644 --- a/src/Fable.Build/Test/Beam.fs +++ b/src/Fable.Build/Test/Beam.fs @@ -60,17 +60,148 @@ let private runErl (workingDir: string) (paArgs: string) (expr: string) = // stderr is folded into the returned text so a crash report reaches the failure message. stdout.Result + stderr.Result, proc.ExitCode +/// Compile a fixture with the local Fable CLI while retaining diagnostics and the non-zero exit code. +let private runFable (workingDir: string) (args: string list) = + let startInfo = + System.Diagnostics.ProcessStartInfo( + FileName = "dotnet", + WorkingDirectory = workingDir, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = System.Text.Encoding.UTF8, + StandardErrorEncoding = System.Text.Encoding.UTF8 + ) + + let localFableDir = Path.Resolve("src", "Fable.Cli") + + for arg in [ "run"; "-c"; "Release"; "--project"; localFableDir; "--" ] @ args do + startInfo.ArgumentList.Add(arg) + + use proc = System.Diagnostics.Process.Start(startInfo) + let stdout = proc.StandardOutput.ReadToEndAsync() + let stderr = proc.StandardError.ReadToEndAsync() + proc.WaitForExit() + stdout.Result + stderr.Result, proc.ExitCode + let private expect (what: string) (expected: 'a) (actual: 'a) = if expected <> actual then - failwith $"Entry point test: %s{what}\n expected: %A{expected}\n actual: %A{actual}" + failwith $"BEAM test: %s{what}\n expected: %A{expected}\n actual: %A{actual}" /// Assert that a program's output contains `substring`, showing the output when it does not. /// `expect` on a bare `output.Contains ...` reports only `expected: true / actual: false`, which /// leaves a mismatch undiagnosable without re-running the program by hand. let private expectContains (what: string) (substring: string) (output: string) = if not (output.Contains substring) then - failwith - $"Entry point test: %s{what}\n expected output to contain: %s{substring}\n actual output:\n%s{output}" + failwith $"BEAM test: %s{what}\n expected output to contain: %s{substring}\n actual output:\n%s{output}" + +let private testUnsupportedDiagnostics () = + let programSourceDir = Path.Resolve("tests", "Beam", "UnsupportedNumericLiteral") + let programBuildDir = Path.Resolve("temp", "tests", "BeamUnsupportedNumericLiteral") + Directory.clean programBuildDir + + let output, exitCode = + runFable + programBuildDir + [ + programSourceDir + "--outDir" + programBuildDir + "--lang" + "beam" + "--exclude" + "Fable.Core" + "--noCache" + ] + + expect "unsupported numeric literal compilation fails" 1 exitCode + + expectContains + "unsupported numeric literal has an actionable source range" + "Program.fs(3,12): (3,31) error FABLE: Numeric literal kind 'Float16' is not supported for Beam target" + output + + expectContains + "colliding record fields have an actionable diagnostic" + "Record 'Fable.Tests.UnsupportedNumericLiteral.CollidingRecord' has fields 'foo-bar' and 'foo_bar' that both compile to Erlang atom 'foo_bar_'. Rename one of the fields." + output + + expectContains + "colliding anonymous record fields have an actionable diagnostic" + "Anonymous record has fields 'foo-bar' and 'foo_bar' that both compile to Erlang atom 'foo_bar_'. Rename one of the fields." + output + + let generatedFile = + Path.Combine(programBuildDir, "src", "unsupported_numeric_literal_program.erl") + + let generatedCode = File.ReadAllText(generatedFile) + + expect + "unsupported numeric literal does not emit a runtime error placeholder" + false + (generatedCode.Contains("erlang:error(unsupported_")) + +let private testIdentifierCollisionDiagnostics () = + let programSourceDir = Path.Resolve("tests", "Beam", "CollidingMemberNames") + let programBuildDir = Path.Resolve("temp", "tests", "BeamCollidingMemberNames") + Directory.clean programBuildDir + + let output, exitCode = + runFable + programBuildDir + [ + programSourceDir + "--outDir" + programBuildDir + "--lang" + "beam" + "--exclude" + "Fable.Core" + "--noCache" + ] + + expect "colliding member name compilation fails" 1 exitCode + + expectContains + "colliding member names have an actionable diagnostic" + "Declarations 'fooBar' and 'foo_bar' in 'colliding_member_names_program' compile to duplicate Erlang function 'foo_bar/0'. Rename one declaration." + output + + expectContains + "class-generated and module member collisions have an actionable diagnostic" + "Declarations in 'colliding_member_names_program' compile to duplicate Erlang function 'foo_ctor/0'. Rename one declaration or change its arity." + output + + expectContains + "members differing only in case have an actionable diagnostic" + "compile to duplicate Erlang function 'renderer_work/1'. Rename one declaration or change its arity." + output + + for owner in [ "CtorFields"; "ValFields" ] do + expectContains + $"colliding fields on '%s{owner}' have an actionable diagnostic" + $"Type 'Fable.Tests.CollidingMemberNames.%s{owner}' has fields 'fooBar' and 'FooBar' that both compile to Erlang atom 'field_foo_bar'. Rename one of the fields." + output + + expectContains + "colliding union case tags have an actionable diagnostic" + "Union 'Fable.Tests.CollidingMemberNames.CollidingUnion' has cases 'FooBar' and 'Foo_Bar' that both compile to Erlang atom 'foo_bar'. Rename one of the cases." + output + + expectContains + "a CompiledName colliding with a case name has an actionable diagnostic" + "Union 'Fable.Tests.CollidingMemberNames.CompiledNameUnion' has cases 'Tagged' and 'Foo_Bar' that both compile to Erlang atom 'foo_bar'. Rename one of the cases." + output + + expectContains + "colliding interface members have an actionable diagnostic" + "Interface 'Fable.Tests.CollidingMemberNames.ICollidingMembers' has members 'FooBar' and 'Foo_Bar' that both dispatch through Erlang atom 'foo_bar'. Rename one of the members." + output + + expectContains + "bounded interface setter keys participate in collision diagnostics" + "Interface 'Fable.Tests.CollidingMemberNames.ILongSetterCollision' has members" + output /// Compile a whole program and run it on the BEAM through the generated `main.erl` shim. /// @@ -233,3 +364,8 @@ let handle (args: string list) = // The suite above calls test functions directly and never runs a program end to end, so the // generated entry point shim is only exercised here. testEntryPointPrograms () + + // Unsupported constructs must stop compilation at their source location rather than leave + // an erlang:error placeholder that fails only when the generated code runs. + testUnsupportedDiagnostics () + testIdentifierCollisionDiagnostics () diff --git a/src/Fable.Build/Test/Dart.fs b/src/Fable.Build/Test/Dart.fs index a4185bdf02..fbc90c9b05 100644 --- a/src/Fable.Build/Test/Dart.fs +++ b/src/Fable.Build/Test/Dart.fs @@ -27,7 +27,7 @@ let handle (args: string list) = Directory.GetFiles(testsFolder, "*.dart") |> Seq.iter (Shell.copyFile buildDir) - let testCmd = $"dart test {buildDir}/main.dart" + let testCmd = $"dart test {buildDir}/src/Main.dart" let fableArgs = CmdLine.concat @@ -52,7 +52,7 @@ let handle (args: string list) = Async.Parallel [ if not noDotnet then - Command.RunAsync("dotnet", "watch test -c Release", workingDirectory = testsFsprojFolder) + Command.RunAsync("dotnet", "watch run -c Release", workingDirectory = testsFsprojFolder) |> Async.AwaitTask Command.WatchFableAsync(fableArgs, workingDirectory = buildDir) @@ -61,6 +61,6 @@ let handle (args: string list) = |> Async.RunSynchronously |> ignore else - Command.Run("dotnet", "test -c Release", workingDirectory = testsFsprojFolder) + Command.Run("dotnet", "run -c Release", workingDirectory = testsFsprojFolder) Command.Fable(fableArgs, workingDirectory = buildDir) diff --git a/src/Fable.Build/Test/Integration.fs b/src/Fable.Build/Test/Integration.fs index 0b74733140..7aa61f41f8 100644 --- a/src/Fable.Build/Test/Integration.fs +++ b/src/Fable.Build/Test/Integration.fs @@ -37,6 +37,7 @@ let private testProjectConfig (projectDirName: string) (configuration: string op let handle (args: string list) = BuildFableLibraryJavaScript().Run() + BuildFableLibraryTypeScript().Run() Command.Run("dotnet", "run -c Release", workingDirectory = integrationProjectDir) diff --git a/src/Fable.Build/Test/JavaScript.fs b/src/Fable.Build/Test/JavaScript.fs index 9280ff78f7..67f66794a9 100644 --- a/src/Fable.Build/Test/JavaScript.fs +++ b/src/Fable.Build/Test/JavaScript.fs @@ -24,6 +24,7 @@ let private testReact (isWatch: bool) = Command.WatchFableAsync( CmdLine.appendRaw "watch" >> CmdLine.appendRaw "--noCache" + >> CmdLine.appendPrefix "--exclude" "Fable.Core" // There seems to be some strange console Log writting >> CmdLine.appendRaw "--verbose" >> CmdLine.appendRaw "--runWatch" @@ -42,7 +43,10 @@ let private testReact (isWatch: bool) = |> Async.RunSynchronously |> ignore else - Command.Fable(CmdLine.appendRaw "--noCache", workingDirectory = workingDirectory) + Command.Fable( + CmdLine.appendRaw "--noCache" >> CmdLine.appendPrefix "--exclude" "Fable.Core", + workingDirectory = workingDirectory + ) Command.Run("npx", "jest", workingDirectory = workingDirectory) @@ -86,11 +90,20 @@ let private testAdaptive (isWatch: bool) = else Command.Fable(fableArgs, workingDirectory = destinationDir) -let private handleMainTests (isWatch: bool) (noDotnet: bool) = +type private DateTimeRepresentation = + | JsDate + | Temporal + +let private runMainTests (representation: DateTimeRepresentation) (isWatch: bool) (noDotnet: bool) = let folderName = "Main" let sourceDir = Path.Resolve("tests", "Js", folderName) - let destinationDir = Path.Resolve("temp", "tests", "JavaScript", folderName) + let outputFolder = + match representation with + | JsDate -> "JavaScript" + | Temporal -> "JavaScriptTemporal" + + let destinationDir = Path.Resolve("temp", "tests", outputFolder, folderName) let testCommand = CmdLine.empty @@ -111,6 +124,7 @@ let private handleMainTests (isWatch: bool) (noDotnet: bool) = |> CmdLine.appendPrefix "--lang" "javascript" |> CmdLine.appendPrefix "--exclude" "Fable.Core" |> CmdLine.appendRaw "--noCache" + |> CmdLine.appendIf (representation = Temporal) "--test:js-temporal" if isWatch then CmdLine.empty @@ -122,7 +136,6 @@ let private handleMainTests (isWatch: bool) (noDotnet: bool) = ] if isWatch then - // In watch mode, we only test the Main tests to not pollute the logs too much Async.Parallel [ if not noDotnet then @@ -138,11 +151,21 @@ let private handleMainTests (isWatch: bool) (noDotnet: bool) = ] |> Async.RunSynchronously |> ignore + else + Command.Fable(fableArgs, workingDirectory = destinationDir) + +let private handleMainTests (isWatch: bool) (noDotnet: bool) = + if isWatch then + // In watch mode, we only test the Main tests to not pollute the logs too much + runMainTests JsDate isWatch noDotnet else Command.Run("dotnet", "run -c Release", workingDirectory = Path.Combine("tests", "Js", "Main")) // Test the Main tests against JavaScript - Command.Fable(fableArgs, workingDirectory = destinationDir) + runMainTests JsDate false noDotnet + + // Re-run them with the Temporal date/time representation enabled + runMainTests Temporal false noDotnet testReact false testAdaptive false @@ -157,17 +180,23 @@ let handle (args: string list) = let isReactOnly = args |> List.contains "--react-only" let isStandaloneOnly = args |> List.contains "--standalone-only" let isAdaptiveOnly = args |> List.contains "--adaptive-only" + let isTemporalOnly = args |> List.contains "--temporal-only" let forceFableLibrary = args |> List.contains "--force-fable-library" let isWatch = args |> List.contains "--watch" let noDotnet = args |> List.contains "--no-dotnet" - match (isReactOnly, isStandaloneOnly, isAdaptiveOnly) with - | (true, true, _) - | (true, _, true) - | (_, true, true) -> - failwith "Cannot use '--react-only', '--standalone-only' and '--adaptive-only' at the same time" + let exclusiveArgs = + [ + "--react-only", isReactOnly + "--standalone-only", isStandaloneOnly + "--adaptive-only", isAdaptiveOnly + "--temporal-only", isTemporalOnly + ] + |> List.filter snd + |> List.map (fun (name, _) -> $"'%s{name}'") - | _ -> () + if exclusiveArgs.Length > 1 then + failwith $"""Cannot use %s{String.Join(", ", exclusiveArgs)} at the same time""" BuildFableLibraryJavaScript().Run(forceFableLibrary) @@ -177,5 +206,7 @@ let handle (args: string list) = Standalone.handleStandaloneFast () else if isAdaptiveOnly then testAdaptive isWatch + else if isTemporalOnly then + runMainTests Temporal isWatch noDotnet else handleMainTests isWatch noDotnet diff --git a/src/Fable.Build/Test/Python.fs b/src/Fable.Build/Test/Python.fs index 87081b3596..caaaffb692 100644 --- a/src/Fable.Build/Test/Python.fs +++ b/src/Fable.Build/Test/Python.fs @@ -23,6 +23,10 @@ let handle (args: string list) = Directory.clean buildDir + // Native runtime tests use controlled timers and real threads alongside generated F# regressions. + for testFile in Directory.GetFiles(sourceDir, "test_*.py") do + File.Copy(testFile, Path.Combine(buildDir, Path.GetFileName(testFile)), overwrite = true) + Command.Run("uv", "sync") // Install local fable-library as editable package for testing diff --git a/src/Fable.Build/Test/Rust.fs b/src/Fable.Build/Test/Rust.fs index e37805cdbe..e4222c197c 100644 --- a/src/Fable.Build/Test/Rust.fs +++ b/src/Fable.Build/Test/Rust.fs @@ -27,12 +27,37 @@ let handle (args: string list) = let forceFableLibrary = args |> List.contains "--force-fable-library" let isWatch = args |> List.contains "--watch" let astOnly = args |> List.contains "--ast-only" - let noStd = args |> List.contains "--no_std" - let threaded = args |> List.contains "--threaded" let noDotnet = args |> List.contains "--no-dotnet" + let requestedFeatures = + match args |> List.tryFindIndex ((=) "--features") with + | Some index -> + args + |> List.tryItem (index + 1) + |> Option.defaultValue "," + |> fun features -> + features.Split([| ',' |], System.StringSplitOptions.RemoveEmptyEntries) + |> Array.map (fun feature -> feature.Trim()) + |> Array.filter (fun feature -> feature <> "") + |> Array.toList + | None -> [] + + let legacyFeatures = + [ "--no_std", "no_std"; "--threaded", "threaded" ] + |> List.choose (fun (flag, feature) -> + if args |> List.contains flag then + Some feature + else + None + ) + + let features = requestedFeatures @ legacyFeatures |> List.distinct + + let noStd = features |> List.contains "no_std" + let threaded = features |> List.contains "threaded" + if noStd && threaded then - failwith "Cannot use --no-std and --threaded at the same time" + failwith "Cannot use no_std and threaded features together" BuildFableLibraryRust().Run(forceFableLibrary) @@ -58,13 +83,19 @@ let handle (args: string list) = Shell.copyFile destination file ) + let cargoFeatures = features |> String.concat "," + let cargoTestArgs = - if noStd then - "cargo test --features no_std -- --test-threads=1" - elif threaded then - "cargo test --features threaded" + let cargoTest = + if List.isEmpty features then + "cargo test" + else + $"cargo test --features {cargoFeatures}" + + if threaded then + cargoTest else - "cargo test -- --test-threads=1" + $"{cargoTest} -- --test-threads=1" let fableArgs = CmdLine.concat diff --git a/src/Fable.Build/Test/TypeScript.fs b/src/Fable.Build/Test/TypeScript.fs index 8755d34876..097ea704b5 100644 --- a/src/Fable.Build/Test/TypeScript.fs +++ b/src/Fable.Build/Test/TypeScript.fs @@ -36,7 +36,8 @@ let handle (args: string list) = |> CmdLine.appendPrefix "--outDir" fableDest |> CmdLine.appendPrefix "--lang" "typescript" |> CmdLine.appendPrefix "--exclude" "Fable.Core" - // |> CmdLine.appendPrefix "--typedArrays" "false" + // JavaScript tests use the default typed arrays, so cover the other mode here + |> CmdLine.appendPrefix "--typedArrays" "false" |> CmdLine.appendRaw "--noCache" // Let Fable handle the TypeScript invocation diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index bba32a64f7..541f521959 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: b517d52df145ee30d1d3bd7e20fded1a9003ac54 +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,348 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.27.0 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(all)* Recompile when a source file is added or its output is missing (#4870) ([a782afce](https://github.com/fable-compiler/Fable/commit/a782afcea79f91bc5649e7db5eea57f9d11a1529)) +* *(all)* Keep reads of imported mutable values in place (#5242) ([1190dfa5](https://github.com/fable-compiler/Fable/commit/1190dfa5e5f12793fc2b3b2d15a24afb8253bce3)) +* *(all)* Give imported .fsx files their output extension (#5241) ([60771e07](https://github.com/fable-compiler/Fable/commit/60771e077577fdcdf5e40e82630cf88e8cfad560)) +* *(all)* Rename idents of a witness inlined twice (#5249) ([1e41dcc5](https://github.com/fable-compiler/Fable/commit/1e41dcc5ac5a990a81aa15d43921d824b5466740)) +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Unescape doubled braces in String.Format (#5231) ([24e01fd3](https://github.com/fable-compiler/Fable/commit/24e01fd30e3eec7571816b4d6cc9f6281cce0de4)) +* *(js/ts)* Treat null as empty in String.Join and Concat (#5230) ([a7dcab8d](https://github.com/fable-compiler/Fable/commit/a7dcab8d770ac56f825e15361df7b2e05f618e5e)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Match Dictionary/HashSet array keys by reference (#5239) ([1d77d628](https://github.com/fable-compiler/Fable/commit/1d77d6282b8dea284bca3cff9a6455e561575e3a)) +* *(js/ts)* Round exact midpoints to even in numeric formats (#5245) ([350a3900](https://github.com/fable-compiler/Fable/commit/350a390071473bb4e93fb055a3876d7b3215ada1)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Escape */ in JSDoc comments (#5255) ([1072231c](https://github.com/fable-compiler/Fable/commit/1072231c4c64467547d7f69ab741824f2e1a522c)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Write negated integer zero without a sign (#5256) ([ce7fc717](https://github.com/fable-compiler/Fable/commit/ce7fc717c7597b133dc6f999f88a15b2fb636d65)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python)* Call base ctor from explicit constructors (#5226) ([508026e7](https://github.com/fable-compiler/Fable/commit/508026e73511f2636245515b8ec9476176e34b5c)) +* *(js/ts/python)* Clamp negative slice bounds like .NET (#5232) ([7319a994](https://github.com/fable-compiler/Fable/commit/7319a994a9c3be7d31348d29d2c464482b5f21f9)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) +* *(rust)* Inline hashing, more Decimal tests (#5236) ([705b6b70](https://github.com/fable-compiler/Fable/commit/705b6b70bf0b732811bd6e69cfc8fc0614b831c7)) +* *(ts)* Declare Mangle(false) interface members unmangled (#5234) ([86c00004](https://github.com/fable-compiler/Fable/commit/86c000041f286668353615003692b07e9082ff01)) +* *(ts)* Extend library classes for BCL collection bases (#5240) ([d82ef893](https://github.com/fable-compiler/Fable/commit/d82ef8932c58ddc73ff5aa6ca669be86aedbe9db)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + +## 5.26.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + +## 5.25.0 - 2026-10-29 + +### 🚀 Features + +* *(all)* Error on unsupported RegexOptions at compile time (#5180) ([a8cbc5dc](https://github.com/fable-compiler/Fable/commit/a8cbc5dc0c3a96639028057e132b39e559f40ced)) +* *(js/ts/python/dart)* Support Nullable conversions (#5183) ([6efa86d3](https://github.com/fable-compiler/Fable/commit/6efa86d35c5943dc8bbb6da0f27cc37095276064)) +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Warn on duplicate names in erased unions (#5169) ([459166d2](https://github.com/fable-compiler/Fable/commit/459166d2500a963a3616d8b1a63e1d8585d61ec6)) +* *(all)* Recompile inline users of a file with a signature (#5179) ([b383673d](https://github.com/fable-compiler/Fable/commit/b383673da61cfe3ca06416a624aaafbd732f876c)) +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(all)* Keep F# diagnostics for scripts on Windows (#5218) ([ce886964](https://github.com/fable-compiler/Fable/commit/ce886964cb4521dc4c01c8da089b97c9d9742814)) +* *(beam)* Raise ArgumentNullException from nullArg ([0b8f77f9](https://github.com/fable-compiler/Fable/commit/0b8f77f9880bcbd9317272d9b0d89f8fe8ac0257)) +* *(beam)* Keep parameter name in ArgumentException ([9e5d34f7](https://github.com/fable-compiler/Fable/commit/9e5d34f7b32cfdfb8b1adc93378895b26a5d69b9)) +* *(js/ts)* Format DateTimeOffset in its own offset (#5171) ([04ddbd20](https://github.com/fable-compiler/Fable/commit/04ddbd2027e03f4cf7b0b22fe886fdabadf26d93)) +* *(js/ts)* Support %o in sprintf and interpolated strings (#5175) ([e13e7b0f](https://github.com/fable-compiler/Fable/commit/e13e7b0fdc16d26793833c77a11d242ccc61109b)) +* *(js/ts)* Keep column 0 mappings in source maps (#5176) ([04ba3853](https://github.com/fable-compiler/Fable/commit/04ba3853620811f6ed66eb6812bd9df3d87a1b5d)) +* *(js/ts)* Zero-fill Array.zeroCreate for numeric types without a typed array (#5211) ([d96b6ab8](https://github.com/fable-compiler/Fable/commit/d96b6ab82f0181be4a379c806abcedde23f1359a)) +* *(js/ts)* Throw on out-of-range string indexer (#5214) ([d5f1f8fd](https://github.com/fable-compiler/Fable/commit/d5f1f8fdfab535eaff5c492661d0178be1c4c76a)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/dart)* Float to small integer and char conversions (#5182) ([bb0faa79](https://github.com/fable-compiler/Fable/commit/bb0faa794080a04e7791d419b2fa4d2ad107c186)) +* *(js/ts/python)* Compare ReferenceEquality types by ref (#5174) ([9c351626](https://github.com/fable-compiler/Fable/commit/9c3516268674b54aec2efae381cad920d4f15922)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) +* *(python/beam/rust)* Implicit DateTime to DateTimeOffset (#5173) ([401c881f](https://github.com/fable-compiler/Fable/commit/401c881f729494d06d20da9687e78f7315f5efb2)) +* *(ts)* Annotate Fable.Core typed arrays with native TS types ([e35d06c3](https://github.com/fable-compiler/Fable/commit/e35d06c36ba8ce4ab6cce75d6b332b9dcd50d711)) +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +### ⚡ Performance Improvements + +* *(js/ts)* Standard library combine numeric hash from snapshot of object instead of mapping (#5151) ([44495104](https://github.com/fable-compiler/Fable/commit/44495104b5c7ac65bfcea184d1305fd29f1db9b4)) +* *(js/ts)* Standard library remove .map result array from record hash using snapshot (#5150) ([0ceb5e9e](https://github.com/fable-compiler/Fable/commit/0ceb5e9ec5b470d00295a5cea9ead1c621c7858e)) +* *(js/ts)* Standard library one pass List.append builder (#5148) ([2ed9d6dd](https://github.com/fable-compiler/Fable/commit/2ed9d6dd0a372e5bcf53dfe24d786bf9b9256fd3)) +* *(js/ts)* Cast erased option unwraps in TypeScript output (#5181) ([65cd419d](https://github.com/fable-compiler/Fable/commit/65cd419d78e4dbc2008f8a707d5298e390defbee)) +* *(js/ts)* List node as a class with a shared empty node (#5185) ([187f8985](https://github.com/fable-compiler/Fable/commit/187f89852413186b52d4ecdde55e1be7cf736785)) +* *(js/ts)* Compile isNull to a plain null check (#5186) ([71a0ad92](https://github.com/fable-compiler/Fable/commit/71a0ad92237586e9569fd1483f35c88f8477f478)) +* *(js/ts)* Check array bounds only on undefined reads (#5188) ([435fc0bb](https://github.com/fable-compiler/Fable/commit/435fc0bb008ff2ab7c14a08c7218be94f4735029)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Keep the curried original on its adapter (#5191) ([bfb4a2a4](https://github.com/fable-compiler/Fable/commit/bfb4a2a4e3bb1b5bd4491955a1e1cb90c14050b6)) +* *(js/ts)* Walk arrays by index when enumerated as seq (#5192) ([d88621c3](https://github.com/fable-compiler/Fable/commit/d88621c3b9d6ce48a72dc09b86a6805f612ac185)) +* *(js/ts)* Throw invalidArg and invalidOp at the call site (#5194) ([e6a742b1](https://github.com/fable-compiler/Fable/commit/e6a742b1ebda71342d4445705ea9198c2059ae90)) +* *(js/ts)* Hash unions without allocating (#5195) ([2b66a5d2](https://github.com/fable-compiler/Fable/commit/2b66a5d2184f3a35f0d73fb397ab902e300ac4bd)) +* *(js/ts)* Probe dictionary buckets without allocating (#5196) ([9517eafd](https://github.com/fable-compiler/Fable/commit/9517eafd93aa7b84061662a421b7a39a7269318b)) +* *(js/ts)* Pass array literals to typed array constructors as plain arrays (#5209) ([cb7d6d6e](https://github.com/fable-compiler/Fable/commit/cb7d6d6e49bb43b83afe80477ea2d6d667be3f5b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + +## 5.24.0 - 2026-10-22 + +### 🚀 Features + +* *(all)* Support suppressing warnings via `// fable-disable` comments (#4808) ([f4a1a8cd](https://github.com/fable-compiler/Fable/commit/f4a1a8cdae2e8ac81edbdc6eba36cc140234170b)) +* *(all)* Warn on duplicate CompiledName on union cases ([2098f64a](https://github.com/fable-compiler/Fable/commit/2098f64a25e96385306ef19ed8c289f3b6c84db9)) +* *(js/ts)* Map static Decimal conversion methods (#5129) ([cd63e57e](https://github.com/fable-compiler/Fable/commit/cd63e57e909393ab493a1d8bfedbea9a50a377c9)) +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +### 🐞 Bug Fixes + +* *(all)* Kill --run process on SIGTERM everywhere (#5140) ([09158b05](https://github.com/fable-compiler/Fable/commit/09158b05acf4c35b54111f4f1d72b96930bce18e)) +* *(all)* Render warning code without repeating the tag (#5142) ([5c44e514](https://github.com/fable-compiler/Fable/commit/5c44e514626a230227e2aad040d439fa7da0491c)) +* *(all)* Serialize output directory deduplication (#5143) ([b775276e](https://github.com/fable-compiler/Fable/commit/b775276eb843c5bf6533424b325eef25940abcaa)) +* *(all)* Report compiler agent failures instead of hanging (#5144) ([09d06614](https://github.com/fable-compiler/Fable/commit/09d066146f662e331d1b73c0ba60f2a0ed935fbc)) +* *(all)* Refresh unchanged outputs so up-to-date skip works (#5145) ([0529fcf8](https://github.com/fable-compiler/Fable/commit/0529fcf8b57cc5a6aca0a92aae5f444e53ea0f28)) +* *(js/ts)* Round decimals below 0.1 correctly ([fca4c408](https://github.com/fable-compiler/Fable/commit/fca4c408a64e33c58293f12869975e5b61bcb96a)) +* *(js/ts)* Keep decimal zero scale without leading zeros ([3076b45a](https://github.com/fable-compiler/Fable/commit/3076b45acacf39b8c99ec57025560ad355ab74db)) +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Keep .NET decimal scale through arithmetic (#5126) ([bdf0a6c8](https://github.com/fable-compiler/Fable/commit/bdf0a6c8cb56151a5431b5e3703f7ee165b26dba)) +* *(js/ts)* Warn on type test against Pojo class (#5128) ([798df101](https://github.com/fable-compiler/Fable/commit/798df10179576a28f602fad58e204881792a0aef)) +* *(js/ts)* Accept decimal integers up to 2^96 ([c9feb339](https://github.com/fable-compiler/Fable/commit/c9feb339fb8a4e98bebf17b3facdc42ecd22d628)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Respect maxDegreeOfParallelism and cancel on failure in Async.Parallel (#5098) ([5de3271d](https://github.com/fable-compiler/Fable/commit/5de3271d8b47f1752c4e74c010e4314cf40bbcbe)) +* *(js/ts)* Inline interface cases of TypeScriptTaggedUnion (#5146) ([2be66611](https://github.com/fable-compiler/Fable/commit/2be66611d884cd564fa3f7b06fd4a015add1cefd)) + +### ⚡ Performance Improvements + +* *(all)* Faster project cracking (#5136) ([ba042783](https://github.com/fable-compiler/Fable/commit/ba0427839107c157436479958898688952df05c3)) +* *(all)* Disable .NET telemetry for dotnet build (#5137) ([e9ffc41e](https://github.com/fable-compiler/Fable/commit/e9ffc41e5f53df018d4c141b9bfaa51ca76e0be5)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + +## 5.23.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +### 🐞 Bug Fixes + +* *(standalone)* Warn and skip plugins instead of throwing (#5114) ([021ab65c](https://github.com/fable-compiler/Fable/commit/021ab65c6d3adfe57cffb9f5ba6f595b034ac8bd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..5ee8bbb832d9677ad702104ded8e00c09619393f) + +## 5.22.0 - 2026-10-16 + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) + +### 🐞 Bug Fixes + +* *(all)* Recompile dependents on signature change in watch (#5084) ([87fc89f7](https://github.com/fable-compiler/Fable/commit/87fc89f75b6ad22fafec72b4a57415682b4059df)) +* *(all)* Forward `--targetprofile` for precompile (#5085) ([50129598](https://github.com/fable-compiler/Fable/commit/50129598a13d4ab7a6e50ae1be0d9aa9d3c68ef8)) +* *(beam)* Make lazy sequence wrappers process-portable (#5063) ([9e78a3b3](https://github.com/fable-compiler/Fable/commit/9e78a3b327ca78d01863bf19b18c543673633af7)) +* *(js/ts)* Avoid full array copy in Encoding.GetString (#5069) ([e6b72ec7](https://github.com/fable-compiler/Fable/commit/e6b72ec73be454bd3b3233167542a65fc6553c8a)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) +* *(python)* Settle cancelled tasks awaited by async workflows (#5064) ([52186937](https://github.com/fable-compiler/Fable/commit/5218693738defc8a4502d28c206e391b241f9fd8)) +* *(python)* Preserve reusable replies and unit continuations (#5082) ([f19e2c56](https://github.com/fable-compiler/Fable/commit/f19e2c56566488dfeb48e71508bdb218aaa58582)) +* *(python)* Emit widening integer casts (#5087) ([07989b16](https://github.com/fable-compiler/Fable/commit/07989b16214c4fcf8e33318fba1dd3f77df77cbb)) +* *(rust)* Support inref struct unions (#5060) ([a3f27e0e](https://github.com/fable-compiler/Fable/commit/a3f27e0e8c149d6bfeb347ca4f4146ccfd8fb063)) + +### ⚡ Performance Improvements + +* Crack project references in parallel (#5079) ([d42aa6b9](https://github.com/fable-compiler/Fable/commit/d42aa6b926ac531aba7069aea408009f96584ae5)) +* Update inline expressions incrementally in watch mode (#5080) ([594d17b4](https://github.com/fable-compiler/Fable/commit/594d17b4b551ae33b5f2df5e4049611b9dd24bc2)) +* *(all)* Enable server GC and immediate JIT tiering (#5073) ([9bde2e41](https://github.com/fable-compiler/Fable/commit/9bde2e41d8483100ed14bbb37fa261896a6c88a4)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..e33c57870454fea189a3eb33519f583a5003f8c3) + +## 5.21.0 - 2026-10-12 + +### 🚀 Features + +* *(php)* System.Environment (#4782) ([a8661874](https://github.com/fable-compiler/Fable/commit/a8661874d60694f0faad235adb7f760fed6f72a5)) + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) +* *(beam)* Preserve async cancellation and finalize resources once (#5057) ([617f7111](https://github.com/fable-compiler/Fable/commit/617f7111bf02f8536b321b4373bdffa7a0f7d633)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..a8661874d60694f0faad235adb7f760fed6f72a5) + +## 5.20.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(beam)* Disambiguate property accessors from ordinary methods (#5049) ([b30dc89d](https://github.com/fable-compiler/Fable/commit/b30dc89d0091c2bd1d9fd7a20042b7fbb4e5a3fa)) +* *(beam)* Normalize exception messages and string split separators (#5053) ([f13a9e14](https://github.com/fable-compiler/Fable/commit/f13a9e1494d8bbde8e74bfbf9dfbc8bd051a157e)) +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Preserve JS/TS generic unit arguments and Python unit continuations (#5037) ([e05bc752](https://github.com/fable-compiler/Fable/commit/e05bc752f1d8f16f08e2409a191d9c33463a1f04)) +* *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159)) +* *(rust)* Fix tailcall elimination for members (#5051) ([4eb21772](https://github.com/fable-compiler/Fable/commit/4eb2177263f544ec354076da07d8a097d56c273e)) +* *(rust)* Fix temporary struct locals type (#5052) ([17b36588](https://github.com/fable-compiler/Fable/commit/17b3658876a3bd776c4a0129ec3a4cddc9d7e05d)) +* *(rust)* Add main function for no_std (#5054) ([10fdccc0](https://github.com/fable-compiler/Fable/commit/10fdccc04bb0e421643426d21dfcbd940e3e5a16)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/23bc5c54db51469431e6fdab713dfe1b6a3df019..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + +## 5.20.0 - 2026-10-07 + +### 🚀 Features + +* *(python)* Give System exceptions native Python bases (#5040) ([4ee31586](https://github.com/fable-compiler/Fable/commit/4ee31586fd88ed0708e218e7f170432baa33551e)) + +### 🐞 Bug Fixes + +* *(beam)* Recognize boxed discriminated unions in type tests (#5033) ([dcfaa3d6](https://github.com/fable-compiler/Fable/commit/dcfaa3d6181f6789d0868e0bf9132ecdcdc76287)) +* *(beam)* Prevent identifier collisions and atom overflow (#5017) ([b9ce2fdb](https://github.com/fable-compiler/Fable/commit/b9ce2fdb53fc86a615970f4828005ee3a490385f)) +* *(python)* Share per-object locks and align StartChild timeouts (#5035) ([2da7144c](https://github.com/fable-compiler/Fable/commit/2da7144c68efded840fc6585c23d3a4e28e8e37f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/aaf35f200be5bbc2698abe242f77b8de745e4337..23bc5c54db51469431e6fdab713dfe1b6a3df019) + +## 5.19.0 - 2026-10-03 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(all)* Minor type const strings cleanup (#5029) ([9b69e5be](https://github.com/fable-compiler/Fable/commit/9b69e5be8a2cffc3cbcb3920bf9924b72ed385d6)) +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Use common null storage (#4995) ([ae165dd0](https://github.com/fable-compiler/Fable/commit/ae165dd0cf7248e22c3520d1e00347d0713d0526)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) +* *(rust)* Remove trailing comma from macro args (#5031) ([e6e9f54c](https://github.com/fable-compiler/Fable/commit/e6e9f54c905750a1bf78ec454bca7d83cdffb61e)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..aaf35f200be5bbc2698abe242f77b8de745e4337) + +## 5.18.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +### 🐞 Bug Fixes + +* Retry MSBuild project cracking serially (#4991) ([b0c6129e](https://github.com/fable-compiler/Fable/commit/b0c6129e146318701bd723488af6cf8e3d4bdf9b)) +* *(beam)* Preserve local spawn calls (#4992) ([70278ec6](https://github.com/fable-compiler/Fable/commit/70278ec636b7f948a6ba3d215b36a3974de89edc)) +* *(dart)* Prefix file scope names shadowed by class members (#4982) ([ece7b071](https://github.com/fable-compiler/Fable/commit/ece7b0714eae18650124cad017b0bda4da74ebc8)) +* *(js/ts)* Fix Dictionary.Remove and HashSet.Remove to remove empty buckets (#4985) ([aea61b56](https://github.com/fable-compiler/Fable/commit/aea61b562c461a9b383e805f06f68a3b21ed45c2)) +* *(js/ts)* Clear custom System.Collections.IList implementations ([2369fbd2](https://github.com/fable-compiler/Fable/commit/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36)) +* *(python)* Keep the payload type when reading an erased union field (#4981) ([e0536ecd](https://github.com/fable-compiler/Fable/commit/e0536ecd9b9aeb12cc96c7a2ef2edde7089e2dd0)) +* *(rust)* Less allocations when enumerating strings (#4970) ([5d62380c](https://github.com/fable-compiler/Fable/commit/5d62380cb30dc1080c6943445a256e27db6246f8)) +* *(rust)* Remove reflection registration on boxing (#4983) ([50a37f78](https://github.com/fable-compiler/Fable/commit/50a37f78f31394d61349b09a025c1713529d8ec0)) + +### ⚡ Performance Improvements + +* *(js/ts)* Faster `resizeArray.Clear()` in JS/TS ([32d85add](https://github.com/fable-compiler/Fable/commit/32d85add01422547dba03b15e0a0787d8afb031b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/66efd10960c967660084db2b364e944d167897f4..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + +## 5.17.2 - 2026-09-12 + +### 🐞 Bug Fixes + +* *(python)* Use the .NET range for Decimal.MaxValue and Decimal.MinValue (#4967) ([66efd109](https://github.com/fable-compiler/Fable/commit/66efd10960c967660084db2b364e944d167897f4)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..66efd10960c967660084db2b364e944d167897f4) + +## 5.17.1 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(js/ts)* Keep the sign of negative decimals below 0.1 ([91d80f18](https://github.com/fable-compiler/Fable/commit/91d80f185fb38e4d23fd9e906037a14daab3e3b9)) +* *(js/ts)* Respect the precision when formatting decimals below 0.1 ([1c02557e](https://github.com/fable-compiler/Fable/commit/1c02557efd113d8d30a922c74c3fc9d1e7168cfd)) +* *(js/ts)* Omit the decimal sign when the rounded result is zero ([ef63087d](https://github.com/fable-compiler/Fable/commit/ef63087d97b61927763023c4b2e5bb16bcd64af2)) +* *(python)* Preserve record field names in structured formatting (#4952) ([fbc162d6](https://github.com/fable-compiler/Fable/commit/fbc162d6912d62b9a75e73f8264814f6519b6194)) +* *(python)* Support bytes() on generic and bool arrays (#4961) ([94b59294](https://github.com/fable-compiler/Fable/commit/94b59294b4749c1f871484b843604da81ceb71ad)) +* *(python)* Parse uint64 values above Int64.MaxValue ([584d8ce4](https://github.com/fable-compiler/Fable/commit/584d8ce4f6d434df56195ed6d09d6336df1970d6)) +* *(python)* Report DateTimeKind.Utc for DateTime.UtcNow ([4abf06ea](https://github.com/fable-compiler/Fable/commit/4abf06ea86186f64a15078fb19c6006cca0676b0)) +* *(python)* Reflect an erased union as the type it erases to ([0f564c74](https://github.com/fable-compiler/Fable/commit/0f564c744e86096c779d0acea6d677bce6d56ee1)) +* *(python)* Implement Decimal.GetBits and the bits constructors ([cf0c9305](https://github.com/fable-compiler/Fable/commit/cf0c9305759af17fde6dcd433618bbeaca5b60d0)) +* *(python)* Round a float to .NET's precision when converting to decimal ([4a441b19](https://github.com/fable-compiler/Fable/commit/4a441b1926387a42db6a3a4294acd97b02760ca3)) +* *(ts)* Declare the type parameter of a flexible-type overload ([6f1f1b8a](https://github.com/fable-compiler/Fable/commit/6f1f1b8afd9158743978720507ced5c4a8b2144f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + +## 5.17.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Match .NET for G, E and custom numeric formats ([6fb03090](https://github.com/fable-compiler/Fable/commit/6fb03090ce328b8c5d6a36422f120dd641aefe3a)) +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) +* *(python)* Build typed arrays from lists and sequences (#4944) ([5af37779](https://github.com/fable-compiler/Fable/commit/5af37779ce33c1e9036df7a9548830c4f914bdd5)) +* *(rust)* Fix IEnumerable casting, interop and type testing (#4946) ([93c2df5b](https://github.com/fable-compiler/Fable/commit/93c2df5ba2b3316893dc85dc4fd746a09a07bf39)) +* *(rust)* Restore missing bindings (#4947) ([2fb46a26](https://github.com/fable-compiler/Fable/commit/2fb46a269bc4827494cab878f6e3f0e1b4052035)) +* *(ts)* Declare local function type params only reachable via constraints (#4943) ([8aa6d3b9](https://github.com/fable-compiler/Fable/commit/8aa6d3b9e9f3c1dc013d67406878ea93637ad74c)) +* *(ts/python)* Erase byref from struct copy-and-update locals (#4945) ([19a6522e](https://github.com/fable-compiler/Fable/commit/19a6522ea517db53dabd5e221206849d382e1025)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + +## 5.16.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) +* *(python)* Support quotation derived patterns (#4928) ([f6d939a1](https://github.com/fable-compiler/Fable/commit/f6d939a14c67010cb6cc02c7994d6f56c7156dab)) +* *(rust)* Observables, Events and IEnumerable (PR 6 of 7 Rust improvements) (#4789) ([20175eea](https://github.com/fable-compiler/Fable/commit/20175eea841adf14ed57c0068458fad591c8d5f4)) +* *(rust)* System.Uri (PR 7 of 7 Rust improvements) (#4790) ([8a551bc9](https://github.com/fable-compiler/Fable/commit/8a551bc9b2433035fc704dffc639e5f031ca7593)) + +### 🐞 Bug Fixes + +* Preserve project FSharp.Core reference (#4924) ([1289360a](https://github.com/fable-compiler/Fable/commit/1289360a11398387712df2769069b9c949659966)) +* Report Fable's own logs from the Fable.Compiler API (#4923) ([0775c604](https://github.com/fable-compiler/Fable/commit/0775c604d319d230796da0d47817428b1da8fece)) +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) +* *(all)* Allow private inline functions to use private values (#4915) ([b0740ba9](https://github.com/fable-compiler/Fable/commit/b0740ba95859a80e7add07f21e2fc81a31c00853)) +* *(python)* Emit Python names for float32 specials (#4938) ([b4c45a93](https://github.com/fable-compiler/Fable/commit/b4c45a93385f249a8586789037b0af18005786e9)) +* *(python)* Compare nested options of different depth (#4940) ([5afa2948](https://github.com/fable-compiler/Fable/commit/5afa2948edd221b64aa14553509ba96028d064c5)) +* *(rust)* Observables and Events cleanup (#4908) ([ca165045](https://github.com/fable-compiler/Fable/commit/ca165045703797f0558da4bc6e06cbea7af231de)) +* *(rust)* Support `string` on a tuple (#4916) ([510c8a43](https://github.com/fable-compiler/Fable/commit/510c8a43c159ffb9d7d7bdc4e2ee16b84b80b28e)) +* *(rust)* Decimal culture overloads, and mutables assigned in try/finally (#4918) ([58402471](https://github.com/fable-compiler/Fable/commit/584024712450d333193baddc239db977af05bd54)) +* *(rust)* Let a string be enumerated (#4921) ([395c152b](https://github.com/fable-compiler/Fable/commit/395c152bd4ef0c26af7a4ff48ed44c728aa73d25)) +* *(rust)* Async perf: stop the async runtime sleeping, and add Parallel (#4920) ([88135b90](https://github.com/fable-compiler/Fable/commit/88135b90382f63bd40d58dfc75c4f915cc4bb868)) +* *(rust)* F# exceptions, non-exhaustive matches and obj type tests (#4917) ([28da1d76](https://github.com/fable-compiler/Fable/commit/28da1d764e63c39b96ca20e94d9a08786b3c9052)) +* *(rust)* Wait for the other files instead of sleeping before the last one. (#4927) ([d329a823](https://github.com/fable-compiler/Fable/commit/d329a823298af9f934058f245d54155ccb555a69)) +* *(rust)* Cleanup AsyncBuilder and TaskBuilder (#4936) ([3e6bc9bd](https://github.com/fable-compiler/Fable/commit/3e6bc9bd4d4337e901cc748f35df2a5933f0ba55)) +* *(rust)* Fix interface casting, move reflection into declarations (#4939) ([04299157](https://github.com/fable-compiler/Fable/commit/042991572969e1864a160b0a0145b1d59bc77236)) +* *(rust)* Support reflection for no_std (#4941) ([bf694309](https://github.com/fable-compiler/Fable/commit/bf694309ae5dbc6bb546d9acc4d091f5e35fe330)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b517d52df145ee30d1d3bd7e20fded1a9003ac54..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 5.15.0 - 2026-08-25 ### 🚀 Features diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index 74921cbfb8..e10a8fa026 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -126,6 +126,7 @@ let knownCliArgs () = [ "--trimRootModule" ], [] [ "--fableLib" ], [] [ "--replace" ], [] + [ "--test:js-temporal" ], [] ] let printKnownCliArgs () = @@ -158,6 +159,10 @@ Arguments: When set to '1' or 'true', Fable watch will poll the file system for changes. This is required for some file systems, such as network shares, Docker mounted volumes, and other virtual file systems. + + DOTNET_TieredPGO + When set to '0', non-watch compilations can run 10 to 20%% faster + depending on the project, at the cost of slower rebuilds in watch mode. """ let generateHelpWithPrefix prefixText = @@ -314,12 +319,16 @@ type Runner = | Some c when String.IsNullOrWhiteSpace c -> defaultConfiguration | Some configurationArg -> configurationArg + let jsTemporal = args.FlagEnabled "--test:js-temporal" + let define = args.Values "--define" |> List.append [ "FABLE_COMPILER" "FABLE_COMPILER_5" + if jsTemporal then + "FABLE_COMPILER_JAVASCRIPT_TEMPORAL" match language with | Php -> "FABLE_COMPILER_PHP" | Rust -> "FABLE_COMPILER_RUST" @@ -353,7 +362,8 @@ type Runner = debugMode = (configuration = "Debug"), optimizeFSharpAst = args.FlagEnabled "--optimize", noReflection = args.FlagEnabled "--noReflection", - verbosity = verbosity + verbosity = verbosity, + jsTemporal = jsTemporal ) let cliArgs = diff --git a/src/Fable.Cli/Fable.Cli.fsproj b/src/Fable.Cli/Fable.Cli.fsproj index ffb73f4a73..b86fb6ebea 100644 --- a/src/Fable.Cli/Fable.Cli.fsproj +++ b/src/Fable.Cli/Fable.Cli.fsproj @@ -12,7 +12,12 @@ true F# to JS compiler $(OtherFlags) --nowarn:3536 + true + + + + @@ -53,7 +58,7 @@ all - + diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 707771c415..624f8771e5 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -12,10 +12,11 @@ open Fable open Fable.AST open Fable.Transforms open Fable.Transforms.State +open Fable.Compiler open Fable.Compiler.ProjectCracker open Fable.Compiler.Util -module private Util = +module Util = type PathResolver with static member Dummy = @@ -78,45 +79,22 @@ module private Util = | Severity.Error -> "error" | Severity.Info -> "info" + // MSBuild-style ` `: the code carries the FABLE prefix, so it replaces the tag + let tag = + match log.Code with + | Some code -> code + | None -> log.Tag + match log.Range with | Some r -> - $"%s{file}(%i{r.start.line},%i{r.start.column}): (%i{r.``end``.line},%i{r.``end``.column}) %s{severity} %s{log.Tag}: %s{log.Message}" - | None -> $"%s{file}(1,1): %s{severity} %s{log.Tag}: %s{log.Message}" + $"%s{file}(%i{r.start.line},%i{r.start.column}): (%i{r.``end``.line},%i{r.``end``.column}) %s{severity} %s{tag}: %s{log.Message}" + | None -> $"%s{file}(1,1): %s{severity} %s{tag}: %s{log.Message}" let logErrors rootDir (logs: LogEntry seq) = logs |> Seq.filter (fun log -> log.Severity = Severity.Error) |> Seq.iter (fun log -> Log.error (formatLog rootDir log)) - let getFSharpDiagnostics (diagnostics: FSharpDiagnostic array) = - diagnostics - |> Array.map (fun er -> - let severity = - match er.Severity with - | FSharpDiagnosticSeverity.Hidden - | FSharpDiagnosticSeverity.Info -> Severity.Info - | FSharpDiagnosticSeverity.Warning -> Severity.Warning - | FSharpDiagnosticSeverity.Error -> Severity.Error - - let range = - SourceLocation.Create( - start = - { - line = er.StartLine - column = er.StartColumn + 1 - }, - ``end`` = - { - line = er.EndLine - column = er.EndColumn + 1 - } - ) - - let msg = $"%s{er.Message} (code %i{er.ErrorNumber})" - - LogEntry.Make(severity, msg, fileName = er.FileName, range = range, tag = "FSHARP") - ) - let getOutPath (cliArgs: CliArgs) pathResolver file = match cliArgs.CompilerOptions.Language with // For Python we must have an outDir since all compiled files must be inside the same subdir, so if `outDir` is not @@ -248,6 +226,7 @@ module private Util = Logs = com.Logs InlineExprs = Array.empty WatchDependencies = com.WatchDependencies + SignatureDependencies = com.SignatureDependencies |} with e -> return @@ -380,6 +359,18 @@ type FsWatcher(delayMs: int) = type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFiles: Fable.Compiler.File array) = + let sourceReader = lazy (snd (Fable.Compiler.File.MakeSourceReader sourceFiles)) + + // Shared by every file of the project: a warning can point at any file (inlined calls) and + // files compile in parallel. A new ProjectCracked is built on every watch rebuild, so this + // never outlives the source it was computed from. + let warningSuppression = + lazy + (WarningSuppression.Resolver.FromCompilerOptions( + crackerResponse.ProjectOptions.OtherOptions, + sourceReader.Value + )) + member _.CliArgs = cliArgs member _.ProjectFile = cliArgs.ProjectFile member _.FableOptions = cliArgs.CompilerOptions @@ -403,11 +394,11 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir - let watchDependencies = + let watchDependencies, signatureDependencies = if cliArgs.IsWatch then - Some(HashSet()) + Some(HashSet()), Some(HashSet()) else - None + None, None CompilerImpl( currentFile, @@ -416,8 +407,32 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi fableLibDir, crackerResponse.OutputType, ?outDir = cliArgs.OutDir, - ?watchDependencies = watchDependencies + ?watchDependencies = watchDependencies, + ?signatureDependencies = signatureDependencies, + warningSuppression = warningSuppression.Value + ) + + /// Problems with the `fable-disable` directives themselves (typo'd codes, directives that + /// suppress nothing). Only valid once every file has been compiled, since a directive in one + /// file can be what suppresses a warning raised while compiling another. + member _.DirectiveDiagnostics(files: string seq) = + warningSuppression.Value.GetDiagnostics(files) + |> List.map (fun (file, d) -> + let pos: Position = + { + line = d.Line + column = 0 + } + + LogEntry.Make( + Severity.Warning, + d.Message, + fileName = file, + range = SourceLocation.Create(pos, pos, file), + code = d.Code + ) ) + |> Array.ofList member _.MapSourceFiles(f) = ProjectCracked(cliArgs, crackerResponse, Array.map f sourceFiles) @@ -468,6 +483,7 @@ type FableCompileResult = Logs: LogEntry[] InlineExprs: (string * InlineExpr)[] WatchDependencies: string[] + SignatureDependencies: string[] |}, {| File: string @@ -485,6 +501,7 @@ type FableCompilerMsg = pathResolver: PathResolver * isSilent: bool * isTriggeredByDependency: (string -> bool) * + getSignatureDependents: (string list * string list -> string[]) * ReplyChannel | FSharpFileTypeChecked of FSharpImplementationFileContents | FSharpCompilationFinished of FSharpCheckProjectResults @@ -497,11 +514,20 @@ type FableCompilerState = PathResolver: PathResolver IsSilent: bool TriggeredByDependency: string -> bool + SourceFiles: Fable.Compiler.File[] + GetSignatureDependents: string list * string list -> string[] + /// Files whose public signature changed in this cycle, and the subset whose literal values changed + SignatureChanges: string list + LiteralChanges: string list FilesToCompile: string[] FilesToCompileSet: Set + FilesCompiledInCycle: Set FilesCheckedButNotCompiled: Set FableFilesToCompileExpectedCount: int FableFilesCompiledCount: int + /// For Rust: the project's last file, held back until every other file + /// has compiled. See the note where it is set. + DelayedLastFile: string option FSharpLogs: LogEntry[] FableResults: FableCompileResult list HasFSharpCompilationFinished: bool @@ -509,18 +535,33 @@ type FableCompilerState = } static member Create - (fableProj, filesToCompile: string[], ?pathResolver, ?isSilent, ?triggeredByDependency, ?replyChannel) + ( + fableProj, + filesToCompile: string[], + ?sourceFiles, + ?pathResolver, + ?isSilent, + ?triggeredByDependency, + ?getSignatureDependents, + ?replyChannel + ) = { FableProj = fableProj PathResolver = defaultArg pathResolver PathResolver.Dummy IsSilent = defaultArg isSilent false TriggeredByDependency = defaultArg triggeredByDependency (fun _ -> false) + SourceFiles = defaultArg sourceFiles [||] + GetSignatureDependents = defaultArg getSignatureDependents (fun _ -> [||]) + SignatureChanges = [] + LiteralChanges = [] FilesToCompile = filesToCompile FilesToCompileSet = set filesToCompile + FilesCompiledInCycle = set filesToCompile FilesCheckedButNotCompiled = Set.empty FableFilesToCompileExpectedCount = filesToCompile |> Array.filter isImplementationFile |> Array.length FableFilesCompiledCount = 0 + DelayedLastFile = None FSharpLogs = [||] FableResults = [] HasFSharpCompilationFinished = false @@ -528,6 +569,8 @@ type FableCompilerState = } and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fableProj: Project) = + let activeReplyChannel: ReplyChannel option ref = ref None + let agent = MailboxProcessor.Start(fun agent -> let postTo toMsg work = @@ -541,6 +584,56 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl let startInThreadPool toMsg work = postTo toMsg work |> Async.Start + let subscriber = + if projCracked.CliArgs.NoParallelTypeCheck then + None + else + Some(FSharpFileTypeChecked >> agent.Post) + + let typeCheck (sourceFiles: Fable.Compiler.File[]) (lastFile: string) = + startInThreadPool + FSharpCompilationFinished + (fun () -> + let filePaths, sourceReader = Fable.Compiler.File.MakeSourceReader sourceFiles + + let filePaths, sourceReader = CodeServices.checkerFileNames filePaths sourceReader + + checker.ParseAndCheckProject( + projCracked.ProjectFile, + filePaths, + sourceReader, + CodeServices.checkerFileName lastFile, + ?subscriber = subscriber + ) + ) + + let updateProject (state: FableCompilerState) (files: FSharpImplementationFileContents list) = + let fableProj = state.FableProj.Update(files) + + let signatureChanges, literalChanges = + ((state.SignatureChanges, state.LiteralChanges), files) + ||> List.fold (fun (signatureChanges, literalChanges) file -> + let fileName = Path.normalizePathAndEnsureFsExtension file.FileName + + match + Map.tryFind fileName state.FableProj.ImplementationFiles, + Map.tryFind fileName fableProj.ImplementationFiles + with + | Some before, Some after when FileSignature.Changed(before.Signature, after.Signature) -> + fileName :: signatureChanges, + (if FileSignature.LiteralsChanged(before.Signature, after.Signature) then + fileName :: literalChanges + else + literalChanges) + | _ -> signatureChanges, literalChanges + ) + + { state with + FableProj = fableProj + SignatureChanges = signatureChanges + LiteralChanges = literalChanges + } + let fableCompile state fileName = let fableProj = state.FableProj @@ -586,51 +679,29 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl pathResolver, isSilent, isTriggeredByDependency, + getSignatureDependents, replyChannel) -> + activeReplyChannel.Value <- Some replyChannel + let state = FableCompilerState.Create( state.FableProj, filesToCompile, + sourceFiles, pathResolver, isSilent, isTriggeredByDependency, + getSignatureDependents, replyChannel ) - startInThreadPool - FSharpCompilationFinished - (fun () -> - let filePaths, sourceReader = Fable.Compiler.File.MakeSourceReader sourceFiles - - let subscriber = - if projCracked.CliArgs.NoParallelTypeCheck then - None - else - Some(FSharpFileTypeChecked >> agent.Post) - - checker.ParseAndCheckProject( - projCracked.ProjectFile, - filePaths, - sourceReader, - Array.last filesToCompile, - ?subscriber = subscriber - ) - ) - + typeCheck sourceFiles (Array.last filesToCompile) return! loop state | FSharpFileTypeChecked file -> // It seems when there's a pair .fsi/.fs the F# compiler gives the .fsi extension to the implementation file let fileName = file.FileName |> Path.normalizePath |> Path.ensureFsExtension - // For Rust, delay last file's compilation so other files can finish compiling - if - projCracked.CliArgs.CompilerOptions.Language = Rust - && fileName = Array.last state.FilesToCompile - && state.FableFilesCompiledCount < state.FableFilesToCompileExpectedCount - 1 - then - do! Async.Sleep(1000) - Log.verbose ( lazy $"Type checked: {IO.Path.GetRelativePath(projCracked.CliArgs.RootDir, file.FileName)}" ) @@ -645,8 +716,29 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl if not (state.FilesToCompileSet.Contains(fileName)) then state else - let state = { state with FableProj = state.FableProj.Update([ file ]) } - fableCompile state fileName + let state = updateProject state [ file ] + + // For Rust the last file has to compile after every other + // one: it re-exports the namespaces the other files + // register into a shared registry while they compile, so + // compiling it early silently drops whatever had not + // registered yet. It is held back here and released by + // FableFileCompiled once the rest are done -- a wait on + // the actual condition, where the fixed sleep this + // replaces only shrank the race for small projects. + if + projCracked.CliArgs.CompilerOptions.Language = Rust + && fileName = Array.last state.FilesToCompile + && state.FableFilesCompiledCount < state.FableFilesToCompileExpectedCount - 1 + then + { state with + DelayedLastFile = Some fileName + // Marked pending so the finished check keeps + // waiting for it. + FilesCheckedButNotCompiled = Set.add fileName state.FilesCheckedButNotCompiled + } + else + fableCompile state fileName return! loop state @@ -655,7 +747,10 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl let state = { state with - FSharpLogs = getFSharpDiagnostics results.Diagnostics + FSharpLogs = + Array.append + state.FSharpLogs + (CodeServices.getFSharpDiagnostics results.Diagnostics) HasFSharpCompilationFinished = true } @@ -667,7 +762,7 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl else results.AssemblyContents.ImplementationFiles - let state = { state with FableProj = state.FableProj.Update(implFiles) } + let state = updateProject state (List.ofSeq implFiles) let filesToCompile = state.FilesToCompile @@ -680,8 +775,37 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl else state - FableCompiler.CheckIfCompilationIsFinished(state) - return! loop state + let dependents = + if List.isEmpty state.SignatureChanges then + [||] + else + state.GetSignatureDependents(state.SignatureChanges, state.LiteralChanges) + |> Array.filter (fun file -> not (state.FilesCompiledInCycle.Contains file)) + + if Array.isEmpty dependents then + FableCompiler.CheckIfCompilationIsFinished(state) + return! loop state + else + Log.verbose ( + lazy + $"""Signature changed in {String.concat ", " (state.SignatureChanges |> List.map File.relPathToCurDir)}, recompiling {dependents.Length} dependent files""" + ) + + let state = + { state with + SignatureChanges = [] + LiteralChanges = [] + FilesToCompile = dependents + FilesToCompileSet = set dependents + FilesCompiledInCycle = Set.union state.FilesCompiledInCycle (set dependents) + FableFilesToCompileExpectedCount = + state.FableFilesToCompileExpectedCount + + (dependents |> Array.filter isImplementationFile |> Array.length) + HasFSharpCompilationFinished = false + } + + typeCheck state.SourceFiles (Array.last dependents) + return! loop state | FableFileCompiled(fileName, result) -> let state = @@ -691,6 +815,16 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl FilesCheckedButNotCompiled = Set.remove fileName state.FilesCheckedButNotCompiled } + // Everything else is compiled: release the held-back Rust + // last file (see FSharpFileTypeChecked). + let state = + match state.DelayedLastFile with + | Some delayed when + state.FableFilesCompiledCount = state.FableFilesToCompileExpectedCount - 1 + -> + fableCompile { state with DelayedLastFile = None } delayed + | _ -> state + if not state.IsSilent then let msg = let fileName = IO.Path.GetRelativePath(projCracked.CliArgs.RootDir, fileName) @@ -745,10 +879,18 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl FableCompilerState.Create(fableProj, [||]) |> loop ) + do + agent.Error.Add(fun e -> + activeReplyChannel.Value + |> Option.iter (fun channel -> Error e |> channel.Reply) + ) + member _.GetFableProject() = agent.PostAndAsyncReply(GetFableProject) - member _.StartCompilation(sourceFiles, filesToCompile, pathResolver, isSilent, isTriggeredByDependency) = + member _.StartCompilation + (sourceFiles, filesToCompile, pathResolver, isSilent, isTriggeredByDependency, getSignatureDependents) + = async { if Array.isEmpty filesToCompile then return [||], [] @@ -766,6 +908,7 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl pathResolver, isSilent, isTriggeredByDependency, + getSignatureDependents, channel ) ) @@ -810,6 +953,8 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl let filePaths, sourceReader = Fable.Compiler.File.MakeSourceReader projCracked.SourceFiles + let filePaths, sourceReader = CodeServices.checkerFileNames filePaths sourceReader + checker.Compile(filePaths, sourceReader, outFile) type Watcher = @@ -866,6 +1011,7 @@ type State = CliArgs: CliArgs ProjectCrackedAndFableCompiler: (ProjectCracked * FableCompiler) option WatchDependencies: Map + SignatureDependencies: Map PendingFiles: string[] DeduplicateDic: ConcurrentDictionary Watcher: Watcher option @@ -893,7 +1039,16 @@ type State = // as some OS use case insensitive paths let importDir = importDir.ToLower() - this.DeduplicateDic.GetOrAdd(importDir, (fun _ -> set this.DeduplicateDic.Values |> addTargetDir)) + lock + this.DeduplicateDic + (fun () -> + match this.DeduplicateDic.TryGetValue(importDir) with + | true, targetDir -> targetDir + | false, _ -> + let targetDir = set this.DeduplicateDic.Values |> addTargetDir + this.DeduplicateDic[importDir] <- targetDir + targetDir + ) } static member Create(cliArgs, ?watchDelay, ?recompileAllFiles) = @@ -901,6 +1056,7 @@ type State = CliArgs = cliArgs ProjectCrackedAndFableCompiler = None WatchDependencies = Map.empty + SignatureDependencies = Map.empty Watcher = watchDelay |> Option.map Watcher.Create DeduplicateDic = ConcurrentDictionary() PendingFiles = [||] @@ -1450,23 +1606,49 @@ let private compilationCycle (state: State) (changes: ISet) = | None -> FableCompiler.Init(projCracked) | Some fableCompiler -> async.Return fableCompiler + // Literal values are inlined by the F# compiler, so a changed literal file is followed by every later file + let getSignatureDependents (changedFiles: string list, literalFiles: string list) = + let changedFiles = set changedFiles + + let firstLiteralFile = + literalFiles + |> List.choose (fun file -> Array.tryFindIndex ((=) file) projCracked.SourceFilePaths) + |> List.sort + |> List.tryHead + + projCracked.SourceFilePaths + |> Array.indexed + |> Array.filter (fun (index, path) -> + (match Map.tryFind path state.SignatureDependencies with + | Some dependencies -> dependencies |> Array.exists changedFiles.Contains + | None -> false) + || ( + match firstLiteralFile with + | Some literalIndex -> index > literalIndex + | None -> false + ) + ) + |> Array.map snd + let! fsharpLogs, fableResults = fableCompiler.StartCompilation( projCracked.SourceFiles, // Make sure to pass the up-to-date source files (with cleared hashes for changed files) filesToCompile, state.GetPathResolver(?precompiledInfo = projCracked.PrecompiledInfo), state.SilentCompilation, - fun f -> state.TriggeredByDependency(f, changes) + (fun f -> state.TriggeredByDependency(f, changes)), + getSignatureDependents ) - let logs, watchDependencies = - ((fsharpLogs, state.WatchDependencies), fableResults) - ||> List.fold (fun (logs, deps) -> + let logs, watchDependencies, signatureDependencies = + ((fsharpLogs, state.WatchDependencies, state.SignatureDependencies), fableResults) + ||> List.fold (fun (logs, deps, sigDeps) -> function | Ok res -> let logs = Array.append logs res.Logs let deps = Map.add res.File res.WatchDependencies deps - logs, deps + let sigDeps = Map.add res.File res.SignatureDependencies sigDeps + logs, deps, sigDeps | Error e -> let log = match e.Exception with @@ -1476,13 +1658,19 @@ let private compilationCycle (state: State) (changes: ISet) = LogEntry.MakeError(msg, fileName = e.File, tag = "EXCEPTION") - Array.append logs [| log |], deps + Array.append logs [| log |], deps, sigDeps ) + // Every file is compiled by now, so a directive that still hasn't suppressed anything + // really is unused - checking earlier would flag directives that only ever fire for a + // warning raised while some later file was being compiled. + let logs = Array.append logs (projCracked.DirectiveDiagnostics filesToCompile) + let state = { state with PendingFiles = [||] WatchDependencies = watchDependencies + SignatureDependencies = signatureDependencies SilentCompilation = false } @@ -1563,7 +1751,7 @@ let private compilationCycle (state: State) (changes: ISet) = match result with | Some ex -> - getFSharpDiagnostics diagnostics |> logErrors cliArgs.RootDir + CodeServices.getFSharpDiagnostics diagnostics |> logErrors cliArgs.RootDir Log.error (ex.Message) return 1 | None -> @@ -1703,7 +1891,7 @@ let startCompilationAsync state = let logs = Array.append compilationResult.ErrorLogs compilationResult.OtherLogs match compilationResult.ExitCode with - | 0 -> return Ok(state, logs) + | 0 -> return Ok(compilationResult.State, logs) | _ -> return Error("Compilation failed", logs) with diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index eaf00773b8..5f8721dc7b 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -61,7 +61,10 @@ type Stream = return! areStreamsEqual memoryStream fileStream } - static member WriteToFileIfChanged(memoryStream: IO.Stream, targetPath: string) : Async = + static member WriteToFileIfChanged + (memoryStream: IO.Stream, targetPath: string, refreshUnchanged: bool) + : Async + = async { if memoryStream.Length = 0 then return false @@ -75,7 +78,11 @@ type Stream = else match! Stream.IsEqualToFile(memoryStream, targetPath) with | false -> return! Stream.WriteToFile(memoryStream, targetPath) - | true -> return false + | true -> + if refreshUnchanged then + IO.File.SetLastWriteTimeUtc(targetPath, DateTime.UtcNow) + + return false } module Js = @@ -102,7 +109,8 @@ module Js = do! stream.FlushAsync() |> Async.AwaitTask - let! written = Stream.WriteToFileIfChanged(memoryStream, targetPath) + let! written = + Stream.WriteToFileIfChanged(memoryStream, targetPath, not cliArgs.IsWatch) if written && cliArgs.SourceMaps then use fs = IO.File.Open(targetPath + ".map", IO.FileMode.Create) @@ -131,7 +139,10 @@ module Js = let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path - if path.EndsWith(".fs", StringComparison.Ordinal) then + if + path.EndsWith(".fs", StringComparison.Ordinal) + || path.EndsWith(".fsx", StringComparison.Ordinal) + then let isInFableModules = Path.Combine(targetDir, path) |> Naming.isInFableModules File.changeExtensionButUseDefaultExtensionInFableModules JavaScript isInFableModules path fileExt @@ -161,12 +172,8 @@ module Js = defaultArg file sourcePath |> Path.getRelativeFileOrDirPath false targetPath false - // This is a workaround for: - // https://github.com/fable-compiler/Fable/issues/3980 - // We are still investigating why some of the F# code don't have source information - // I believe for now we can ship it like that because it only deteriorate the source map - // it should not break them completely. - if srcLine <> 0 && srcCol <> 0 && file <> Some "unknown" then + // Synthetic expressions come from FCS with line 0 and file "unknown", which SourceMapSharp rejects (#3980) + if srcLine > 0 && file <> Some "unknown" then mapGenerator.Force().AddMapping(generated, original, source = sourcePath, ?name = displayName) let compileFile (com: Compiler) (cliArgs: CliArgs) pathResolver isSilent (outPath: string) = @@ -408,7 +415,10 @@ module Php = let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path - if path.EndsWith(".fs", StringComparison.Ordinal) then + if + path.EndsWith(".fs", StringComparison.Ordinal) + || path.EndsWith(".fsx", StringComparison.Ordinal) + then Path.ChangeExtension(path, fileExt) else path @@ -447,7 +457,10 @@ module Dart = let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path - if path.EndsWith(".fs", StringComparison.Ordinal) then + if + path.EndsWith(".fs", StringComparison.Ordinal) + || path.EndsWith(".fsx", StringComparison.Ordinal) + then Path.ChangeExtension(path, fileExt) else path @@ -503,7 +516,10 @@ module Rust = let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path - if path.EndsWith(".fs", StringComparison.Ordinal) then + if + path.EndsWith(".fs", StringComparison.Ordinal) + || path.EndsWith(".fsx", StringComparison.Ordinal) + then Path.ChangeExtension(path, fileExt) else path diff --git a/src/Fable.Compiler/BrowserInlineExprs.fs b/src/Fable.Compiler/BrowserInlineExprs.fs index e8aaeb4c2f..ab4c98a449 100644 --- a/src/Fable.Compiler/BrowserInlineExprs.fs +++ b/src/Fable.Compiler/BrowserInlineExprs.fs @@ -25,13 +25,13 @@ let private extra = (Decode.uint64 |> Decode.map unativeint) let private decoder = - lazy (Decode.Auto.generateDecoder<(string * Fable.InlineExpr) array> (extra = extra)) + lazy (Decode.Auto.generateDecoder<(string * Fable.InlineExpr) array>(extra = extra)) let fromString (json: string) : Result<(string * Fable.InlineExpr) array, string> = Decode.fromString decoder.Value json #if !FABLE_COMPILER let private encoder = - lazy (Encode.Auto.generateEncoder<(string * Fable.InlineExpr) array> (extra = extra)) + lazy (Encode.Auto.generateEncoder<(string * Fable.InlineExpr) array>(extra = extra)) let toString (chunk: (string * Fable.InlineExpr) array) : string = chunk |> encoder.Value |> Encode.toString 0 diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index a2e5531e88..1fd3d0d9ac 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: b517d52df145ee30d1d3bd7e20fded1a9003ac54 +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,341 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.26.0 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(all)* Recompile when a source file is added or its output is missing (#4870) ([a782afce](https://github.com/fable-compiler/Fable/commit/a782afcea79f91bc5649e7db5eea57f9d11a1529)) +* *(all)* Keep reads of imported mutable values in place (#5242) ([1190dfa5](https://github.com/fable-compiler/Fable/commit/1190dfa5e5f12793fc2b3b2d15a24afb8253bce3)) +* *(all)* Give imported .fsx files their output extension (#5241) ([60771e07](https://github.com/fable-compiler/Fable/commit/60771e077577fdcdf5e40e82630cf88e8cfad560)) +* *(all)* Rename idents of a witness inlined twice (#5249) ([1e41dcc5](https://github.com/fable-compiler/Fable/commit/1e41dcc5ac5a990a81aa15d43921d824b5466740)) +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Unescape doubled braces in String.Format (#5231) ([24e01fd3](https://github.com/fable-compiler/Fable/commit/24e01fd30e3eec7571816b4d6cc9f6281cce0de4)) +* *(js/ts)* Treat null as empty in String.Join and Concat (#5230) ([a7dcab8d](https://github.com/fable-compiler/Fable/commit/a7dcab8d770ac56f825e15361df7b2e05f618e5e)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Match Dictionary/HashSet array keys by reference (#5239) ([1d77d628](https://github.com/fable-compiler/Fable/commit/1d77d6282b8dea284bca3cff9a6455e561575e3a)) +* *(js/ts)* Round exact midpoints to even in numeric formats (#5245) ([350a3900](https://github.com/fable-compiler/Fable/commit/350a390071473bb4e93fb055a3876d7b3215ada1)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Escape */ in JSDoc comments (#5255) ([1072231c](https://github.com/fable-compiler/Fable/commit/1072231c4c64467547d7f69ab741824f2e1a522c)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Write negated integer zero without a sign (#5256) ([ce7fc717](https://github.com/fable-compiler/Fable/commit/ce7fc717c7597b133dc6f999f88a15b2fb636d65)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python)* Call base ctor from explicit constructors (#5226) ([508026e7](https://github.com/fable-compiler/Fable/commit/508026e73511f2636245515b8ec9476176e34b5c)) +* *(js/ts/python)* Clamp negative slice bounds like .NET (#5232) ([7319a994](https://github.com/fable-compiler/Fable/commit/7319a994a9c3be7d31348d29d2c464482b5f21f9)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) +* *(rust)* Inline hashing, more Decimal tests (#5236) ([705b6b70](https://github.com/fable-compiler/Fable/commit/705b6b70bf0b732811bd6e69cfc8fc0614b831c7)) +* *(ts)* Declare Mangle(false) interface members unmangled (#5234) ([86c00004](https://github.com/fable-compiler/Fable/commit/86c000041f286668353615003692b07e9082ff01)) +* *(ts)* Extend library classes for BCL collection bases (#5240) ([d82ef893](https://github.com/fable-compiler/Fable/commit/d82ef8932c58ddc73ff5aa6ca669be86aedbe9db)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + +## 5.25.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + +## 5.24.0 - 2026-10-29 + +### 🚀 Features + +* *(all)* Error on unsupported RegexOptions at compile time (#5180) ([a8cbc5dc](https://github.com/fable-compiler/Fable/commit/a8cbc5dc0c3a96639028057e132b39e559f40ced)) +* *(js/ts/python/dart)* Support Nullable conversions (#5183) ([6efa86d3](https://github.com/fable-compiler/Fable/commit/6efa86d35c5943dc8bbb6da0f27cc37095276064)) +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Warn on duplicate names in erased unions (#5169) ([459166d2](https://github.com/fable-compiler/Fable/commit/459166d2500a963a3616d8b1a63e1d8585d61ec6)) +* *(all)* Recompile inline users of a file with a signature (#5179) ([b383673d](https://github.com/fable-compiler/Fable/commit/b383673da61cfe3ca06416a624aaafbd732f876c)) +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(all)* Keep F# diagnostics for scripts on Windows (#5218) ([ce886964](https://github.com/fable-compiler/Fable/commit/ce886964cb4521dc4c01c8da089b97c9d9742814)) +* *(beam)* Raise ArgumentNullException from nullArg ([0b8f77f9](https://github.com/fable-compiler/Fable/commit/0b8f77f9880bcbd9317272d9b0d89f8fe8ac0257)) +* *(beam)* Keep parameter name in ArgumentException ([9e5d34f7](https://github.com/fable-compiler/Fable/commit/9e5d34f7b32cfdfb8b1adc93378895b26a5d69b9)) +* *(js/ts)* Format DateTimeOffset in its own offset (#5171) ([04ddbd20](https://github.com/fable-compiler/Fable/commit/04ddbd2027e03f4cf7b0b22fe886fdabadf26d93)) +* *(js/ts)* Support %o in sprintf and interpolated strings (#5175) ([e13e7b0f](https://github.com/fable-compiler/Fable/commit/e13e7b0fdc16d26793833c77a11d242ccc61109b)) +* *(js/ts)* Zero-fill Array.zeroCreate for numeric types without a typed array (#5211) ([d96b6ab8](https://github.com/fable-compiler/Fable/commit/d96b6ab82f0181be4a379c806abcedde23f1359a)) +* *(js/ts)* Throw on out-of-range string indexer (#5214) ([d5f1f8fd](https://github.com/fable-compiler/Fable/commit/d5f1f8fdfab535eaff5c492661d0178be1c4c76a)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/dart)* Float to small integer and char conversions (#5182) ([bb0faa79](https://github.com/fable-compiler/Fable/commit/bb0faa794080a04e7791d419b2fa4d2ad107c186)) +* *(js/ts/python)* Compare ReferenceEquality types by ref (#5174) ([9c351626](https://github.com/fable-compiler/Fable/commit/9c3516268674b54aec2efae381cad920d4f15922)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) +* *(python/beam/rust)* Implicit DateTime to DateTimeOffset (#5173) ([401c881f](https://github.com/fable-compiler/Fable/commit/401c881f729494d06d20da9687e78f7315f5efb2)) +* *(ts)* Annotate Fable.Core typed arrays with native TS types ([e35d06c3](https://github.com/fable-compiler/Fable/commit/e35d06c36ba8ce4ab6cce75d6b332b9dcd50d711)) +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +### ⚡ Performance Improvements + +* *(js/ts)* Standard library combine numeric hash from snapshot of object instead of mapping (#5151) ([44495104](https://github.com/fable-compiler/Fable/commit/44495104b5c7ac65bfcea184d1305fd29f1db9b4)) +* *(js/ts)* Standard library remove .map result array from record hash using snapshot (#5150) ([0ceb5e9e](https://github.com/fable-compiler/Fable/commit/0ceb5e9ec5b470d00295a5cea9ead1c621c7858e)) +* *(js/ts)* Standard library one pass List.append builder (#5148) ([2ed9d6dd](https://github.com/fable-compiler/Fable/commit/2ed9d6dd0a372e5bcf53dfe24d786bf9b9256fd3)) +* *(js/ts)* Cast erased option unwraps in TypeScript output (#5181) ([65cd419d](https://github.com/fable-compiler/Fable/commit/65cd419d78e4dbc2008f8a707d5298e390defbee)) +* *(js/ts)* List node as a class with a shared empty node (#5185) ([187f8985](https://github.com/fable-compiler/Fable/commit/187f89852413186b52d4ecdde55e1be7cf736785)) +* *(js/ts)* Compile isNull to a plain null check (#5186) ([71a0ad92](https://github.com/fable-compiler/Fable/commit/71a0ad92237586e9569fd1483f35c88f8477f478)) +* *(js/ts)* Check array bounds only on undefined reads (#5188) ([435fc0bb](https://github.com/fable-compiler/Fable/commit/435fc0bb008ff2ab7c14a08c7218be94f4735029)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Keep the curried original on its adapter (#5191) ([bfb4a2a4](https://github.com/fable-compiler/Fable/commit/bfb4a2a4e3bb1b5bd4491955a1e1cb90c14050b6)) +* *(js/ts)* Walk arrays by index when enumerated as seq (#5192) ([d88621c3](https://github.com/fable-compiler/Fable/commit/d88621c3b9d6ce48a72dc09b86a6805f612ac185)) +* *(js/ts)* Throw invalidArg and invalidOp at the call site (#5194) ([e6a742b1](https://github.com/fable-compiler/Fable/commit/e6a742b1ebda71342d4445705ea9198c2059ae90)) +* *(js/ts)* Hash unions without allocating (#5195) ([2b66a5d2](https://github.com/fable-compiler/Fable/commit/2b66a5d2184f3a35f0d73fb397ab902e300ac4bd)) +* *(js/ts)* Probe dictionary buckets without allocating (#5196) ([9517eafd](https://github.com/fable-compiler/Fable/commit/9517eafd93aa7b84061662a421b7a39a7269318b)) +* *(js/ts)* Pass array literals to typed array constructors as plain arrays (#5209) ([cb7d6d6e](https://github.com/fable-compiler/Fable/commit/cb7d6d6e49bb43b83afe80477ea2d6d667be3f5b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + +## 5.23.0 - 2026-10-22 + +### 🚀 Features + +* *(all)* Support suppressing warnings via `// fable-disable` comments (#4808) ([f4a1a8cd](https://github.com/fable-compiler/Fable/commit/f4a1a8cdae2e8ac81edbdc6eba36cc140234170b)) +* *(all)* Warn on duplicate CompiledName on union cases ([2098f64a](https://github.com/fable-compiler/Fable/commit/2098f64a25e96385306ef19ed8c289f3b6c84db9)) +* *(js/ts)* Map static Decimal conversion methods (#5129) ([cd63e57e](https://github.com/fable-compiler/Fable/commit/cd63e57e909393ab493a1d8bfedbea9a50a377c9)) +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +### 🐞 Bug Fixes + +* *(all)* Kill --run process on SIGTERM everywhere (#5140) ([09158b05](https://github.com/fable-compiler/Fable/commit/09158b05acf4c35b54111f4f1d72b96930bce18e)) +* *(js/ts)* Round decimals below 0.1 correctly ([fca4c408](https://github.com/fable-compiler/Fable/commit/fca4c408a64e33c58293f12869975e5b61bcb96a)) +* *(js/ts)* Keep decimal zero scale without leading zeros ([3076b45a](https://github.com/fable-compiler/Fable/commit/3076b45acacf39b8c99ec57025560ad355ab74db)) +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Keep .NET decimal scale through arithmetic (#5126) ([bdf0a6c8](https://github.com/fable-compiler/Fable/commit/bdf0a6c8cb56151a5431b5e3703f7ee165b26dba)) +* *(js/ts)* Warn on type test against Pojo class (#5128) ([798df101](https://github.com/fable-compiler/Fable/commit/798df10179576a28f602fad58e204881792a0aef)) +* *(js/ts)* Accept decimal integers up to 2^96 ([c9feb339](https://github.com/fable-compiler/Fable/commit/c9feb339fb8a4e98bebf17b3facdc42ecd22d628)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Respect maxDegreeOfParallelism and cancel on failure in Async.Parallel (#5098) ([5de3271d](https://github.com/fable-compiler/Fable/commit/5de3271d8b47f1752c4e74c010e4314cf40bbcbe)) +* *(js/ts)* Inline interface cases of TypeScriptTaggedUnion (#5146) ([2be66611](https://github.com/fable-compiler/Fable/commit/2be66611d884cd564fa3f7b06fd4a015add1cefd)) + +### ⚡ Performance Improvements + +* *(all)* Faster project cracking (#5136) ([ba042783](https://github.com/fable-compiler/Fable/commit/ba0427839107c157436479958898688952df05c3)) +* *(all)* Disable .NET telemetry for dotnet build (#5137) ([e9ffc41e](https://github.com/fable-compiler/Fable/commit/e9ffc41e5f53df018d4c141b9bfaa51ca76e0be5)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + +## 5.22.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +### 🐞 Bug Fixes + +* *(standalone)* Warn and skip plugins instead of throwing (#5114) ([021ab65c](https://github.com/fable-compiler/Fable/commit/021ab65c6d3adfe57cffb9f5ba6f595b034ac8bd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..5ee8bbb832d9677ad702104ded8e00c09619393f) + +## 5.21.0 - 2026-10-16 + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) + +### 🐞 Bug Fixes + +* *(all)* Recompile dependents on signature change in watch (#5084) ([87fc89f7](https://github.com/fable-compiler/Fable/commit/87fc89f75b6ad22fafec72b4a57415682b4059df)) +* *(all)* Forward `--targetprofile` for precompile (#5085) ([50129598](https://github.com/fable-compiler/Fable/commit/50129598a13d4ab7a6e50ae1be0d9aa9d3c68ef8)) +* *(beam)* Make lazy sequence wrappers process-portable (#5063) ([9e78a3b3](https://github.com/fable-compiler/Fable/commit/9e78a3b327ca78d01863bf19b18c543673633af7)) +* *(js/ts)* Avoid full array copy in Encoding.GetString (#5069) ([e6b72ec7](https://github.com/fable-compiler/Fable/commit/e6b72ec73be454bd3b3233167542a65fc6553c8a)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) +* *(python)* Settle cancelled tasks awaited by async workflows (#5064) ([52186937](https://github.com/fable-compiler/Fable/commit/5218693738defc8a4502d28c206e391b241f9fd8)) +* *(python)* Preserve reusable replies and unit continuations (#5082) ([f19e2c56](https://github.com/fable-compiler/Fable/commit/f19e2c56566488dfeb48e71508bdb218aaa58582)) +* *(python)* Emit widening integer casts (#5087) ([07989b16](https://github.com/fable-compiler/Fable/commit/07989b16214c4fcf8e33318fba1dd3f77df77cbb)) +* *(rust)* Support inref struct unions (#5060) ([a3f27e0e](https://github.com/fable-compiler/Fable/commit/a3f27e0e8c149d6bfeb347ca4f4146ccfd8fb063)) + +### ⚡ Performance Improvements + +* Crack project references in parallel (#5079) ([d42aa6b9](https://github.com/fable-compiler/Fable/commit/d42aa6b926ac531aba7069aea408009f96584ae5)) +* Update inline expressions incrementally in watch mode (#5080) ([594d17b4](https://github.com/fable-compiler/Fable/commit/594d17b4b551ae33b5f2df5e4049611b9dd24bc2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..e33c57870454fea189a3eb33519f583a5003f8c3) + +## 5.20.0 - 2026-10-12 + +### 🚀 Features + +* *(php)* System.Environment (#4782) ([a8661874](https://github.com/fable-compiler/Fable/commit/a8661874d60694f0faad235adb7f760fed6f72a5)) + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) +* *(beam)* Preserve async cancellation and finalize resources once (#5057) ([617f7111](https://github.com/fable-compiler/Fable/commit/617f7111bf02f8536b321b4373bdffa7a0f7d633)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..a8661874d60694f0faad235adb7f760fed6f72a5) + +## 5.19.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(beam)* Disambiguate property accessors from ordinary methods (#5049) ([b30dc89d](https://github.com/fable-compiler/Fable/commit/b30dc89d0091c2bd1d9fd7a20042b7fbb4e5a3fa)) +* *(beam)* Normalize exception messages and string split separators (#5053) ([f13a9e14](https://github.com/fable-compiler/Fable/commit/f13a9e1494d8bbde8e74bfbf9dfbc8bd051a157e)) +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Preserve JS/TS generic unit arguments and Python unit continuations (#5037) ([e05bc752](https://github.com/fable-compiler/Fable/commit/e05bc752f1d8f16f08e2409a191d9c33463a1f04)) +* *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159)) +* *(rust)* Fix tailcall elimination for members (#5051) ([4eb21772](https://github.com/fable-compiler/Fable/commit/4eb2177263f544ec354076da07d8a097d56c273e)) +* *(rust)* Fix temporary struct locals type (#5052) ([17b36588](https://github.com/fable-compiler/Fable/commit/17b3658876a3bd776c4a0129ec3a4cddc9d7e05d)) +* *(rust)* Add main function for no_std (#5054) ([10fdccc0](https://github.com/fable-compiler/Fable/commit/10fdccc04bb0e421643426d21dfcbd940e3e5a16)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/23bc5c54db51469431e6fdab713dfe1b6a3df019..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + +## 5.19.0 - 2026-10-07 + +### 🚀 Features + +* *(python)* Give System exceptions native Python bases (#5040) ([4ee31586](https://github.com/fable-compiler/Fable/commit/4ee31586fd88ed0708e218e7f170432baa33551e)) + +### 🐞 Bug Fixes + +* *(beam)* Recognize boxed discriminated unions in type tests (#5033) ([dcfaa3d6](https://github.com/fable-compiler/Fable/commit/dcfaa3d6181f6789d0868e0bf9132ecdcdc76287)) +* *(beam)* Prevent identifier collisions and atom overflow (#5017) ([b9ce2fdb](https://github.com/fable-compiler/Fable/commit/b9ce2fdb53fc86a615970f4828005ee3a490385f)) +* *(python)* Share per-object locks and align StartChild timeouts (#5035) ([2da7144c](https://github.com/fable-compiler/Fable/commit/2da7144c68efded840fc6585c23d3a4e28e8e37f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/aaf35f200be5bbc2698abe242f77b8de745e4337..23bc5c54db51469431e6fdab713dfe1b6a3df019) + +## 5.18.0 - 2026-10-03 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(all)* Minor type const strings cleanup (#5029) ([9b69e5be](https://github.com/fable-compiler/Fable/commit/9b69e5be8a2cffc3cbcb3920bf9924b72ed385d6)) +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Use common null storage (#4995) ([ae165dd0](https://github.com/fable-compiler/Fable/commit/ae165dd0cf7248e22c3520d1e00347d0713d0526)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) +* *(rust)* Remove trailing comma from macro args (#5031) ([e6e9f54c](https://github.com/fable-compiler/Fable/commit/e6e9f54c905750a1bf78ec454bca7d83cdffb61e)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..aaf35f200be5bbc2698abe242f77b8de745e4337) + +## 5.17.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +### 🐞 Bug Fixes + +* Retry MSBuild project cracking serially (#4991) ([b0c6129e](https://github.com/fable-compiler/Fable/commit/b0c6129e146318701bd723488af6cf8e3d4bdf9b)) +* *(beam)* Preserve local spawn calls (#4992) ([70278ec6](https://github.com/fable-compiler/Fable/commit/70278ec636b7f948a6ba3d215b36a3974de89edc)) +* *(dart)* Prefix file scope names shadowed by class members (#4982) ([ece7b071](https://github.com/fable-compiler/Fable/commit/ece7b0714eae18650124cad017b0bda4da74ebc8)) +* *(js/ts)* Fix Dictionary.Remove and HashSet.Remove to remove empty buckets (#4985) ([aea61b56](https://github.com/fable-compiler/Fable/commit/aea61b562c461a9b383e805f06f68a3b21ed45c2)) +* *(js/ts)* Clear custom System.Collections.IList implementations ([2369fbd2](https://github.com/fable-compiler/Fable/commit/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36)) +* *(python)* Keep the payload type when reading an erased union field (#4981) ([e0536ecd](https://github.com/fable-compiler/Fable/commit/e0536ecd9b9aeb12cc96c7a2ef2edde7089e2dd0)) +* *(rust)* Less allocations when enumerating strings (#4970) ([5d62380c](https://github.com/fable-compiler/Fable/commit/5d62380cb30dc1080c6943445a256e27db6246f8)) +* *(rust)* Remove reflection registration on boxing (#4983) ([50a37f78](https://github.com/fable-compiler/Fable/commit/50a37f78f31394d61349b09a025c1713529d8ec0)) + +### ⚡ Performance Improvements + +* *(js/ts)* Faster `resizeArray.Clear()` in JS/TS ([32d85add](https://github.com/fable-compiler/Fable/commit/32d85add01422547dba03b15e0a0787d8afb031b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/66efd10960c967660084db2b364e944d167897f4..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + +## 5.16.2 - 2026-09-12 + +### 🐞 Bug Fixes + +* *(python)* Use the .NET range for Decimal.MaxValue and Decimal.MinValue (#4967) ([66efd109](https://github.com/fable-compiler/Fable/commit/66efd10960c967660084db2b364e944d167897f4)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..66efd10960c967660084db2b364e944d167897f4) + +## 5.16.1 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(js/ts)* Keep the sign of negative decimals below 0.1 ([91d80f18](https://github.com/fable-compiler/Fable/commit/91d80f185fb38e4d23fd9e906037a14daab3e3b9)) +* *(js/ts)* Respect the precision when formatting decimals below 0.1 ([1c02557e](https://github.com/fable-compiler/Fable/commit/1c02557efd113d8d30a922c74c3fc9d1e7168cfd)) +* *(js/ts)* Omit the decimal sign when the rounded result is zero ([ef63087d](https://github.com/fable-compiler/Fable/commit/ef63087d97b61927763023c4b2e5bb16bcd64af2)) +* *(python)* Preserve record field names in structured formatting (#4952) ([fbc162d6](https://github.com/fable-compiler/Fable/commit/fbc162d6912d62b9a75e73f8264814f6519b6194)) +* *(python)* Support bytes() on generic and bool arrays (#4961) ([94b59294](https://github.com/fable-compiler/Fable/commit/94b59294b4749c1f871484b843604da81ceb71ad)) +* *(python)* Parse uint64 values above Int64.MaxValue ([584d8ce4](https://github.com/fable-compiler/Fable/commit/584d8ce4f6d434df56195ed6d09d6336df1970d6)) +* *(python)* Report DateTimeKind.Utc for DateTime.UtcNow ([4abf06ea](https://github.com/fable-compiler/Fable/commit/4abf06ea86186f64a15078fb19c6006cca0676b0)) +* *(python)* Reflect an erased union as the type it erases to ([0f564c74](https://github.com/fable-compiler/Fable/commit/0f564c744e86096c779d0acea6d677bce6d56ee1)) +* *(python)* Implement Decimal.GetBits and the bits constructors ([cf0c9305](https://github.com/fable-compiler/Fable/commit/cf0c9305759af17fde6dcd433618bbeaca5b60d0)) +* *(python)* Round a float to .NET's precision when converting to decimal ([4a441b19](https://github.com/fable-compiler/Fable/commit/4a441b1926387a42db6a3a4294acd97b02760ca3)) +* *(ts)* Declare the type parameter of a flexible-type overload ([6f1f1b8a](https://github.com/fable-compiler/Fable/commit/6f1f1b8afd9158743978720507ced5c4a8b2144f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + +## 5.16.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Match .NET for G, E and custom numeric formats ([6fb03090](https://github.com/fable-compiler/Fable/commit/6fb03090ce328b8c5d6a36422f120dd641aefe3a)) +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) +* *(python)* Build typed arrays from lists and sequences (#4944) ([5af37779](https://github.com/fable-compiler/Fable/commit/5af37779ce33c1e9036df7a9548830c4f914bdd5)) +* *(rust)* Fix IEnumerable casting, interop and type testing (#4946) ([93c2df5b](https://github.com/fable-compiler/Fable/commit/93c2df5ba2b3316893dc85dc4fd746a09a07bf39)) +* *(rust)* Restore missing bindings (#4947) ([2fb46a26](https://github.com/fable-compiler/Fable/commit/2fb46a269bc4827494cab878f6e3f0e1b4052035)) +* *(ts)* Declare local function type params only reachable via constraints (#4943) ([8aa6d3b9](https://github.com/fable-compiler/Fable/commit/8aa6d3b9e9f3c1dc013d67406878ea93637ad74c)) +* *(ts/python)* Erase byref from struct copy-and-update locals (#4945) ([19a6522e](https://github.com/fable-compiler/Fable/commit/19a6522ea517db53dabd5e221206849d382e1025)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + +## 5.15.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) +* *(python)* Support quotation derived patterns (#4928) ([f6d939a1](https://github.com/fable-compiler/Fable/commit/f6d939a14c67010cb6cc02c7994d6f56c7156dab)) +* *(rust)* Observables, Events and IEnumerable (PR 6 of 7 Rust improvements) (#4789) ([20175eea](https://github.com/fable-compiler/Fable/commit/20175eea841adf14ed57c0068458fad591c8d5f4)) +* *(rust)* System.Uri (PR 7 of 7 Rust improvements) (#4790) ([8a551bc9](https://github.com/fable-compiler/Fable/commit/8a551bc9b2433035fc704dffc639e5f031ca7593)) + +### 🐞 Bug Fixes + +* Preserve project FSharp.Core reference (#4924) ([1289360a](https://github.com/fable-compiler/Fable/commit/1289360a11398387712df2769069b9c949659966)) +* Report Fable's own logs from the Fable.Compiler API (#4923) ([0775c604](https://github.com/fable-compiler/Fable/commit/0775c604d319d230796da0d47817428b1da8fece)) +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) +* *(all)* Allow private inline functions to use private values (#4915) ([b0740ba9](https://github.com/fable-compiler/Fable/commit/b0740ba95859a80e7add07f21e2fc81a31c00853)) +* *(python)* Emit Python names for float32 specials (#4938) ([b4c45a93](https://github.com/fable-compiler/Fable/commit/b4c45a93385f249a8586789037b0af18005786e9)) +* *(python)* Compare nested options of different depth (#4940) ([5afa2948](https://github.com/fable-compiler/Fable/commit/5afa2948edd221b64aa14553509ba96028d064c5)) +* *(rust)* Observables and Events cleanup (#4908) ([ca165045](https://github.com/fable-compiler/Fable/commit/ca165045703797f0558da4bc6e06cbea7af231de)) +* *(rust)* Support `string` on a tuple (#4916) ([510c8a43](https://github.com/fable-compiler/Fable/commit/510c8a43c159ffb9d7d7bdc4e2ee16b84b80b28e)) +* *(rust)* Decimal culture overloads, and mutables assigned in try/finally (#4918) ([58402471](https://github.com/fable-compiler/Fable/commit/584024712450d333193baddc239db977af05bd54)) +* *(rust)* Let a string be enumerated (#4921) ([395c152b](https://github.com/fable-compiler/Fable/commit/395c152bd4ef0c26af7a4ff48ed44c728aa73d25)) +* *(rust)* Async perf: stop the async runtime sleeping, and add Parallel (#4920) ([88135b90](https://github.com/fable-compiler/Fable/commit/88135b90382f63bd40d58dfc75c4f915cc4bb868)) +* *(rust)* F# exceptions, non-exhaustive matches and obj type tests (#4917) ([28da1d76](https://github.com/fable-compiler/Fable/commit/28da1d764e63c39b96ca20e94d9a08786b3c9052)) +* *(rust)* Cleanup AsyncBuilder and TaskBuilder (#4936) ([3e6bc9bd](https://github.com/fable-compiler/Fable/commit/3e6bc9bd4d4337e901cc748f35df2a5933f0ba55)) +* *(rust)* Fix interface casting, move reflection into declarations (#4939) ([04299157](https://github.com/fable-compiler/Fable/commit/042991572969e1864a160b0a0145b1d59bc77236)) +* *(rust)* Support reflection for no_std (#4941) ([bf694309](https://github.com/fable-compiler/Fable/commit/bf694309ae5dbc6bb546d9acc4d091f5e35fe330)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b517d52df145ee30d1d3bd7e20fded1a9003ac54..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 5.14.1 - 2026-08-25 ### 🐞 Bug Fixes diff --git a/src/Fable.Compiler/Fable.Compiler.fsproj b/src/Fable.Compiler/Fable.Compiler.fsproj index 2410db69d8..c9a64ab38c 100644 --- a/src/Fable.Compiler/Fable.Compiler.fsproj +++ b/src/Fable.Compiler/Fable.Compiler.fsproj @@ -41,9 +41,9 @@ all - - - + + + diff --git a/src/Fable.Compiler/Library.fs b/src/Fable.Compiler/Library.fs index e7b6c7c6df..8f6e87daf6 100644 --- a/src/Fable.Compiler/Library.fs +++ b/src/Fable.Compiler/Library.fs @@ -17,12 +17,24 @@ type TypeCheckProjectResult = ProjectCheckResults: FSharpCheckProjectResults } +type FableASTResult = + { + /// The current file transformed into the Fable AST + FableAST: AST.Fable.File + /// Everything the compiler reported: the diagnostics of the F# project as checked up to + /// the current file, tagged "FSHARP", followed by the logs Fable raised while + /// transforming that file, tagged "FABLE". + Logs: LogEntry array + } + type CompileResult = { /// A map of absolute file path to transpiled JavaScript code CompiledFiles: Map - /// Diagnostics of the entire checked F# project - Diagnostics: FSharpDiagnostic array + /// Everything the compiler reported: the diagnostics of the entire checked F# project, + /// tagged "FSHARP", followed by the logs Fable raised while translating the compiled + /// files, tagged "FABLE". + Logs: LogEntry array } type BabelWriter @@ -94,6 +106,50 @@ type BabelWriter module CodeServices = + // FCS keeps the first spelling registered for a file and drops diagnostics whose file name differs from it + let checkerFileName (fileName: string) = + (FSharp.Compiler.Text.Range.mkFirstLineOfFile fileName).FileName + + let checkerFileNames (fileNames: string[]) (sourceReader: SourceReader) = + let checkerNames = Array.map checkerFileName fileNames + let originalNames = Array.zip checkerNames fileNames |> Map.ofArray + + let checkerSourceReader fileName = + match Map.tryFind fileName originalNames with + | Some originalName -> sourceReader originalName + | None -> sourceReader fileName + + checkerNames, checkerSourceReader + + let getFSharpDiagnostics (diagnostics: FSharpDiagnostic array) = + diagnostics + |> Array.map (fun er -> + let severity = + match er.Severity with + | FSharpDiagnosticSeverity.Hidden + | FSharpDiagnosticSeverity.Info -> Severity.Info + | FSharpDiagnosticSeverity.Warning -> Severity.Warning + | FSharpDiagnosticSeverity.Error -> Severity.Error + + let range = + AST.SourceLocation.Create( + start = + { + line = er.StartLine + column = er.StartColumn + 1 + }, + ``end`` = + { + line = er.EndLine + column = er.EndColumn + 1 + } + ) + + let msg = $"%s{er.Message} (code %i{er.ErrorNumber})" + + LogEntry.Make(severity, msg, fileName = Path.normalizePath er.FileName, range = range, tag = "FSHARP") + ) + let compileFileToJs (com: Compiler) (pathResolver: PathResolver) @@ -125,12 +181,11 @@ module CodeServices = async { let! assemblies = checker.GetImportedAssemblies() + let fileNames, checkerSourceReader = + checkerFileNames crackerResponse.ProjectOptions.SourceFiles sourceReader + let! checkProjectResult = - checker.ParseAndCheckProject( - cliArgs.ProjectFile, - crackerResponse.ProjectOptions.SourceFiles, - sourceReader - ) + checker.ParseAndCheckProject(cliArgs.ProjectFile, fileNames, checkerSourceReader) return { @@ -145,18 +200,21 @@ module CodeServices = (cliArgs: CliArgs) (crackerResponse: CrackerResponse) (currentFile: string) - : Async + : Async = async { let! assemblies = checker.GetImportedAssemblies() + let fileNames, checkerSourceReader = + checkerFileNames crackerResponse.ProjectOptions.SourceFiles sourceReader + // Type-check the project up until the current file. let! checkProjectResult = checker.ParseAndCheckProject( cliArgs.ProjectFile, - crackerResponse.ProjectOptions.SourceFiles, - sourceReader, - lastFile = currentFile + fileNames, + checkerSourceReader, + lastFile = checkerFileName currentFile ) let fableProj = @@ -173,22 +231,31 @@ module CodeServices = let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir - let compiler: Compiler = + let compiler: CompilerImpl = CompilerImpl( currentFile, fableProj, opts, fableLibDir, crackerResponse.OutputType, - ?outDir = cliArgs.OutDir + ?outDir = cliArgs.OutDir, + warningSuppression = + WarningSuppression.Resolver.FromCompilerOptions( + crackerResponse.ProjectOptions.OtherOptions, + sourceReader + ) ) // TODO: make it configurable if FableTransforms.transformFile is applied? let fableAST = - FSharp2Fable.Compiler.transformFile compiler - |> FableTransforms.transformFile compiler + FSharp2Fable.Compiler.transformFile (compiler :> Compiler) + |> FableTransforms.transformFile (compiler :> Compiler) - return fableAST + return + { + FableAST = fableAST + Logs = Array.append (getFSharpDiagnostics checkProjectResult.Diagnostics) compiler.Logs + } } let compileMultipleFilesToJavaScript @@ -214,13 +281,16 @@ module CodeServices = let opts = cliArgs.CompilerOptions - let! compiledFiles = + let! results = inputFiles |> Seq.map (fun currentFile -> async { let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir - let compiler: Compiler = + // No `warningSuppression`: this overload is handed an already type-checked + // project rather than a `SourceReader`, so there is no source to scan for + // `// fable-disable` comments. Not a bug, just a limitation of the entry point. + let compiler: CompilerImpl = CompilerImpl( currentFile, fableProj, @@ -234,17 +304,28 @@ module CodeServices = Path.ChangeExtension(currentFile, cliArgs.CompilerOptions.FileExtension) let! js = - compileFileToJs compiler pathResolver outputPath cliArgs.CompilerOptions.FileExtension - - return currentFile, js + compileFileToJs + (compiler :> Compiler) + pathResolver + outputPath + cliArgs.CompilerOptions.FileExtension + + // `AddLog` is not thread-safe, so every file keeps its own compiler and + // the logs are gathered once all files are done. + return (currentFile, js), compiler.Logs } ) |> Async.Parallel + let compiledFiles, fableLogs = Array.unzip results + return { CompiledFiles = Map.ofArray compiledFiles - Diagnostics = typeCheckProjectResult.ProjectCheckResults.Diagnostics + Logs = + Array.append + (getFSharpDiagnostics typeCheckProjectResult.ProjectCheckResults.Diagnostics) + (Array.concat fableLogs) } } @@ -262,12 +343,22 @@ module CodeServices = crackerResponse.ProjectOptions.SourceFiles |> Array.tryFind (fun f -> f = String.Concat(currentFile, "i")) + let fileNames, checkerSourceReader = + checkerFileNames crackerResponse.ProjectOptions.SourceFiles sourceReader + + let originalFileNames = + Array.zip fileNames crackerResponse.ProjectOptions.SourceFiles |> Map.ofArray + let! dependentFiles = - match signatureFile with - | None -> - checker.GetDependentFiles(currentFile, crackerResponse.ProjectOptions.SourceFiles, sourceReader) - | Some signatureFile -> - checker.GetDependentFiles(signatureFile, crackerResponse.ProjectOptions.SourceFiles, sourceReader) + checker.GetDependentFiles( + checkerFileName (Option.defaultValue currentFile signatureFile), + fileNames, + checkerSourceReader + ) + + let dependentFiles = + dependentFiles + |> Array.map (fun f -> Map.tryFind f originalFileNames |> Option.defaultValue f) Log.info ( sprintf @@ -288,9 +379,9 @@ module CodeServices = let! checkProjectResult = checker.ParseAndCheckProject( cliArgs.ProjectFile, - crackerResponse.ProjectOptions.SourceFiles, - sourceReader, - lastFile = lastFile + fileNames, + checkerSourceReader, + lastFile = checkerFileName lastFile ) let fableProj = @@ -305,36 +396,51 @@ module CodeServices = let opts = cliArgs.CompilerOptions - let! compiledFiles = + let warningSuppression = + WarningSuppression.Resolver.FromCompilerOptions( + crackerResponse.ProjectOptions.OtherOptions, + sourceReader + ) + + let! results = dependentFiles |> Array.filter (fun filePath -> not (filePath.EndsWith(".fsi", StringComparison.Ordinal))) |> Array.map (fun currentFile -> async { let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir - let compiler: Compiler = + let compiler: CompilerImpl = CompilerImpl( currentFile, fableProj, opts, fableLibDir, crackerResponse.OutputType, - ?outDir = cliArgs.OutDir + ?outDir = cliArgs.OutDir, + warningSuppression = warningSuppression ) let outputPath = Path.ChangeExtension(currentFile, ".js") let! js = - compileFileToJs compiler pathResolver outputPath cliArgs.CompilerOptions.FileExtension - - return currentFile, js + compileFileToJs + (compiler :> Compiler) + pathResolver + outputPath + cliArgs.CompilerOptions.FileExtension + + // `AddLog` is not thread-safe, so every file keeps its own compiler and + // the logs are gathered once all files are done. + return (currentFile, js), compiler.Logs } ) |> Async.Parallel + let compiledFiles, fableLogs = Array.unzip results + return { CompiledFiles = Map.ofArray compiledFiles - Diagnostics = checkProjectResult.Diagnostics + Logs = Array.append (getFSharpDiagnostics checkProjectResult.Diagnostics) (Array.concat fableLogs) } } diff --git a/src/Fable.Compiler/Library.fsi b/src/Fable.Compiler/Library.fsi index ed852b8a28..025434d8aa 100644 --- a/src/Fable.Compiler/Library.fsi +++ b/src/Fable.Compiler/Library.fsi @@ -4,15 +4,28 @@ open FSharp.Compiler.SourceCodeServices open FSharp.Compiler.Diagnostics open FSharp.Compiler.CodeAnalysis open Fable +open Fable.Transforms.State open Fable.Compiler.Util open Fable.Compiler.ProjectCracker +type FableASTResult = + { + /// The current file transformed into the Fable AST + FableAST: AST.Fable.File + /// Everything the compiler reported: the diagnostics of the F# project as checked up to + /// the current file, tagged "FSHARP", followed by the logs Fable raised while + /// transforming that file, tagged "FABLE". + Logs: LogEntry array + } + type CompileResult = { /// A map of absolute file path to transpiled JavaScript CompiledFiles: Map - /// Diagnostics of the entire checked F# project - Diagnostics: FSharpDiagnostic array + /// Everything the compiler reported: the diagnostics of the entire checked F# project, + /// tagged "FSHARP", followed by the logs Fable raised while translating the compiled + /// files, tagged "FABLE". + Logs: LogEntry array } type TypeCheckProjectResult = @@ -24,6 +37,16 @@ type TypeCheckProjectResult = [] module CodeServices = + /// The spelling the F# checker uses for a file, which is the one its diagnostics carry. + val checkerFileName: fileName: string -> string + + /// The file names as the F# checker spells them, with a source reader that still receives the names given here. + val checkerFileNames: fileNames: string[] -> sourceReader: SourceReader -> string[] * SourceReader + + /// Convert the diagnostics of the F# compiler into log entries tagged "FSHARP". + /// The error number is folded into the message, as `Fable.Cli` prints it. + val getFSharpDiagnostics: diagnostics: FSharpDiagnostic array -> LogEntry array + /// Type check a project using the InteractiveChecker val typeCheckProject: sourceReader: SourceReader -> @@ -39,7 +62,7 @@ module CodeServices = cliArgs: CliArgs -> crackerResponse: CrackerResponse -> currentFile: string -> - Async + Async /// And compile multiple files of a project to JavaScript. /// The expected usage of this function is either every file in the project or only the user files. diff --git a/src/Fable.Compiler/MSBuildCrackerResolver.fs b/src/Fable.Compiler/MSBuildCrackerResolver.fs index ee81a3e976..52d95b3d3d 100644 --- a/src/Fable.Compiler/MSBuildCrackerResolver.fs +++ b/src/Fable.Compiler/MSBuildCrackerResolver.fs @@ -2,11 +2,13 @@ namespace Fable.Compiler open System open System.Reflection +open System.Collections.Concurrent open System.IO open System.Diagnostics open System.Text.Json open Fable open Fable.AST +open Fable.Compiler.Util open Fable.Compiler.ProjectCracker module private MSBuildCrackerResolver = @@ -28,13 +30,7 @@ module private MSBuildCrackerResolver = type FullPath = string - let private dotnet_msbuild_with_defines - (cwd: string) - (fsproj: FullPath) - (args: string) - (defines: string list) - : Async - = + let private run_dotnet_msbuild (cwd: string) (fsproj: FullPath) (args: string) (defines: string list) = backgroundTask { let psi = ProcessStartInfo "dotnet" @@ -53,6 +49,9 @@ module private MSBuildCrackerResolver = // https://github.com/fable-compiler/Fable/issues/4014 psi.Environment.Add("DOTNET_NOLOGO", "1") + for key, value in Fable.Compiler.Util.Process.dotnetEnvVars () do + psi.Environment[key] <- value + if not (List.isEmpty defines) then let definesValue = defines |> String.concat ";" psi.Environment.Add("DefineConstants", definesValue) @@ -64,40 +63,63 @@ module private MSBuildCrackerResolver = let error = ps.StandardError.ReadToEnd() do! ps.WaitForExitAsync() - let fullCommand = $"dotnet %s{psi.Arguments}" - - if ps.ExitCode <> 0 then - failwithf $"In %s{psi.WorkingDirectory}:\n%s{fullCommand}\nfailed with\n%s{error}" - - return output.Trim() + return + {| + ExitCode = ps.ExitCode + Output = output.Trim() + Error = error.Trim() + Command = $"dotnet %s{psi.Arguments}" + |} } |> Async.AwaitTask + let private dotnet_msbuild_with_defines cwd fsproj args defines = + // MSBuild can fail without diagnostics in some environments when building project references in parallel, + // so retry this opaque failure once with BuildInParallel disabled. + let rec run retrySerially args = + async { + let! result = run_dotnet_msbuild cwd fsproj args defines + + if result.ExitCode = 0 then + return result.Output + elif + retrySerially + && String.IsNullOrWhiteSpace result.Output + && not (result.Error.Contains(": error ", StringComparison.OrdinalIgnoreCase)) + && result.Error.Contains( + "Build failed. Properties, Items, and Target results cannot be obtained.", + StringComparison.Ordinal + ) + then + Log.warning "MSBuild project evaluation failed without diagnostics. Retrying serially." + return! run false $"%s{args} /p:BuildInParallel=false" + else + return + $"In %s{cwd}:\n%s{result.Command}\nfailed with exit code %i{result.ExitCode}\n%s{result.Error}" + |> Fable.FableError + |> raise + } + + run true args + let private dotnet_msbuild (cwd: string) (fsproj: FullPath) (args: string) : Async = dotnet_msbuild_with_defines cwd fsproj args List.empty - let mkOptionsFromDesignTimeBuildAux (fsproj: FileInfo) (options: CrackerOptions) : Async = + let private queryTargetFramework (fsproj: FileInfo) (options: CrackerOptions) (configurationProperty: string) = async { - let configurationProperty = - if String.IsNullOrWhiteSpace options.Configuration then - "" - else - $"/p:Configuration=%s{options.Configuration}" - let! targetFrameworkJson = dotnet_msbuild options.RootDir fsproj.FullName $"%s{configurationProperty} --getProperty:TargetFrameworks --getProperty:TargetFramework" - let targetFramework = - let properties = - try - JsonDocument.Parse targetFrameworkJson - |> fun json -> json.RootElement.GetProperty "Properties" - with ex -> - failwith - $"""Failed to parse MSBuild output + let properties = + try + JsonDocument.Parse targetFrameworkJson + |> fun json -> json.RootElement.GetProperty "Properties" + with ex -> + failwith + $"""Failed to parse MSBuild output JSON: %s{targetFrameworkJson} @@ -105,35 +127,118 @@ Exception: %O{ex} """ - let tf, tfs = - properties.GetProperty("TargetFramework").GetString(), - properties.GetProperty("TargetFrameworks").GetString() + let tf, tfs = + properties.GetProperty("TargetFramework").GetString(), + properties.GetProperty("TargetFrameworks").GetString() - if not (String.IsNullOrWhiteSpace tf) then - tf + if not (String.IsNullOrWhiteSpace tf) then + return tf + else + return tfs.Split ';' |> Array.head + } + + let private restoreInputNames = + [ + "Directory.Build.props" + "Directory.Build.targets" + "Directory.Packages.props" + "nuget.config" + "NuGet.Config" + "NuGet.config" + "global.json" + "packages.lock.json" + "paket.dependencies" + "paket.lock" + "paket.references" + ] + + /// Files that can change the package graph of a project: the ones next to it and in every directory above it + let private restoreInputs (projectFile: string) = + let rec walk (dir: DirectoryInfo) acc = + let acc = + restoreInputNames + |> List.map (fun name -> System.IO.Path.Combine(dir.FullName, name)) + |> List.filter File.Exists + |> List.append acc + + match dir.Parent with + | null -> acc + | parent -> walk parent acc + + walk (FileInfo(projectFile).Directory) [ projectFile ] + + /// Restore is skipped only when the last crack of this project, with the same defines and configuration, + /// left project.assets.json newer than every file the package graph depends on, for it and its references. + let private canSkipRestore (fsproj: FileInfo) (options: CrackerOptions) = + match options.CacheInfo with + | None -> false + | Some cache -> + let sameProject = + cache.ProjectPath = fsproj.FullName + && cache.Configuration = options.Configuration + && set cache.FableOptions.Define = set options.FableOptions.Define + + let assetsOf (project: string) = + FileInfo(System.IO.Path.Combine(System.IO.Path.GetDirectoryName project, "obj", "project.assets.json")) + + let assets = assetsOf fsproj.FullName + + let restoredAfterInputs (project: string) = + File.Exists project + && (assetsOf project).Exists + && restoreInputs project + |> List.forall (fun input -> File.GetLastWriteTimeUtc input < assets.LastWriteTimeUtc) + + sameProject + && assets.Exists + && fsproj.FullName :: cache.References |> List.forall restoredAfterInputs + + let private declaresTargetFrameworks (fsproj: FileInfo) = + try + File.ReadAllText(fsproj.FullName).Contains(" + true + + let mkOptionsFromDesignTimeBuildAux + (referenceTargetFrameworks: ConcurrentDictionary) + (isMain: bool) + (fsproj: FileInfo) + (options: CrackerOptions) + : Async + = + async { + let configurationProperty = + if String.IsNullOrWhiteSpace options.Configuration then + "" else - tfs.Split ';' |> Array.head + $"/p:Configuration=%s{options.Configuration}" + // A single-target project evaluates its own TargetFramework, so the design-time build can report it + // and the separate query is only needed when the project multi-targets. + let! knownTargetFramework = + match referenceTargetFrameworks.TryGetValue fsproj.FullName with + | true, targetFramework -> async.Return(Some targetFramework) + | _ when declaresTargetFrameworks fsproj -> + async { + let! targetFramework = queryTargetFramework fsproj options configurationProperty + return Some targetFramework + } + | _ -> async.Return None // When CoreCompile does not need a rebuild, MSBuild will skip that target and thus will not populate the FscCommandLineArgs items. // To overcome this we want to force a design time build, using the NonExistentFile property helps prevent a cache hit. let nonExistentFile = Path.Combine("__NonExistentSubDir__", "__NonExistentFile__") - let properties = + let properties targetFramework = [ if not (String.IsNullOrWhiteSpace configurationProperty) then configurationProperty - $"/p:TargetFramework=%s{targetFramework}" + match targetFramework with + | Some targetFramework -> $"/p:TargetFramework=%s{targetFramework}" + | None -> () "/p:DesignTimeBuild=True" "/p:SkipCompilerExecution=True" "/p:ProvideCommandLineArgs=True" - // See https://github.com/NuGet/Home/issues/13046 - "/p:RestoreUseStaticGraphEvaluation=False" - // Avoid restoring with an existing lock file - "/p:RestoreLockedMode=false" - "/p:RestorePackagesWithLockFile=false" - // We trick NuGet into believing there is no lock file create, if it does exist it will try and create it. - " /p:NuGetLockFilePath=Fable.lock" // Avoid skipping the CoreCompile target via this property. $"/p:NonExistentFile=\"%s{nonExistentFile}\"" ] @@ -142,19 +247,36 @@ Exception: let targets = "ResolveAssemblyReferencesDesignTime,ResolveProjectReferencesDesignTime,ResolvePackageDependenciesDesignTime,FindReferenceAssembliesForReferences,_GenerateCompileDependencyCache,_ComputeNonExistentFileProperty,BeforeBuild,BeforeCompile,CoreCompile" + // MSBuild ignores the -p properties during restore when -restoreProperty is given, so the restore covers every TargetFramework let restoreArg = - if options.NoRestore then + if options.NoRestore || not isMain then + "" + elif canSkipRestore fsproj options then + Log.verbose (lazy $"Skipping restore of %s{fsproj.Name}: project.assets.json is up to date") "" else - "/restore" - - let arguments = - $"%s{restoreArg} /t:%s{targets} %s{properties} --getItem:FscCommandLineArgs --getItem:ProjectReference --getProperty:OutputType -warnAsMessage:NU1608" - - let! json = - dotnet_msbuild_with_defines options.RootDir fsproj.FullName arguments options.FableOptions.Define - - let jsonDocument = + let restoreProperties = + [ + if not (String.IsNullOrWhiteSpace options.Configuration) then + $"Configuration=%s{options.Configuration}" + // See https://github.com/NuGet/Home/issues/13046 + "RestoreUseStaticGraphEvaluation=False" + // Avoid restoring with an existing lock file + "RestoreLockedMode=false" + "RestorePackagesWithLockFile=false" + // We trick NuGet into believing there is no lock file create, if it does exist it will try and create it. + "NuGetLockFilePath=Fable.lock" + for constant in options.FableOptions.Define do + $"%s{constant}=True" + ] + |> String.concat ";" + + $"/restore -restoreProperty:%s{restoreProperties}" + + let arguments targetFramework = + $"%s{restoreArg} /t:%s{targets} %s{properties targetFramework} --getItem:FscCommandLineArgs --getItem:ProjectReference --getItem:_MSBuildProjectReferenceExistent --getProperty:OutputType --getProperty:TargetFramework --getProperty:TargetFrameworks -warnAsMessage:NU1608" + + let parse (json: string) = try JsonDocument.Parse json with ex -> @@ -167,6 +289,41 @@ Exception: %O{ex} """ + let crack targetFramework = + async { + let! json = + dotnet_msbuild_with_defines + options.RootDir + fsproj.FullName + (arguments targetFramework) + options.FableOptions.Define + + return parse json + } + + let evaluatedTargetFramework (doc: JsonDocument) = + doc.RootElement.GetProperty("Properties").GetProperty("TargetFramework").GetString() + + let! jsonDocument, targetFramework = + async { + match knownTargetFramework with + | Some targetFramework -> + let! doc = crack (Some targetFramework) + return doc, targetFramework + | None -> + // A multi-target project not caught by the fsproj text check fails here or reports no + // framework, and takes the two-call path below. + let! attempt = crack None |> Async.Catch + + match attempt with + | Choice1Of2 doc when not (String.IsNullOrWhiteSpace(evaluatedTargetFramework doc)) -> + return doc, evaluatedTargetFramework doc + | _ -> + let! targetFramework = queryTargetFramework fsproj options configurationProperty + let! doc = crack (Some targetFramework) + return doc, targetFramework + } + let items = jsonDocument.RootElement.GetProperty "Items" let properties = jsonDocument.RootElement.GetProperty "Properties" @@ -192,6 +349,17 @@ Exception: ) |> Seq.toArray + match items.TryGetProperty "_MSBuildProjectReferenceExistent" with + | true, existentReferences -> + for reference in existentReferences.EnumerateArray() do + match reference.TryGetProperty "TargetFrameworks" with + | true, targetFrameworks when not (String.IsNullOrWhiteSpace(targetFrameworks.GetString())) -> + let referencePath = FileInfo(reference.GetProperty("FullPath").GetString()).FullName + + referenceTargetFrameworks[referencePath] <- targetFrameworks.GetString().Split(';').[0] + | _ -> () + | _ -> () + let outputType = properties.GetProperty("OutputType").GetString() return @@ -204,6 +372,7 @@ Exception: } type MSBuildCrackerResolver() = + let referenceTargetFrameworks = ConcurrentDictionary() interface ProjectCrackerResolver with member _.GetProjectOptionsFromProjectFile(isMain, options: CrackerOptions, projectFile) = @@ -214,7 +383,7 @@ type MSBuildCrackerResolver() = // Bad I know... let result = - MSBuildCrackerResolver.mkOptionsFromDesignTimeBuildAux fsproj options + MSBuildCrackerResolver.mkOptionsFromDesignTimeBuildAux referenceTargetFrameworks isMain fsproj options |> Async.RunSynchronously result diff --git a/src/Fable.Compiler/ProjectCracker.fs b/src/Fable.Compiler/ProjectCracker.fs index 24bc8b08e2..69f3a6db1e 100644 --- a/src/Fable.Compiler/ProjectCracker.fs +++ b/src/Fable.Compiler/ProjectCracker.fs @@ -6,6 +6,7 @@ open System open System.Xml.Linq open System.Text.RegularExpressions open System.Collections.Generic +open System.Threading.Tasks open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Text open Fable @@ -113,6 +114,7 @@ type CrackerOptions(cliArgs: CliArgs, evaluateOnly: bool) = getFableModulesFromProject (projDir, cliArgs.OutDir, cliArgs.NoCache, cliArgs.NoGitignore, evaluateOnly) let builtDlls = HashSet() + let pendingDllBuilds = Dictionary>() let cacheInfo = if cliArgs.NoCache then @@ -138,7 +140,10 @@ type CrackerOptions(cliArgs: CliArgs, evaluateOnly: bool) = member _.EvaluateOnly: bool = evaluateOnly member _.BuildDll(normalizedDllPath: string) = - if not (builtDlls.Contains(normalizedDllPath)) then + if + not (builtDlls.Contains(normalizedDllPath)) + && not (pendingDllBuilds.ContainsKey(normalizedDllPath)) + then let projDir = normalizedDllPath.Split('/') |> Array.rev @@ -151,11 +156,24 @@ type CrackerOptions(cliArgs: CliArgs, evaluateOnly: bool) = |> Array.rev |> String.concat "/" - let stdout = - Process.runSyncWithOutput projDir "dotnet" [ "build"; "-c"; cliArgs.Configuration ] - - Log.always stdout - + pendingDllBuilds[normalizedDllPath] <- + Task.Run(fun () -> + Process.runSyncWithOutputEnv + (Process.dotnetEnvVars ()) + projDir + "dotnet" + [ "build"; "-c"; cliArgs.Configuration; "--no-restore" ] + ) + + member _.WaitForBuiltDlls() = + for KeyValue(normalizedDllPath, build) in pendingDllBuilds |> Seq.toArray do + try + build.GetAwaiter().GetResult() |> Log.always + with e -> + let projName = IO.Path.GetFileNameWithoutExtension(normalizedDllPath) + Log.always ("Couldn't build " + projName + ": " + e.Message) + + pendingDllBuilds.Remove(normalizedDllPath) |> ignore builtDlls.Add(normalizedDllPath) |> ignore member _.ResetFableModulesDir() = @@ -449,6 +467,9 @@ let private extractUsefulOptionsAndSources // Forward pathmap from the main project so CallerFilePath respects / elif line.StartsWith("--pathmap:", StringComparison.Ordinal) && isMainProj then accSources, line :: accOptions + // FCS defaults the primary assembly to mscorlib, a facade in the .NET ref pack, which breaks IL-erased union helpers in precompiled assemblies + elif line.StartsWith("--targetprofile:", StringComparison.Ordinal) && isMainProj then + accSources, line :: accOptions elif line.StartsWith("--nowarn", StringComparison.Ordinal) || line.StartsWith("--warnon", StringComparison.Ordinal) @@ -566,15 +587,12 @@ let crackMainProject (resolver: ProjectCrackerResolver) (opts: CrackerOptions) : /// For project references of main project, ignore dll and package references let crackReferenceProject - (resolver: ProjectCrackerResolver) (opts: CrackerOptions) dllRefs (projFile: string) + (projectOptionsResponse: ProjectOptionsResponse) : CrackedFsproj = - let projectOptionsResponse = - resolver.GetProjectOptionsFromProjectFile(false, opts, projFile) - let sourceFiles, otherOpts = Array.foldBack (extractUsefulOptionsAndSources false) projectOptionsResponse.ProjectOptions ([], []) @@ -600,27 +618,60 @@ let getCrackedProjectsFromMainFsproj : CrackedFsproj list * CrackedFsproj = let mainProj = crackMainProject resolver opts + let crackedRefs = Dictionary() + + let rec crackLevel (projFiles: string list) = + let pending = + projFiles |> List.distinct |> List.filter (crackedRefs.ContainsKey >> not) + + if not (List.isEmpty pending) then + let responses = + pending + |> List.chunkBySize Environment.ProcessorCount + |> List.collect (fun chunk -> + chunk + |> List.map (fun projFile -> + Task.Factory.StartNew( + (fun () -> resolver.GetProjectOptionsFromProjectFile(false, opts, projFile)), + TaskCreationOptions.LongRunning + ) + ) + |> List.map _.GetAwaiter().GetResult() + ) - let rec crackProjects (acc: CrackedFsproj list) (projFile: string) = - let crackedFsproj = - match acc |> List.tryFind (fun x -> x.ProjectFile = projFile) with - | None -> crackReferenceProject resolver opts mainProj.DllReferences projFile - | Some crackedFsproj -> crackedFsproj + List.zip pending responses + |> List.collect (fun (projFile, response) -> + let crackedFsproj = + crackReferenceProject opts mainProj.DllReferences projFile response + + crackedRefs[projFile] <- crackedFsproj + crackedFsproj.ProjectReferences + ) + |> crackLevel + + crackLevel mainProj.ProjectReferences + + let rec addProjects (acc: CrackedFsproj list) (projFile: string) = + let crackedFsproj = crackedRefs[projFile] // Add always a reference to the front to preserve compilation order // Duplicated items will be removed later - List.fold crackProjects (crackedFsproj :: acc) crackedFsproj.ProjectReferences + List.fold addProjects (crackedFsproj :: acc) crackedFsproj.ProjectReferences let refProjs = - List.fold crackProjects [] mainProj.ProjectReferences + List.fold addProjects [] mainProj.ProjectReferences |> List.distinctBy (fun x -> x.ProjectFile) refProjs, mainProj let getCrackedProjects (resolver: ProjectCrackerResolver) (opts: CrackerOptions) = - match (Path.GetExtension opts.ProjFile).ToLower() with - | ".fsx" -> [], getProjectOptionsFromScript opts - | ".fsproj" -> getCrackedProjectsFromMainFsproj resolver opts - | s -> failwith $"Unsupported project type: %s{s}" + let crackedProjects = + match (Path.GetExtension opts.ProjFile).ToLower() with + | ".fsx" -> [], getProjectOptionsFromScript opts + | ".fsproj" -> getCrackedProjectsFromMainFsproj resolver opts + | s -> failwith $"Unsupported project type: %s{s}" + + opts.WaitForBuiltDlls() + crackedProjects // It is common for editors with rich editing or 'intellisense' to also be watching the project // file for changes. In some cases that editor will lock the file which can cause fable to @@ -840,6 +891,73 @@ let loadPrecompiledInfo (opts: CrackerOptions) otherOptions sourceFiles = Some info, otherOptions, sourceFiles | None -> None, otherOptions, sourceFiles +/// Transitive `` targets plus the implicit Directory.Build.props/targets +/// and Directory.Packages.props found upwards. These can change the `` list +/// without touching any .fsproj timestamp, so the cache must invalidate on them too. +let private getProjectImports (projFile: string) : string list = + let imports = ResizeArray() + let visited = HashSet(StringComparer.OrdinalIgnoreCase) + + // Paths built from other properties are skipped below: missing an import costs + // a stale cache, guessing wrong costs a bogus path. + let expandMacros (fileDir: string) (path: string) = + let withSep = fileDir + string IO.Path.DirectorySeparatorChar + + path.Replace("$(MSBuildThisFileDirectory)", withSep).Replace("$(MSBuildProjectDirectory)", withSep) + + let rec collectFrom (file: string) = + // Guard against import cycles and repeated visits + if visited.Add(file) && IO.File.Exists(file) then + let fileDir = IO.Path.GetDirectoryName(file: string) + + let importPaths = + try + XDocument.Load(file).Descendants() + |> Seq.filter (fun el -> el.Name.LocalName = "Import") + |> Seq.choose (fun el -> + match el.Attribute(XName.Get "Project") with + | null -> None + | attr -> Some(expandMacros fileDir attr.Value) + ) + // Unresolved properties or wildcards need a real MSBuild evaluation + |> Seq.filter (fun path -> not (path.Contains("$(") || path.Contains("*"))) + |> Seq.toList + with _ -> + // A malformed or unreadable project is the cracker's problem, not ours + [] + + for importPath in importPaths do + let fullPath = + if IO.Path.IsPathRooted(importPath) then + importPath + else + IO.Path.Combine(fileDir, importPath) + + let fullPath = IO.Path.GetFullPath(fullPath) + + if IO.File.Exists(fullPath) then + imports.Add(fullPath) + collectFrom fullPath + + collectFrom (IO.Path.GetFullPath(projFile)) + + // Implicit imports the SDK adds for every project + let projDir = IO.Path.GetDirectoryName(IO.Path.GetFullPath(projFile)) + + for implicitFile in + [ + "Directory.Build.props" + "Directory.Build.targets" + "Directory.Packages.props" + ] do + match File.tryFindUpwards implicitFile projDir with + | Some path -> + imports.Add(path) + collectFrom path + | None -> () + + List.ofSeq imports + let getFullProjectOpts (resolver: ProjectCrackerResolver) (opts: CrackerOptions) : CrackerResponse = if not (IO.File.Exists(opts.ProjFile)) then Fable.FableError("Project file does not exist: " + opts.ProjFile) |> raise @@ -872,6 +990,10 @@ let getFullProjectOpts (resolver: ProjectCrackerResolver) (opts: CrackerOptions) && cacheInfo.ProjectPath = opts.ProjFile && cacheInfo.FableLib = opts.FableLib && cacheInfo.Replace = opts.Replace + // An imported .props/.targets can add or remove source files with no .fsproj touched + && ([ cacheInfo.ProjectPath; yield! cacheInfo.References ] + |> List.collect getProjectImports + |> List.forall isOlderThanCache) && ([ cacheInfo.ProjectPath; yield! cacheInfo.References ] |> List.forall (fun fsproj -> if IO.File.Exists(fsproj) && isOlderThanCache fsproj then @@ -1002,7 +1124,6 @@ let getFullProjectOpts (resolver: ProjectCrackerResolver) (opts: CrackerOptions) let ignoredRefs = HashSet [ - "FSharp.Core" "WindowsBase" "Microsoft.Win32.Primitives" "Microsoft.VisualBasic" @@ -1027,14 +1148,7 @@ let getFullProjectOpts (resolver: ProjectCrackerResolver) (opts: CrackerOptions) let projRefs = projRefs |> List.map (fun p -> p.ProjectFile) - let otherOptions = - [| - yield! otherOptions - yield! dllRefs - // For some reason, in my tests it seems to work without the FSharp.Core reference - // but we add it just in case - yield "-r:" + typeof>.Assembly.Location - |] + let otherOptions = [| yield! otherOptions; yield! dllRefs |] let outputType = match mainProj.OutputType with diff --git a/src/Fable.Compiler/ProjectCracker.fsi b/src/Fable.Compiler/ProjectCracker.fsi index b742f5b1ed..0ddd4bdd32 100644 --- a/src/Fable.Compiler/ProjectCracker.fsi +++ b/src/Fable.Compiler/ProjectCracker.fsi @@ -48,6 +48,7 @@ type CrackerOptions = member SourceMapsRoot: string option member EvaluateOnly: bool member BuildDll: normalizedDllPath: string -> unit + member WaitForBuiltDlls: unit -> unit type CrackerResponse = { diff --git a/src/Fable.Compiler/Util.fs b/src/Fable.Compiler/Util.fs index 9b938f5cbb..c312778ac3 100644 --- a/src/Fable.Compiler/Util.fs +++ b/src/Fable.Compiler/Util.fs @@ -438,24 +438,54 @@ module Process = if not p.HasExited then p.Kill(entireProcessTree = true) - let startWithEnv envVars = - let mutable runningProcess = None + let mutable private runProcess: Process option = None + + let private killRunProcess () = + runProcess + |> Option.iter (fun p -> + try + kill p + with _ -> + () + ) + + // Terminating Fable doesn't terminate the processes it spawned, so kill them when Fable exits. + // SIGTERM is what IDEs and task runners send, and the runtime only intercepts it when registered. + let private killRunProcessOnExit = + lazy + (AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> killRunProcess ()) + + if isWindows () then + Console.CancelKeyPress.AddHandler(ConsoleCancelEventHandler(fun _ _ -> killRunProcess ())) + + let assemblyLoadContext = + getCurrentAssembly () |> Loader.AssemblyLoadContext.GetLoadContext + + assemblyLoadContext.add_Unloading (fun _ -> killRunProcess ()) - // In Windows, terminating the main process doesn't kill the spawned ones so we need - // to listen for the Console.CancelKeyPress and AssemblyLoadContext.Unloading events - if isWindows () then - Console.CancelKeyPress.AddHandler(ConsoleCancelEventHandler(fun _ _ -> runningProcess |> Option.iter kill)) + Runtime.InteropServices.PosixSignalRegistration.Create( + Runtime.InteropServices.PosixSignal.SIGTERM, + (fun _ -> killRunProcess ()) + )) - let assemblyLoadContext = - getCurrentAssembly () |> Loader.AssemblyLoadContext.GetLoadContext + let private track (p: Process) = + killRunProcessOnExit.Force() |> ignore + runProcess <- Some p - assemblyLoadContext.add_Unloading (fun _ -> runningProcess |> Option.iter kill) + let private untrack (p: Process) = + if runProcess = Some p then + runProcess <- None + let startWithEnv envVars = fun (workingDir: string) (exePath: string) (args: string list) -> try - runningProcess |> Option.iter kill - let p = startProcess false envVars workingDir exePath args - runningProcess <- Some p + runProcess + |> Option.iter (fun p -> + kill p + untrack p + ) + + startProcess false envVars workingDir exePath args |> track with ex -> Log.always ("Cannot run: " + ex.Message) @@ -464,7 +494,9 @@ module Process = let runSyncWithEnv envVars (workingDir: string) (exePath: string) (args: string list) = try let p = startProcess false envVars workingDir exePath args + track p p.WaitForExit() + untrack p p.ExitCode with ex -> Log.always ("Cannot run: " + ex.Message) @@ -474,13 +506,24 @@ module Process = let runSync (workingDir: string) (exePath: string) (args: string list) = runSyncWithEnv [] workingDir exePath args - let runSyncWithOutput workingDir exePath args = - let p = startProcess true [] workingDir exePath args + /// Environment for the `dotnet` processes Fable starts for itself, not for the user's --run commands + let dotnetEnvVars () = + // The dotnet CLI blocks on sending telemetry at exit, which can take seconds on a restricted network + if isNull (Environment.GetEnvironmentVariable "DOTNET_CLI_TELEMETRY_OPTOUT") then + [ "DOTNET_CLI_TELEMETRY_OPTOUT", "1" ] + else + [] + + let runSyncWithOutputEnv envVars workingDir exePath args = + let p = startProcess true envVars workingDir exePath args // Don't wait indefinitely to run process // This call is used to build local plugins, if the binary is used by another process this process will never end. p.WaitForExit 7000 |> ignore p.StandardOutput.ReadToEnd() + let runSyncWithOutput workingDir exePath args = + runSyncWithOutputEnv [] workingDir exePath args + [] module Async = let fold f (state: 'State) (xs: 'T seq) = @@ -637,6 +680,7 @@ module Imports = if isAbsolutePath importPath then if importPath.EndsWith(".fs", StringComparison.Ordinal) + || importPath.EndsWith(".fsx", StringComparison.Ordinal) || importPath.EndsWith(".rs", StringComparison.Ordinal) then getTargetRelativePath pathResolver importPath targetDir projDir outDir diff --git a/src/Fable.Compiler/Util.fsi b/src/Fable.Compiler/Util.fsi index f856957270..4cecd1a3a4 100644 --- a/src/Fable.Compiler/Util.fsi +++ b/src/Fable.Compiler/Util.fsi @@ -94,6 +94,11 @@ module Process = val runSyncWithOutput: string -> string -> string list -> string + val runSyncWithOutputEnv: envVars: (string * string) list -> string -> string -> string list -> string + + /// Environment for the `dotnet` processes Fable starts for itself, not for the user's --run commands + val dotnetEnvVars: unit -> (string * string) list + type PathResolver = abstract TryPrecompiledOutPath: sourceDir: string * relativePath: string -> string option diff --git a/src/Fable.Core/CHANGELOG.md b/src/Fable.Core/CHANGELOG.md index 9b498af19a..ad0ca135ff 100644 --- a/src/Fable.Core/CHANGELOG.md +++ b/src/Fable.Core/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: c977d78b39225a51c7bd051a1fe363ed0ccbe201 +last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 --- # Changelog @@ -9,6 +9,49 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.6.1 - 2026-10-29 + +### 🐞 Bug Fixes + +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + +## 5.6.0 - 2026-10-22 + +### 🚀 Features + +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + +## 5.5.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..5ee8bbb832d9677ad702104ded8e00c09619393f) + +## 5.4.0 - 2026-10-12 + +### 🚀 Features + +* *(all)* Add implicit conversion to erased unions U2-U9 ([4e486f20](https://github.com/fable-compiler/Fable/commit/4e486f201fb229964af213d6ce3030ac3c7e26de)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + +## 5.3.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c977d78b39225a51c7bd051a1fe363ed0ccbe201..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + ## 5.2.0 - 2026-06-30 ### 🚀 Features diff --git a/src/Fable.Core/Fable.Core.Dart.fs b/src/Fable.Core/Fable.Core.Dart.fs index 79873d3880..72bcfbc7ce 100644 --- a/src/Fable.Core/Fable.Core.Dart.fs +++ b/src/Fable.Core/Fable.Core.Dart.fs @@ -27,6 +27,12 @@ type Future<'T> = interface end [] type Stream<'T> = interface end +type Async with + static member AwaitFuture(future: Future<'T>) : Async<'T> = nativeOnly + + static member StartAsFuture(workflow: Async<'T>, ?token: System.Threading.CancellationToken) : Future<'T> = + nativeOnly + // [] [] let print (item: obj) : unit = nativeOnly diff --git a/src/Fable.Core/Fable.Core.JS.fs b/src/Fable.Core/Fable.Core.JS.fs index 56a6e450aa..aad46ff6e0 100644 --- a/src/Fable.Core/Fable.Core.JS.fs +++ b/src/Fable.Core/Fable.Core.JS.fs @@ -33,6 +33,10 @@ module JSX = /// that can be resolved at compile-time. let create (componentOrTag: ElementType) (props: Prop list) : Element = nativeOnly + /// Spreads an object into the props of a JSX Element: `
`. + /// Can only be used as an element of the `props` list passed to `JSX.create`. + let spread (props: obj) : Prop = nativeOnly + /// Creates a JSX Element directly from a string template, which can be interpolated. /// When using interpolation note the holes must follow JSX syntax rules. /// E.g. holes in the middle of a string or in the position of a prop key are not valid. @@ -583,7 +587,8 @@ module JS = abstract Create: data: obj -> Uint8Array - and Uint8ClampedArray = TypedArray + and Uint8ClampedArray = + inherit TypedArray and Uint8ClampedArrayConstructor = [] diff --git a/src/Fable.Core/Fable.Core.JsInterop.fs b/src/Fable.Core/Fable.Core.JsInterop.fs index f9841984db..ded58d88e0 100644 --- a/src/Fable.Core/Fable.Core.JsInterop.fs +++ b/src/Fable.Core/Fable.Core.JsInterop.fs @@ -3,7 +3,24 @@ module Fable.Core.JsInterop open System open Fable.Core -/// Compiles to ?? operator in JavaScript +/// +/// Compiles to the JavaScript nullish coalescing operator ??. +/// Returns when it is neither null nor undefined at runtime, +/// otherwise . +/// +/// +/// let name = maybeName ??= "anonymous" +/// +/// Compiles to: +/// +/// const name = maybeName ?? "anonymous"; +/// +/// +/// Value tested for null or undefined. +/// Value returned when is null or undefined. +/// +/// MDN: Nullish coalescing operator +/// [] let (??=) (nullable: 'T) (defaultValue: 'T) : 'T = nativeOnly @@ -48,21 +65,36 @@ let emitJsExpr<'T> (args: obj) (jsCode: string) : 'T = nativeOnly let emitJsStatement<'T> (args: obj) (jsCode: string) : 'T = nativeOnly /// -/// Emit a directive prologue at the top of the file (before the imports) -/// -/// This is useful when working with Next.js or other frameworks that require -/// a specific directive at the top of the file, such as "use client" or "use server". +/// Emits a directive prologue at the top of the file, before the imports. +/// Frameworks such as Next.js require directives like "use client" or "use server" there. /// -/// Directive text +/// +/// let x = 5 +/// emitJsTopDirectivePrologue "use server" +/// +/// Compiles to: +/// +/// "use server"; +/// export const x = 5; +/// +/// +/// Directive text. The quotes are added. let emitJsTopDirectivePrologue (text: string) : unit = nativeOnly /// -/// Emit a directive prologue at the calling position. -/// -/// This is useful when you need to emit a directive prologue for a specific part of the code, -/// such as "use client" or "use server". +/// Emits a directive prologue at the calling position. /// -/// Directive text +/// +/// let x = 5 +/// emitJsDirectivePrologue "use server" +/// +/// Compiles to: +/// +/// export const x = 5; +/// "use server"; +/// +/// +/// Directive text. The quotes are added. let emitJsDirectivePrologue (text: string) : unit = nativeOnly /// Create a literal JS object from a collection of key-value tuples. diff --git a/src/Fable.Core/Fable.Core.Types.fs b/src/Fable.Core/Fable.Core.Types.fs index 1e28ded11f..9c1bcef475 100644 --- a/src/Fable.Core/Fable.Core.Types.fs +++ b/src/Fable.Core/Fable.Core.Types.fs @@ -85,6 +85,36 @@ type TypeScriptTaggedUnionAttribute(tagName: string, caseRules: CaseRules) = new(tagName: string) = TypeScriptTaggedUnionAttribute(tagName, CaseRules.LowerFirst) +/// +/// Adds a test to a case of a TypeScriptTaggedUnion, checked after the tag matches, +/// or to a case of an Erase union, checked after its type test. When the type of +/// an erased case cannot be tested at runtime, the test is used on its own. +/// Use it when several cases share a tag value and their fields cannot tell them apart, +/// or when erased cases share a runtime type. +/// +/// +/// The test must be true for every value of its case. Its negation is used to test +/// the other cases sharing the tag, or the erased cases with the same runtime type, +/// so one case per group may be left without a test. +/// +/// The test is a JS expression in Emit syntax, where $0 is the tested value. +/// It only applies when targeting JavaScript or TypeScript. +/// +/// [<TypeScriptTaggedUnion("state")>] +/// type Terminal = +/// | [<CompiledName("done"); CaseTest("'value' in $0")>] Done of value: float +/// | [<CompiledName("done"); CaseTest("'error' in $0")>] DoneError of error: string +/// | Pending +/// +/// [<Erase>] +/// type Id = +/// | [<CaseTest("$0.startsWith('n:')")>] Name of string +/// | Slug of string +/// +/// +type CaseTestAttribute(test: string) = + inherit Attribute() + /// /// Used in place of CompiledNameAttribute if the target is not a string. /// @@ -227,8 +257,15 @@ type NamedParamsAttribute = ParamObjectAttribute type InjectAttribute() = inherit Attribute() +/// /// Erased union type to represent one of two possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U2<'a, 'b> = | Case1 of 'a @@ -236,9 +273,18 @@ type U2<'a, 'b> = static member op_ErasedCast(x: 'a) = Case1 x static member op_ErasedCast(x: 'b) = Case2 x + static member inline op_Implicit(x: 'a) : U2<'a, 'b> = Case1 x + static member inline op_Implicit(x: 'b) : U2<'a, 'b> = Case2 x +/// /// Erased union type to represent one of three possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U3<'a, 'b, 'c> = | Case1 of 'a @@ -248,9 +294,19 @@ type U3<'a, 'b, 'c> = static member op_ErasedCast(x: 'a) = Case1 x static member op_ErasedCast(x: 'b) = Case2 x static member op_ErasedCast(x: 'c) = Case3 x + static member inline op_Implicit(x: 'a) : U3<'a, 'b, 'c> = Case1 x + static member inline op_Implicit(x: 'b) : U3<'a, 'b, 'c> = Case2 x + static member inline op_Implicit(x: 'c) : U3<'a, 'b, 'c> = Case3 x +/// /// Erased union type to represent one of four possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U4<'a, 'b, 'c, 'd> = | Case1 of 'a @@ -262,9 +318,20 @@ type U4<'a, 'b, 'c, 'd> = static member op_ErasedCast(x: 'b) = Case2 x static member op_ErasedCast(x: 'c) = Case3 x static member op_ErasedCast(x: 'd) = Case4 x + static member inline op_Implicit(x: 'a) : U4<'a, 'b, 'c, 'd> = Case1 x + static member inline op_Implicit(x: 'b) : U4<'a, 'b, 'c, 'd> = Case2 x + static member inline op_Implicit(x: 'c) : U4<'a, 'b, 'c, 'd> = Case3 x + static member inline op_Implicit(x: 'd) : U4<'a, 'b, 'c, 'd> = Case4 x +/// /// Erased union type to represent one of five possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U5<'a, 'b, 'c, 'd, 'e> = | Case1 of 'a @@ -278,9 +345,21 @@ type U5<'a, 'b, 'c, 'd, 'e> = static member op_ErasedCast(x: 'c) = Case3 x static member op_ErasedCast(x: 'd) = Case4 x static member op_ErasedCast(x: 'e) = Case5 x + static member inline op_Implicit(x: 'a) : U5<'a, 'b, 'c, 'd, 'e> = Case1 x + static member inline op_Implicit(x: 'b) : U5<'a, 'b, 'c, 'd, 'e> = Case2 x + static member inline op_Implicit(x: 'c) : U5<'a, 'b, 'c, 'd, 'e> = Case3 x + static member inline op_Implicit(x: 'd) : U5<'a, 'b, 'c, 'd, 'e> = Case4 x + static member inline op_Implicit(x: 'e) : U5<'a, 'b, 'c, 'd, 'e> = Case5 x +/// /// Erased union type to represent one of six possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U6<'a, 'b, 'c, 'd, 'e, 'f> = | Case1 of 'a @@ -296,9 +375,22 @@ type U6<'a, 'b, 'c, 'd, 'e, 'f> = static member op_ErasedCast(x: 'd) = Case4 x static member op_ErasedCast(x: 'e) = Case5 x static member op_ErasedCast(x: 'f) = Case6 x + static member inline op_Implicit(x: 'a) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case1 x + static member inline op_Implicit(x: 'b) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case2 x + static member inline op_Implicit(x: 'c) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case3 x + static member inline op_Implicit(x: 'd) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case4 x + static member inline op_Implicit(x: 'e) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case5 x + static member inline op_Implicit(x: 'f) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case6 x +/// /// Erased union type to represent one of seven possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = | Case1 of 'a @@ -316,9 +408,23 @@ type U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = static member op_ErasedCast(x: 'e) = Case5 x static member op_ErasedCast(x: 'f) = Case6 x static member op_ErasedCast(x: 'g) = Case7 x + static member inline op_Implicit(x: 'a) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case1 x + static member inline op_Implicit(x: 'b) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case2 x + static member inline op_Implicit(x: 'c) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case3 x + static member inline op_Implicit(x: 'd) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case4 x + static member inline op_Implicit(x: 'e) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case5 x + static member inline op_Implicit(x: 'f) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case6 x + static member inline op_Implicit(x: 'g) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case7 x +/// /// Erased union type to represent one of eight possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = | Case1 of 'a @@ -338,9 +444,24 @@ type U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = static member op_ErasedCast(x: 'f) = Case6 x static member op_ErasedCast(x: 'g) = Case7 x static member op_ErasedCast(x: 'h) = Case8 x + static member inline op_Implicit(x: 'a) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case1 x + static member inline op_Implicit(x: 'b) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case2 x + static member inline op_Implicit(x: 'c) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case3 x + static member inline op_Implicit(x: 'd) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case4 x + static member inline op_Implicit(x: 'e) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case5 x + static member inline op_Implicit(x: 'f) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case6 x + static member inline op_Implicit(x: 'g) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case7 x + static member inline op_Implicit(x: 'h) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case8 x -/// Erased union type to represent one of nine or more possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// Erased union type to represent one of nine possible values. +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = | Case1 of 'a @@ -362,6 +483,15 @@ type U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = static member op_ErasedCast(x: 'g) = Case7 x static member op_ErasedCast(x: 'h) = Case8 x static member op_ErasedCast(x: 'i) = Case9 x + static member inline op_Implicit(x: 'a) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case1 x + static member inline op_Implicit(x: 'b) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case2 x + static member inline op_Implicit(x: 'c) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case3 x + static member inline op_Implicit(x: 'd) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case4 x + static member inline op_Implicit(x: 'e) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case5 x + static member inline op_Implicit(x: 'f) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case6 x + static member inline op_Implicit(x: 'g) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case7 x + static member inline op_Implicit(x: 'h) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case8 x + static member inline op_Implicit(x: 'i) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case9 x static member inline op_ErasedCast(x: 't) : U9<_, _, _, _, _, _, _, _, ^U> = Case9(^U: (static member op_ErasedCast: 't -> ^U) x) diff --git a/src/Fable.Transforms/Babel/Babel.fs b/src/Fable.Transforms/Babel/Babel.fs index e0428756d7..4946e3e225 100644 --- a/src/Fable.Transforms/Babel/Babel.fs +++ b/src/Fable.Transforms/Babel/Babel.fs @@ -26,7 +26,7 @@ type AssignmentOperator = /// Since the left-hand side of an assignment may be any expression in general, an expression can also be a pattern. type Expression = | CommentedExpression of comment: string * expr: Expression - | JsxElement of componentOrTag: Expression * props: (string * Expression) list * children: Expression list + | JsxElement of componentOrTag: Expression * props: JsxProp list * children: Expression list | JsxTemplate of parts: string array * values: Expression array | Literal of Literal | Identifier of Identifier @@ -535,6 +535,11 @@ type TypeParameter = | TypeParameter of name: string * bound: TypeAnnotation opt type FunctionTypeParam = | FunctionTypeParam of name: Identifier * typeAnnotation: TypeAnnotation * isOptional: bool +[] +type JsxProp = + | KeyValue of key: string * value: Expression + | Spread of value: Expression + [] module Helpers = type Expression with diff --git a/src/Fable.Transforms/Babel/BabelPrinter.fs b/src/Fable.Transforms/Babel/BabelPrinter.fs index 0dfd49ebc9..6a5ee77f13 100644 --- a/src/Fable.Transforms/Babel/BabelPrinter.fs +++ b/src/Fable.Transforms/Babel/BabelPrinter.fs @@ -447,9 +447,7 @@ module PrinterExtensions = printer.Print(Array.last parts |> escape) - member printer.PrintJsxElement - (componentOrTag: Expression, props: (string * Expression) list, children: Expression list) - = + member printer.PrintJsxElement(componentOrTag: Expression, props: JsxProp list, children: Expression list) = let printTag = function | StringConstant tag -> printer.Print(tag) @@ -475,14 +473,21 @@ module PrinterExtensions = props |> List.iter ( function - | _, NullOrUndefinedOrVoid -> () - | key, StringConstant value -> printProp (fun () -> printer.Print($"%s{key}=\"%s{value}\"")) - | key, value -> + | JsxProp.KeyValue(_, NullOrUndefinedOrVoid) -> () + | JsxProp.KeyValue(key, StringConstant value) -> + printProp (fun () -> printer.Print($"%s{key}=\"%s{value}\"")) + | JsxProp.KeyValue(key, value) -> printProp (fun () -> printer.Print(key + "={") printer.Print(value) printer.Print("}") ) + | JsxProp.Spread value -> + printProp (fun () -> + printer.Print("{...") + printer.Print(value) + printer.Print("}") + ) ) printer.PopIndentation() @@ -691,7 +696,7 @@ module PrinterExtensions = printer.PrintNewLine() for line in lines do - let line = Naming.xmlDecode line + let line = (Naming.xmlDecode line).Replace("*/", "*\\/") printer.Print(" * ") printer.Print(line.Trim()) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 19186f764d..84a762fb02 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -47,6 +47,7 @@ type Context = TailCallOpportunity: ITailCallOpportunity option OptimizeTailCall: unit -> unit ScopedTypeParams: Set + BoundTypes: Map ForcedIdents: Set } @@ -363,6 +364,7 @@ module Reflection = | Replacements.Util.BclDateTimeOffset | Replacements.Util.BclDateOnly | Replacements.Util.BclTimeOnly + | Replacements.Util.BclRune | Replacements.Util.BclTimer -> genericEntity fullName [||] | Replacements.Util.BclHashSet gen | Replacements.Util.FSharpSet gen -> @@ -465,16 +467,28 @@ module Reflection = |> Map |> Some - yield + let parent = Fable.DeclaredType(d.Entity, d.GenericArgs) |> transformTypeInfoFor Reflection com ctx r genMap + + let rec hasDeclaredType (t: Fable.Type) = + match t with + | Fable.DeclaredType _ -> true + | t -> t.Generics |> List.exists hasDeclaredType + + // A generic argument can be this type or one that inherits from it, so evaluating it eagerly recurses forever + if d.GenericArgs |> List.exists hasDeclaredType then + yield Expression.arrowFunctionExpression ([||], parent) + else + yield parent | None -> () ] |> libReflectionCall com ctx r "class" + // Keep in sync with FSharp2Fable.Util.tryRuntimeTypeTestKey, which predicts these tests let transformTypeTest (com: IBabelCompiler) ctx range expr typ : Expression = let warnAndEvalToFalse msg = - "Cannot type test (evals to false): " + msg |> addWarning com [] range + WarningCodes.typeTestEvalsToFalse msg |> addWarningWithCode com [] range Expression.booleanLiteral (false) @@ -529,7 +543,10 @@ module Reflection = |> libCall com ctx range "Util" "isException" [] | _ -> match com.GetEntity(entRef) with - | Patterns.Try (Util.tryFindAnyEntAttribute [ Atts.stringEnum; Atts.erase; Atts.tsTaggedUnion ]) (att, _) as ent -> + | Patterns.Try (Util.tryFindAnyEntAttribute [ Atts.stringEnum + Atts.erase + Atts.tsTaggedUnion + Atts.pojoDefinedByConsArgs ]) (att, _) as ent -> match att with | Atts.stringEnum -> jsTypeof "string" expr | Atts.erase when ent.IsFSharpUnion -> @@ -539,6 +556,7 @@ module Reflection = | cases when cases |> List.forall (fun c -> List.isEmpty c.UnionCaseFields) -> jsTypeof "string" expr | _ -> warnAndEvalToFalse (ent.FullName + " (erased)") + | Atts.pojoDefinedByConsArgs -> warnAndEvalToFalse (ent.FullName + " (pojo)") | _ -> warnAndEvalToFalse (ent.FullName + " (erased)") | Patterns.Try (tryJsConstructorFor ActualConsRef com ctx) cons -> @@ -749,11 +767,32 @@ module Annotation = let makeBuiltinTypeAnnotation com ctx typ kind = match kind with | Replacements.Util.BclGuid -> StringTypeAnnotation - | Replacements.Util.BclTimeSpan -> NumberTypeAnnotation - | Replacements.Util.BclDateTime -> makeAliasTypeAnnotation com ctx "Date" - | Replacements.Util.BclDateTimeOffset -> makeAliasTypeAnnotation com ctx "Date" - | Replacements.Util.BclDateOnly -> makeAliasTypeAnnotation com ctx "Date" - | Replacements.Util.BclTimeOnly -> NumberTypeAnnotation + | Replacements.Util.BclTimeSpan -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "TimeSpanTemporal" "Duration" + else + NumberTypeAnnotation + | Replacements.Util.BclDateTime -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "DateTimeTemporal" "PlainDateTime" + else + makeAliasTypeAnnotation com ctx "Date" + | Replacements.Util.BclDateTimeOffset -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "DateTimeOffsetTemporal" "ZonedDateTime" + else + makeAliasTypeAnnotation com ctx "Date" + | Replacements.Util.BclDateOnly -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "DateOnlyTemporal" "PlainDate" + else + makeAliasTypeAnnotation com ctx "Date" + | Replacements.Util.BclTimeOnly -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "TimeOnlyTemporal" "PlainTime" + else + NumberTypeAnnotation + | Replacements.Util.BclRune -> NumberTypeAnnotation | Replacements.Util.BclTimer -> makeFableLibImportTypeAnnotation com ctx [] "Timer" "Timer" | Replacements.Util.BclHashSet key -> makeFableLibImportTypeAnnotation com ctx [ key ] "Util" "ISet" | Replacements.Util.BclDictionary(key, value) -> @@ -841,12 +880,32 @@ module Annotation = | Types.ievent2 -> makeFableLibImportTypeAnnotation com ctx genArgs "Event" "IEvent$2" |> Some | "Fable.Core.JS.Set`1" -> makeFableLibImportTypeAnnotation com ctx genArgs "Util" "ISet" |> Some | "Fable.Core.JS.Map`2" -> makeFableLibImportTypeAnnotation com ctx genArgs "Util" "IMap" |> Some + | "Fable.Core.JS.Uint8ClampedArray" -> makeNativeTypeAnnotation com ctx [] "Uint8ClampedArray" |> Some + // FCS erases the Fable.Core aliases, so only TypedArray reaches here + | "Fable.Core.JS.TypedArray`1" -> + match genArgs with + | [ Fable.Number(kind, _) ] -> + match kind with + | Int8 -> Some "Int8Array" + | UInt8 -> Some "Uint8Array" + | Int16 -> Some "Int16Array" + | UInt16 -> Some "Uint16Array" + | Int32 -> Some "Int32Array" + | UInt32 -> Some "Uint32Array" + | Int64 + | BigInt -> Some "BigInt64Array" + | UInt64 -> Some "BigUint64Array" + | Float32 -> Some "Float32Array" + | Float64 -> Some "Float64Array" + | _ -> None + | _ -> None + |> Option.map (makeNativeTypeAnnotation com ctx []) | _ -> None let makeStringEnumTypeAnnotation (ent: Fable.Entity) (attArgs: obj list) = let rule = match List.tryHead attArgs with - | Some(:? int as rule) -> enum (rule) + | Some(:? int as rule) -> enum(rule) | _ -> Core.CaseRules.LowerFirst ent.UnionCases @@ -876,7 +935,7 @@ module Annotation = let makeTypeScriptTaggedUnionTypeAnnotation com ctx genArgs (ent: Fable.Entity) (attArgs: obj list) = let tag, rule = match attArgs with - | (:? string as tag) :: (:? int as rule) :: _ -> tag, enum (rule) + | (:? string as tag) :: (:? int as rule) :: _ -> tag, enum(rule) | (:? string as tag) :: _ -> tag, Core.CaseRules.LowerFirst | _ -> "kind", Core.CaseRules.LowerFirst @@ -894,16 +953,16 @@ module Annotation = AbstractMember.abstractProperty (prop, tagType, isComputed = isComputed) - match uci.UnionCaseFields with - // | [ field ] when field.Name = "Item" -> - // IntersectionTypeAnnotation - // [| - // field.FieldType |> resolveInlineType genArgs |> makeFieldAnnotation com ctx - // ObjectTypeAnnotation [| tagMember |] - // |] - | fields -> + match tryInlinedTaggedUnionCaseType com uci with + | Some fieldType -> + IntersectionTypeAnnotation + [| + fieldType |> resolveInlineType genArgs |> makeFieldAnnotation com ctx + ObjectTypeAnnotation [| tagMember |] + |] + | None -> let names, types = - fields |> List.map (fun fi -> fi.Name, fi.FieldType) |> List.unzip + uci.UnionCaseFields |> List.map (fun fi -> fi.Name, fi.FieldType) |> List.unzip makeAnonymousRecordTypeAnnotation com ctx (List.toArray names) types |> function @@ -1079,17 +1138,70 @@ module Util = ctx.UsedNames.RootScope.Add(name) |> ignore name - let getUniqueNameInDeclarationScope (ctx: Context) name = + let getUniqueNameInDeclarationScopeWith (ctx: Context) (isUsed: string -> bool) name = let name = (name, Naming.NoMemberPart) ||> Naming.sanitizeJsIdent (fun name -> ctx.UsedNames.RootScope.Contains(name) || ctx.UsedNames.CurrentDeclarationScope.Contains(name) + || isUsed name ) ctx.UsedNames.CurrentDeclarationScope.Add(name) |> ignore name + let getUniqueNameInDeclarationScope (ctx: Context) name = + getUniqueNameInDeclarationScopeWith ctx (fun _ -> false) name + + let rec isStableForLoopLimit (limit: Fable.Expr) = + match limit with + | Fable.Value(Fable.NumberConstant _, _) -> true + | Fable.IdentExpr ident -> not ident.IsMutable + | Fable.Get(Fable.IdentExpr ident, Fable.FieldGet info, _, _) when info.Name = "length" -> + match ident.Type with + | Fable.String + | Fable.Array(_, (Fable.MutableArray | Fable.ImmutableArray)) -> not ident.IsMutable + | _ -> false + | Fable.Operation(Fable.Unary(_, operand), _, _, _) -> isStableForLoopLimit operand + | Fable.Operation(Fable.Binary(_, left, right), _, _, _) -> + isStableForLoopLimit left && isStableForLoopLimit right + | _ -> false + + let private isMutableArrayLength (com: Compiler) (expr: Fable.Expr) = + let isArray = + function + | Fable.Array(_, (Fable.MutableArray | Fable.ImmutableArray)) -> true + | _ -> false + + let isGetterCall (callInfo: Fable.CallInfo) = + Option.isNone callInfo.ThisArg && List.isEmpty callInfo.Args + + match expr with + | Fable.Get(Fable.IdentExpr ident, Fable.FieldGet info, _, _) when info.Name = "length" -> + ident.IsMutable && isArray ident.Type + | Fable.Get(Fable.Call(Fable.IdentExpr ident, callInfo, _, _), Fable.FieldGet info, _, _) when + info.Name = "length" && isGetterCall callInfo + -> + ident.IsMutable && isArray ident.Type + | Fable.Get(Fable.Call(Fable.Import({ Kind = Fable.MemberImport memberRef }, _, _), callInfo, typ, _), + Fable.FieldGet info, + _, + _) when info.Name = "length" && isGetterCall callInfo && isArray typ -> + match com.TryGetMember(memberRef) with + | Some memb -> memb.IsValue && memb.IsMutable + | None -> false + | _ -> false + + let isReReadForLoopLimit (com: Compiler) isUp (limit: Fable.Expr) = + match limit with + | Fable.Operation(Fable.Binary(BinaryMinus, + length, + Fable.Value(Fable.NumberConstant(Fable.NumberValue.Int32 1, _), _)), + _, + _, + _) when isUp -> isMutableArrayLength com length + | _ -> false + type NamedTailCallOpportunity(_com: Compiler, ctx, name, args: Fable.Ident list) = // Capture the current argument values to prevent delayed references from getting corrupted, // for that we use block-scoped ES2015 variable declarations. See #681, #1859 @@ -1272,7 +1384,8 @@ module Util = variables |> Seq.distinctBy (fun (id, _) -> id.Name) |> Seq.map (fun (id, value) -> - let ta, tp = makeTypeAnnotationWithParametersIfTypeScript com ctx id.Type value + let idType = Replacements.Util.getIdentValueType com id id.Type + let ta, tp = makeTypeAnnotationWithParametersIfTypeScript com ctx idType value let value = if com.IsTypeScript && Option.isNone value then @@ -1319,24 +1432,32 @@ module Util = // let e = defaultArg eOpt (Expression.numericLiteral (0.)) Expression.unaryExpression ("void", e, ?loc = range) - let getTypeParameters (ctx: Context) (types: Fable.Type list) = - let mutable scopedTypeParams = ctx.ScopedTypeParams + let private getConstraintTypes (c: Fable.Constraint) = + match c with + | Fable.Constraint.CoercesTo target -> [ target ] + | Fable.Constraint.IsDelegate(argsType, retType) -> [ argsType; retType ] + | Fable.Constraint.IsEnum baseType -> [ baseType ] + | Fable.Constraint.SimpleChoice types -> types + | _ -> [] - let typeParams = - types - |> FSharp2Fable.Util.getGenParamTypes - |> List.filter (fun typ -> + let getTypeParameters (ctx: Context) (types: Fable.Type list) = + // A type parameter can appear only in another one's constraint (e.g. `$b :> Lazy<$c>`) and still be referenced by the body. + let rec collect state types = + (state, FSharp2Fable.Util.getGenParamTypes types) + ||> List.fold (fun (scopedTypeParams, typeParams) typ -> match typ with - | Fable.GenericParam(name = name) -> - if Set.contains name scopedTypeParams then - false - else - scopedTypeParams <- Set.add name scopedTypeParams - true - | _ -> false + | Fable.GenericParam(name = name; constraints = constraints) when + not (Set.contains name scopedTypeParams) + -> + constraints + |> List.collect getConstraintTypes + |> collect (Set.add name scopedTypeParams, typ :: typeParams) + | _ -> scopedTypeParams, typeParams ) - scopedTypeParams, typeParams + let scopedTypeParams, typeParams = collect (ctx.ScopedTypeParams, []) types + + scopedTypeParams, List.rev typeParams [] type MemberKind = @@ -1594,10 +1715,17 @@ module Util = match e.Type with | Fable.DeclaredType(sourceEntRef, _) when com.IsTypeScript -> - let sourceEntRef = com.GetEntity(sourceEntRef) + let sourceEnt = com.GetEntity(sourceEntRef) // Because we use a wrapper type for multi-case unions, TypeScript // won't automatically cast them to implementing interfaces - if sourceEntRef.IsFSharpUnion && List.isMultiple sourceEntRef.UnionCases then + let fromUnion = sourceEnt.IsFSharpUnion && List.isMultiple sourceEnt.UnionCases + + // The interface value of an inlined case is not assignable to the literal tag type + let toTaggedUnion = + com.TryGetEntity(entRef) + |> Option.exists (fun ent -> hasAttribute Atts.tsTaggedUnion ent.Attributes) + + if fromUnion || toTaggedUnion then AsExpression(jsExpr, makeTypeAnnotation com ctx typ) else jsExpr @@ -1692,7 +1820,21 @@ module Util = | Fable.StringTemplate(tag, parts, values) -> let tag = tag |> Option.map (fun e -> com.TransformAsExpr(ctx, e)) - let values = values |> List.mapToArray (fun e -> com.TransformAsExpr(ctx, e)) + let values = + values + |> List.mapToArray (fun e -> + let uncasted = + match e with + | Fable.TypeCast(e, _) -> e + | e -> e + + match tag, uncasted.Type with + | None, Fable.Number(Float64, _) -> + libCall com ctx None "String" "float64ToString" [] [ com.TransformAsExpr(ctx, uncasted) ] + | None, Fable.Number(Float32, _) -> + libCall com ctx None "String" "float32ToString" [] [ com.TransformAsExpr(ctx, uncasted) ] + | _ -> com.TransformAsExpr(ctx, e) + ) StringTemplate(tag, List.toArray parts, values, r) |> Literal | Fable.NumberConstant(v, _) -> @@ -1793,6 +1935,11 @@ module Util = | Some(Fable.Call(baseRef, info, _, _)), _ -> let baseExpr = match com.Options.Language, baseType, baseRef with + // A BCL base like Dictionary is annotated with an interface, so extend the library class it runs as + | TypeScript, Some(Fable.DeclaredType(_, genArgs)), Fable.Import({ Kind = Fable.LibraryImport _ }, _, _) -> + match transformAsExpr com ctx baseRef with + | Expression.Identifier id -> makeGenericTypeAnnotation com ctx genArgs id |> SuperType + | expr -> SuperExpression expr | TypeScript, Some typ, _ -> makeTypeAnnotation com ctx typ |> SuperType | TypeScript, None, Fable.IdentExpr id -> makeTypeAnnotation com ctx id.Type |> SuperType | _ -> transformAsExpr com ctx baseRef |> SuperExpression @@ -1809,7 +1956,7 @@ module Util = // Some (baseExpr, []) // default base constructor let range = baseCall |> Option.bind (fun x -> x.Range) - $"Ignoring base call for %s{entRef.FullName}" |> addWarning com [] range + WarningCodes.baseCallIgnored entRef.FullName |> addWarningWithCode com [] range None | Some _, _ -> @@ -2032,7 +2179,7 @@ module Util = | Some(Assign left) -> ExpressionStatement(assign babelExpr.Location left babelExpr) | Some(Target left) -> ExpressionStatement(assign babelExpr.Location (left |> Expression.Identifier) babelExpr) - let transformOperation com ctx range opKind : Expression = + let transformOperation com ctx range typ opKind : Expression = match opKind with | Fable.Unary(op, TransformExpr com ctx expr) -> Expression.unaryExpression (op, expr, ?loc = range) @@ -2068,11 +2215,49 @@ module Util = Expression.binaryExpression (op, left, right, ?loc = range) | _, TransformExpr com ctx left, TransformExpr com ctx right -> - Expression.binaryExpression (op, left, right, ?loc = range) + let e = Expression.binaryExpression (op, left, right, ?loc = range) + + match op, typ with + // Rounding the float64 result of one operation to float32 equals the float32 operation + | (BinaryPlus | BinaryMinus | BinaryMultiply | BinaryDivide | BinaryModulus), Fable.Number(Float32, _) -> + let fround = + Expression.memberExpression (Expression.identifier "Math", Expression.identifier "fround") + + Expression.callExpression (fround, [| e |], ?loc = range) + | _ -> e | Fable.Logical(op, TransformExpr com ctx left, TransformExpr com ctx right) -> Expression.logicalExpression (left, op, right, ?loc = range) + let isTypedArrayConstructor (expr: Fable.Expr) = + match expr with + | Fable.IdentExpr ident -> + match ident.Name with + | "Int8Array" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float32Array" + | "Float64Array" + | "BigInt64Array" + | "BigUint64Array" -> true + | _ -> false + | _ -> false + + // Typed array constructors copy and convert their argument, so the literal does not need to be a typed array itself + let unwrapTypedArrayConstructorArg (macro: string) (info: Fable.CallInfo) = + match macro, info.ThisArg, info.Args with + | "new $0($1...)", + Some cons, + [ MaybeCasted(Fable.Value(Fable.NewArray(Fable.ArrayValues _ as values, typ, _), r)) ] when + isTypedArrayConstructor cons + -> + { info with Args = [ Fable.Value(Fable.NewArray(values, typ, Fable.ResizeArray), r) ] } + | _ -> info + let transformEmit (com: IBabelCompiler) ctx range (info: Fable.EmitInfo) = match info.CallInfo.Tags with | Fable.Tags.Contains "topDirectiveProloge" -> @@ -2080,6 +2265,9 @@ module Util = // Return empty sequence expression so it can be erased at print time Expression.sequenceExpression ([||]) | _ -> + let info = + { info with CallInfo = unwrapTypedArrayConstructorArg info.Macro info.CallInfo } + let macro = stripImports com ctx range info.Macro let info = info.CallInfo @@ -2092,6 +2280,11 @@ module Util = |> List.append thisArg |> emitExpression range macro + [] + type JsxPropExpr = + | KeyValue of key: string * value: Fable.Expr + | Spread of value: Fable.Expr + let transformJsxProps (com: IBabelCompiler) props = (Some([], []), props) ||> List.fold (fun propsAndChildren prop -> @@ -2103,7 +2296,7 @@ module Util = | Replacements.Util.ArrayOrListLiteral(children, _) -> Some(props, children) | value -> Some(props, [ value ]) else - Some((key, value) :: props, children) + Some(JsxPropExpr.KeyValue(key, value) :: props, children) // When the string value exceeds ~100 chars, Fable wraps it in a Let binding. // The Let body still contains the tuple with the prop key and a reference to the bound var; // we use the bound value (letValue) as the actual prop value. @@ -2114,7 +2307,15 @@ module Util = | Replacements.Util.ArrayOrListLiteral(children, _) -> Some(props, children) | _ -> Some(props, [ letValue ]) else - Some((key, letValue) :: props, children) + Some(JsxPropExpr.KeyValue(key, letValue) :: props, children) + | Some(props, children), + MaybeCasted(Fable.Call(_, + { + Tags = Fable.Tags.Contains "jsx-spread" + Args = [ value ] + }, + _, + _)) -> Some(JsxPropExpr.Spread value :: props, children) | Some _, e -> addError com [] e.Range "Cannot detect JSX prop key at compile time" @@ -2184,6 +2385,7 @@ but thanks to the optimisation done below we get | _ -> ValueNone // Make it easy to check if we are calling the expected function + and [] (|CalledExpression|_|) (callerText: string) value = match value with | CallExpression(IdentifierIs callerText, UnrollerFromArray exprs, _, _) -> ValueSome exprs @@ -2267,7 +2469,13 @@ but thanks to the optimisation done below we get |> List.concat let props = - props |> List.rev |> List.map (fun (k, v) -> k, transformAsExpr com ctx v) + props + |> List.rev + |> List.map ( + function + | JsxPropExpr.KeyValue(key, value) -> JsxProp.KeyValue(key, transformAsExpr com ctx value) + | JsxPropExpr.Spread value -> JsxProp.Spread(transformAsExpr com ctx value) + ) Expression.jsxElement (componentOrTag, props, children) @@ -2327,6 +2535,10 @@ but thanks to the optimisation done below we get "Expecting a static list or array literal (no generator) for JSX props" |> addErrorAndReturnNull com range |> Some + | Fable.Tags.Contains "jsx-spread", _ -> + "JSX.spread can only be used as an element of the props list passed to JSX.create" + |> addErrorAndReturnNull com range + |> Some | Fable.Tags.Contains "jsx-template", args -> match args with | StringConst template :: _ -> @@ -2419,17 +2631,35 @@ but thanks to the optimisation done below we get | Some(TransformExpr com ctx thisArg) -> callFunction com ctx range callee callInfo.GenericArgs (thisArg :: args) - let transformCurriedApply com ctx range (TransformExpr com ctx applied) args = - (applied, args) - ||> List.fold (fun expr arg -> - match arg with - // TODO: If arg type is unit but it's an expression with potential - // side-effects, we need to extract it and execute it before the call - | Fable.Value(Fable.UnitConstant, _) -> [] - | Fable.IdentExpr ident when ident.Type = Fable.Unit -> [] - | TransformExpr com ctx arg -> [ arg ] - |> callFunction com ctx range expr [] + let transformCurriedApply (com: IBabelCompiler) ctx range (applied: Fable.Expr) args = + // decision: declaration types preserve generic unit arguments despite specialized identifier types at call sites + let appliedType = + match applied with + // invariant: FSharp2Fable makes local names unique within a declaration (getIdentUniqueName) + | MaybeCasted(Fable.IdentExpr id) -> Map.tryFind id.Name ctx.BoundTypes |> Option.defaultValue id.Type + | _ -> applied.Type + + let applied = com.TransformAsExpr(ctx, applied) + + ((applied, appliedType), args) + ||> List.fold (fun (expr, calleeType) arg -> + let argType, returnType = + match calleeType with + | Fable.LambdaType(argType, returnType) -> Some argType, returnType + | _ -> None, Fable.Any + + let args = + match arg, argType with + | _, Some argType when argType <> Fable.Unit -> [ com.TransformAsExpr(ctx, arg) ] + // TODO: If arg type is unit but it's an expression with potential + // side-effects, we need to extract it and execute it before the call + | Fable.Value(Fable.UnitConstant, _), _ -> [] + | Fable.IdentExpr ident, _ when ident.Type = Fable.Unit -> [] + | TransformExpr com ctx arg, _ -> [ arg ] + + callFunction com ctx range expr [] args, returnType ) + |> fst let transformCallAsStatements com ctx range typ returnStrategy callee callInfo = let argsLen (i: Fable.CallInfo) = @@ -2577,8 +2807,11 @@ but thanks to the optimisation done below we get | Fable.OptionValue -> let expr = com.TransformAsExpr(ctx, fableExpr) - if mustWrapOption typ || com.IsTypeScript then + // Functions are stored uncurried, so their annotation would not match the value + if mustWrapOption typ || (com.IsTypeScript && containsFunctionType typ) then libCall com ctx None "Option" "value" [] [ expr ] + elif com.IsTypeScript then + AsExpression(expr, makeTypeAnnotation com ctx typ) else expr @@ -2663,15 +2896,7 @@ but thanks to the optimisation done below we get transformBindingExprBody com ctx var value |> assign var.Range (identAsExpr var) let transformBindingAsStatements (com: IBabelCompiler) ctx (var: Fable.Ident) (value: Fable.Expr) = - // Compiler-generated copy-update locals (inputRecord, copyOfStruct) are treated as plain - // values at runtime even though their Fable type is byref. Strip the wrapper for TS annotation. - let varType = - if var.IsCompilerGenerated then - match var.Type with - | Replacements.Util.IsByRefType com innerType -> innerType - | typ -> typ - else - var.Type + let varType = Replacements.Util.getIdentValueType com var var.Type if isJsStatement ctx false value then let ta, tp = makeTypeAnnotationWithParametersIfTypeScript com ctx varType None @@ -3155,7 +3380,7 @@ but thanks to the optimisation done below we get | Fable.CurriedApply(callee, args, _, range) -> transformCurriedApply com ctx range callee args - | Fable.Operation(kind, _, _, range) -> transformOperation com ctx range kind + | Fable.Operation(kind, _, t, range) -> transformOperation com ctx range t kind | Fable.Get(expr, kind, typ, range) -> transformGet com ctx range typ expr kind @@ -3173,6 +3398,7 @@ but thanks to the optimisation done below we get | Fable.Let(ident, value, body) -> if ctx.HoistVars [ ident ] then let assignment = transformBindingAsExpr com ctx ident value + let ctx = { ctx with BoundTypes = Map.add ident.Name ident.Type ctx.BoundTypes } Expression.sequenceExpression ([| assignment; com.TransformAsExpr(ctx, body) |]) else @@ -3180,6 +3406,13 @@ but thanks to the optimisation done below we get | Fable.LetRec(bindings, body) -> if ctx.HoistVars(List.map fst bindings) then + let ctx = + { ctx with + BoundTypes = + (ctx.BoundTypes, bindings) + ||> List.fold (fun types (id, _) -> Map.add id.Name id.Type types) + } + let values = bindings |> List.mapToArray (fun (id, value) -> transformBindingAsExpr com ctx id value) @@ -3279,17 +3512,25 @@ but thanks to the optimisation done below we get [| resolveExpr t returnStrategy e |] | Fable.Operation(kind, _, t, range) -> - [| transformOperation com ctx range kind |> resolveExpr t returnStrategy |] + [| transformOperation com ctx range t kind |> resolveExpr t returnStrategy |] | Fable.Get(expr, kind, t, range) -> [| transformGet com ctx range t expr kind |> resolveExpr t returnStrategy |] | Fable.Let(ident, value, body) -> let binding = transformBindingAsStatements com ctx ident value + let ctx = { ctx with BoundTypes = Map.add ident.Name ident.Type ctx.BoundTypes } Array.append binding (transformAsStatements com ctx returnStrategy body) | Fable.LetRec(bindings, body) -> + let ctx = + { ctx with + BoundTypes = + (ctx.BoundTypes, bindings) + ||> List.fold (fun types (id, _) -> Map.add id.Name id.Type types) + } + let bindings = bindings |> Seq.collect (fun (i, v) -> transformBindingAsStatements com ctx i v) @@ -3356,25 +3597,45 @@ but thanks to the optimisation done below we get let body = transformBlockWithVarHoisting com ctx None body [| Statement.whileStatement (guard, body, ?loc = range) |] - | Fable.ForLoop(var, TransformExpr com ctx start, TransformExpr com ctx limit, body, isUp, range) -> + | Fable.ForLoop(var, fableStart, fableLimit, fableBody, isUp, range) -> + let start = transformAsExpr com ctx fableStart + let limit = transformAsExpr com ctx fableLimit + // Hoist vars within the loop so if a closure captures the variable + // it doesn't change in the next iteration, see #4031 + let body = transformBlockWithVarHoisting com ctx None fableBody + let op1, op2 = if isUp then BinaryOperator.BinaryLessOrEqual, UpdateOperator.UpdatePlus else BinaryOperator.BinaryGreaterOrEqual, UpdateOperator.UpdateMinus + let makeDeclarator name init = + VariableDeclarator.variableDeclarator ( + name, + init = init, + ?annotation = makeTypeAnnotationIfTypeScript com ctx var.Type (Some init) + ) + + let limitDeclarators, limitExpr = + if isStableForLoopLimit fableLimit || isReReadForLoopLimit com isUp fableLimit then + [||], limit + else + let isUsed name = + [ fableStart; fableLimit; fableBody ] |> List.exists (isIdentUsed name) + + let limitName = getUniqueNameInDeclarationScopeWith ctx isUsed "limit" + + [| makeDeclarator limitName limit |], Expression.identifier limitName + [| Statement.forStatement ( - // Hoist vars within the loop so if a closure captures the variable - // it doesn't change in the next iteration, see #4031 - transformBlockWithVarHoisting com ctx None body, + body, VariableDeclaration.variableDeclaration ( Let, - var.Name, - init = start, - ?annotation = makeTypeAnnotationIfTypeScript com ctx var.Type (Some start) + Array.append [| makeDeclarator var.Name start |] limitDeclarators ), - Expression.binaryExpression (op1, identAsExpr var, limit), + Expression.binaryExpression (op1, identAsExpr var, limitExpr), Expression.updateExpression (op2, false, identAsExpr var), ?loc = range ) @@ -3395,6 +3656,9 @@ but thanks to the optimisation done below we get { ctx with TailCallOpportunity = tailcallChance OptimizeTailCall = fun () -> isTailCallOptimized <- true + BoundTypes = + (ctx.BoundTypes, args) + ||> List.fold (fun types id -> Map.add id.Name id.Type types) } let returnStrategy = @@ -3518,11 +3782,15 @@ but thanks to the optimisation done below we get | Fable.Char -> Expression.stringLiteral "\u0000" | Fable.Number(kind, uom) -> com.TransformAsExpr(ctx, Fable.NumberConstant(Fable.NumberValue.GetZero kind, uom) |> makeValue None) + | Builtin BclTimeOnly when com.Options.JsTemporal -> + libCall com ctx None "TimeOnlyTemporal" "minValue" [] [] + | Builtin BclTimeSpan when com.Options.JsTemporal -> libCall com ctx None "TimeSpanTemporal" "zero" [] [] | Builtin(BclTimeSpan | BclTimeOnly) -> Expression.numericLiteral 0 | Builtin BclGuid -> Expression.stringLiteral "00000000-0000-0000-0000-000000000000" - | Builtin BclDateTime -> libCall com ctx None "Date" "minValue" [] [] - | Builtin BclDateTimeOffset -> libCall com ctx None "DateOffset" "minValue" [] [] - | Builtin BclDateOnly -> libCall com ctx None "DateOnly" "minValue" [] [] + | Builtin BclDateTime -> libCall com ctx None (JS.Replacements.dateTimeModule com) "minValue" [] [] + | Builtin BclDateTimeOffset -> + libCall com ctx None (JS.Replacements.dateTimeOffsetModule com) "minValue" [] [] + | Builtin BclDateOnly -> libCall com ctx None (JS.Replacements.dateOnlyModule com) "minValue" [] [] | _ -> libCall com ctx None "Util" "defaultOf" [] [] let getEntityFieldsAsIdents (ent: Fable.Entity) = @@ -3708,6 +3976,57 @@ but thanks to the optimisation done below we get ModuleDecl(entName + Naming.reflectionSuffix, isPublic = ent.IsPublic) |> declareModuleMember com ctx fnExpr + let hasClassMethod name (classMembers: ClassMember array) = + classMembers + |> Array.exists ( + function + | ClassMethod(ClassFunction(Expression.Identifier(Identifier(key, _)), _), _, _, _, _, _, _, _, _) -> + key = name + | _ -> false + ) + + let withReferenceEquality (com: IBabelCompiler) ctx (ent: Fable.Entity) (classMembers: ClassMember array) = + let isReferenceEqualityType = + (ent.IsFSharpRecord || ent.IsFSharpUnion) + && not ent.IsValueType + && not (FSharp2Fable.Util.hasStructuralEquality ent) + + if not isReferenceEqualityType then + classMembers + else + let ifTypeScript annotation = + if com.IsTypeScript then + Some annotation + else + None + + let makeMethod name parameters returnExpr returnType = + ClassMember.classMethod ( + ClassFunction(Expression.identifier name, false), + parameters, + Some(BlockStatement [| Statement.returnStatement returnExpr |]), + ?returnType = ifTypeScript returnType + ) + + [| + yield! classMembers + if not (hasClassMethod "Equals" classMembers) then + let other = + Parameter.parameter ("other", ?typeAnnotation = ifTypeScript AnyTypeAnnotation) + + makeMethod + "Equals" + [| other |] + (Expression.binaryExpression (BinaryEqual, thisExpr, Expression.identifier other.Name)) + BooleanTypeAnnotation + if not (hasClassMethod "GetHashCode" classMembers) then + makeMethod + "GetHashCode" + [||] + (libCall com ctx None "Util" "physicalHash" [] [ thisExpr ]) + NumberTypeAnnotation + |] + let declareType (com: IBabelCompiler) ctx @@ -3933,6 +4252,7 @@ but thanks to the optimisation done below we get (doc: string option) classMembers = + let classMembers = withReferenceEquality com ctx ent classMembers let isPublic = ent.IsPublic let tagArgName = "Tag" let tagArgTa = makeAliasTypeAnnotation com ctx tagArgName @@ -4231,6 +4551,18 @@ but thanks to the optimisation done below we get declareType com ctx ent entName doc args body baseExpr classMembers + let baseCallIdents (baseCall: Fable.Expr option) = + match baseCall with + | Some(Fable.Call(_, info, _, _)) -> + info.Args + |> List.choose ( + function + | Fable.IdentExpr id -> Some id + | _ -> None + ) + |> List.toArray + | _ -> [||] + let transformClassWithCompilerGeneratedConstructor (com: IBabelCompiler) ctx @@ -4238,22 +4570,29 @@ but thanks to the optimisation done below we get (decl: Fable.ClassDecl) classMembers = + let classMembers = withReferenceEquality com ctx ent classMembers let fieldIds = getEntityFieldsAsIdents ent |> List.toArray let args = fieldIds |> Array.map identAsExpr - let baseExpr = + let baseExpr, superArgs, baseFieldIds = if ent.IsFSharpExceptionDeclaration then - libValue com ctx "Types" "FSharpException" |> SuperExpression |> Some + libValue com ctx "Types" "FSharpException" |> SuperExpression |> Some, [], [||] elif ent.IsFSharpRecord || ent.IsValueType then - libValue com ctx "Types" "Record" |> SuperExpression |> Some + libValue com ctx "Types" "Record" |> SuperExpression |> Some, [], [||] else - None + let baseType = + ent.BaseType + |> Option.map (fun d -> Fable.DeclaredType(d.Entity, d.GenericArgs)) + + match extractSuperClassFromBaseCall com ctx baseType decl.BaseCall with + | Some(baseExpr, superArgs) -> Some baseExpr, superArgs, baseCallIdents decl.BaseCall + | None -> None, [], [||] let body = BlockStatement( [| if Option.isSome baseExpr then - yield callSuperAsStatement [] + yield callSuperAsStatement superArgs yield! ent.FSharpFields |> List.filter (fun field -> not field.IsStatic) @@ -4269,7 +4608,7 @@ but thanks to the optimisation done below we get ) let args = - fieldIds + Array.append baseFieldIds fieldIds |> Array.map (fun fi -> Parameter.parameter (fi.Name, ?typeAnnotation = makeFieldAnnotationIfTypeScript com ctx fi.Type) ) @@ -4397,13 +4736,8 @@ but thanks to the optimisation done below we get let name = fullNamePath + genericSuffix + "." + memb.CompiledName + overloadSuffix name - let transformAbstractMember com ctx (ent: Fable.Entity) (memb: Fable.MemberFunctionOrValue) = - // TODO: replicate the mangling logic from FSharp2Fable.Util.getAbstractMemberInfo - // let info = FSharp2Fable.Util.getAbstractMemberInfo com ent memb - let isMangled = - (ent.IsAbstractClass && not (hasAttribute Atts.attachMembers ent.Attributes)) - || hasAttribute Atts.mangle ent.Attributes - + let transformAbstractMember (com: IBabelCompiler) ctx (ent: Fable.Entity) (memb: Fable.MemberFunctionOrValue) = + let isMangled = FSharp2Fable.Util.isMangledAbstractFableEntity com ent let isGetter = memb.IsGetter let isSetter = memb.IsSetter @@ -4411,7 +4745,7 @@ but thanks to the optimisation done below we get if isMangled then getMangledMemberName ent memb elif isGetter || isSetter then - memb.CompiledName |> Naming.removeGetSetPrefix + memb.DisplayName else memb.CompiledName @@ -4993,9 +5327,10 @@ module Compiler = member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = BabelCompiler(com) @@ -5024,6 +5359,7 @@ module Compiler = TailCallOpportunity = None OptimizeTailCall = fun () -> () ScopedTypeParams = Set.empty + BoundTypes = Map.empty ForcedIdents = Set.empty } diff --git a/src/Fable.Transforms/Beam/Beam.AST.fs b/src/Fable.Transforms/Beam/Beam.AST.fs index dec6df7873..3147f58624 100644 --- a/src/Fable.Transforms/Beam/Beam.AST.fs +++ b/src/Fable.Transforms/Beam/Beam.AST.fs @@ -35,7 +35,13 @@ type ErlExpr = | Block of exprs: ErlExpr list | BinOp of op: string * left: ErlExpr * right: ErlExpr | UnaryOp of op: string * operand: ErlExpr - | TryCatch of body: ErlExpr list * catchVar: string * catchBody: ErlExpr list * after: ErlExpr list + | TryCatch of + body: ErlExpr list * + catchClassVar: string * + catchReasonVar: string * + catchStackVar: string * + catchBody: ErlExpr list * + after: ErlExpr list | Emit of template: string * args: ErlExpr list | Receive of clauses: ErlCaseClause list * after: (ErlExpr * ErlExpr list) option diff --git a/src/Fable.Transforms/Beam/ErlangPrinter.fs b/src/Fable.Transforms/Beam/ErlangPrinter.fs index fc2192ddce..ccf36b555c 100644 --- a/src/Fable.Transforms/Beam/ErlangPrinter.fs +++ b/src/Fable.Transforms/Beam/ErlangPrinter.fs @@ -206,8 +206,7 @@ module Output = | "throw" | "exit" | "self" - | "node" - | "spawn" -> $"erlang:%s{func}" + | "node" -> $"erlang:%s{func}" | _ -> func sb.Append($"%s{qualifiedFunc}(") |> ignore @@ -408,7 +407,7 @@ module Output = complexExprWithParens sb indent expr - | TryCatch(body, catchVar, catchBody, after) -> + | TryCatch(body, catchClassVar, catchReasonVar, catchStackVar, catchBody, after) -> sb.AppendLine("try") |> ignore let tryBody = stripNoEffect body @@ -428,7 +427,9 @@ module Output = writeIndent () sb.AppendLine($"catch") |> ignore writeIndent () - sb.AppendLine($" _:%s{catchVar} ->") |> ignore + + sb.AppendLine($" %s{catchClassVar}:%s{catchReasonVar}:%s{catchStackVar} ->") + |> ignore let catchBody' = stripNoEffect catchBody diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 11d8cb296d..2964d5833f 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -1,1579 +1,614 @@ -# Fable.Beam — F# on the BEAM +# Fable.Beam — F# to Erlang -An Erlang/BEAM target for Fable. +Fable.Beam is Fable's Erlang/BEAM target. It compiles F# to readable `.erl` +source and supplies an Erlang runtime for F# and .NET library semantics. -## Motivation +| Item | Value | +| --- | --- | +| CLI | `dotnet fable --lang beam` (`--lang erlang` is an alias) | +| Output | Erlang source plus a rebar3 application scaffold | +| Minimum runtime | Erlang/OTP 25 | +| Target status | Alpha | +| Full validation | `./build.sh test beam` | -- Learn the BEAM/OTP platform deeply by building a compiler target (the same - approach that made Fable.Python a success for learning Python) -- Bring F#'s type system, pattern matching, and computation expressions to the - BEAM ecosystem -- Bring F#'s `MailboxProcessor` to the BEAM as an in-process, source-compatible - abstraction (so existing F# async/agent code just compiles) — note this is a - same-process CPS model, **not** an OTP actor; `MailboxProcessor` turned out not to be - the right surface for real process-isolated OTP actors -- Provide real OTP concurrency (process-isolated actors, supervision trees, hot code - reloading) through the separate [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) - bindings library and the [Fable.Actor](https://github.com/fable-hub/Fable.Actor) model, - rather than overloading `MailboxProcessor` +OTP 25 is the support floor. The generated code also relies on maps and named +funs, `monotonic_time`/`system_time`, `atomics`, and `uri_string`; all are +available by that release. -## Minimum OTP Version: 25 +The compiler target is responsible for correct Erlang output. OTP bindings and +process-based application models are separate: -Fable.Beam requires **OTP 25 or later**. This is the oldest version still available -via `apt install erlang` on current Linux LTS distributions (Ubuntu 22.04, Debian 12, -Pop!_OS). Users should be able to install Erlang from their distro package manager -and have it work without adding third-party repos. +- [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) provides typed OTP + bindings. +- [Fable.Actor](https://github.com/fable-hub/Fable.Actor) provides an actor model. +- `MailboxProcessor`, `Async`, and `Task` provide F#-compatible APIs; they are not + replacements for OTP behaviours. `Task` deliberately shares the cold `Async` + runtime, so evaluating a task does not start it as a .NET hot task would. -Key OTP features the runtime depends on: +A known private consumer is an application of about 92,000 lines of F# that uses +file and network I/O, OTP supervision trees, actors, long-running services, and +third-party packages. This is evidence that the target can support substantial +applications; the alpha status reflects the semantic gaps documented below. -| Feature | Minimum OTP | Used In | -| --- | --- | --- | -| Maps (`#{}`) | 17 | All modules | -| Named funs (`fun F(X) -> ...`) | 17 | Generated recursive lambdas | -| `erlang:monotonic_time/1` | 18 | `fable_stopwatch.erl` | -| `erlang:system_time/1` | 18 | `fable_date.erl` | -| `atomics` module | 21 | `fable_utils.erl` (byte arrays), `fable_random.erl` | -| `uri_string` module | 21 | `fable_uri.erl` | -| JIT compiler | 24 (improved in 25) | Bit-syntax integer wrapping performance | -| `maybe` keyword reserved | 25 | Escaped in `sanitizeErlangName` | +## Compiler pipeline -## Target Language: Erlang Source +```text +F# source + -> FSharp2Fable + -> Fable AST + -> shared Fable transforms + -> Fable2Beam + -> Erlang AST + -> ErlangPrinter + -> .erl source + -> rebar3 / Erlang compiler +``` -Generate `.erl` files (not Core Erlang, not Elixir). Rationale: +| Path | Responsibility | +| --- | --- | +| `Beam.AST.fs` | Minimal Erlang AST | +| `Fable2Beam.fs` | Fable AST to Erlang AST | +| `Fable2Beam.Util.fs` | Shared lowering helpers | +| `Fable2Beam.Reflection.fs` | Compile-time reflection metadata | +| `Replacements.fs` | .NET and FSharp.Core calls to Erlang/runtime calls | +| `ErlangPrinter.fs` | Erlang source generation | +| `Prelude.fs` | Names, keywords, module identities, and OTP collision checks | +| `src/fable-library-beam/` | Erlang and F# runtime library | +| `src/Fable.Build/Test/Beam.fs` | .NET, generated Erlang, and entry-point validation | +| `tests/Beam/` | Target test suite and native Erlang fixtures | + +The Erlang AST contains only the forms needed by the target: literals, patterns, +tuples, lists, maps, calls, functions, `case`, matches, blocks, operators, +`try`/`catch`, `receive`, and `Emit`. Target-specific syntax that does not justify +a dedicated node uses `Emit`. + +## Design rules + +- Prefer Erlang/OTP primitives when they preserve F# semantics. +- Handle target-specific calls in `Beam/Replacements.fs` before the JavaScript + replacement fallback can add JavaScript-only helpers or arguments. +- Keep Fable AST to Erlang AST lowering in the transform files; put reusable + behavior in utilities or the runtime. +- Treat the F#/.NET tests as the semantic contract. Do not weaken a passing .NET + test to accommodate the target. +- Keep generated Erlang readable and compatible with ordinary Erlang tooling. +- Keep OTP behaviours out of the compiler. They belong in typed bindings and + libraries built on the generated modules. + +Common native mappings: + +| F# operation | Erlang implementation | +| --- | --- | +| Integer arithmetic | Native arbitrary-precision arithmetic plus width wrapping | +| Bitwise operations | `band`, `bor`, `bxor`, `bsl`, `bsr`, `bnot` | +| Structural equality | `=:=` | +| Lists | Native linked lists and `lists` | +| Records and maps | Erlang maps and `maps` | +| Sets | `ordsets` plus `fable_set` | +| Hashing | `erlang:phash2/1` | +| Pattern matching | Erlang `case` clauses and patterns | -- **Learning-first**: reading generated Erlang output teaches the language -- **OTP integration**: OTP docs and patterns are written in Erlang -- **Debuggable**: users can read and understand the output -- **Sufficient**: Erlang surface syntax is regular enough for code generation -- Can always add a Core Erlang backend later if performance demands it +## Modules and generated projects -## Architecture +Erlang has one flat, global module namespace. A generated module name therefore +contains its OTP application name and source path. -Same pipeline as all Fable targets: +| F# source | Erlang module | +| --- | --- | +| `Program.fs` in `MyApp` | `my_app_program` | +| `Misc/Util2.fs` in `MyApp` | `my_app_misc_util2` | +| `DSL.fs` in `Scriptorium.Quill` | `scriptorium_quill_dsl` | +| `fable_modules/Hedgehog.0.11/Gen.fs` | `hedgehog_gen` | + +`Fable.Beam.Naming.erlangModuleName` is the single naming implementation used by +code generation and output-path generation. The CLI rejects: + +- two sources that resolve to the same module atom; +- generated names that collide with modules from `erts`, `kernel`, or `stdlib`. + +Exceptions: + +- `fable-library-beam` keeps its established module names such as `fable_list` and + `seq`; +- native Erlang modules referenced through interop keep their native names. + +Function, field, case, and module names are sanitized to Erlang atoms. Reserved +words receive a trailing underscore. Encoded punctuation in F# compiled names uses +`_xNNNN_` segments so distinct backticked identifiers do not collapse to one atom; +remaining module-level name/arity collisions fail compilation. Reflection metadata +stores both the original F# name and the emitted atom when both are needed. + +Property accessors carry an `_accessor` suffix after any overload hash. This keeps +`set_StatusCode` distinct from the ordinary method `SetStatusCode`, and likewise +`get_StatusCode` from `GetStatusCode`, despite snake_case sanitization. Declarations +and calls, including calls from other files and referenced projects or Fable +packages, derive the suffix from the same F# member metadata. Ordinary method names +are unchanged; genuine remaining name/arity collisions still fail compilation. +This changes generated accessor names: rebuild generated libraries, the BEAM +runtime, and their callers together, and update native Erlang calls to those +accessors. The public F# property and method APIs are unchanged. + +Names are capped at 223 characters to leave room for atoms generated by `erlc`. +Native compilation uses a SHA-256 suffix for overlong names. Standalone compilation +uses the existing 32-bit djb2 hash to avoid cryptography dependencies in the web UI; +overlong names can therefore differ between compiler hosts. Detected collisions, +including module-value state keys, fail compilation. + +The generated project contains: ```text -F# Source - ↓ FSharp2Fable (existing) -Fable AST - ↓ FableTransforms (existing) -Fable AST (optimized) - ↓ Fable2Beam -Erlang AST - ↓ ErlangPrinter -.erl source files +/ + rebar.config + src/ + .app.src + main.erl + _.erl + fable_modules/ + fable-library-beam/ + rebar.config + src/ + fable_library_beam.app.src + ... ``` -## Design Principles: Beam as an Independent Target - -Fable.Beam should be its own target with its own idioms. While the compiler pipeline -is shared with other targets, the **Replacements layer and runtime library should -take full advantage of Erlang/BEAM capabilities** rather than inheriting patterns -from JavaScript or Python that don't fit. - -### Don't inherit JS/Python patterns when Erlang is simpler - -Many .NET BCL operations that require complex library emulation in JS or Python map -directly to Erlang built-ins: - -| Area | JS/Python approach | Erlang approach | -| --- | --- | --- | -| **Integers** | Fixed-width emulation (JS BigInt, Python PyO3 Rust) | Native arbitrary-precision — direct `+`, `-`, `*`, `div`, `rem` | -| **Int64/BigInt** | Library calls (`big_int:op_add`, etc.) | Direct binary ops — Erlang integers ARE arbitrary-precision | -| **Bitwise ops** | JS routes Int64 through BigInt library | Native `band`, `bor`, `bxor`, `bsl`, `bsr`, `bnot` | -| **Lists** | Array-based emulation | Native linked lists — direct `[H\|T]`, `lists:*` | -| **Maps** | Library objects/dicts | Native `#{}` maps, `maps:*` | -| **Sets** | Library Set class | Native `ordsets` (sorted lists) | -| **Structural equality** | `Util.equals()` library call | Native `=:=` (deep comparison on all types) | -| **Structural comparison** | `Util.compare()` library call | Native `<`, `>`, `=<`, `>=` for most types; `fable_comparison:compare_union/3` for DUs (see below) | -| **Hashing** | Custom hash functions | `erlang:phash2/1` | -| **Pattern matching** | Compiled to if/else chains | Native pattern matching in `case` expressions | -| **Sequences** | Lazy iterators | Lazy seqs via compiled `seq.erl`/`seq2.erl` | - -**Rule: If Erlang can do it natively, do it natively.** Only create library modules -(`fable-library-beam/*.erl`) for operations that genuinely need helper code. Avoid -falling through to the JS Replacements fallback for Beam-specific operations. - -#### Exception: ordered comparison of discriminated unions - -F# requires `<`, `<=`, `>`, `>=`, `compare`, sorting, `min`/`max` on a DU to order cases -by **declaration order** (the case tag index). The Beam DU representation is tagless — -nullary cases are bare atoms (`error`, `warning`, …) and other cases are `{atom, Field…}` -tuples — so Erlang's native term ordering falls back to **alphabetical atom order**, which -does not match F# semantics (e.g. `Warning <= Info` is `true` in F# but `false` for atoms). - -Since a value carries no case index, the compiler supplies the declaration order at the -comparison site and routes DU comparisons through `fable_comparison:compare_union(TagOrder, A, B)`, -where `TagOrder` is the list of case tag names in declaration order. This is wired up in -`Beam/Replacements.fs`: - -- `compare` helper → `compare_union` when the operand type is an F# union (covers `compare`, - `GenericComparison`, `CompareTo`, and the comparer used by `List.sortWith` when the user - passes `compare`). Note this does **not** reach `Set`/`Map`, which use native `ordsets`/`#{}` - and never call `fable_comparison` — see the gaps below. -- `makeRelational` → relational operators on unions. -- `min`/`max` operators on unions (via the union-aware `compare` helper). -- `List.sort`/`List.sortDescending` and `Array.sort`/`Array.sortDescending` → an inline - `lists:sort/2` with a `compare_union` comparator when the element type is a union. - -Equality (`=:=`) is already correct for unions and is left untouched. - -**Known gaps (routing is site-driven, so only statically-typed union comparisons are covered):** -comparisons that reach the *generic runtime* `fable_comparison:compare/2` — or Erlang's native -term ordering — on a union value still use alphabetical order. This affects: - -- `List.sortBy`/`sortByDescending` with a **union key**. -- `List.min`/`max` and `Array.min`/`max` (native `lists:min`/`lists:max`). -- A union used as a **field of another union** (nested comparison when outer tags are equal). -- **`Set` and `Map`** — F# Sets/Maps are ordered collections, but the Beam - representation is native `ordsets`/`#{}`, which order by native term ordering and never call - `fable_comparison`. So `Set.minElement`/`maxElement`, `Set`/`Map` iteration order, - `Map.toList`/`Keys`, etc. sort union elements/keys **alphabetically**, not by declaration - order. (Set/Map *membership* and *equality* are unaffected — those rely on `=:=`, which is - correct.) - -These are not currently exercised by the test suite. - -**Possible future follow-up — make the representation carry the tag index.** The complete, -uniform fix is to encode the case's declaration index in the value itself (e.g. an -index-first tuple `{Index, atom, Field…}`, nullary → `{Index, atom}`). Then the *generic* -`fable_comparison:compare/2` orders correctly for **every** path — relational ops, `sortBy`, -`min`/`max`, nested unions, generic containers — with no per-site tag lists and no gaps. -Why it was **not** done here: (1) the tagless bare-atom / `{atom, Field…}` shape is a -deliberate, idiomatic-Erlang design goal and is FFI-exposed (DU values flow through -`Erlang.receive` message-passing and hand-written interop); (2) Erlang orders tuples by -**size first**, so even an index-carrying tuple can't rely on native `=<`/`lists:sort` for -mixed-arity unions — union comparison would still have to route through `fable_comparison`; -(3) the blast radius is broad and must move in lockstep — `NewUnion`, `UnionCaseTest`, -`UnionTag`, `UnionField` (offset), `Erlang.receive`, `fable_reflection` (make/get union), -the `Result` shape (`{ok, V}` → `{Index, ok, V}`), and `%A`/`~p` printed output. If the gaps -above start to matter in real code, revisit this representation change. - -### Never modify F# tests to accommodate Erlang quirks - -The F# test suite represents valid F# code that must compile and run correctly on all -targets. When an Erlang edge case causes a test failure: - -- **DO**: Add a helper function in `fable-library-beam/` that handles the edge case - (e.g., `fable_convert:to_float/1` handles `"1."` which `binary_to_float/1` rejects) -- **DO**: Use `#if FABLE_COMPILER` blocks for genuine cross-platform differences - (e.g., .NET CultureInfo in parsing) -- **DON'T**: Change the F# test input to avoid the edge case (e.g., changing - `float("1.")` to `float("1.0")` — this hides the bug) - -### Intercept in Beam Replacements, not JS fallback - -The Replacements pipeline tries `Beam.Replacements.tryCall` first, then falls back to -`JS.Replacements.tryCall`. The JS fallback injects extra arguments (comparers, adders) -that Erlang doesn't need and generates library imports (`Util`, `BigInt`, etc.) that -don't exist in Erlang. - -**Handle operations in Beam Replacements** to get clean, original argument lists. -Reserve the JS fallback only for operations that genuinely work the same way. - -## New Files - -### Compiler transforms (`src/Fable.Transforms/Beam/`) - -| File | Purpose | Reference | Status | -| -------------------------- | --------------------------------------------------- | ----------------------------- | ------ | -| `Beam.AST.fs` | Erlang AST type definitions (intentionally minimal) | `Python/Python.AST.fs` | Done | -| `Fable2Beam.fs` | Main Fable AST → Erlang AST transforms | `Fable2Php.fs` / Python | Done | -| `Fable2Beam.Util.fs` | Shared helpers for the transforms | `Python/Fable2Python.Util.fs` | Done | -| `Fable2Beam.Reflection.fs` | Compile-time reflection type-info generation | Python reflection | Done | -| `Replacements.fs` | .NET BCL → Erlang mappings (full implementation) | `Python/Replacements.fs` | Done | -| `ErlangPrinter.fs` | Erlang AST → `.erl` source code | `Python/PythonPrinter.fs` | Done | -| `Prelude.fs` | Name sanitization + Erlang keyword escaping | — | Done | - -Started as a single `Fable2Beam.fs` (PHP pattern) and has since split out -`Fable2Beam.Util.fs` and `Fable2Beam.Reflection.fs` as complexity grew (Python pattern). -The Erlang AST (`Beam.AST.fs`) deliberately stayed small — see "Erlang AST" below. - -### Runtime library (`src/fable-library-beam/`) - -Erlang modules implementing F# core types: - -| Module | Purpose | Notes | Status | -| --- | --- | --- | --- | -| fable_option.erl | Option | Some(x) = x, None = undefined | Done | -| fable_list.erl | FSharpList | fold, find, choose, collect, etc. | Done | -| fable_map.erl | FSharpMap | Erlang native maps, pick/try_pick/min/max | Done | -| fable_seq.erl | Seq / IEnumerable | Eager lists, delay/singleton/unfold | Done | -| fable_string.erl | String utilities | Erlang binaries, pad/replace/join, sprintf/printf/String.Format | Done | -| fable_comparison.erl | Comparison | compare/2 returning -1/0/1 | Done | -| fable_char.erl | Char utilities | is_letter/digit/upper/lower/whitespace | Done | -| fable_convert.erl | Type conversions | Robust to_float handling edge cases | Done | -| fable_reflection.erl | Reflection | Full FSharpType/FSharpValue support: TypeInfo as maps, record/union/tuple/function type tests, GetRecordFields/MakeRecord, GetUnionFields/MakeUnion, GetTupleFields/MakeTuple, PropertyInfo.GetValue | Done | -| fable_result.erl | Result | {ok, V} or {error, E} | Done | -| fable_set.erl | FSharpSet | ordsets (sorted lists), fold/map/filter/partition/union_many/intersect_many | Done | -| fable_async_builder.erl | AsyncBuilder | CPS builder operations (bind, return, delay, etc.) | Done | -| fable_async.erl | Async | High-level ops (RunSynchronously, Parallel, Sleep, etc.) | Done | -| fable_regex.erl | Regex | Wraps Erlang `re` module (PCRE), IsMatch/Match/Matches/Replace/Split | Done | -| fable_resize_array.erl | ResizeArray | List manipulation helpers (set_item, remove, insert, find, sort) | Done | -| fable_dictionary.erl | Dictionary | Mutable dictionary via process dict + Erlang maps, TryGetValue with out-refs | Done | -| fable_hashset.erl | HashSet | Mutable set via process dict + Erlang maps, UnionWith/IntersectWith/ExceptWith | Done | -| fable_queue.erl | Queue | FIFO queue via process dict + Erlang queue module | Done | -| fable_stack.erl | Stack | LIFO stack via process dict + list | Done | -| fable_timespan.erl | TimeSpan | Ticks-based, create/from_*/component accessors/total_*/arithmetic/parse/to_string | Done | -| fable_date.erl | DateTime | 2-tuple {Ticks, Kind}, calendar module, formatting, parsing, arithmetic | Done | -| fable_date_offset.erl | DateTimeOffset | 3-tuple {Ticks, OffsetTicks, Kind}, wraps fable_date | Done | -| fable_guid.erl | Guid | UUID v4 generation, parse, toString, comparison | Done | -| fable_uri.erl | Uri | URI parsing and manipulation | Done | -| fable_utils.erl | Utilities | IEnumerator (lists/refs/maps/HashSet), apply_curried, infinity/NaN helpers, atomics byte arrays (new_byte_array, byte_array_get/set/length) | Done | -| fable_bit_converter.erl | BitConverter | Byte conversion, endianness | Done | -| fable_decimal.erl | Decimal | Fixed-scale integer (value × 10^28), multiply/divide/to_string/parse/from_parts | Done | -| fable_mailbox.erl | MailboxProcessor | In-process CPS continuation model (same as JS/Python) | Done | -| fable_cancellation.erl | CancellationToken | Process dict pattern, create/cancel/register/is_cancellation_requested, timer-based auto-cancel | Done | -| fable_stopwatch.erl | Stopwatch | StartNew, Elapsed, ElapsedMilliseconds, Stop, Reset, Restart, IsRunning, Frequency, GetTimestamp | Done | -| fable_observable.erl | Observable | subscribe, add, choose, filter, map, merge, pairwise, partition, scan, split | Done | -| fable_event.erl | Event / IEvent | trigger/publish/add, choose/filter/map/merge/pairwise/partition/scan/split | Done | -| fable_date_only.erl | DateOnly | create/components/day_number/add_*/from_date_time/to_string/parse | Done | -| fable_time_only.erl | TimeOnly | create/from_*/components/ticks/add_*/is_between/to_time_span/to_string/parse | Done | -| fable_parallel.erl | Array.Parallel | spawn-based parallel_map/mapi/init/iter/iteri/collect/choose/for | Done | -| fable_quotation.erl | F# Quotations (Expr) | `mk_*` constructors, `is_*` tests, evaluate, substitute, get_free_vars | Done | - -### Registration & CLI (modified existing files) -- All Done - -| File | Change | Status | -| ---------------------------------------------- | ----------------------------------------------- | ------ | -| `src/Fable.AST/Plugins.fs` | Added `Beam` to `Language` DU | Done | -| `src/Fable.Compiler/Util.fs` | Added `.erl` file extension | Done | -| `src/Fable.Compiler/ProjectCracker.fs` | Added Beam library path | Done | -| `src/Fable.Cli/Entry.fs` | Added `--lang beam` argument parsing | Done | -| `src/Fable.Cli/Pipeline.fs` | Added `Beam.compileFile` dispatch | Done | -| `src/Fable.Transforms/Replacements.Api.fs` | Beam dispatch for all 15 API functions | Done | -| `src/Fable.Transforms/Transforms.Util.fs` | Added Beam to `getLibPath` | Done | -| `src/Fable.Transforms/FSharp2Fable.Util.fs` | Added Beam to `isModuleValueCompiledAsFunction` | Done | -| `src/Fable.Transforms/Fable.Transforms.fsproj` | Added Beam files to project | Done | - -### Build system (`src/Fable.Build/`) - -| File | Change | Status | -| ---------------------- | ------------------------------------------------ | ------ | -| `FableLibrary/Beam.fs` | `BuildFableLibraryBeam` class | Done | -| `Quicktest/Beam.fs` | Quicktest handler for beam | Done | -| `Test/Beam.fs` | Test handler (`./build.sh test beam`) | Done | -| `Main.fs` | Added beam to quicktest + test + fable-library | Done | -| `Fable.Build.fsproj` | Added new Beam files | Done | - -### Quicktest project (`src/quicktest-beam/`) - -| File | Purpose | Status | -| --------------------- | ----------------------------------- | ------ | -| `quicktest.fs` | `printfn "Hello from BEAM!"` | Done | -| `quicktest.fsproj` | Project file referencing Fable.Core | Done | - -## Module Naming - -Erlang's module namespace is **flat and global**. The atom in `-module(...)` is a module's only -identity: neither the directory the `.erl` file sits in nor the OTP application it belongs to -scopes it, and the code server resolves the atom across the whole code path. An `.erl` file must -also be named after the module it declares. - -So every generated module name is qualified by the application it belongs to — the same -convention OTP itself follows (`cowboy_req`, `rebar_app_info`) and that Fable's own runtime -already used (`fable_list`, `fable_map`): - -| F# source | Erlang module | -| ------------------------------------ | ----------------------- | -| `/Program.fs` in `MyApp` | `my_app_program` | -| `/Misc/Util2.fs` in `MyApp` | `my_app_misc_util2` | -| `../Scriptorium.Quill/DSL.fs` | `scriptorium_quill_dsl` | -| `fable_modules/Hedgehog.0.11/Gen.fs` | `hedgehog_gen` | - -Naming a module after the bare basename of its file, as the backend used to, breaks in three ways -— all silent at compile time and fatal at runtime: - -1. **OTP is shadowed.** `Gen.fs`, `Random.fs`, `String.fs`, `Timer.fs`, `Queue.fs`, ... all name - real OTP stdlib modules. OTP's win, and calls into the generated code raise `undef`. -2. **Assemblies overwrite each other.** Two `DSL.fs` files in two projects both emit `dsl.erl` - into the flat output `src/`; the one compiled last overwrites the other **on disk**, and the - loser's functions vanish from the output entirely. -3. **Files within an assembly collide** the same way (`Foo/Types.fs` vs `Bar/Types.fs`). - -Two exemptions: - -- **fable-library** keeps its bare, hand-maintained names (`fable_list`, `seq`, `range`, ...). - It is the one project whose *compiled* output ships as a dependency, and `getLibPath` in - `Transforms.Util` refers to its modules by exactly those names. -- **Native Erlang modules** reached through `BeamInterop` (`string`, `lists`, ...) are of course - referenced by their own names. - -Naming lives in one place, `Fable.Beam.Naming.erlangModuleName` (`Beam/Prelude.fs`), because the -code generator (which must resolve an import to the atom the imported file declared) and the CLI -(which must write the file under the name of the module inside it) have to agree exactly. - -Qualification is a convention, not a guarantee, so `checkBeamModuleNames` (`Fable.Cli/Main.fs`) -**fails the build** on the two ways it can still go wrong, rather than letting either surface as an -`undef` at runtime: - -- two source files mapping to the same module name — it names both files; -- a module name that is one of OTP's own (`Naming.otpModules`). Qualification rules out the bare - names, but a two-segment name can still land on a real OTP module — an app named `Gen` with a - `Server.fs` produces `gen_server` — and fable-library's exempt modules are not qualified at all, - so a `Timer.fs` added to it would silently shadow OTP's `timer`. +Fable regenerates its own scaffold files. An existing user-owned `rebar.config` +is left unchanged. ### Entry point -Since module names are qualified, the entry point of a project compiled from `Program.fs` is -`my_app_program:main/0`, not `main:main/0`. Fable therefore also emits a small `src/main.erl` -shim exporting `main/0` and `main/1` that forwards to it, so runners have a stable, well-known -entry module: +The last F# source file owns module-level actions and the program entry point. The +CLI emits `src/main.erl` as a stable shim: ```sh -erl -noshell -pa src -eval "main:main([])" -s init stop -``` - -As elsewhere in Fable's Beam output, the entry point is the *last* source file of the project: -its module-level actions compile to that module's `main/0`. - -## Type Mappings - -### Natural fits (F# → Erlang) - -| F# | Erlang | Notes | | -| ---------------- | ---------------------- | ------------------------------------------ | ---------------------------- | -| `int`, `float` | `integer()`, `float()` | Direct | | -| `string` | `binary()` | `<<"hello">>` | | -| `char` | `integer()` | Unicode codepoint; see caveat below | | -| `bool` | `true \ | false` | Atoms | -| `unit` | `ok` | Atom | | -| `tuple` | `tuple` | Direct: `{A, B, C}` | | -| `list` | `list()` | Both are linked lists! | | -| `option` | `x` or `{some, x}` \ | `undefined` | Erased or wrapped (see below)| -| `Result` | `{ok, V} \ | {error, E}` | Matches Erlang idiom exactly | -| Pattern matching | Pattern matching | Both languages excel here | | -| Immutability | Immutability | Erlang is immutable by default | | -| `bigint` | `integer()` | Erlang has native arbitrary-precision ints | | - -### Requires design decisions - -| F# | Erlang Strategy | Alternatives | -| --------------------------------- | ------------------------------------------------- | ------------------------------------ | -| **DU cases** | Tagged tuples: `{some, V}`, `{node, L, R}` | Maps, records | -| **Records** | Erlang maps: `#{name => <<"Dag">>}` | Erlang records (compile-time tuples) | -| **Classes** | Module + map (state as map, methods as functions) | Processes holding state | -| **Interfaces** | Dispatch maps: `#{method => fun(...) -> ... end}` | Done (object expressions) | -| **Mutability** (`ref`, `mutable`) | Process dictionary, ETS, or process state | Agent pattern | -| **Exceptions** | `throw`/`catch` with tagged tuples | Error tuples (Erlang way) | -| **Generics** | Erased (Erlang is dynamically typed) | — | -| **Currying** | Lambda wrapping (same as Python target) | — | -| **Nested modules** | Flat module names: `My_Module_Sub` | One file per module | -| **Computation expressions** | Transformed at Fable AST level; async/task → CPS | — | - -### Known limitation: `char` at a generic type stringifies to its codepoint - -A `char` is a plain `integer()` at runtime — the same representation as an `int`, and the same -choice the Dart target makes. Nothing at runtime can tell the two apart, so converting a `char` to -a string is correct only where the compiler can still see the type statically. It can in every -ordinary case: - -```fsharp -string c // "2" -c.ToString() // "2" -"x" + string c // "x2" -System.Char.ToString c // "2" -$"{c}" // "2" -string (box c) // "2" — the boxing cast is still visible at the call site -System.String.Format("{0}", c) // "2" -System.String.Format("{0}", box c) // "2" -``` - -Boxing only survives while the cast is part of the same expression. Once the boxed char is bound to -an `obj` — by a `let`, or by flowing through a pipe — the static type is gone before the conversion -is reached, and these degrade to the generic case below: - -```fsharp -box c |> string // "50" — the pipe binds it as obj first -let b: obj = box c in string b // "50" -sprintf "%O" c // "50" — printf applies its arguments through a curried runtime -sprintf "%A" c // "50" — .NET gives "'2'" -[ box '2' ] |> List.map string // ["50"] — element type is obj +erl -noshell -pa _build/default/lib/*/ebin \ + -eval 'main:main([])' -s init stop ``` -The `%O`/`%A` cases are not a boxing problem but a printf one: `printf` parses its format string and -applies its arguments at runtime, so no argument's static type reaches the formatter at all. Fixing -those means threading per-argument type info from the call site — see "Structured formatting (`%A`)" -below. +`main:main/0` forwards to the generated entry module. `main:main/1` forwards command +line arguments. An integer F# entry-point result becomes the VM exit code. The shim +also sets `standard_io` and `standard_error` to Unicode. -But when `string x` is applied where `x`'s type is a *generic parameter*, the backend can only emit -`fable_convert:to_string/1`, which sees an integer and prints the number: +## Runtime representation -```fsharp -// F# generalizes this to Op<'a> -> Op, so `string c` is applied at a generic type. -let toStr = - function - | Keep c -> Keep(string c) - | Drop c -> Drop(string c) +| F# value | Erlang representation | Notes | +| --- | --- | --- | +| `unit` | `ok` | Non-final unit expressions are removed by the printer | +| `bool` | `true` / `false` | Atoms | +| Signed and unsigned integers | `integer()` | Width restored after operations that can overflow | +| `bigint` | `integer()` | No width wrapping | +| `float` | `float()` | Erlang floating point | +| `decimal` | scaled `integer()` | Value multiplied by 10^28; `GetBits` emits the smallest equivalent scale | +| `string` | UTF-8 `binary()` | Not a charlist | +| `char` | `integer()` | Unicode codepoint; see known limitations | +| Enum | `integer()` | Casts erase to the underlying number | +| `[]` | atom | `[]` literals remain literals | +| Tuple | tuple | `{A, B}` | +| `list<'T>` | list | Native linked list | +| `array<'T>` | process-dictionary reference to a list | Mutable and process-local | +| `byte[]` | `{byte_array, Size, AtomicsRef}` | O(1) reads and writes through `atomics` | +| `seq<'T>` | Fable sequence runtime | Lazy factory wrappers are immutable maps; cursor state is local to each enumeration | +| `Map<'K,'V>` | map | Native key ordering applies | +| `Set<'T>` | ordset | Sorted native list | +| `option<'T>` | erased value or `undefined` | Ambiguous nested/generic cases use `{some, Value}` | +| `Result<'T,'E>` | `{ok, Value}` / `{error, Error}` | Erlang convention | +| Nullary union case | atom | Sanitized case tag | +| Union case with fields | tuple | `{case_tag, Field1, ...}` | +| Record / anonymous record | map | Sanitized field atoms | +| Immutable class | map | Used when construction does not require self-reference or mutation | +| Mutable/self-referencing class | process-dictionary reference | State is process-local | +| Interface / object expression | map of values and closures | Calls dispatch through map entries | +| Exception | map | Includes `message` and, for custom exceptions, `exn_type` | +| Function | Erlang fun | Curry/uncurry adapters preserve identity where supported | +| Local mutable / ref cell | process-dictionary reference | Erased at scope exit where possible | + +## Implemented features + +The checklists record current capabilities, not development phases. + +### Language and code generation + +- [x] Literals, tuples, lists, maps, functions, delegates, and partial application +- [x] Arithmetic, logical, bitwise, relational, and equality operators +- [x] `if`, loops, sequence expressions, ranges, and pipelines +- [x] Pattern matching, active patterns, decision trees, and guards +- [x] Recursive and mutually recursive functions +- [x] Tail calls and named recursive Erlang funs +- [x] Records, anonymous records, discriminated unions, options, results, and enums +- [x] Class construction and common instance/member calls; interfaces and object expressions +- [x] Custom exceptions, `AggregateException`, `try`/`with`, `try`/`finally`, `raise`, `failwith`, and `failwithf` +- [x] Modules, nested modules, imports, exports, and cross-file calls +- [x] Module-qualified names and OTP module collision detection +- [x] Quotations and derived quotation patterns +- [x] Compile-time and runtime reflection metadata +- [x] `FABLE_COMPILER_BEAM` conditional compilation symbol + +### Numerics, text, and time + +- [x] Fixed-width signed and unsigned integer wrapping +- [x] `int64`, `uint64`, `bigint`, and decimal arithmetic +- [x] Integer and floating-point conversions and parsing +- [x] String, character, encoding, regular-expression, and URI operations +- [x] `sprintf`, `printf`, `printfn`, `eprintfn`, `failwithf`, and `String.Format` +- [x] Static dispatch to custom `ToString()` for direct calls, `string`, and `%O` +- [x] `DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`, and `TimeSpan` +- [x] `Guid`, `Random`, `Stopwatch`, `BitConverter`, and environment APIs + +### Collections + +- [x] `List`, `Seq`, `Array`, and `Array.Parallel` +- [x] `Map` and `Set` +- [x] `ResizeArray`, `Dictionary`, `HashSet`, `Queue`, and `Stack` +- [x] Mutable byte arrays backed by `atomics` +- [x] Structural collection equality and comparison on supported element shapes +- [x] Enumeration through lists, references, maps, and mutable collections + +### Effects and concurrency + +- [x] `Async` computation expressions +- [x] `Task` computation expressions mapped to the Async runtime +- [x] `Async.StartChild`, `AwaitTask`, `Parallel`, `Sequential`, and continuations +- [x] Cancellation tokens and cancellation-aware sleep +- [x] `MailboxProcessor` +- [x] Observables and events +- [x] Parallel array operations using spawned worker processes + +### Interop and tooling + +- [x] `[]` inline Erlang expressions +- [x] `[]` native module calls +- [x] `[]` with erased interfaces for typed module bindings +- [x] Erlang keyword and quoted-atom escaping +- [x] rebar3 scaffold generation +- [x] Runtime-library placement under `fable_modules` +- [x] Quicktest support +- [x] .NET, generated Erlang, native Erlang, and entry-point tests + +## Core semantics + +### Replacements + +Call lowering follows this order: -[ Keep '2'; Drop '1' ] |> List.map toStr // "5049", not "21" +```text +Beam.Replacements.tryCall + -> Beam-native operator or OTP/runtime call + -> JavaScript replacement fallback when target-neutral ``` -The trigger is F# generalizing a let-bound lambda, which is easy to hit by accident — the DU is -incidental. `%A` and structural printing of a boxed char have the same limitation. - -Fable erases generics by design, so there is no type witness to dispatch on. The fix is the same -one the compiler already recommends when `typeof<'T>` fails for this reason (see -`genericTypeInfoError` in `Replacements.Util.fs`): make the function `inline`, so the type is -resolved at the call site. +BEAM replacements own numerics, equality and comparison, collections, conversion, +formatting, Async/Task, reflection, mutable storage, and Erlang interop. This avoids +JavaScript-only modules and injected comparers/adders. -```fsharp -let inline toStr op = - match op with - | Keep c -> Keep(string c) - | Drop c -> Drop(string c) +### Lowering invariants -[ Keep '2'; Drop '1' ] |> List.map toStr // "21" -``` - -Annotating the parameter concretely (`Op`) works too, for the same reason — it defeats -generalization. - -Giving `char` a tagged runtime form such as `{char, 50}` would fix this everywhere, but it is a -breaking change to a core type, costs arithmetic and comparison performance, and fights Erlang's own -convention that strings are lists of integer codepoints. Not worth it for a case with a one-keyword -workaround. - -### Structured formatting (`%A`) reads shapes, not types - -`%A` renders a value in F# syntax. The other targets get this for free because their generated types -carry a real `ToString` — a JS record is a class instance, a Python record defines `__str__` — so the -formatter just calls it. Beam has nothing to call: a record is a bare Erlang map and a union a bare -tagged tuple, neither carrying any back-pointer to its type. Reflection does not help either, since -every `fable_reflection` accessor takes a type-info map and a runtime value cannot produce one. - -So `fable_string:format_any/1` dispatches on the *shape* of the term. That gets the common cases -right — strings are quoted, lists print as `[a; b]`, tuples as `(a, b)`, unions as `Case value`, and -arrays are dereferenced from their ref cell to `[|a; b|]` instead of printing an opaque `#Ref<...>`. - -Where several F# types share one Erlang shape, the ambiguity is unresolvable and `%A` deviates from -.NET: - -| Case | .NET | Beam | Why | -| --- | --- | --- | --- | -| record field order | declaration order | Erlang term order | a map does not remember key insertion order | -| record/union names | original casing | reconstructed | names compile to lowercased atoms; `my_case` → `MyCase` is a convention, not a recovery | -| `Some x` | `Some x` | `x` | `option` is erased (JS and Python collapse it too) | -| `set [1; 2]` | `set [1; 2]` | `[1; 2]` | an F# `Set` is an ordset, i.e. a plain list | -| `(Empty, 1)` | `(Empty, 1)` | `Empty 1` | `{empty, 1}` is also the shape of a one-field union case | -| `'x'` | `'x'` | `120` | a `char` is an `integer()`, per the limitation above | -| `ref 5` | `{ contents = 5 }` | `5` | a ref cell is the same process-dictionary reference an array is, with no `contents` field to name | -| `ref [1; 2]` | `{ contents = [1; 2] }` | `[\|1; 2\|]` | the same collision, landing on the array rendering because the stored value is a list | -| `1.0M` | `1.0M` | `10000000000000000000000000000` | a `decimal` is a fixed-scale integer (value × 10²⁸) and is indistinguishable from one | -| `DateTime(...)` | `1/2/2024 3:04:05 AM` | `(638396498450000000, 1)` | a `DateTime` is a `{Ticks, Kind}` tuple; likewise `TimeSpan`, which is a bare integer | - -The last four are worse than the ambiguities above them, in that the value is not merely rendered in -the wrong style but is unreadable. They are still shape collisions rather than bugs — nothing about -the runtime term says "this reference is a ref cell, not an array" or "this integer is scaled" — and -all four print at least as well as the `~p` dump they replaced. - -Recovering the first three needs the argument's static type threaded from the `%A` call site, where -it does still exist: `printfn`'s `PrintfFormat<'Printer, _, _, _>` carries the per-argument types as -a `LambdaType` chain in `CallInfo.GenericArgs`, which `NestedLambdaType` decomposes. That would mean -emitting a `makeTypeInfo` per argument in `fsFormat` and pairing each format specifier with it in -`create_printer` — machinery no Fable target has today, and it would still need a fallback for -generic parameters (which erase to a placeholder) and for `%a`/`%t`/`%*d` specifiers whose arity does -not line up with the value list. - -Anything the formatter does not recognise — pids, ports, a cyclic term past the depth cap — falls -back to `~tp`, which is what `%A` did for *everything* before, so it can never be worse than it was. - -### Console output requires a unicode io device - -An F# `string` is a UTF-8 `binary()`, and every path that prints one — `Console.Write`, -`Console.WriteLine`, `printf`/`printfn`, `eprintfn` — writes it with io's `t` (unicode) modifier -(`~ts`). That reaches the terminal intact only on a device whose encoding is `unicode`; on the -latin1 default of `erl -noshell` a codepoint above latin1 comes out as a `\x{2713}` escape instead. - -Programs started through Fable's generated `main.erl` need to do nothing: the shim's `setup_io/0` -sets both `standard_io` and `standard_error` to unicode before calling into F#. Anything else — -a Fable-compiled module called from a hand-written OTP release, an `escript`, a `gen_server` -started by someone else's supervisor — has to set the device itself: +| Area | Invariant | +| --- | --- | +| Curried calls | Apply one argument at a time. Combining curried arguments into one Erlang call changes arity. | +| Unit arguments | Remove trailing unit parameters and their call-site arguments symmetrically. | +| Expression blocks | Hoist leading matches before calls, operators, and literals; Erlang argument positions cannot contain Fable statement blocks. | +| Recursion | Emit self-recursive lambdas as named funs; emit a mutual-recursion group through one tagged dispatcher. | +| BIF names | Qualify known BIF calls with `erlang:` and emit `no_auto_import` when a generated local function shares an auto-imported name. | +| Arrays at FFI boundaries | Cancel an inline `new_ref`/`get` pair only when every use of that literal argument is dereferenced. Bound arrays retain reference semantics. | +| Mutable locals | Erase generated process-dictionary keys at scope exit where their lifetime is known. | +| Module initialization | Preserve declaration order and isolate each initializer's Erlang variables before merging it into `main/0`. | +| Unit expressions | Remove non-final bare `ok` expressions without removing a final function result. | +| Classes | Choose a self-contained map only when fields and stored closures do not require mutation or `this`; otherwise use a process-local reference. | + +### Fixed-width integers + +Erlang integers are arbitrary precision. `fable_int.erl` restores .NET widths with +bit-syntax wrapping: ```erlang -ok = io:setopts(standard_io, [{encoding, unicode}]), -ok = io:setopts(standard_error, [{encoding, unicode}]). +wrap_i32(N) -> + <> = <>, + V. ``` -The `+pc unicode` VM flag is *not* a substitute: it only affects printable-list detection in `~p`, -not the device encoding. - -There is no device-independent alternative to fall back on. `io:put_chars` follows the device -encoding for both binaries and codepoint lists, so writing a string's raw bytes with `~s` would -merely move the corruption to unicode devices instead of latin1 ones. +The compiler wraps operations that can leave the type's range: addition, +subtraction, multiplication, left shift, negation, complement, and narrowing +conversion. Shift counts are masked to the .NET width. Operations that cannot grow +an in-range value remain native. `bigint` and decimal are not width-wrapped. -### Class instance representation: map vs process-dict ref +Unsigned values keep their unsigned range, so `UInt64.MaxValue` is represented as +`18446744073709551615` and right shift is logical. -A class instance has two possible representations, chosen per class at construction: +### Equality, ordering, and hashing -- **Immutable classes** (no mutable instance fields, no `as self`, no base-class - state to merge, and whose stored interface closures / field initializers don't use - `this` as a value) are emitted as a **self-contained map** — the instance term *is* - the state map, exactly like a non-self-referencing object expression. These are - process-portable: an interface or regular method invoked from another process reads - fields from the value itself, not from the constructing process's dictionary. -- **Mutable classes** (any `let mutable` / `val mutable` instance field, self-reference, - etc.) keep their state in the **process dictionary**, keyed by a `make_ref()`. The BEAM - has no shared mutable memory across processes, so mutation is single-process by design - — cross-process object sharing of mutable instances is intentionally unsupported (use - actors / message passing for that). +- Native `=:=` supplies deep equality for numbers, atoms, binaries, tuples, lists, + and maps. +- `fable_comparison:hash/1` supplies structural hashing through `erlang:phash2/1`; + array `GetHashCode()` hashes the storage handle to preserve identity across mutation. +- Direct comparison of a statically known union uses + `fable_comparison:compare_union/3` with case declaration order. +- Direct union relational operators, `compare`, collection `min`/`max`, and + `List`/`Array` sorting operations use the same declaration-order comparison + when the union type is statically known. +- Function physical equality unwraps compiler-generated curry/eta adapters before + comparing the underlying fun. -Field reads are decoupled from the representation: `fable_utils:field_get/2` (and -`iface_get`, `inst_state`, `move_next`, `get_current`, `safe_dispose`) accept both a map -and a ref, so call sites and runtime helpers work regardless of which form the -constructor chose. See `transformClassDeclaration` in `Fable2Beam.fs`. +The runtime union representation does not carry the declaration index. Generic +comparison paths that lack static union type information therefore fall back to +Erlang term order. See the roadmap. -## Concurrency & Async +### Mutability and process locality -This compiler/runtime implements F#'s async and agent model **in-process** (CPS-based), -keeping mutable process-dict state reachable. Real OTP process concurrency (`gen_server`, -supervision, distribution) is layered on top via the separate -[Fable.Beam](https://github.com/fable-compiler/Fable.Beam) bindings — see -"OTP Processes, Supervision & Actors" below. +Local mutables, non-byte arrays, mutable collections, and mutable class state use +`make_ref()` keys in the process dictionary. Benefits and constraints for those +process-dictionary-backed values: -### MailboxProcessor — In-Process CPS Model (IMPLEMENTED) +- mutation is isolated to the owning process; +- a value sent to another process does not carry its mutable state; +- non-byte list/map collection updates replace the stored collection; +- mutable local keys are erased when their scope exits where code generation can + prove the lifetime. -F#'s `MailboxProcessor` uses same-process CPS continuations (matching JS/Python targets), -NOT OTP gen_server. This design was chosen because: +Lazy sequence factory wrappers are immutable maps and can cross processes when +their factories, sources, and captured values are process-portable. Enumeration +stays lazy: each factory call creates cursor state in the enumerating process. +Active enumerators with process-dictionary-backed state must remain in that +process. Captured mutable state, `Seq.cache`, and non-byte array or mutable +collection sources retain their process-local restrictions. -1. F# MailboxProcessor body closures capture mutable state from the caller — a separate - Erlang process can't access process dict state from the parent -2. The CPS async framework (`run_synchronously`) uses process dict `put`/`erase`, - requiring same-process execution -3. gen_server would require compile-time extraction of message handlers from an opaque - async body — extremely complex for no semantic benefit - -The implementation mirrors `src/fable-library-py/fable_library/mailbox_processor.py`. - -| F# | Erlang | -| --- | --- | -| `new MailboxProcessor(body)` | `fable_mailbox:default(Body)` — creates agent with empty queue | -| `MailboxProcessor.Start(body)` | `fable_mailbox:start(Body)` — create + start | -| `agent.Start()` | `fable_mailbox:start_instance(Agent)` — run body via `start_immediate` | -| `inbox.Receive()` | `fable_mailbox:receive_msg(Agent)` — returns `Async` via `from_continuations` | -| `agent.Post(msg)` | `fable_mailbox:post(Agent, Msg)` — queue + `process_events` | -| `agent.PostAndAsyncReply(f)` | `fable_mailbox:post_and_async_reply(Agent, F)` — reply channel + `Async` | -| `replyChannel.Reply(v)` | `(maps:get(reply, Channel))(V)` — emitExpr inline | +Module-level mutables use module-qualified process-dictionary keys and initialize +through the generated module's `main/0`. Immutable module values that read a mutable +are snapshotted in declaration order. This is correct only in a process that has run +that initializer. -**OTP actors live in Fable.Beam**: Real process-isolated actors (`gen_server` / -`gen_statem`, supervision, fault tolerance) are provided by the separate -[Fable.Beam](https://github.com/fable-compiler/Fable.Beam) OTP bindings and the -[Fable.Actor](https://github.com/fable-hub/Fable.Actor) library — not this in-process -MailboxProcessor. +Byte arrays are the exception. A `byte[]` carries an `atomics` reference in its +`{byte_array, Size, AtomicsRef}` value. Sending that value to another process shares +the backing storage, and `byte_array_set/3` mutations are visible in both processes. -### Async & Task — In-Process CPS (IMPLEMENTED) +Use OTP processes and message passing for shared application state. Do not pass a +process-dictionary-backed mutable F# object to another process and expect +shared-object semantics; treat a byte array passed between processes as explicitly +shared mutable state. -`Async` compiles to a **continuation-passing-style (CPS) function**, not an Erlang -process: +### Exceptions -```erlang -Async = fun(Ctx) -> ok end -Ctx = #{on_success, on_error, on_cancel, cancel_token} -``` +F# and built-in .NET exceptions use maps with a nominal `exn_type` atom. Built-in +type tests follow the .NET exception hierarchy, so typed handlers distinguish sibling +exceptions while accepting derived exceptions through their base type. -The function is *cold* — it does nothing until invoked with a context, matching F#'s -cold-async semantics. Composition (`bind`, `return`, `try/with`, `while`, `for`, …) just -threads new contexts through these functions; see `fable_async_builder.erl`. +Generated Erlang catches the native `Class:Reason:Stacktrace` triple. `reraise` and +unmatched filtered handlers use `erlang:raise/3`, preserving the original Erlang +exception class, reason, and stacktrace. -**Everything runs inline in the caller's process.** This is deliberate, not a -limitation: the CPS body reaches mutable state stored in the process dictionary (mutable -`let`, ref cells, arrays, MailboxProcessor queues), and running in the same process keeps -that state reachable. `RunSynchronously`, `StartImmediate`, and `StartWithContinuations` -all execute the chain in the current process — no `spawn`, no trampoline (Erlang has -native TCO). +### Async, Task, and MailboxProcessor -| F# | Erlang (`fable_async` / `fable_async_builder`) | -| --- | --- | -| `async { return x }` | `fun(Ctx) -> (maps:get(on_success, Ctx))(X) end` | -| `let! x = comp` / `do!` | `bind(Comp, fun(X) -> ... end)` — CPS monadic bind | -| `return` / `return!` | `return/1` / `return_from/1` | -| `try/with`, `try/finally` | `try_with/2`, `try_finally/2` (CPS `on_error`/compensation + Erlang try/catch) | -| `while` / `for` | `while/2` / `for/2` (recursive bind) | -| `Async.StartImmediate` | `start_immediate/1` — inline, default context (fire-and-forget) | -| `Async.RunSynchronously` | `run_synchronously/1` — inline, result stashed via a process-dict ref | -| `Async.StartWithContinuations` | `start_with_continuations/4` — inline with caller continuations | -| `Async.Sleep` | `timer:sleep(Ms)` inline; with a cancel token, `receive` waits on a timer/cancel message (still same process) | -| `Async.Sequential` | run each computation inline via `run_synchronously` | -| `Async.Catch` | `catch_async/1` — wraps result in `{choice1_of2,_}` / `{choice2_of2,_}` | -| `Async.Ignore` | `bind` + `return ok` | -| `Async.FromContinuations` | `from_continuations/1` — lower-level CPS primitive | -| `task { ... }` | alias for async (Task is an alias for Async on Beam) | -| `task.Result` | `fable_async:run_synchronously(Comp)` | - -**The one exception — `Async.Parallel` spawns.** To get real parallelism it `spawn`s one -process per child computation and collects results in order via message passing -(`fable_async:parallel/1`). Each child runs `run_synchronously` in its own process, so -parallel children do **not** share the parent's process-dict state. This is the only -place the async runtime leaves the caller's process. - -Cancellation (`fable_cancellation.erl`) is also in-process: a token is a `make_ref()` -keying a process-dict map `#{cancelled, listeners, next_id}`. `Async.Sleep` is the only -operation that observes it cooperatively (via the `receive` path above). - -### OTP Processes, Supervision & Actors — Provided by Fable.Beam (separate library) - -Real BEAM concurrency — spawning processes, `gen_server`, `supervisor`, supervision -trees, applications, ETS, distribution — is **not** part of this compiler or runtime -library. It lives in the separate -[Fable.Beam](https://github.com/fable-compiler/Fable.Beam) bindings library, which provides -typed F# bindings to OTP modules (`Fable.Beam.GenServer`, `Fable.Beam.Supervisor`, -`Fable.Beam.Application`, `Fable.Beam.Erlang`, `Fable.Beam.Ets`, …) plus an actor model -([Fable.Actor](https://github.com/fable-hub/Fable.Actor)). - -The compiler's job is only to emit correct Erlang; OTP behaviours are opt-in F# bindings -layered on top. Speculative OTP API design (attributes, `supervisor { }` CEs, direct -`gen_server` interop, etc.) belongs in the Fable.Beam repo, not here. - -## Implementation Phases - -### Phase 1: Hello World -- COMPLETE - -Get the full pipeline working end-to-end with minimal features. - -- [x] Add `Language.Beam` to the DU -- [x] Minimal Erlang AST (module, function, expression, literal) -- [x] Minimal Fable2Beam (constants, `printfn`, simple functions) -- [x] Minimal ErlangPrinter (output valid `.erl`) -- [x] CLI integration (`--lang beam` / `--lang erlang`) -- [x] Compile and run: `printfn "Hello from BEAM!"` - -**Goal**: `dotnet fable --lang beam` produces a `.erl` file that `erlc` compiles and runs. - -**What works now**: String/int/float/bool literals, tuples, lists, `printfn`, let bindings, -sequential expressions, type casts, curried apply, emit expressions. Unhandled Fable -expressions produce `todo_*` atom placeholders. The `printfn` chain goes through -`printf` → `toConsole` → `io:format`. - -**How to test**: - -```bash -dotnet build src/Fable.Cli -dotnet run --project src/Fable.Cli --no-launch-profile -- \ - --cwd src/quicktest-beam src/quicktest-beam/quicktest.fsproj \ - --lang beam --outDir /tmp/beam-out --noCache -``` +`Async<'T>` is a cold continuation-passing function: -**Note**: Phase 1 quicktest command above is for manual exploration. For the full -automated test suite, use `./build.sh test beam` (see Phase 2). - -### Phase 2: Core Language -- COMPLETE - -Core F# language features that map naturally to Erlang. All implemented in -`Fable2Beam.fs` with corresponding AST additions and printer updates. - -- [x] Arithmetic operators (`+`, `-`, `*`, `div`/`/`, `rem`) with int/float distinction -- [x] Comparison operators (`=:=`, `=/=`, `<`, `=<`, `>`, `>=`) -- [x] Bitwise operators (`band`, `bor`, `bxor`, `bsl`, `bsr`, `bnot`) -- [x] Logical operators (`andalso`, `orelse`, `not`) -- [x] Exponentiation via `math:pow/2` -- [x] If/else → `case Guard of true -> Then; false -> Else end` -- [x] Lambda (single arg, curried) → `fun(Arg) -> Body end` -- [x] Delegate (multi-arg, uncurried) → `fun(A, B) -> Body end` -- [x] CurriedApply → `Apply` node for calling fun values -- [x] Test expressions (`UnionCaseTest`, `ListTest`, `OptionTest`) -- [x] Get expressions (`TupleIndex`, `UnionTag`, `UnionField`, `ListHead`, `ListTail`, `OptionValue`, `FieldGet`, `ExprGet`) -- [x] DecisionTree / DecisionTreeSuccess (following JS target pattern) -- [x] NewList fix → `ListCons` (`[H | T]` instead of `[H, T]`) -- [x] NewUnion → atom-tagged tuples `{atom_tag, Field1, Field2, ...}`, bare atoms for fieldless cases -- [x] NewOption → value or `undefined` atom -- [x] Set expressions (ValueSet → variable rebind, FieldSet → `maps:put`) -- [x] LetRec → sequential fun assignments -- [x] AST additions: `ListCons`, `Apply` on `ErlExpr`; `PList` on `ErlPattern` -- [x] Printer: `BinOp` parenthesization, `UnaryOp` word operator spacing - -**What works now**: Most basic F# programs compile to valid Erlang. Operators, -conditionals, functions (named, anonymous, higher-order, partial application), -pattern matching (integers, strings, booleans, DUs, options, lists, tuples), -decision trees, and let/letrec bindings all produce correct Erlang output. - -**Test suite**: `tests/Beam/` with xUnit. Run with `./build.sh test beam` which: - -1. Runs all tests on .NET via `dotnet test` -2. Compiles tests to `.erl` via Fable (library files auto-copied to `fable_modules/fable-library-beam/`) -3. Compiles library `.erl` files in `fable_modules/fable-library-beam/` with `erlc` -4. Compiles test `.erl` files with `erlc -pa fable_modules/fable-library-beam` -5. Runs an Erlang test runner (`erl_test_runner.erl`) with `-pa fable_modules/fable-library-beam` that discovers and executes all `test_`-prefixed functions - -The Erlang test runner discovers and runs every `test_`-prefixed arity-1 function. The -suite currently has **2446 passing tests across 63 test files** — more than the Python -target. Coverage spans the F# core library and language features: - -- **Collections**: Seq, List, Array (incl. byte arrays via `atomics`), Map, Set, - ResizeArray, Dictionary, HashSet, Queue, Stack -- **Primitives & text**: arithmetic (incl. Int64, BigInt, decimal), bitwise/logical/ - comparison, string, char, regex, conversions, encoding -- **Types**: records, unions, tuples, anonymous records, enums, classes/interfaces, - object expressions, units of measure, structural equality/comparison, reflection -- **Date/time**: DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, Guid, Uri, - Stopwatch -- **Control flow & effects**: pattern matching, active patterns, loops, exceptions, - type testing, tail calls, async/task, MailboxProcessor, cancellation, observables/events -- **Interop**: `emitErl`, `Import`, `ImportAll` + `Erase` interfaces, module calls -- **Integration**: a Sudoku solver and a raytracer demo - -See `tests/Beam/` for the individual test files. - -### Phase 3: Discriminated Unions & Records -- COMPLETE - -F#'s defining feature on BEAM. DU basics (construction and pattern matching via -DecisionTree) were implemented in Phase 2. This phase adds records and structural equality. - -- [x] DU declaration → tagged tuple constructors (Phase 2) -- [x] DU pattern matching → clause matching on tagged tuples (Phase 2) -- [x] Records → Erlang maps (`#{field => value}`) -- [x] Record update syntax → F# compiler decomposes into `NewRecord` + `FieldGet` (works automatically) -- [x] Anonymous records → Erlang maps (field names provided inline) -- [x] Structural equality for DUs and records → Erlang's native `=:=` (deep comparison) - -**Design decisions**: - -- Records map to Erlang maps (`#{name => <<"Alice">>, age => 30}`) -- Field names converted to snake_case atoms -- `FieldGet` → `maps:get(field, Map)`, `FieldSet` → `maps:put(field, Value, Map)` -- Structural equality uses Erlang's native `=:=` operator (deep comparison for all - types: tuples, maps, lists, atoms, numbers, binaries) — no runtime library needed. - Implemented via `Beam/Replacements.fs` intercepting `GenericEquality`/`op_Equality` - before JS Replacements generates `Util.equals` library calls. - -### Phase 4: Collections -- COMPLETE - -- [x] `list` → Erlang lists (cons cells — natural fit) -- [x] List module functions → `lists:` module calls + `fable_list.erl` library -- [x] `array` → process dict refs wrapping Erlang lists (mutable via `put`/`get`); byte arrays use `atomics` for O(1) read/write -- [x] `Map` → Erlang native `#{}` maps, `maps:` module calls + `fable_map.erl` -- [x] `Set` → Erlang `ordsets` (sorted lists), `ordsets:` module calls + `fable_set.erl` -- [x] `Seq` → eager Erlang lists with `fable_seq.erl` library -- [x] `fable-library-beam` runtime: `fable_list.erl`, `fable_map.erl`, `fable_string.erl`, `fable_option.erl`, `fable_seq.erl` -- [x] Range expressions: `[1..n]` → `lists:seq(1, n)`, `[1..2..n]` → `lists:seq(1, n, 2)` -- [x] Array indexing: `arr.[i]` → `lists:nth(i + 1, arr)` (0-based to 1-based) -- [x] Array comprehensions: `[| for i in 0..n -> expr |]` via Seq desugaring - -**Design decisions**: - -- Sequences use **lazy evaluation** via Fable-compiled `seq.erl`/`seq2.erl` modules - (compiled from `Seq.fs`/`Seq2.fs`). List-backed operations delegate to `fable_list.erl` BIFs. -- Seq operations intercepted in Beam Replacements (not JS fallback) to avoid - injected comparers/adders that Erlang doesn't need. -- Complex operations in `fable_list.erl`/`fable_seq.erl`, simple BIF mappings via `emitExpr`. -- Scalar Seq operations (Fold, Reduce, Find, etc.) routed through compiled `seq.erl` - (Fable-compiled from `Seq.fs`) via `Helper.LibCall` with `SignatureArgTypes`. This enables - the `uncurrySendingArgs` FableTransform to automatically convert curried callbacks to - uncurried Delegates, matching the pattern used by the Python target. -- BIF qualification: `ErlangPrinter.fs` automatically prefixes known BIFs (`length`, `hd`, - `tl`, `element`, `put`, `get`, etc.) with `erlang:` in `Call(None, ...)` nodes. This - prevents shadowing when compiled library modules (like `seq.erl`) define functions with - the same name as BIFs. -- Integration tested with a Sudoku solver (SudokuTests.fs) using Seq, Array, ranges, and - array comprehensions. -- Sets use Erlang's `ordsets` module (sorted lists). Maintains ordering compatible with - F#'s structural comparison. Simple operations (`add`, `contains`, `union`, etc.) map - directly to `ordsets:*` BIFs. Higher-order operations (`fold`, `map`, `filter`) use - `fable_set.erl` for curried function handling. Set `+`/`-` operators intercepted in - Beam `operators` via `Builtin(FSharpSet _)` arg type matching. `set [1;2;3]` handled - via `CreateSet` → `ordsets:from_list`. - -### Phase 5: Modules & Imports -- COMPLETE - -- [x] F# modules → Erlang modules (one `.erl` per file) -- [x] Nested modules → flattened into the enclosing file's Erlang module via qualified - member names (incl. private, deeply-nested, and shadowed modules — see `MiscTests.fs`) -- [x] Module function calls → `module:function(args)` syntax -- [x] Import resolution and path handling -- [x] Export lists (`-export([...])`) -- [x] Snake_case output filenames (matching Erlang module name convention) -- [x] Module names qualified by their OTP app, so they can neither shadow an OTP module nor - collide across assemblies (see [Module Naming](#module-naming)) -- [x] Function name sanitization (`$XXXX` hex sequences from F# backtick names) -- [x] Cross-module call resolution (derive module from `importInfo.Path`) -- [x] `Assert.AreEqual`/`NotEqual` lowered to `fable_utils:assert_equal`/`assert_not_equal` -- [x] `fable_modules/fable-library-beam/` output structure (aligned with JS/Dart/Rust targets) - -### Phase 6: Error Handling -- COMPLETE - -- [x] try/with → try/catch with `erlang:error` for exceptions -- [x] `failwith` → `erlang:error(<<"message">>)` -- [x] Exception message access via `#{message => Reason}` map wrapping -- [x] Nested try/catch works -- [x] `Result` integration with Erlang `{ok,V}/{error,E}` convention -- [x] Custom F# exception types (`exception MyError of string`) → maps with `exn_type` tag -- [x] Exception type discrimination in catch: `maps:get(exn_type, X, undefined) =:= type_name` -- [x] Multi-field exceptions: `exception MyError2 of code: int * message: string` -- [x] Exception `.Message` property via `message` field in exception map - -### Phase 6b: Types & Type Testing -- COMPLETE - -Extend type system support for common F# patterns. - -- [x] **Enum support** — F# enums are just integers in Erlang (trivial, works out of box) - - Enum declaration, construction, pattern matching — all native - - `int` ↔ enum conversion, `enum(n)` — TypeCast is erased - - Enum comparison, flags (bitwise) — native Erlang operators - - `EnumOfValue`/`EnumToValue` — TypeCast is erased -- [x] **Custom exceptions** — `exception MyError of string` → maps with `exn_type` atom tag - - Exception construction via `NewRecord` adds `exn_type` and `message` fields - - Pattern matching in try/catch: `maps:get(exn_type, X, undefined) =:= type_name` - - Exception `.Message` property via `message` field in exception map - - TryCatch handler preserves exception maps (is_map check), wraps non-maps in `#{message => ...}` - - Multi-field exceptions with named fields work correctly -- [x] **Type testing (`:?`)** — runtime type checks via Erlang guards - - `match x with :? int as i -> ...` → `is_integer(X)` guard - - Primitive types: `is_binary` (string), `is_boolean` (bool), `is_float` (float), `is_integer` (int) - - Collection types: `is_list` (list/array), `is_tuple` (tuple), `is_map` (record/class) - - Exception types: `is_map(X) andalso maps:get(exn_type, X, undefined) =:= type_name` - - `box`/`unbox` are erased (TypeCast) -- [x] **String interpolation fix** — `fable_string:to_string/1` for generic value formatting - - Replaces `~p` format (which showed `<<"...">>` for binaries) with runtime type dispatch - - Handles binary/integer/float/atom natively, falls back to `~p` for complex terms -- [x] **Curry expressions** — uses `Replacements.Api.curryExprAtRuntime` to generate nested lambdas at compile time (no runtime module needed) - -### Phase 7: Async & Task -- COMPLETE - -CPS (Continuation-Passing Style) implementation. `Async` = `fun(Ctx) -> ok end` where -`Ctx = #{on_success, on_error, on_cancel, cancel_token}`. CPS naturally gives cold semantics -(F# Async is cold — doesn't execute until started). Task CE is an alias for Async on the Beam -target since Erlang has no equivalent of .NET's hot Task distinction. - -- [x] `async { }` computation expression → CPS builder via `fable_async_builder.erl` -- [x] `let!` / `do!` → `bind/2` (monadic bind — run computation, pass result to binder) -- [x] `return` / `return!` → `return/1` / `return_from/1` -- [x] `try/with` in async → `try_with/2` (CPS on_error override + synchronous try/catch) -- [x] `while` / `for` in async → `while/2` / `for/2` (recursive bind) -- [x] `Async.RunSynchronously` → runs in same process (preserves process dict access) -- [x] `Async.StartImmediate` → runs with default context (fire-and-forget) -- [x] `Async.Parallel` → spawn one process per computation, collect via message passing -- [x] `Async.Sleep` → `timer:sleep/1` -- [x] `Async.Ignore` → bind + return unit -- [x] `Async.StartWithContinuations` → direct CPS invocation -- [x] `Async.FromContinuations` → lower-level CPS primitive -- [x] `task { }` computation expression → alias for async builder -- [x] `task.Result` → `fable_async:run_synchronously` -- [x] Cancellation tokens → process dict pattern with `fable_cancellation.erl` - -**Design decisions**: - -- **CPS over spawn**: `Async` is a function `fun(Ctx) -> ok end`, not a spawned process. - CPS naturally gives cold semantics matching F# Async. No trampoline needed — Erlang has - native tail call optimization. -- **Everything runs inline in the caller's process** — `RunSynchronously`, `StartImmediate`, - `StartWithContinuations`, and `Sequential` never `spawn`. The only exception is - `Async.Parallel`, which spawns one process per child for real parallelism (children run - `run_synchronously` in their own process and don't share the parent's process-dict state). -- **RunSynchronously runs in same process**: Uses a process-dict ref to store the result, NOT - `spawn` + `receive`. This preserves mutable variable (process dict) access from the async body. -- **Task = Async alias**: Task CE builder methods route to `fable_async_builder`, Task - instance methods (`.Result`, `.GetAwaiter().GetResult()`) route to `run_synchronously`. -- **try_with dual handler**: Both the CPS `on_error` override AND the `try/catch` must invoke - the Handler — synchronous throws (like `erlang:error`) bypass the CPS on_error path. -- **Erlang function naming**: `return`, `for`, `while` are NOT reserved words in Erlang — - they work as function names in remote calls (`fable_async_builder:return(V)`). - -### Phase 8: MailboxProcessor -- COMPLETE - -In-process CPS continuation model (same pattern as JS/Python targets). Uses process dict -for mutable state, `fable_async:from_continuations` for the receive/reply coordination. - -- [x] `MailboxProcessor.Start` → `fable_mailbox:start(Body)` (create + start_immediate) -- [x] `new MailboxProcessor(body)` → `fable_mailbox:default(Body)` (create only) -- [x] `agent.Start()` → `fable_mailbox:start_instance(Agent)` -- [x] `inbox.Receive()` → `fable_mailbox:receive_msg(Agent)` (Async via from_continuations) -- [x] `agent.Post(msg)` → `fable_mailbox:post(Agent, Msg)` (queue + process_events) -- [x] `agent.PostAndAsyncReply(f)` → `fable_mailbox:post_and_async_reply(Agent, F)` -- [x] `replyChannel.Reply(v)` → `(maps:get(reply, Channel))(V)` (emitExpr inline) - -**Design decisions**: - -- **Same-process, not gen_server**: MailboxProcessor body closures capture mutable state via - process dict. A separate process can't access this state. The CPS model runs everything - inline in the caller's process, matching F# semantics exactly. -- **State as process dict map**: Agent = `#{ref => Ref}` where `Ref` keys into process dict - storing `#{body, messages, continuation}`. Mutable queue + continuation slot. -- **Synchronous reply coordination**: `post_and_async_reply` stores a reply callback in the - reply channel map. Since everything runs synchronously via CPS, by the time `post` returns - the inbox has processed the message and called Reply, so the value is available immediately. -- **Named `receive_msg`**: Erlang's `receive` is a reserved keyword, so the function is named - `receive_msg`. The Replacements dispatch maps `"Receive"` → `receive_msg`. -- **OTP actors live in Fable.Beam**: Process-isolated actors and supervision are provided by - the separate [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) OTP bindings and - [Fable.Actor](https://github.com/fable-hub/Fable.Actor) — not this in-process MailboxProcessor. - -### Phase 9: OTP Patterns — Moved to Fable.Beam - -OTP integration (supervision trees, application behaviour, hot code reloading, -distribution / multi-node) is **out of scope for the compiler**. It is provided by the -separate [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) bindings library -(`gen_server`, `supervisor`, `application`, `ets`, `erlang` process BIFs, …) and the -[Fable.Actor](https://github.com/fable-hub/Fable.Actor) actor model. The compiler only -needs to emit correct Erlang that those bindings can call. - -### Phase 10: Ecosystem - -- [x] Build integration: `rebar3` project generation — `Main.fs` lays out files under - `src/` (rebar3 convention), generates the root `rebar.config`, and per-dependency - `src/.app.src` + `rebar.config`. Fable-generated configs are regenerated; user-owned - ones are detected and left untouched. -- [x] Test suite (`tests/Beam/` — 2446 tests passing, `./build.sh test beam`) -- [x] Erlang test runner (`tests/Beam/erl_test_runner.erl` — discovers and runs all `test_`-prefixed arity-1 functions) -- [x] `erlc` compilation step in build pipeline (per-file with graceful failure) -- [x] Quicktest setup (`src/quicktest-beam/`, `Fable.Build/Quicktest/Beam.fs`) -- [ ] Documentation - -## Erlang AST - -`Beam.AST.fs` is deliberately small (~80 lines) and that proved sufficient for the entire -test suite. Operators are plain strings (not typed DUs), `if` lowers to `case`, and there -is no dedicated `ListComprehension` / `BinaryExpr` / `MapUpdate` node — those F# -constructs are expressed with the existing nodes plus `Emit` as a raw-Erlang escape hatch. -The actual node set: `ErlLiteral`; `ErlPattern` (`PVar`/`PLiteral`/`PTuple`/`PList`/ -`PWildcard`); `ErlExpr` (literals, variables, tuples, lists/`ListCons`, maps, `Call`/ -`Apply`, `Fun`/`NamedFun`, `Case`, `Match`, `Block`, `BinOp`/`UnaryOp`, `TryCatch`, -`Emit`, `Receive`); attributes; function defs; and modules. - -The richer AST below was the *original* aspirational design. It was **never needed** and -is kept only as a reference for what a fuller Erlang AST could look like if a future -feature (e.g. native list comprehensions or bit-syntax literals) ever warrants it: - -```fsharp -module rec Fable.AST.Beam - -type Atom = Atom of string - -type Literal = - | Integer of int64 - | Float of float - | StringLit of string // binary literal <<"...">> - | AtomLit of Atom - | BoolLit of bool - | NilLit // empty list [] - -type Pattern = - | PVar of string - | PLiteral of Literal - | PTuple of Pattern list - | PList of Pattern list * Pattern option // [H|T] pattern - | PCons of Pattern * Pattern - | PWildcard // _ - | PMap of (Pattern * Pattern) list - -type Guard = Expression list // guard sequences - -type Expression = - | Literal of Literal - | Variable of string - | Tuple of Expression list - | List of Expression list * Expression option // [H|T] - | Map of (Expression * Expression) list - | MapUpdate of Expression * (Expression * Expression) list - | BinOp of BinaryOp * Expression * Expression - | UnaryOp of UnaryOp * Expression - | Call of module_: Expression option * func: Expression * args: Expression list - | Fun of FunClause list // fun(Args) -> Body end - | Case of Expression * CaseClause list - | If of IfClause list - | Receive of CaseClause list * Timeout option - | Try of body: Expression list * catch_: CatchClause list * after_: Expression list - | Block of Expression list // begin ... end - | Match of Pattern * Expression // Pattern = Expr - | ListComprehension of Expression * Qualifier list - | BinaryExpr of BinaryElement list // <<"hello">> - -and CaseClause = { Pattern: Pattern; Guard: Guard; Body: Expression list } -and IfClause = { Guard: Guard; Body: Expression list } -and CatchClause = { Class: Atom option; Pattern: Pattern; Guard: Guard; Body: Expression list } -and FunClause = { Patterns: Pattern list; Guard: Guard; Body: Expression list } -and Timeout = { Duration: Expression; Body: Expression list } - -type BinaryOp = Add | Sub | Mul | Div | IntDiv | Rem | Band | Bor | Bxor | Bsl | Bsr | And | Or | Andalso | Orelse | Append | Subtract -type UnaryOp = Not | Bnot | UAdd | USub -type ComparisonOp = Eq | NotEq | Lt | LtE | Gt | GtE | ExactEq | ExactNotEq - -type Qualifier = - | Generator of Pattern * Expression // X <- List - | BinaryGenerator of Pattern * Expression // <> <= Binary - | Filter of Expression - -type BinaryElement = { Value: Expression; Size: Expression option; TypeSpecifiers: Atom list } - -type Attribute = - | Module of Atom - | Export of (Atom * int) list // function/arity pairs - | Import of Atom * (Atom * int) list - | Behaviour of Atom - | TypeSpec of name: Atom * spec: string // -spec - | CustomAttr of Atom * Expression list - -type FunctionDef = - { Name: Atom - Arity: int - Clauses: FunClause list } - -type Form = - | Attribute of Attribute - | Function of FunctionDef - | Comment of string - -type Module = - { Name: Atom - Forms: Form list } +```text +Async = fun(Context) -> ok end +Context = #{on_success, on_error, on_cancel, cancel_token} ``` -## Sized & Signed Integer Semantics - -> **Status: implemented** using Strategy A (bit-syntax wrapping), described below. -> The wrapping helpers live in `src/fable-library-beam/fable_int.erl` (`wrap_i8`..`wrap_i64`, -> `wrap_u8`..`wrap_u64`). Codegen routes the operations that can leave a type's width — -> `+`, `-`, `*`, `bsl`, negation, `bnot` and narrowing conversions — through them, and masks -> shift counts to the width the way .NET does. `band`/`bor`/`bxor`/`bsr`/`rem` cannot grow an -> in-range value, so they stay bare. `bigint` and the fixed-scale `decimal` are never wrapped. -> -> Unsigned types are represented as their unsigned value (0..2^n-1), not as a signed bit -> pattern, so `UInt64.MaxValue` is `18446744073709551615` and `bsr` is a logical shift. +`RunSynchronously`, `StartImmediate`, `StartWithContinuations`, and sequential +composition execute in the caller's process so process-local mutable state remains +visible. `Async.Parallel`, `Async.StartChild`, and parallel array operations spawn +workers; those workers do not share the caller's process dictionary. -### The Problem +Cancellation tokens use shared `atomics` state and can cross process boundaries. +Registration, cancellation, and `CancelAfter` are serialized by one runtime broker. +`Cancel()` invokes callbacks in the cancelling process; `CancelAfter` uses a +short-lived worker. A cross-process callback must therefore use process-safe state +such as PIDs and messages rather than captured process-dictionary-backed mutables. +Dispose a registration when its source may never cancel, because the broker retains +active callbacks until cancellation or disposal. -.NET has fixed-width integers (`int8`, `int16`, `int32`, `int64`, `uint8`...`uint64`) -with specific overflow/wrapping behavior. Erlang, like Python, has arbitrary-precision -integers — no overflow, no fixed bit width. +`Async.StartChild` and `Async.Parallel` do not implicitly propagate their parent +cancellation token. Pass or capture a token explicitly when a spawned computation +must observe it. -For Fable.Python this required reimplementing all sized integer types in **Rust via -PyO3** (`src/fable-library-py/src/ints.rs`, ~1200 lines) using `wrapping_add`, -`wrapping_sub`, `wrapping_mul`, etc. Plus typed arrays in Rust for the same reason. -This was a major effort. +`Task` uses the same runtime and is not a separate hot-task abstraction. +`MailboxProcessor` also uses in-process CPS and a process-local queue. Real OTP +processes, `gen_server`, supervisors, ETS, and distribution come from external +bindings. -### Erlang Advantage: Bit Syntax +### Formatting and Unicode -Erlang has a feature Python lacks — **binary pattern matching with bit-level -type specifications**. This can express wrapping semantics in pure Erlang: +Strings are UTF-8 binaries. Generated console calls use Unicode-aware Erlang `io` +formats. The generated `main.erl` configures both standard devices; native launchers +must do the same: ```erlang -%% Wrapping int32 arithmetic — pure Erlang, no NIF needed --module(fable_int32). --export([add/2, sub/2, mul/2, from_int/1]). +ok = io:setopts(standard_io, [{encoding, unicode}]), +ok = io:setopts(standard_error, [{encoding, unicode}]). +``` -add(A, B) -> wrap32(A + B). -sub(A, B) -> wrap32(A - B). -mul(A, B) -> wrap32(A * B). -from_int(N) -> wrap32(N). +`%O` receives statically selected converters for values with a custom +`System.Object.ToString()` override. `%A` uses runtime term shape because records, +unions, primitives, and mutable references do not carry complete type metadata. -wrap32(N) -> - <> = <>, - V. +### Reflection -%% Same pattern for all widths: -%% wrap8(N) -> <> = <>, V. -%% wrap16(N) -> <> = <>, V. -%% wrap64(N) -> <> = <>, V. -%% uwrap32(N)-> <> = <>, V. -``` +Generated reflection functions return maps containing full names, generic +arguments, record fields, or union cases. Recursive record/union metadata is lazy. +Field and case entries retain both source names and emitted Erlang atoms. -How it works: +`fable_reflection.erl` implements record, union, tuple, function, and type queries, +plus construction and field/case access. Erased runtime values still limit dynamic +type tests and formatting where two F# types have the same Erlang shape. -1. `<>` — constructs a 32-bit binary, truncating to low 32 bits -2. `<> =` — pattern-matches it back, interpreting as signed -3. Result: correct two's complement wrapping, same as .NET +Runtime `:?` tests for ordinary discriminated unions match the compiled case tags +and exact tuple arities; fieldless cases match their bare atoms. These tests accept +arbitrary terms safely and do not inspect payload fields. Generic arguments and +nominal union identity are erased, so types with identical emitted case tags and +arities cannot be distinguished by these tests. -The BEAM JIT compiler (OTP 24+) optimizes binary operations heavily — this is one -of Erlang's most performance-critical paths (telecom/protocol workloads). +### Interop -### Strategy Options +| F# | Erlang | +| --- | --- | +| `[]` | `lists:map(...)` | +| `[]` | Inline Erlang expression | +| `[]` erased interface | Typed remote calls such as `gen_server:call(...)` | +| `[]` case | Erlang atom | -| Strategy | Effort | Performance | Correctness | -| -------- | ------ | ----------- | ----------- | -| **A. Bit syntax wrapping** (pure Erlang) | Low | Good (JIT-optimized) | Exact | -| **B. Band + sign extension** (pure Erlang) | Low | Good | Exact | -| **C. Rust NIF** (like Python) | High | Best | Exact | -| **D. Wrap at boundaries only** | Low | Best | Risky | +Interop values use their actual Erlang representation. Public APIs that expose +records, unions, options, arrays, or mutable objects therefore depend on the target +ABI described above. -**Recommendation: Strategy A (bit syntax)** for initial implementation. +## Runtime coverage -- No NIF compilation needed — pure Erlang, trivial to deploy -- Correct by construction (bit truncation + signed reinterpretation) -- If profiling shows it's a bottleneck, can move to NIF later -- Much simpler than the Python/PyO3 approach (~50 lines vs ~1200 lines) +| Area | Runtime modules | +| --- | --- | +| Core values | `fable_option`, `fable_result`, `fable_convert`, `fable_comparison`, `fable_utils` | +| Collections | `fable_list`, `fable_seq`, `fable_map`, `fable_set`, `fable_resize_array`, `fable_dictionary`, `fable_hashset`, `fable_queue`, `fable_stack` | +| Numerics | `fable_int`, `fable_decimal`, `fable_bit_converter`, `fable_random` | +| Text | `fable_string`, `fable_char`, `fable_regex` | +| Date and time | `fable_date`, `fable_date_offset`, `fable_date_only`, `fable_time_only`, `fable_timespan`, `fable_stopwatch` | +| Effects | `fable_async_builder`, `fable_async`, `fable_cancellation`, `fable_mailbox`, `fable_parallel` | +| Events | `fable_observable`, `fable_event` | +| Metadata | `fable_reflection`, `fable_quotation` | +| Platform values | `fable_guid`, `fable_uri`, `fable_environment` | -Strategy B alternative using `band`: +## Validation -```erlang -%% int32 via bitwise masking -wrap32(N) -> - R = N band 16#FFFFFFFF, - case R >= 16#80000000 of - true -> R - 16#100000000; - false -> R - end. -``` +Fast iteration: -Both A and B are pure Erlang. A is more idiomatic and arguably clearer. +```sh +./build.sh quicktest beam +``` -### Wrapping in Generated Code +Build the runtime library: -Two approaches for where to wrap: +```sh +./build.sh fable-library --beam +``` -**Wrap every operation** (safe, slower): +Run the complete target validation: -```erlang -%% F#: let x = a + b * c -X = fable_int32:add(A, fable_int32:mul(B, C)) +```sh +./build.sh test beam ``` -**Wrap at assignment only** (faster, still correct for most code): +The full command: -```erlang -%% F#: let x = a + b * c -X = fable_int32:wrap(A + B * C) -``` +1. runs the F# tests on .NET; +2. builds `fable-library-beam`; +3. transpiles `tests/Beam/` to `temp/tests/Beam/`; +4. compiles the generated project with rebar3; +5. runs every exported `test_*` function through `erl_test_runner`; +6. compiles and runs entry-point fixtures through the generated `main.erl` shim. -The second is valid when intermediate overflow doesn't cross the 64-bit boundary -(extremely rare in practice). Start with wrap-every-op for correctness, optimize -later with a compiler flag if needed. +The passing-test count is intentionally omitted because it changes whenever the +suite grows. -### Parsing +## Known limitations -.NET integer parsing (`Int32.Parse`, `Int32.TryParse`) with `NumberStyles` support -needs implementation. In Erlang: +| Area | Current behavior | +| --- | --- | +| Union ordering | Generic comparison, nested union fields, and union keys/elements in `Map`/`Set` can use Erlang atom order instead of declaration order. | +| Options | Erasure can still conflate `None`, `Some null`/`Some undefined`, and some nested option paths after static type information is lost. | +| `char` | A generic or `obj`-erased character is an integer at runtime, so `string` and `%A` can print its codepoint. UTF-16 surrogate behavior is not complete. | +| Structured formatting | `%A` reconstructs values from term shape. Record field order, original names, erased options, sets, chars, refs/arrays, decimals, and date/time values can differ from .NET output. | +| Identifiers | Property accessors and ordinary `Get`/`Set` methods have distinct generated names. Other distinctions lost by sanitization, such as `work` versus `Work`, can still collide; detected name/arity collisions fail compilation. | +| Type tests | Erlang cannot distinguish integer widths or unrelated F# types with the same runtime shape. Some interface/class downcasts and abstract/base dispatch paths are unsupported. | +| Classes and structs | Mutable record fields, class reference equality, some self-referencing/base constructors, mutually recursive class hierarchies, and default struct construction remain incomplete. | +| Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | +| Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | +| Collection comparers | `Dictionary` and `HashSet` constructors ignore custom `IEqualityComparer` instances and use native structural keys. | +| Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | +| Numeric APIs | Special floating-point values need parity work. | +| Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | +| Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | +| Recursive values | Recursive value bindings that lower through `Lazy` and some inline module-value side effects are incomplete. | +| Cancellation | Cross-process callbacks cannot safely mutate captured process-local values. Callback exceptions are suppressed, and `CancellationTokenSource.Dispose()` remains a no-op. | + +For `char` conversion in generic code, making the function `inline` or using a +concrete `char` annotation keeps the type available at the call site. Other entries +above have no general source-level workaround and should remain visible in tests. + +## Related projects + +- [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) — typed Erlang/OTP bindings +- [Fable.Actor](https://github.com/fable-hub/Fable.Actor) — actor model on the bindings +- [Gleam](https://github.com/gleam-lang/gleam) — typed language compiling to Erlang +- [Caramel](https://github.com/AbstractMachinesLab/caramel) — OCaml to Erlang compiler +- [LFE](https://github.com/lfe/lfe) — Lisp on the BEAM + +## Roadmap + +The CLI reports the target as alpha. The next steps are ordered by semantic risk, +not by the age of the feature. Beta means that the documented supported surface is +reliable enough for broader use; it does not require complete .NET parity. This is +the same maturity model used by the established targets: target-specific behavior, +unsupported APIs, and disabled parity cases can remain when they are deliberate and +visible rather than silent correctness failures. + +### Correctness priorities for beta + +| Priority | Gap | Suggested direction | +| --- | --- | --- | +| P0 | Union declaration ordering is incomplete | Thread union-aware comparers through nested comparison and ordered collections. If type-directed routing cannot cover generic containers, define a versioned DU/collection representation change. | +| P0 | Option erasure loses states in generic and null-like paths | Carry the nested-option decision through replacements and collection helpers, or adopt an unambiguous tagged form where erasure is unsafe. | +| P0 | Module initialization is process-dependent | Define library initialization semantics. Prefer explicit generated initialization invoked by entry points/process owners; use global storage only if cross-process mutation is intentionally supported. | +| P0 | Object-model gaps affect valid F# | Fix silent wrong-code paths in the claimed object-model surface. Keep unsupported class and struct forms as explicit exclusions until implemented. | +| P0 | Numeric APIs have correctness gaps | Fix incorrect results in claimed numeric APIs. Missing APIs can remain documented exclusions; restore regression tests as implementations land. | -```erlang -parse_int32(Str) -> - N = binary_to_integer(Str), - case N >= -2147483648 andalso N =< 2147483647 of - true -> {ok, N}; - false -> {error, overflow} - end. - -parse_int32(Str, 16) -> - N = binary_to_integer(Str, 16), - wrap32(N). -``` +### Fidelity and diagnostics + +| Priority | Gap | Suggested direction | +| --- | --- | --- | +| P1 | Disabled or commented parity cases are not an auditable support boundary | Remove stale skips whose underlying defect is fixed. Track relevant remaining cases as tests, documented exclusions, or linked issues without requiring complete parity for beta. | +| P1 | `%O`, `%A`, interpolation, and `String.Format` have separate type-information needs | Build one compiler-generated argument-slot plan that records value, width, printer, and thunk arguments plus optional static formatters. Reuse it across all formatting entry points. | +| P1 | Runtime shapes cannot distinguish several F# types | Pass compact type descriptors or generated recursive formatters at typed call sites. Treat self-describing record/union values as a versioned ABI option, not an incidental formatting patch. | +| P1 | Type tests and downcasts are shape-based | Add compact type tokens only where F# semantics require nominal identity; keep ordinary data representations untagged where possible. | +| P1 | Warnings do not define the supported surface | Document intentional deviations, make unsupported features actionable diagnostics, and test diagnostic text and source ranges. | +| P1 | OTP/version support is a single minimum statement | Run CI against OTP 25 and the current supported OTP release; publish the tested range. | + +### Performance and operability -### What About Arrays? - -For Python, arrays also required Rust/PyO3 typed storage (`Int32Array`, -`Float64Array`, etc.) because Python lists box every element. - -Erlang is better here: - -- **Erlang tuples** — fixed-size, O(1) element access via `element(Index, Tuple)`, - but immutable (copy-on-update via `setelement/3`) -- **Erlang `array` module** — functional sparse arrays, O(log n) access, good for - large mutable-style arrays -- **ETS tables** — true mutable storage, O(1) access, but heavier setup -- **`atomics` module** (OTP 21+) — mutable integer arrays in shared memory, - excellent for `int32[]` / `int64[]` use cases - -Suggested approach: - -- Small/read-heavy arrays → **tuples** (fast reads, copies on write) -- General case → **`array` module** (functional updates, good enough perf) -- Hot-path mutable int arrays → **`atomics`** (true O(1) mutable access) -- No Rust NIF needed — Erlang's built-in options cover the use cases - -### Summary: Much Simpler Than Python - -| Concern | Python Solution | Erlang Solution | -| ------- | --------------- | --------------- | -| Sized integers | Rust/PyO3 (~1200 lines) | Bit syntax (~50 lines pure Erlang) | -| Integer parsing | Rust/PyO3 (~100 lines) | `binary_to_integer` + bounds check | -| Typed arrays | Rust/PyO3 typed storage | Tuples / `array` module / `atomics` | -| Deployment | Needs Rust toolchain + compilation | Pure Erlang, no external deps | - -This is a significant advantage of targeting BEAM over Python. The bit syntax -alone eliminates the single hardest piece of the Fable.Python runtime. - -## Decisions Made - -- **Strings**: Erlang binaries (`<<"hello">>`) — modern convention, more efficient -- **Module naming**: Snake_case derived from filename (`MyModule.fs` → `my_module`) -- **Replacements strategy**: Beam has its own dispatch in `Replacements.Api.fs` with - JS fallback (`Beam.Replacements.tryCall` → `JS.Replacements.tryCall` if `None`). - Beam handles equality, comparison, numerics, collections, and conversions natively; - only operations that genuinely work the same way fall through to JS. All 15 - `Replacements.Api.fs` functions now have explicit Beam dispatch — `error` returns - the message directly (wrapped by `makeThrow`), `defaultof` returns type-appropriate - zero values, and ref cell operations use the process dictionary (`make_ref` + - `put`/`get`). -- **File structure**: Single `Fable2Beam.fs` for Phase 1 (PHP pattern), split later - as complexity grows (Python pattern) -- **DU representation**: Atom-tagged tuples `{atom_tag, Field1, ...}` for cases with - fields, bare atoms for fieldless cases. Tag names derived via `sanitizeErlangName` - (snake_case). `UnionCaseTest` guards non-fieldless checks with `is_tuple` to handle - mixed DUs. `UnionTag` uses `case is_atom(X)` to dispatch between bare atoms and tuples. -- **If/else**: Uses `case Guard of true -> ...; false -> ... end` rather than - Erlang's limited `if` (which only supports guard expressions) -- **Division**: `div` for integer types, `/` for float — determined by Fable's - type information at compile time -- **Comparison operators**: Erlang's exact equality (`=:=`, `=/=`) rather than - structural (`==`, `/=`), matching F#'s value equality semantics -- **DecisionTree**: Follows the JS (Babel) target pattern — inline targets with - Let bindings, producing nested case expressions -- **Test framework**: xUnit with `[]` attributes (matching Python/Rust targets), - conditional compilation via `Util.fs` for future BEAM-side test execution -- **Records**: Erlang maps (`#{field => value}`), field names as snake_case atoms. - Field access via `maps:get/2`, field update via `maps:put/3`. -- **Structural equality**: Erlang's native `=:=` for all types (no runtime library). - Intercepted in `Beam/Replacements.fs` before JS Replacements generates `Util.equals` - library calls. Works because `=:=` does deep comparison on tuples, maps, and lists. -- **Function name sanitization**: `sanitizeErlangName` in `Prelude.fs` decodes - `$XXXX` hex sequences from F# compiled names (e.g. `$0020` → space), strips - apostrophes, converts to snake_case, collapses/trims underscores, and escapes - Erlang reserved words (e.g. `maybe` → `maybe_`, `receive` → `receive_`). The - keyword escaping uses `checkErlKeywords` against the full OTP 25+ keyword set. - Example: `test$0020infix$0020add$0020can$0020be$0020generated` → `test_infix_add_can_be_generated` -- **Output filenames**: Snake_case, following the Python target pattern. Uses - `Naming.applyCaseRule Core.CaseRules.SnakeCase` in `Pipeline.fs` Beam module. - Erlang requires module name to match filename, so `ArithmeticTests.fs` → - `arithmetic_tests.erl` with `-module(arithmetic_tests).` -- **Library output layout**: Aligned with JS/Dart/Rust targets — library files go to - `fable_modules/fable-library-beam/` under the output directory, not mixed with compiled - project files. `ProjectCracker.fs` uses non-empty `buildDir` to trigger the standard - `copyDir` mechanism. `getOutPath` in `Main.fs` checks `Naming.isInFableModules` to preserve - the subdirectory structure for library files while keeping project files flat in `outDir`. - Erlang resolves modules via code path (`-pa fable_modules/fable-library-beam`) rather than - hierarchical imports. Third-party project output structure: - `output/my_module.erl` + `output/fable_modules/fable-library-beam/{fable_list,fable_string,seq,...}.erl`. -- **Assertions**: `Fable.Core.Testing.Assert.AreEqual`/`NotEqual` lower to - `fable_utils:assert_equal`/`assert_not_equal`, which raise - `#{message, actual, expected}` on failure, matching JS and Python. They used to be - inlined at any call site whose import selector happened to be named `assertEqual`/ - `Testing_equal` (and the `NotEqual` variants), which rewrote the body of *any* - same-named user function; that special case is gone. -- **Unit parameters**: Erlang unused variable warnings suppressed by prefixing unit - parameters with `_` via `toErlangVar` in `Fable2Beam.fs`. -- **discardUnitArg / dropUnitCallArg**: Symmetric unit stripping matching JS/Python/Dart. - `discardUnitArg` strips trailing unit parameters from function definitions (Lambda, - Delegate, ObjectExpr members, MemberDeclaration, class methods/constructors). - `dropUnitCallArg` strips the corresponding unit argument at call sites (in `transformCall`). - Both sides must be stripped symmetrically so Erlang arity matching works. The old - workaround of appending `Value(UnitConstant)` in `bclType` was removed. -- **Block hoisting**: When `Let` bindings produce `Block [Match(...); body]` expressions, - these Blocks are invalid inside Erlang argument positions (Call, Apply, BinOp). The - `extractBlock` / `hoistBlocksFromArgs` / `wrapWithHoisted` helpers extract leading - assignments and hoist them before the enclosing expression. This fixes - match-in-expression patterns like `match x with ... |> equal "abc"`. -- **Recursive lambdas**: Self-recursive `let rec f x = ... f (x-1) ...` inside function - bodies generates Erlang named funs: `fun F(X) -> ... F(X-1) ... end` (OTP 17+). - Detected via `containsIdentRef` which checks if a lambda body references its own - binding ident. Mutual recursion (`let rec ... and ...`) is supported by bundling the - group into a single named fun dispatched by atom tag (`MutualRecBindings`). -- **String interpolation**: `$"text: {value}"` generates - `iolist_to_binary([<<"text: ">>, integer_to_binary(Value)])`. Integer values use - `integer_to_binary/1`, string values pass through, other types use - `io_lib:format("~p", [Value])`. Note: `io_lib:format` needs a charlist format string, - not a binary — uses `binary_to_list(<<"~p">>)` to convert. String concatenation from - Fable's Replacements (`string:concat`) is intercepted and replaced with - `iolist_to_binary([A, B])` since `string:concat` returns charlists, not binaries. - `to_string` conversion lives in `fable_string:to_string/1`. -- **Option representation**: `None` = `undefined` atom. Simple `Some(x)` is **erased** - (just `x`). Nested options (`Option>`), `GenericParam`, and `Any` types use - **wrapped** representation: `Some(x)` = `{some, x}`. This avoids ambiguity when - `Some(None)` would otherwise be indistinguishable from `None`. Runtime smart constructor - `fable_option:some/1` handles wrapping at generic call sites. Unlike JS/Python, `Unit` - does NOT need wrapping because Erlang's `ok` atom is distinct from `undefined`. -- **Object expressions / Interfaces**: `{ new IFoo with member _.Bar(x) = ... }` compiles - to an Erlang map of closures: `#{bar => fun(X) -> ... end}`. Property getters are stored - as evaluated values (not closures) since call sites use `Get(obj, FieldGet(name))` → - `maps:get(name, Obj)`. Interface method calls use `(maps:get(method, Obj))(Args)`. - Detection: `transformCall`'s `Get(calleeExpr, FieldGet, _, _)` branch checks if - `calleeExpr.Type` is a `DeclaredType` with `entity.IsInterface`. Self-referencing - members (e.g., `member x2.Test(i) = x2.Value - i`) are supported: when any member - references its `this` arg, the object is built behind a process-dict ref - (`ObjRef_N = make_ref()`, aliased to each self-ident, `put` the closure map, return the - ref) so closures can reach the object under construction. -- **ImportAll + Erase interface**: `[]` + `[]` interface pattern - for typed FFI bindings. `myModule.someMethod(args)` → `module:some_method(Args)`. - Detected in both `transformCall` (method calls) and `transformGet` (property access) by - matching `calleeExpr` as `Import` with `Selector = "*"`. Emits direct Erlang remote calls - instead of `fable_utils:iface_get` dispatch. Method names are converted via - `sanitizeErlangName` (camelCase → snake_case). Same pattern as JS/Python but with `:` call - syntax instead of attribute access. -- **`[]` → atoms**: `[]` means "a closed set of string-literal constants - for interop". On JS each case lowers to a string literal because that is what a JS API expects; - the Beam analogue is an **atom**, not a binary — the OTP functions such a binding targets (ETS - table types, `logger` levels, transport names, `gen_server` name registration) pattern-match - atoms and reject binaries. So on Beam a case compiles to a bare atom, quoted when the name is - not valid unquoted atom syntax (`[]` → `'Horizontal'`, - `CaseRules.KebabCase` → `'content-box'`). This makes `[]` and a plain nullary DU - equivalent on Beam — both are bare atoms — which is the intended end state. Consequences: - - `[]` cases are genuine bool/int/float constants rather than tags, - and keep their literal. An explicit `[]` on a case still wins. - - The type no longer maps to `Fable.String` on Beam (`makeType` in `FSharp2Fable.Util.fs`), so - `string x` routes through `fable_convert:to_string` (giving the atom's text as a binary) - instead of erasing to a no-op that would hand an atom to code expecting a binary. Ordered - comparison also becomes declaration-order correct, since union values now reach - `compare_union` (see the DU ordering section above). - - `[]` unions are deliberately *not* included: an erased case with no fields keeps its - binary. `[]` means "no runtime representation", not "a constant tag". - -- **Async/Task CE**: CPS (Continuation-Passing Style) implementation. `Async` is a function - `fun(Ctx) -> ok end` with context map `#{on_success, on_error, on_cancel, cancel_token}`. - No trampoline needed (Erlang has native TCO). `RunSynchronously` runs in same process - (not spawned) to preserve process dict access for mutable variables. `Parallel` spawns one - process per computation and collects results via message passing. Task CE is an alias — - Task builder methods route to `fable_async_builder`, `.Result` routes to `run_synchronously`. - Replacements routing: `FSharpAsyncBuilder`/`AsyncActivation` → `asyncBuilder`; - `FSharpAsync`/`AsyncPrimitives` → `asyncs`; `TaskBuilder`/`TaskBuilderBase` → `taskBuilder`; - `Task`/`Task` → `tasks`. `DefaultAsyncBuilder` in operators → `fable_async_builder:singleton`. -- **NewArray block hoisting**: `NewArray(ArrayValues ...)` uses `hoistBlocksFromArgs` + - `wrapWithHoisted` to hoist Let bindings out of array literal positions, matching the - pattern used for Call/Apply/BinOp arguments. -- **sprintf / printfn / String.Format**: Full F# format string support via `fable_string.erl` - runtime. `printf/1` parses format strings (`%d`, `%s`, `%.2f`, `%g`, `%x`, etc.) into a - continuation-based format object `#{input, cont}`. `to_text` (sprintf), `to_console` - (printfn), `to_console_error` (eprintfn), `to_fail` (failwithf) apply continuations with - appropriate handlers. Multi-arity overloads (`to_text/1..5`) handle Fable's inlined arg - passing where `CurriedApply` flattens curried args into a single call. `format/2` handles - .NET `String.Format("{0} {1}", args)` with positional placeholders. Replacements routing: - `fsFormat` function handles `PrintfFormat.ctor` (→ `printf`), `PrintFormatToString` - (→ `to_text`), `PrintFormatLine`/`PrintFormat` (→ `to_console`), etc. Dispatched from both - `operators` (for `ExtraTopLevelOperators.sprintf`) and `tryCall` (for `PrintfModule`/ - `PrintfFormat` entities). The old `toConsole` → `io:format("~s~n")` hack in Fable2Beam.fs - was removed. -- **MailboxProcessor**: In-process CPS continuation model, NOT gen_server. Agent state lives - in process dict keyed by `make_ref()`: `#{body, messages, continuation}`. `receive_msg` - uses `from_continuations` to store OnSuccess as pending continuation; `post` adds to queue - and calls `process_events`; `post_and_async_reply` creates a reply channel map - `#{reply => Fun}` where Fun stores the reply value in process dict, and the synchronous - CPS execution guarantees the value is available when `post` returns. `Reply` is dispatched - via `emitExpr` as `(maps:get(reply, $0))($1)`. Replacements route - `FSharpMailboxProcessor` and `FSharpAsyncReplyChannel` to the `mailbox` handler. -- **CurriedApply**: Uses a simple `List.fold` applying args one at a time: - `cleanArgs |> List.fold (fun fn arg -> Apply(fn, [arg])) cleanApplied`. This matches - JS (Fable2Babel.fs) and Python (Fable2Python.Transforms.fs). Never merge CurriedApply - args into Call nodes — that causes badarity errors when calling curried closures. -- **Erlang keyword escaping**: `sanitizeErlangName` pipes through `checkErlKeywords` at - the end to append `_` suffix to Erlang reserved words (e.g. `maybe` → `maybe_`, - `receive` → `receive_`). The `erlKeywords` set covers OTP 25+ keywords including - `maybe` and `else`. This is needed because F# identifiers like `maybe` are valid but - would generate Erlang syntax errors. -- **Arrays as process-dict refs**: All non-byte arrays are now process-dict refs - (`make_ref()` + `put/get`), enabling cross-function mutation. `NewArray` wraps with - `fable_utils:new_ref([...])`. `derefArr`/`wrapArr` helpers in Replacements convert - between refs and plain lists for bulk operations. Binary comparison operators on arrays - use `fable_comparison:compare(A, B) op 0`. TypeTest for arrays uses `is_reference`. - When an array *literal* flows directly into an FFI/Emit binding that derefs its - argument (e.g. `maps:from_list(erlang:get($0))`), the naive output is - `...erlang:get(fable_utils:new_ref([...]))` — a pointless process-dict round-trip - on an immutable literal (it also leaks an un-erased process-dict entry). - `simplifyArrayRefDerefs` in `Fable2Beam.fs` cancels it on the Beam AST when - building an `Emit` node: if an argument is an inline `new_ref(list)` and *every* - occurrence of its `$N` placeholder in the macro template is deref-wrapped - (`erlang:get($N)`/`get($N)`), the deref is dropped from the template and the - underlying list is passed directly. Gating on the argument's AST shape (rather than - parsing rendered Erlang) keeps it robust; a ref-typed *variable* (a bound/mutable - array) keeps its deref. -- **Byte arrays via atomics**: Byte arrays (`Array`) use Erlang's `atomics` module - for true O(1) mutable read/write. Represented as `{byte_array, Size, AtomicsRef}` tuples. - Runtime helpers in `fable_utils.erl` handle both direct tuples and process-dict - ref-wrapped byte arrays: `byte_array_get/2`, `byte_array_set/3`, `byte_array_length/1`. - `Array.zeroCreate` uses `new_byte_array_zeroed` (atomics are zero by default). - `Array.create n v` uses `new_byte_array_filled` (avoids intermediate list allocation). - This enabled a 2048x2048 raytracer demo to run at ~40s on BEAM with no hand-patches. -- **Mutable variable ref erasure**: Process dict refs for mutable `let` bindings are erased - at scope exit (`erase(Key)`) to prevent leaks. The `EraseMutableRefs` expression is emitted - at the end of function bodies and appended to Block expressions containing mutable bindings. -- **Stray ok atom removal**: The ErlangPrinter strips bare `ok` atoms (F# unit values) from - non-final positions in all body contexts: top-level functions, fun/named-fun clauses, case - clauses, try/catch bodies, and block expressions. Matches both `Literal(AtomLit "ok")` and - `Emit("ok", [])` forms (the latter from `System.Object..ctor`). -- **CancellationToken**: Process dict pattern with `make_ref()` storing - `#{cancelled, listeners, next_id}`. Supports `create`, `cancel`, `cancel_after` (timer-based), - `register`/`unregister`, `is_cancellation_requested` (drains cancel messages from mailbox). - Sleep integration uses `receive` instead of `timer:sleep` when token is present. -- **Stopwatch**: Runtime `fable_stopwatch.erl` using `erlang:monotonic_time(microsecond)`. - Supports `StartNew`, `Start`, `Stop`, `Reset`, `Restart`, `Elapsed`, `ElapsedMilliseconds`, - `IsRunning`, `Frequency`, `GetTimestamp`. -- **FSharp.Reflection**: Full runtime support via `fable_reflection.erl` + compile-time type - info generation in `Fable2Beam.Reflection.fs`. TypeInfo = Erlang map with `fullname`, - `generics`, and optional `fields` (records) or `cases` (unions). PropertyInfo/CaseInfo are - maps with `name`, `typ`, `tag`, `fields`. Reflection functions renamed to avoid Erlang BIF - clashes: `is_tuple` → `is_tuple_type`, `is_function` → `is_function_type`. Union tag matching - uses atom tags via `erl_tag` field (matching Beam's `{atom_tag, Field1, Field2}` representation). `MakeRecord`/ - `MakeUnion` handle both plain lists and process-dict ref arrays. `GetRecordFields` resolves - concrete types through `TypeCast` wrappers via `getConcreteType` helper. -- **Async error wrapping**: `wrap_error/1` in `fable_async.erl` normalizes raw errors so - `.Message` accessor works: raw binaries (from `failwith`) → `#{message => Bin}`, maps - (from `raise (exn ...)`) pass through, refs pass through, everything else → formatted map. - Used in `catch_async`, `try_with`, and `try_finally`. `catch_async` uses atom tags - `{choice1_of2, V}` / `{choice2_of2, wrap_error(E)}` matching Beam's Choice union representation. -- **OperationCanceledException**: Added to the exception type pattern in Beam Replacements - alongside `BuiltinSystemException` and `KeyNotFoundException`. -- **Module-level mutable variables**: `let mutable x = v` at module level is routed through - the process dictionary (same mechanism as local mutable `let` bindings). The process-dict - key is namespaced by the emitting Erlang module name — `_` (e.g. `x` in module - `foo` → key `foo_x`) — so identically-named mutables in different modules don't collide in - the shared, process-local process dictionary. The key is built by the single `mutableStateKey` - helper (`Fable2Beam.Util.fs`) and used at every site so reads, writes and the initialiser - always agree. The declaration emits a `main/0` fragment that initialises the value - (`put(foo_x, v)`); reads of the ident emit `get(foo_x)` and writes (`x <- e`) emit - `put(foo_x, e)` (see the `IdentExpr`/`Set` branches and the `MemberDeclaration` value case in - `Fable2Beam.fs`). All `main/0` fragments — mutable inits, - snapshot inits (below), and `do` actions — are merged in declaration order, so module - initialisation runs as a single ordered sequence, mirroring F#. A value initialiser that - lowers to a multi-statement block (e.g. it contains a `let`) is stored as `put(x, )` - using the *whole* block (its final expression is the value), and is wrapped in an - immediately-invoked `fun` so its local Erlang variables stay isolated — Erlang `begin...end` - does not introduce a scope, so two initialisers reusing the same local name would otherwise - clash in the shared `main/0` clause. - - **Snapshotting immutable values that read a mutable**: an *immutable* module value whose - initializer reads a module-level mutable (e.g. `let c = topA`) must capture the value at - binding time, because F# evaluates module bindings once, in order, before any later - reassignment. Compiled naively as a lazy 0-arity accessor it would re-read the *live* - process-dict value and observe later writes. Such values are therefore also eagerly - initialised in `main/0` (`put(foo_c, )`, using the same module-namespaced key and - emitted in declaration order so it captures the mutable's value at that point) plus an - accessor — named by the bare value name — that reads the snapshot (`c() -> get(foo_c)`). - Detection is `readsFreeMutable`, which walks the Fable body tracking locally - bound names and triggers only on a *free* (module-level) mutable reference — - self-contained bodies whose only mutables are local stay lazy, so they don't gain a - spurious dependency on `main/0`. - - **Module init runs before tests**: the Erlang test runner (`erl_test_runner.erl`) calls - `test_*/0` functions directly and would never run `main/0`, so module-level - initialisation (mutable inits and `do` actions) would never execute and reads would - return `undefined`. The runner now invokes each module's `main/0` (if exported) before - its tests, mirroring .NET module initialisation (which runs before any module code). - This is safe because Beam test modules contain no top-level side effects beyond these - initialisers. -- **Function reference identity** (`LanguagePrimitives.PhysicalEquality` / `ReferenceEquals`): - the *same* F# function value is represented by different Erlang funs at different sites — - uncurried (a single N-arity fun), re-curried (`fun(A0) -> fun(A1) -> F(A0, A1) end end`, from a - `Curry` node), or wrapped in an uncurrying (eta) adapter to fill an uncurried slot. Erlang - compares funs by closure identity, so these are never `=:=` even though they denote one value — - which made `PhysicalEquality` wrongly return `false` (e.g. a stored callback could never be - removed by identity). Fix: both adapter kinds are built through tagged helpers in `fable_utils` — - `make_curry/2` (routed from the `Curry` node in `Fable2Beam.fs`) and `make_eta/2` (routed from - the `EtaAdapterDelegate` pattern) — each capturing an underlying `F` inside a - `{fable_curry_adapter, F, N}` / `{fable_eta_adapter, F, N}` marker as the outermost fun's ONLY - captured variable. `fun_ref_eq/2` then peels those markers via `erlang:fun_info(F, env)` before - `=:=`, normalising every representation of a value to the same `F`. Both helpers apply `F` exactly - as the default lowering would (`make_eta` via `apply_curried`, `make_curry` via `erlang:apply` — - all args at once, matching the multi-arg `Apply`), so they are behaviour-identical bar the marker. - Key points: - - **Adapter cancellation** (mirrors the Python target's `curry.py` memoization, where - `curry(uncurry(g))` returns the cached original `g`): if `make_eta`/`make_curry` receives a - value that is already the *opposite*, same-arity adapter, it returns the original underlying - fun instead of stacking a second wrapper — `uncurry ∘ curry = curry ∘ uncurry = id`. A - round-tripped value is then literally the *same* fun (native `=:=` holds with no unwrap and no - wrapper cost), and adapters never accumulate. The marker travels inside the closure env, so - unlike Python's process-local `WeakKeyDictionary` this survives a fun being sent between - processes. The arity in the marker guards the cancellation (a mismatch wraps instead of - cancelling — no `badarity`). `unwrap_fun` still covers the non-round-trip case (a value stored - in one representation and compared against the other). - - This is **precise reference identity, not structural equality**: only Fable's own - compiler-generated adapters carry a marker, so a hand-written eta expansion (`fun x -> g x`) - or a partial application (which captures the collected args, so its outermost fun has >1 - captured variable) is left intact and stays distinct — matching .NET. An earlier version used - a *shape heuristic* (unwrap any 1-arity fun capturing a single function) which wrongly - collapsed `fun x -> g x` onto `g`; the marker approach replaces it. Regression-guarded by - `test PhysicalEquality distinguishes eta expansion from the original`. - - **Arity cap 2..7** (matching `make_eta`). Functions with 8+ curried args fall back to the - default lowering and are not identity-preserved; extend the `make_curry_marked`/ - `make_eta_marked` clauses if needed. - - **Generic call sites**: routing to `fun_ref_eq` is gated on the operand being a - `LambdaType`/`DelegateType` at the call site (`isFunctionType` in `Beam/Replacements.fs`). A - value typed as a bare generic `'T` (Fable does not monomorphise) falls back to plain `=:=`, so - wrapping `PhysicalEquality` in a generic helper does not get the function-aware path. - - **Correctness over performance**: `make_curry` sits on the pervasive currying path and adds a - marker allocation + `erlang:apply` per call versus the inline nested lambdas — but this is the - same shape of incremental-application cost the Python target already pays (`_curry_n` / - `_uncurry_n`), and adapter cancellation removes it entirely on round-trips. Any residue is an - accepted alpha-stage trade-off — a passing unit test wins over the micro-cost. - -## Future Improvements - -### Module-level mutable state: per-process, requires `main/0` - -Module-level mutables live in the **process dictionary** and are initialised by `main/0`. -That makes their semantics correct only when `main/0` actually runs in the process that later -reads the state: - -- **Works**: entry-point programs (the `main/0` Fable emits is the program entry — quicktest, - real apps) and the test harness (now calls `main/0` before each module's tests). -- **Does not work**: a *library* module whose functions are called by other code without that - module's `main/0` having run — its module-level mutables/snapshots read `undefined`. Reads - from a **different process** than the one that ran `main/0` also see `undefined`, since the - process dictionary is process-local. - -Keys are namespaced by module (`_`), so multiple modules' mutables with the same -name running in the **same** process no longer collide — the remaining limitation is purely the -process-locality above, not naming. - -This matches the broader Beam design (mutation is single-process by design; see "Class instance -representation" and "Mutable Collections" above), but it means module-level mutable global state -is not a fully general feature. Follow-up options if true cross-process / load-time module state -is ever needed: - -- **`persistent_term`** + `-on_load` for global, load-time initialisation (reads visible from - any process). Downside: writes are global-GC-heavy and meant for write-rarely data, so - frequently-mutated module values would be slow. -- **ETS** table per module for shared mutable state — O(1) writes, but cross-process shared - state, against the isolation model. - -Neither is implemented; the current per-process approach is the right default for typical F# -programs where module-level mutables are entry-point/program state. - -### Mutable Collections: Process Dict vs ETS - -Currently, `Dictionary`, `HashSet`, `ResizeArray`, and `Array` use the process dictionary -pattern: `make_ref()` + `put(Ref, EntireCollection)` / `get(Ref)`. Every mutation replaces -the entire map or list — adding one key to a 10,000-entry Dictionary copies all 10,000 -entries (O(N) per mutation). - -**ETS (Erlang Term Storage)** is an alternative that provides O(1) in-place mutable tables. -However, the current process dict approach is arguably the **better design** for BEAM: - -- **Process isolation preserved**: Mutations stay within a single process, matching the - BEAM philosophy of isolated processes with no shared mutable state -- **No accidental sharing**: ETS tables are cross-process by default, which would break - the isolation model that makes BEAM reliable -- **Simple cleanup**: `erase(Ref)` at scope exit vs explicit `ets:delete(Tab)` with no - finalizers to ensure cleanup -- **Small collections are fine**: Most F# code uses small-to-medium collections where - the full-copy overhead is negligible - -| Aspect | Process Dict (current) | ETS | +| Priority | Gap | Suggested direction | | --- | --- | --- | -| Read | O(1) `get(Ref)` + O(1) `maps:get` | O(1) `ets:lookup` | -| Write | O(N) full map copy via `put` | O(1) `ets:insert` | -| Process isolation | Yes (process-local) | No (shared by default) | -| Small collections | Fast (no table overhead) | Slower (table creation cost) | -| Large collections | Slow (full copy per mutation) | Fast (in-place mutation) | -| Cleanup | `erase(Ref)` | `ets:delete(Tab)` (must be explicit) | - -**Conclusion**: The process dict approach is the right default. ETS could be offered as -an opt-in optimization (e.g., via an attribute) for specific cases where large collection -mutation performance is critical, but it should not be the default since it introduces -shared mutable state — exactly what BEAM is designed to avoid. - -## Open Questions - -- ~~**Records vs Maps**: Erlang records are compile-time tuples (fast, but rigid). - Maps are dynamic (flexible, slower). For F# records, maps seem more natural.~~ - **Decided**: Erlang maps. Field names as snake_case atoms, `maps:get/2` for access, - `maps:put/3` for update. Structural equality via native `=:=`. -- **OTP project structure**: Generate a full OTP application structure with - `rebar3`? Or just standalone `.erl` files initially? -- ~~**Interop**: How should F# code call existing Erlang/Elixir libraries? - Fable.Core attributes like `[]`?~~ - **Decided**: Three interop mechanisms: (1) `[]` for individual - function imports → `module:func(Args)`, (2) `[]` for inline - Erlang expressions, (3) `[]` + `[]` interface for typed - module bindings → `module:method(Args)`. The ImportAll pattern mirrors JS/Python but - emits Erlang remote calls instead of attribute access. -- ~~**Testing**: Use EUnit, Common Test, or just assert in generated code?~~ - **Decided**: xUnit with `[]` on .NET side, `Fable.Core.Testing.Assert` - when compiled to BEAM. Same pattern as Python/Rust targets. -- **Integer wrapping granularity**: Wrap every arithmetic operation, or only at - let-binding boundaries? Former is safer, latter is faster. -- ~~**Function name encoding**: Erlang atom names from double-backtick F# names - currently produce URL-encoded names (e.g. `test$0020_add`). Need to decide - on a cleaner encoding or stick with snake_case conversion.~~ - **Decided**: `sanitizeErlangName` decodes `$XXXX` hex sequences, strips apostrophes, - converts to snake_case, collapses underscores. See "Decisions Made" above. - -## Prior Art - -- **Caramel** — OCaml → Erlang compiler (abandoned, but useful reference for - ML-to-BEAM type mappings): -- **Gleam** — Typed functional language on BEAM, compiles to Erlang. Study its - compiler for BEAM code generation patterns: -- **LFE** (Lisp Flavored Erlang) — another language targeting BEAM, shows OTP - integration from non-Erlang language: -- **Fable.Python** — the direct template for this work. Same architecture, similar - challenges (dynamic target language, no classes for DUs, module-per-file) +| P2 | Mutable list/map storage copies on update | Benchmark realistic collection sizes. Keep process isolation as the default; add an explicit ETS-backed type or optimization only for measured hot paths. | +| P2 | Curry/eta identity adds adapter work | Benchmark generated functions and extend marked adapters beyond arity 7 only when required by real code. | +| P2 | Generated Erlang has no published size/runtime baseline | Track compile time, BEAM file size, startup, allocation, collection workloads, Async, and message-heavy applications. | +| P2 | Application-scale evidence is private and not reproducible in this repository | Keep the downstream application as a compatibility signal and add a public packaged-compiler smoke test using Fable.Beam/Fable.Actor. | + +### Ecosystem work + +- Keep OTP bindings in Fable.Beam rather than adding OTP-specific abstractions to the + compiler. +- Maintain a compatibility matrix for Fable, Fable.Beam, Fable.Actor, Erlang/OTP, + and rebar3. +- Add end-to-end examples for a command-line program, a rebar3 library, an OTP + application, supervised processes, and distribution. +- Test generated modules as dependencies of hand-written Erlang applications, not + only as application entry points. +- Document the public Erlang ABI for unions, records, options, exceptions, + functions, and mutable values before external packages depend on it. + +### Suggested beta criteria + +- There is no known silent miscompilation, data corruption, or process-safety defect + in the documented supported surface. +- Remaining semantic and API gaps are deliberate, documented exclusions and have + regression tests where practical; complete .NET parity is not required. +- Supported constructs do not silently compile to an `unsupported_*` placeholder. + Statically detectable unsupported constructs produce an actionable diagnostic. +- The complete BEAM suite passes on the oldest and newest supported OTP releases. +- Packaged `dotnet fable --lang beam` output builds and runs in a clean consumer + project without repository-local files. +- Module initialization and mutation have documented, tested behavior for the + supported executable, library, and spawned-process scenarios. +- The generated-value ABI used by supported interop scenarios is documented, and + representation changes are called out. +- At least one non-trivial downstream OTP application remains green, with a public + packaged-consumer smoke test covering the reproducible integration path. + +### Suggested stable-target criteria + +- No known high-severity semantic divergence remains in supported F# language + features or core runtime APIs. +- F#/.NET parity exclusions are reviewed, documented, and small enough to form a + deliberate support policy. +- The Erlang ABI has a compatibility and versioning policy. +- OTP, rebar3, Fable.Beam, and Fable.Actor compatibility ranges are published and + exercised in CI. +- Diagnostics fail early for unsupported code and point to a documented alternative. +- Performance baselines show no blocking regressions in representative compute, + collection, Async, and OTP workloads. +- Release, upgrade, and deprecation procedures have been exercised across multiple + Fable releases and real consumer applications. diff --git a/src/Fable.Transforms/Beam/Fable2Beam.Reflection.fs b/src/Fable.Transforms/Beam/Fable2Beam.Reflection.fs index c37527fe1a..6e0cf00447 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.Reflection.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.Reflection.fs @@ -43,7 +43,7 @@ let private makeThunk (elements: Beam.ErlExpr list) = /// Name of the per-entity reflection function generated for a record/union /// (e.g. entity `Tree` -> `tree_reflection/0`). let reflectionFuncName (declarationName: string) = - Fable.Beam.Naming.sanitizeErlangName declarationName + "_reflection" + Fable.Beam.Naming.limitErlangAtomLength (Fable.Beam.Naming.sanitizeErlangName declarationName + "_reflection") /// Erlang variable names bound to an entity's resolved generic arguments inside its /// reflection function (`gen0`, `gen1`, ...). diff --git a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs index 727237303d..edafa0d4de 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs @@ -9,6 +9,37 @@ open Fable.Beam let toErlangVar (ident: Ident) = Naming.capitalizeFirst ident.Name |> Naming.sanitizeErlangVar +/// Test the existing DU representation without inspecting payload types. +let makeUnionTypeTest (cases: UnionCase list) (expr: Beam.ErlExpr) = + // decision: Match emitted case tags and arities because DU values carry no nominal type or generic argument metadata. + // invariant: Patterns accept arbitrary terms safely and evaluate the tested expression once. + let caseClause (uci: UnionCase) : Beam.ErlCaseClause = + let tag = + Naming.unionCaseTagName uci.CompiledName uci.Name + |> Beam.Atom + |> Beam.ErlLiteral.AtomLit + |> Beam.PLiteral + + let pattern = + match uci.UnionCaseFields with + | [] -> tag + | fields -> Beam.PTuple(tag :: List.replicate fields.Length Beam.PWildcard) + + { + Pattern = pattern + Guard = [] + Body = [ Beam.ErlExpr.Literal(Beam.ErlLiteral.BoolLit true) ] + } + + let fallback: Beam.ErlCaseClause = + { + Pattern = Beam.PWildcard + Guard = [] + Body = [ Beam.ErlExpr.Literal(Beam.ErlLiteral.BoolLit false) ] + } + + Beam.ErlExpr.Case(expr, List.map caseClause cases @ [ fallback ]) + let isIntegerType (typ: Fable.AST.Fable.Type) = match typ with | Fable.AST.Fable.Type.Number(kind, _) -> @@ -63,7 +94,7 @@ let private wrappedLiteral (bits: int, signed: bool) (value: int64) = if not signed && truncated < 0L then // 64-bit unsigned above Int64.MaxValue: does not fit an Erlang int64 literal - Beam.ErlExpr.Literal(Beam.ErlLiteral.BigInt(string (uint64 truncated))) + Beam.ErlExpr.Literal(Beam.ErlLiteral.BigInt(string(uint64 truncated))) else Beam.ErlExpr.Literal(Beam.ErlLiteral.Integer truncated) @@ -436,6 +467,45 @@ let wrapWithHoisted (hoisted: Beam.ErlExpr list) (expr: Beam.ErlExpr) : Beam.Erl let atomLit name = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom name)) +/// Whether generated Erlang still reads a variable. This is intentionally checked after +/// lowering: a Fable catch identifier used only by `reraise` becomes the raw +/// class/reason/stacktrace variables and does not need a separately wrapped exception value. +let rec referencesVariable (name: string) (expr: Beam.ErlExpr) : bool = + let anyReferences exprs = + exprs |> List.exists (referencesVariable name) + + match expr with + | Beam.ErlExpr.Variable varName -> varName = name + | Beam.ErlExpr.Tuple elements + | Beam.ErlExpr.List elements + | Beam.ErlExpr.Block elements -> anyReferences elements + | Beam.ErlExpr.ListCons(head, tail) + | Beam.ErlExpr.BinOp(_, head, tail) -> referencesVariable name head || referencesVariable name tail + | Beam.ErlExpr.Map entries -> + entries + |> List.exists (fun (key, value) -> referencesVariable name key || referencesVariable name value) + | Beam.ErlExpr.Call(_, _, args) -> anyReferences args + | Beam.ErlExpr.Apply(func, args) -> referencesVariable name func || anyReferences args + | Beam.ErlExpr.Fun clauses + | Beam.ErlExpr.NamedFun(_, clauses) -> + clauses + |> List.exists (fun clause -> anyReferences clause.Guard || anyReferences clause.Body) + | Beam.ErlExpr.Case(value, clauses) -> + referencesVariable name value + || clauses + |> List.exists (fun clause -> anyReferences clause.Guard || anyReferences clause.Body) + | Beam.ErlExpr.Match(_, value) + | Beam.ErlExpr.UnaryOp(_, value) -> referencesVariable name value + | Beam.ErlExpr.TryCatch(body, _, _, _, catchBody, after) -> + anyReferences body || anyReferences catchBody || anyReferences after + | Beam.ErlExpr.Emit(_, args) -> anyReferences args + | Beam.ErlExpr.Receive(clauses, after) -> + clauses + |> List.exists (fun clause -> anyReferences clause.Guard || anyReferences clause.Body) + || (after + |> Option.exists (fun (timeout, body) -> referencesVariable name timeout || anyReferences body)) + | Beam.ErlExpr.Literal _ -> false + let rec private patternBinds (pattern: Beam.ErlPattern) : bool = match pattern with | Beam.PVar _ -> true @@ -518,4 +588,4 @@ let isolateScope (body: Beam.ErlExpr list) : Beam.ErlExpr = /// and the declaration/initializer must all derive their key through this helper or state /// silently breaks. let mutableStateKey (moduleName: string) (name: string) = - moduleName + "_" + Naming.sanitizeErlangName name + Naming.limitErlangAtomLength (moduleName + "_" + Naming.sanitizeErlangName name) diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 3009193d5f..92b36b83f1 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -4,6 +4,7 @@ open Fable open Fable.AST open Fable.AST.Fable open Fable.Transforms +open Fable.Beam open Fable.Beam.Naming open Fable.Transforms.Beam.Util @@ -84,6 +85,7 @@ type Context = DecisionTargets: (Ident list * Expr) list RecursiveBindings: Set MutualRecBindings: Map // name -> (bundleVarName, atomTag) + PrivateClassBindings: Map // Fable compiled name -> disambiguated Erlang function name MutableVars: Map // F# ident name -> Erlang var name holding the ref LocalVars: Set // names of locally-bound variables (params, let bindings, lambda args) ThisArgVar: string option // Erlang variable name for `this` in class constructors/methods @@ -91,7 +93,8 @@ type Context = CtorFieldExprs: Map // field name -> Erlang expr during constructor ClassFieldPrefix: bool // When true, NewRecord uses "field_" prefix for map keys (for explicit val field class ctors) CtorParamNames: Set // Constructor parameter names (stored as class fields) - CatchReasonVar: (string * string) option // (catch ident name, Erlang reason var name) for reraise support + // invariant: CatchVars retains class, reason, and stacktrace so reraise preserves native exception identity. + CatchVars: (string * string * string * string) option } /// Check if an entity ref refers to an interface type @@ -132,6 +135,86 @@ let private hasMutableRecordFields (com: IBeamCompiler) (typ: Fable.AST.Fable.Ty let private atomLit name = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom name)) +let private functionName (ctx: Context) name = + ctx.PrivateClassBindings + |> Map.tryFind name + |> Option.defaultWith (fun () -> sanitizeErlangName name) + +let private unsupportedExpression (com: IBeamCompiler) range message = + com.AddLog(message, Severity.Error, ?range = range, fileName = com.CurrentFile, tag = "FABLE") + + // invariant: Unsupported source constructs log an error before returning a recovery expression. + atomLit "undefined" + +let private reportFunctionNameCollision (com: IBeamCompiler) owner (Beam.Atom name) arity = + // invariant: Distinct declarations have distinct Erlang function names and arities — Erlang resolves functions only by that pair. + com.AddLog( + $"Declarations in '%s{owner}' compile to duplicate Erlang function '%s{name}/%d{arity}'. Rename one declaration or change its arity.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + +let private reportFieldNameCollisionsBy (com: IBeamCompiler) owner toAtom (fieldNames: string seq) = + fieldNames + |> Seq.groupBy toAtom + |> Seq.iter (fun (erlName, fields) -> + let fields = fields |> Seq.distinct |> Seq.toList + + if fields.Length > 1 then + let names = fields |> List.map (sprintf "'%s'") |> String.concat " and " + + // invariant: Distinct record fields map to distinct Erlang atoms — collisions stop compilation before a map value is lost. + com.AddLog( + $"%s{owner} has fields %s{names} that both compile to Erlang atom '%s{erlName}'. Rename one of the fields.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + ) + +let private reportFieldNameCollisions (com: IBeamCompiler) owner (fieldNames: string seq) = + reportFieldNameCollisionsBy com owner sanitizeFieldName fieldNames + +let private reportUnionCaseTagCollisions (com: IBeamCompiler) (ent: Entity) = + ent.UnionCases + |> List.groupBy (fun case -> unionCaseTagName case.CompiledName case.Name) + |> List.iter (fun (erlTag, cases) -> + let names = cases |> List.map (fun case -> case.Name) |> List.distinct + + if names.Length > 1 then + let names = names |> List.map (sprintf "'%s'") |> String.concat " and " + + // invariant: Distinct union cases carry distinct tags — a shared tag makes `match` pick + // the wrong branch and two different values compare equal, with nothing to see at runtime. + com.AddLog( + $"Union '%s{ent.FullName}' has cases %s{names} that both compile to Erlang atom '%s{erlTag}'. Rename one of the cases.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + ) + +let private reportInterfaceMemberCollisions (com: IBeamCompiler) (ent: Entity) = + ent.MembersFunctionsAndValues + |> Seq.groupBy (fun memb -> interfaceMemberKey memb.IsSetter memb.DisplayName) + |> Seq.iter (fun (erlName, members) -> + let names = + members |> Seq.map (fun memb -> memb.DisplayName) |> Seq.distinct |> Seq.toList + + if names.Length > 1 then + let names = names |> List.map (sprintf "'%s'") |> String.concat " and " + + // invariant: Interface members are keys in the implementation map, so a shared key makes + // one member silently resolve to the other's body. + com.AddLog( + $"Interface '%s{ent.FullName}' has members %s{names} that both dispatch through Erlang atom '%s{erlName}'. Rename one of the members.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + ) + /// Resolve the atom tag name for a union case. /// Returns Some(atomStr, isFieldless) or None if the entity can't be resolved. let private getUnionCaseAtomExpr (com: IBeamCompiler) (ref: EntityRef) (tag: int) = @@ -237,7 +320,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "undefined")) - | Value(kind, _range) -> transformValue com ctx kind + | Value(kind, range) -> transformValue com ctx range kind | Call(callee, info, _typ, _range) -> transformCall com ctx callee info @@ -267,7 +350,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er Beam.ErlExpr.Call(None, "get", [ atomLit (mutableStateKey ctx.ModuleName ident.Name) ]) else // Module-level function reference: call as 0-arity function - Beam.ErlExpr.Call(None, sanitizeErlangName ident.Name, []) + Beam.ErlExpr.Call(None, functionName ctx ident.Name, []) | Sequential exprs -> let erlExprs = exprs |> List.map (transformExpr com ctx) @@ -578,7 +661,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er transformExpr com ctx target - | Set(expr, ValueSet, _typ, value, _range) -> + | Set(expr, ValueSet, _typ, value, range) -> match expr with | IdentExpr ident when ctx.MutableVars.ContainsKey(ident.Name) -> // Mutable variable: update via process dictionary using unique ref key @@ -599,14 +682,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er ] ) | IdentExpr ident -> Beam.ErlExpr.Match(Beam.PVar(capitalizeFirst ident.Name), transformExpr com ctx value) - | _ -> - com.WarnOnlyOnce("Set with non-identifier target is not supported for Beam target") - - Beam.ErlExpr.Call( - Some "erlang", - "error", - [ Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "unsupported_set")) ] - ) + | _ -> unsupportedExpression com range "Set with non-identifier target is not supported for Beam target" | Set(expr, FieldSet fieldName, _, value, _) -> let erlExpr = transformExpr com ctx expr @@ -614,10 +690,9 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er match expr.Type with | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isClassType com entityRef -> - let sanitizedFieldName = sanitizeErlangName fieldName // Use f$ prefix to avoid collision with interface method keys let atomField = - Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom("field_" + sanitizedFieldName))) + Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom(classFieldAtomName fieldName))) // Class instance: update state in process dict // put(Ref, maps:put(field, Value, get(Ref))) Beam.ErlExpr.Call( @@ -635,7 +710,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isInterfaceType com entityRef -> // Interface property setter: dispatch through set_ prefixed key // (fable_utils:iface_get(set_bar, Obj))(Value) - let setterKey = "set_" + sanitizeErlangName fieldName + let setterKey = interfaceMemberKey true fieldName let setterAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom setterKey)) let lookup = @@ -880,16 +955,18 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er match catch_, afterExprs with | Some(ident, catchExpr), _ -> // Create a temp var for the raw reason, then bind the catch ident. - // Custom exceptions (maps with __type) are preserved as-is. + // Custom exceptions (maps with exn_type) are preserved as-is. // Plain errors (binaries from failwith, atoms, etc.) are wrapped in #{message => ...}. let ctr = com.IncrementCounter() + let classVar = $"_Exn_class_%d{ctr}" let reasonVar = $"Exn_reason_%d{ctr}" + let stackVar = $"_Exn_stack_%d{ctr}" let identVar = capitalizeFirst ident.Name let ctx' = { ctx with LocalVars = ctx.LocalVars.Add(ident.Name) - CatchReasonVar = Some(ident.Name, reasonVar) + CatchVars = Some(ident.Name, classVar, reasonVar, stackVar) } let erlCatchBody = transformExpr com ctx' catchExpr @@ -899,99 +976,55 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er | Beam.ErlExpr.Block es -> es | e -> [ e ] - // Only generate the exception wrapping/binding when the catch body - // actually references the exception identifier. This avoids unused - // term warnings from the Erlang compiler. - if containsIdentRef ident.Name catchExpr then + // Only generate the exception wrapping/binding when the lowered catch body still + // reads it. A source-level `reraise` references the Fable catch identifier, but it + // lowers directly to erlang:raise/3 with the raw catch variables. + if catchBodyExprs |> List.exists (referencesVariable identVar) then let reasonRef = Beam.ErlExpr.Variable reasonVar - let formatExpr = - Beam.ErlExpr.Call( - None, - "iolist_to_binary", - [ - Beam.ErlExpr.Call( - Some "io_lib", - "format", - [ - Beam.ErlExpr.Call( - None, - "binary_to_list", - [ Beam.ErlExpr.Literal(Beam.ErlLiteral.StringLit "~p") ] - ) - Beam.ErlExpr.List [ reasonRef ] - ] - ) - ] - ) - - let messageExpr = - Beam.ErlExpr.Case( - reasonRef, - [ - { - Pattern = Beam.PWildcard - Guard = [ Beam.ErlExpr.Call(None, "is_binary", [ reasonRef ]) ] - Body = [ reasonRef ] - } - { - Pattern = Beam.PWildcard - Guard = [] - Body = [ formatExpr ] - } - ] - ) - - // If reason is already a map (F# exception) or reference (class inheriting exn), use it directly. - // Otherwise wrap in #{message => ...} for .Message access. + // invariant: Ordinary catches retain nominal cancellation identity and remain allowed to handle it. + // Use the same normalization as Async.TryWith; reraise still uses the raw catch variables. let bindIdent = Beam.ErlExpr.Match( Beam.PVar identVar, - Beam.ErlExpr.Case( - reasonRef, - [ - { - Pattern = Beam.PWildcard - Guard = [ Beam.ErlExpr.Call(None, "is_map", [ reasonRef ]) ] - Body = [ reasonRef ] - } - { - Pattern = Beam.PWildcard - Guard = [ Beam.ErlExpr.Call(None, "is_reference", [ reasonRef ]) ] - Body = [ reasonRef ] - } - { - Pattern = Beam.PWildcard - Guard = [] - Body = - [ - Beam.ErlExpr.Map - [ - Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "message")), - messageExpr - ] - ] - } - ] - ) + Beam.ErlExpr.Call(Some "fable_async", "wrap_error", [ reasonRef ]) ) - Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, [ bindIdent ] @ catchBodyExprs, afterExprs) + Beam.ErlExpr.TryCatch( + bodyExprs, + classVar, + reasonVar, + stackVar, + [ bindIdent ] @ catchBodyExprs, + afterExprs + ) else - Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, catchBodyExprs, afterExprs) + Beam.ErlExpr.TryCatch(bodyExprs, classVar, reasonVar, stackVar, catchBodyExprs, afterExprs) | None, [] -> // No catch handler and no finalizer erlBody | None, _ -> // No catch handler but has finalizer (use binding) → try/after let ctr = com.IncrementCounter() + let classVar = $"_Exn_class_%d{ctr}" let reasonVar = $"Exn_reason_%d{ctr}" + let stackVar = $"_Exn_stack_%d{ctr}" // Re-throw in catch to preserve the error after running the after block Beam.ErlExpr.TryCatch( bodyExprs, + classVar, reasonVar, + stackVar, [ - Beam.ErlExpr.Call(Some "erlang", "error", [ Beam.ErlExpr.Variable reasonVar ]) + Beam.ErlExpr.Call( + Some "erlang", + "raise", + [ + Beam.ErlExpr.Variable classVar + Beam.ErlExpr.Variable reasonVar + Beam.ErlExpr.Variable stackVar + ] + ) ], afterExprs ) @@ -1215,7 +1248,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er } ] - atomLit ("set_" + methodName), funExpr + atomLit (interfaceMemberKey true memb.Name), funExpr else let argPats = nonThisArgs |> List.map (fun a -> Beam.PVar(toErlangVar a)) @@ -1300,19 +1333,39 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er | Throw(Some exprArg, _typ) -> // For reraise: if the thrown expression references the catch ident, // use the raw Erlang reason variable to preserve the original exception. - match exprArg, ctx.CatchReasonVar with - | IdentExpr ident, Some(catchIdentName, reasonVar) when ident.Name = catchIdentName -> - Beam.ErlExpr.Call(Some "erlang", "error", [ Beam.ErlExpr.Variable reasonVar ]) + match exprArg, ctx.CatchVars with + | IdentExpr ident, Some(catchIdentName, classVar, reasonVar, stackVar) when ident.Name = catchIdentName -> + Beam.ErlExpr.Call( + Some "erlang", + "raise", + [ + Beam.ErlExpr.Variable classVar + Beam.ErlExpr.Variable reasonVar + Beam.ErlExpr.Variable stackVar + ] + ) | _ -> let erlExpr = transformExpr com ctx exprArg Beam.ErlExpr.Call(Some "erlang", "error", [ erlExpr ]) | Throw(None, _typ) -> - // Re-raise (should not normally happen outside catch context) - Beam.ErlExpr.Call( - Some "erlang", - "error", - [ Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "rethrow")) ] - ) + match ctx.CatchVars with + | Some(_, classVar, reasonVar, stackVar) -> + Beam.ErlExpr.Call( + Some "erlang", + "raise", + [ + Beam.ErlExpr.Variable classVar + Beam.ErlExpr.Variable reasonVar + Beam.ErlExpr.Variable stackVar + ] + ) + | None -> + // Re-raise should not occur outside a catch context. + Beam.ErlExpr.Call( + Some "erlang", + "error", + [ Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "rethrow")) ] + ) | Curry(e, arity) when arity >= 2 && arity <= 7 @@ -1410,7 +1463,13 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er } ] -and transformValue (com: IBeamCompiler) (ctx: Context) (value: ValueKind) : Beam.ErlExpr = +and transformValue + (com: IBeamCompiler) + (ctx: Context) + (range: SourceLocation option) + (value: ValueKind) + : Beam.ErlExpr + = match value with | StringConstant s -> Beam.ErlExpr.Literal(Beam.ErlLiteral.StringLit s) @@ -1576,15 +1635,27 @@ and transformValue (com: IBeamCompiler) (ctx: Context) (value: ValueKind) : Beam Beam.ErlExpr.Literal(Beam.ErlLiteral.BigInt(string value)) + | NumberConstant(NumberValue.Float16 _, _) -> + unsupportedExpression com range "Numeric literal kind 'Float16' is not supported for Beam target" + | NumberConstant(NumberValue.Int128 _, _) -> + unsupportedExpression com range "Numeric literal kind 'Int128' is not supported for Beam target" + | NumberConstant(NumberValue.UInt128 _, _) -> + unsupportedExpression com range "Numeric literal kind 'UInt128' is not supported for Beam target" + | NewRecord(values, ref, _genArgs) -> match com.TryGetEntity(ref) with | Some entity -> let fieldEntries = - List.zip (entity.FSharpFields |> List.map (fun f -> f.Name)) values + let fieldNames = + entity.FSharpFields + |> List.filter (fun f -> not f.IsStatic) + |> List.map (fun f -> f.Name) + + List.zip fieldNames values |> List.map (fun (name, value) -> let fieldKey = if ctx.ClassFieldPrefix then - atomLit ("field_" + sanitizeErlangName name) + atomLit (classFieldAtomName name) else Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom(sanitizeFieldName name))) @@ -1625,6 +1696,8 @@ and transformValue (com: IBeamCompiler) (ctx: Context) (value: ValueKind) : Beam Beam.ErlExpr.Map entries | NewAnonymousRecord(values, fieldNames, _genArgs, _isStruct) -> + reportFieldNameCollisions com "Anonymous record" fieldNames + let entries = Array.zip fieldNames (values |> List.toArray) |> Array.toList @@ -1720,21 +1793,6 @@ and transformValue (com: IBeamCompiler) (ctx: Context) (value: ValueKind) : Beam let flagsExpr = Beam.ErlExpr.Literal(Beam.ErlLiteral.Integer(int64 flagBits)) Beam.ErlExpr.Call(Some "fable_regex", "create", [ patternExpr; flagsExpr ]) - | _ -> - let kindName = value.GetType().Name - - com.WarnOnlyOnce( - $"Unhandled Fable value kind '%s{kindName}' — emitting placeholder atom. This may cause runtime errors." - ) - - Beam.ErlExpr.Call( - Some "erlang", - "error", - [ - Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom $"unsupported_%s{kindName.ToLowerInvariant()}")) - ] - ) - and transformOperation (com: IBeamCompiler) (ctx: Context) @@ -1993,13 +2051,28 @@ and transformTest (com: IBeamCompiler) (ctx: Context) (kind: TestKind) (expr: Ex Beam.ErlExpr.BinOp("andalso", isRef, isList) | Fable.AST.Fable.Type.Tuple _ -> Beam.ErlExpr.Call(None, "is_tuple", [ erlExpr ]) | Fable.AST.Fable.Type.DeclaredType(ref, _) -> - // For exception types, check __type tag: (is_map(X) andalso maps:get(__type, X, undefined) =:= type_name) - // For other types, check is_map or is_reference (class instances use refs) + // For exception types, check the nominal exn_type tag. + // Unions use case tags and arities; other types use maps or references. let isMap = Beam.ErlExpr.Call(None, "is_map", [ erlExpr ]) let isRef = Beam.ErlExpr.Call(None, "is_reference", [ erlExpr ]) - match com.TryGetEntity(ref) with - | Some entity when entity.IsFSharpExceptionDeclaration -> + match ref.FullName, com.TryGetEntity(ref) with + | Replacements.Util.BuiltinSystemException typeName, _ -> + Beam.ErlExpr.Call( + Some "fable_utils", + "is_exception_type", + [ erlExpr; atomLit (sanitizeErlangName typeName) ] + ) + | ("System.Exception" | "System.AggregateException" | "System.Collections.Generic.KeyNotFoundException" | "System.OperationCanceledException"), + _ -> + let typeName = ref.FullName.Substring(ref.FullName.LastIndexOf('.') + 1) + + Beam.ErlExpr.Call( + Some "fable_utils", + "is_exception_type", + [ erlExpr; atomLit (sanitizeErlangName typeName) ] + ) + | _, Some entity when entity.IsFSharpExceptionDeclaration -> let typeName = sanitizeErlangName entity.DisplayName let typeCheck = @@ -2010,6 +2083,7 @@ and transformTest (com: IBeamCompiler) (ctx: Context) (kind: TestKind) (expr: Ex ) Beam.ErlExpr.BinOp("andalso", isMap, typeCheck) + | _, Some entity when entity.IsFSharpUnion -> makeUnionTypeTest entity.UnionCases erlExpr | _ -> Beam.ErlExpr.BinOp("orelse", isMap, isRef) | _ -> Beam.ErlExpr.Literal(Beam.ErlLiteral.BoolLit true) @@ -2072,40 +2146,40 @@ and transformGet (com: IBeamCompiler) (ctx: Context) (kind: GetKind) (typ: Type) let funcName = sanitizeErlangName info.Name Beam.ErlExpr.Call(moduleName, funcName, []) | _ -> + // During construction, the Fable AST can type `this` as Any inside a captured + // object expression. Resolve already-initialized fields from the constructor cache + // before dispatching on that unreliable static type. + let isThisRef = + match expr, ctx.ThisArgVar, erlExpr with + | Value(ThisValue _, _), _, _ -> true + | IdentExpr ident, _, _ when ident.Name.StartsWith("_this", System.StringComparison.Ordinal) -> true + | _, Some thisVar, Beam.ErlExpr.Variable v -> v = thisVar + | _ -> false - match expr.Type with - | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isClassType com entityRef -> - let fieldName = sanitizeErlangName info.Name - - // During constructor, field values may reference other fields via this.FieldName. - // Since put(Ref, #{...}) hasn't happened yet, we use the precomputed Erlang expressions. - let isThisRef = - match ctx.ThisArgVar, erlExpr with - | Some thisVar, Beam.ErlExpr.Variable v -> v = thisVar - | _ -> false - - match isThisRef, ctx.CtorFieldExprs.TryFind(info.Name) with - | true, Some cachedExpr -> cachedExpr - | _ -> + match isThisRef, ctx.CtorFieldExprs.TryFind(info.Name) with + | true, Some cachedExpr -> cachedExpr + | _ -> + match expr.Type with + | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isClassType com entityRef -> // Class instance: read via fable_utils:field_get, which supports both // a self-contained map (immutable class) and a process-dict ref (mutable class). // Use field_ prefix to avoid collision with interface method keys. let classFieldAtom = - Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom("field_" + fieldName))) + Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom(classFieldAtomName info.Name))) Beam.ErlExpr.Call(Some "fable_utils", "field_get", [ classFieldAtom; erlExpr ]) - | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isInterfaceType com entityRef -> - let fieldName = sanitizeErlangName info.Name - let fieldAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom fieldName)) - // Interface dispatch: works for both object expressions (maps) and class instances (refs). - // Class interface property getters are stored as 0-arity thunks — iface_get calls them. - // ObjectExpr property getters are stored as plain values — iface_get returns them as-is. - Beam.ErlExpr.Call(Some "fable_utils", "iface_get", [ fieldAtom; erlExpr ]) - | _ -> - // Record/union/anonymous record: direct map access, use sanitizeFieldName for disambiguation - let fieldName = sanitizeFieldName info.Name - let fieldAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom fieldName)) - Beam.ErlExpr.Call(Some "maps", "get", [ fieldAtom; erlExpr ]) + | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isInterfaceType com entityRef -> + let fieldName = sanitizeErlangName info.Name + let fieldAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom fieldName)) + // Interface dispatch: works for both object expressions (maps) and class instances (refs). + // Class interface property getters are stored as 0-arity thunks — iface_get calls them. + // ObjectExpr property getters are stored as plain values — iface_get returns them as-is. + Beam.ErlExpr.Call(Some "fable_utils", "iface_get", [ fieldAtom; erlExpr ]) + | _ -> + // Record/union/anonymous record: direct map access, use sanitizeFieldName for disambiguation + let fieldName = sanitizeFieldName info.Name + let fieldAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom fieldName)) + Beam.ErlExpr.Call(Some "maps", "get", [ fieldAtom; erlExpr ]) | ExprGet indexExpr -> let erlIndex = transformExpr com ctx indexExpr @@ -2454,7 +2528,8 @@ and transformCall (com: IBeamCompiler) (ctx: Context) (callee: Expr) (info: Call // Field-stored function: (fable_utils:field_get(field_, This))(Args) let erlThis = transformExpr com ctx thisExpr let thisH, cleanThis = extractBlock erlThis - let fieldAtom = atomLit ("field_" + sanitizeErlangName ident.Name) + + let fieldAtom = atomLit (classFieldAtomName ident.Name) let lookup = Beam.ErlExpr.Call(Some "fable_utils", "field_get", [ fieldAtom; cleanThis ]) @@ -2506,7 +2581,7 @@ and transformCall (com: IBeamCompiler) (ctx: Context) (callee: Expr) (info: Call apply |> wrapWithHoisted allHoisted else - Beam.ErlExpr.Call(None, sanitizeErlangName ident.Name, allArgs) + Beam.ErlExpr.Call(None, functionName ctx ident.Name, allArgs) |> wrapWithHoisted allHoisted | Get(calleeExpr, FieldGet fieldInfo, _, _) -> @@ -2604,7 +2679,7 @@ and transformCall (com: IBeamCompiler) (ctx: Context) (callee: Expr) (info: Call if isCtorFieldInvoke then // Constructor param field invoke: (fable_utils:field_get(field_, This))(Args) - let fieldAtom = atomLit ("field_" + sanitizeErlangName methodName) + let fieldAtom = atomLit (classFieldAtomName methodName) let lookup = Beam.ErlExpr.Call(Some "fable_utils", "field_get", [ fieldAtom; cleanCallee ]) @@ -2638,6 +2713,21 @@ and transformClassDeclaration (decl: ClassDecl) : Beam.ErlForm list = + // Records key their map by `sanitizeFieldName`; every other type keys it by `classFieldAtomName`, + // which collapses more names, so each has to be checked against the atom it actually emits. + if ent.IsFSharpRecord then + ent.FSharpFields + |> List.map (fun field -> field.Name) + |> reportFieldNameCollisions com $"Record '%s{ent.FullName}'" + elif ent.IsFSharpUnion then + reportUnionCaseTagCollisions com ent + elif ent.IsInterface then + reportInterfaceMemberCollisions com ent + else + ent.FSharpFields + |> List.map (fun field -> field.Name) + |> reportFieldNameCollisionsBy com $"Type '%s{ent.FullName}'" classFieldAtomName + let constructorForms = match decl.Constructor with | Some cons -> @@ -2878,7 +2968,7 @@ and transformClassDeclaration else transformExpr com fieldCtx value - let entries' = entries @ [ atomLit ("field_" + sanitizeErlangName name), erlValue ] + let entries' = entries @ [ atomLit (classFieldAtomName name), erlValue ] let fieldCtx' = { fieldCtx with CtorFieldExprs = fieldCtx.CtorFieldExprs.Add(name, erlValue) } @@ -2898,7 +2988,7 @@ and transformClassDeclaration decl.AttachedMembers |> List.choose (fun memb -> match memb.ImplementedSignatureRef with - | Some sigRef -> + | Some sigRef when not (ObjectOverrides.isSystemObjectOverride memb) -> let sigInfo = com.GetMember(sigRef) let memberName = sanitizeErlangName memb.Name @@ -2980,13 +3070,10 @@ and transformClassDeclaration ] // Setters use set_ prefix to avoid key collision with getters - let keyName = - if sigInfo.IsSetter then - "set_" + memberName - else - memberName + let keyName = interfaceMemberKey sigInfo.IsSetter memb.Name Some(atomLit keyName, closure) + | Some _ | None -> None ) @@ -3114,9 +3201,12 @@ and transformClassDeclaration let memberForms = decl.AttachedMembers |> List.collect (fun memb -> - // Skip interface implementations — they are inlined as closures - // in interfaceEntries and don't need separate module-level functions. - if memb.ImplementedSignatureRef.IsSome then + // Interface implementations are inlined as closures in interfaceEntries. System.Object + // overrides remain module-level functions because records and unions have no instance map. + if + memb.ImplementedSignatureRef.IsSome + && not (ObjectOverrides.isSystemObjectOverride memb) + then [] else @@ -3194,7 +3284,7 @@ and transformClassDeclaration elif not memb.IsMangled && info.IsGetter then // Property getter: class_name_get_prop(This) -> maps:get(prop, get(This)). let propName = sanitizeErlangName memb.Name - let funcName = $"%s{className}_%s{propName}" + let funcName = limitErlangAtomLength $"%s{className}_%s{propName}" let _thisArg, _nonThisArgs, memberCtx = getThisAndArgs () let bodyExpr = transformExpr com memberCtx memb.Body @@ -3222,7 +3312,7 @@ and transformClassDeclaration elif not memb.IsMangled && info.IsSetter then // Property setter: class_name_set_prop(This, Value) -> put(This, maps:put(prop, Value, get(This))). let propName = sanitizeErlangName memb.Name - let funcName = $"%s{className}_set_%s{propName}" + let funcName = limitErlangAtomLength $"%s{className}_set_%s{propName}" let _thisArg, nonThisArgs, memberCtx = getThisAndArgs () let argPatterns = @@ -3254,8 +3344,7 @@ and transformClassDeclaration [ Beam.ErlForm.Function funcDef ] else // Regular method: class_name_method(This, Args...) -> Body. - let methodName = sanitizeErlangName memb.Name - let funcName = $"%s{className}_%s{methodName}" + let funcName = ObjectOverrides.functionName className memb.Name let _thisArg, nonThisArgs, memberCtx = getThisAndArgs () let argPatterns = @@ -3342,12 +3431,19 @@ and transformClassDeclaration ) | _ -> () - // Deduplicate functions by name+arity. This can happen when a property setter - // (e.g., `set StatusCode`) and a method (e.g., `SetStatusCode`) both mangle to - // the same Erlang function name. Unlike JS/Python which have native getter/setter - // syntax, Erlang uses plain functions so name collisions produce duplicate definitions. + // Report remaining name/arity collisions, retaining one recovery form so error output + // stays printable. Accessor/method pairs already have distinct upstream names. let allForms = constructorForms @ memberForms @ reflectionForms + let reflectionKeys = + reflectionForms + |> List.choose ( + function + | Beam.ErlForm.Function def -> Some(def.Name, def.Arity) + | _ -> None + ) + |> Set.ofList + let dedup = allForms |> List.fold @@ -3357,6 +3453,9 @@ and transformClassDeclaration let key = (def.Name, def.Arity) if Set.contains key seen then + if not (Set.contains key reflectionKeys) then + reportFunctionNameCollision com ent.FullName def.Name def.Arity + (seen, acc) else (Set.add key seen, form :: acc) @@ -3376,7 +3475,7 @@ and transformDeclaration (com: IBeamCompiler) (ctx: Context) (decl: Declaration) |> Option.map com.GetMember |> Option.defaultWith (fun () -> com.GetMember(memDecl.MemberRef)) - let name = sanitizeErlangName memDecl.Name + let name = functionName ctx memDecl.Name // Process-dict key for module-level mutable/snapshot values, namespaced by module. let stateKey = mutableStateKey ctx.ModuleName memDecl.Name @@ -3694,6 +3793,7 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule = DecisionTargets = [] RecursiveBindings = Set.empty MutualRecBindings = Map.empty + PrivateClassBindings = Map.empty MutableVars = Map.empty LocalVars = Set.empty ThisArgVar = None @@ -3701,7 +3801,7 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule = CtorFieldExprs = Map.empty ClassFieldPrefix = false CtorParamNames = Set.empty - CatchReasonVar = None + CatchVars = None } let ctorNameRegistry = System.Collections.Generic.Dictionary() @@ -3747,14 +3847,196 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule = member _.TryGetEntity(ref) = com.TryGetEntity(ref) member _.GetInlineExpr(key) = com.GetInlineExpr(key) member _.AddWatchDependency(file) = com.AddWatchDependency(file) + member _.AddSignatureDependency(file) = com.AddSignatureDependency(file) - member _.AddLog(msg, severity, ?range, ?fileName, ?tag) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName, ?tag, ?code) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) } + let rec collectMemberDeclarations declarations = + declarations + |> List.collect ( + function + | MemberDeclaration memDecl -> + let info = + memDecl.ImplementedSignatureRef + |> Option.map beamCom.GetMember + |> Option.defaultWith (fun () -> beamCom.GetMember(memDecl.MemberRef)) + + let arity = + match memDecl.Args with + | _thisArg :: rest when info.IsInstance -> + 1 + (rest |> FSharp2Fable.Util.discardUnitArg |> List.length) + | args -> args |> FSharp2Fable.Util.discardUnitArg |> List.length + + [ memDecl, info, (Beam.Atom(sanitizeErlangName memDecl.Name), arity) ] + | ModuleDeclaration modDecl -> collectMemberDeclarations modDecl.Members + | ActionDeclaration _ + | ClassDeclaration _ -> [] + ) + + collectMemberDeclarations file.Declarations + |> List.choose (fun (memDecl, info, (_, arity)) -> + if + info.IsValue + && not info.IsInstance + && arity = 0 + && (info.IsMutable || readsFreeMutable Set.empty memDecl.Body) + then + Some(mutableStateKey moduleName memDecl.Name, memDecl.Name) + else + None + ) + |> List.groupBy fst + |> List.iter (fun (stateKey, declarations) -> + let names = declarations |> List.map snd |> List.distinct + + if names.Length > 1 then + let names = + names + |> List.map (decodeFSharpCompiledName >> sprintf "'%s'") + |> String.concat " and " + + // invariant: Distinct module values cannot share a process-dictionary key, even when their function names remain distinct. + beamCom.AddLog( + $"Module values %s{names} in '%s{moduleName}' share Erlang state key '%s{stateKey}'. Rename one declaration.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + ) + + let privateClassBindings = + collectMemberDeclarations file.Declarations + |> List.groupBy (fun (_, _, key) -> key) + |> List.collect (fun (_, declarations) -> + declarations + |> List.choose (fun (memDecl, info, _) -> + let separatorIndex = memDecl.Name.LastIndexOf("__", System.StringComparison.Ordinal) + + let memberName = + if separatorIndex < 0 then + memDecl.Name + else + memDecl.Name.Substring(separatorIndex + 2) + + let hasUpperCasePeer = + declarations + |> List.exists (fun (otherDecl, _, _) -> + let otherSeparatorIndex = + otherDecl.Name.LastIndexOf("__", System.StringComparison.Ordinal) + + let otherMemberName = + if otherSeparatorIndex < 0 then + otherDecl.Name + else + otherDecl.Name.Substring(otherSeparatorIndex + 2) + + not (System.String.Equals(otherDecl.Name, memDecl.Name, System.StringComparison.Ordinal)) + && System.String.Equals( + otherDecl.Name, + memDecl.Name, + System.StringComparison.OrdinalIgnoreCase + ) + && not (System.String.IsNullOrEmpty otherMemberName) + && System.Char.IsUpper(otherMemberName[0]) + ) + + // invariant: A captured local has no enclosing entity to be a member of, so no other + // file can name it. `functionName` is consulted on the local-call paths only, so + // renaming anything reachable from elsewhere emits the old atom at that call site. + let isCapturedLocal = info.ApparentEnclosingEntity.IsNone + + if + info.IsInstance + && isCapturedLocal + && not (System.String.IsNullOrEmpty memberName) + && System.Char.IsLower(memberName[0]) + && hasUpperCasePeer + then + // decision: A captured `let work` keeps a separate generated function from + // `member Work`; F# distinguishes their case, while Erlang function atoms do not. + Some(memDecl.Name, sanitizeErlangName ("fable_private_" + memDecl.Name)) + else + None + ) + ) + |> Map.ofList + + let ctx = { ctx with PrivateClassBindings = privateClassBindings } + + let rec collectModuleMemberNames declarations = + declarations + |> List.collect ( + function + | MemberDeclaration memDecl -> + let info = + memDecl.ImplementedSignatureRef + |> Option.map beamCom.GetMember + |> Option.defaultWith (fun () -> beamCom.GetMember(memDecl.MemberRef)) + + if info.IsInstance then + [] + else + let arity = memDecl.Args |> FSharp2Fable.Util.discardUnitArg |> List.length + [ (Beam.Atom(sanitizeErlangName memDecl.Name), arity), memDecl.Name ] + | ModuleDeclaration modDecl -> collectModuleMemberNames modDecl.Members + | ActionDeclaration _ + | ClassDeclaration _ -> [] + ) + + let reportedModuleCollisions = + collectModuleMemberNames file.Declarations + |> List.groupBy fst + |> List.choose (fun ((name, arity), declarations) -> + let sourceNames = + declarations + |> List.map snd + |> List.distinct + |> List.map decodeFSharpCompiledName + + if sourceNames.Length > 1 then + let names = sourceNames |> List.map (sprintf "'%s'") |> String.concat " and " + let (Beam.Atom erlName) = name + + // invariant: Distinct module declarations map to distinct Erlang function keys — otherwise one body is silently dropped. + beamCom.AddLog( + $"Declarations %s{names} in '%s{moduleName}' compile to duplicate Erlang function '%s{erlName}/%d{arity}'. Rename one declaration.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + + Some(name, arity) + else + None + ) + |> Set.ofList + + let declarationForms = + file.Declarations |> List.collect (transformDeclaration beamCom ctx) + + declarationForms + |> List.choose ( + function + | Beam.ErlForm.Function def -> Some((def.Name, def.Arity), def) + | _ -> None + ) + |> List.groupBy fst + |> List.iter (fun ((name, arity), definitions) -> + let key = name, arity + + if + definitions.Length > 1 + && key <> (Beam.Atom "main", 0) + && not (Set.contains key reportedModuleCollisions) + then + // Class constructors, members, and reflection helpers only become visible after transformation. + reportFunctionNameCollision beamCom moduleName name arity + ) + let forms = - file.Declarations - |> List.collect (transformDeclaration beamCom ctx) + declarationForms // Deduplicate functions by name+arity at module level. This handles cases where // a property setter and a method mangle to the same Erlang function name, even // when they come from different declaration paths (ClassDeclaration vs MemberDeclaration). diff --git a/src/Fable.Transforms/Beam/Prelude.fs b/src/Fable.Transforms/Beam/Prelude.fs index ddf4688427..5fd2be6b16 100644 --- a/src/Fable.Transforms/Beam/Prelude.fs +++ b/src/Fable.Transforms/Beam/Prelude.fs @@ -66,11 +66,55 @@ module Naming = sb.ToString() - /// Drop the characters an F# name may carry that an unquoted Erlang atom cannot + /// Decode F# `$XXXX` compiled-name escapes for source-facing diagnostics. + let decodeFSharpCompiledName (name: string) = + Regex.Replace( + name, + @"\$([0-9A-Fa-f]{4})", + fun m -> char (System.Convert.ToInt32(m.Groups.[1].Value, 16)) |> string + ) + + /// Erlang caps atoms at 255 characters, but `erlc` builds atoms of its own out of a function's + /// name for the closures, comprehensions and try blocks inside it (`-name/1-fun-0-`), and it + /// crashes in `list_to_atom` rather than reporting an error when one of those overflows. Leave + /// that decoration room. + let private maxErlangAtomLength = 223 + + let private atomNameHash (name: string) = +#if FABLE_COMPILER + // tradeoff: Standalone uses the existing portable hash to avoid cryptography dependencies; overlong atoms differ from native output. + (uint32 (Fable.Naming.stringHash name)).ToString("x8") +#else + use sha256 = System.Security.Cryptography.SHA256.Create() + + name + |> System.Text.Encoding.UTF8.GetBytes + |> sha256.ComputeHash + |> System.BitConverter.ToString + |> _.Replace("-", "") + |> _.ToLowerInvariant() +#endif + + /// Cap an atom at the length Erlang accepts, keeping a readable prefix plus a hash of the full + /// name. Atoms built by joining already-capped parts have to be capped again. + let limitErlangAtomLength (name: string) = + if name.Length <= maxErlangAtomLength then + name + else + let suffix = "_" + atomNameHash name + let mutable prefixLength = maxErlangAtomLength - suffix.Length + + if System.Char.IsHighSurrogate(name.[prefixLength - 1]) then + prefixLength <- prefixLength - 1 + + // decision: Native compilation uses SHA-256 to distinguish overlong names; standalone keeps its portable hash without adding BCL support. + name.Substring(0, prefixLength) + suffix + + /// Drop or encode the characters an F# name may carry that an unquoted Erlang atom cannot /// (`base'`, `op_$0020`, `Foo.Bar`, ...). Every name that ends up as an atom has to go /// through this, or the printer emits something like `'base'_'` — invalid Erlang. let private stripNonAtomChars (name: string) = - // Decode $XXXX hex sequences from F# compiled names (e.g. $0020 -> space -> _) + // invariant: Encoded F# punctuation remains distinct from literal underscores — sanitization must not merge declarations. Regex.Replace( name, @"\$([0-9A-Fa-f]{4})", @@ -80,7 +124,7 @@ module Naming = if System.Char.IsLetterOrDigit(c) then c.ToString() else - "_" + $"_x%s{m.Groups.[1].Value.ToLowerInvariant()}_" ) |> fun s -> s.Replace("'", "").Replace("$", "_").Replace("@", "").Replace(".", "_").Replace("`", "_").Replace("-", "_") @@ -91,19 +135,21 @@ module Naming = |> fun s -> Regex.Replace(s, "_+", "_") |> fun s -> s.Trim('_') |> checkErlKeywords + |> limitErlangAtomLength /// The atom a union case is tagged with at runtime: `[]` verbatim when present, /// otherwise the snake_cased case name. Codegen and the `erl_tag` in reflection metadata must /// both go through this, or reflection looks up a tag the constructor never emitted. let unionCaseTagName (compiledName: string option) (caseName: string) = match compiledName with - | Some name -> name + | Some name -> limitErlangAtomLength name | None -> sanitizeErlangName caseName let moduleNameFromFile (filePath: string) = Fable.Path.GetFileNameWithoutExtension(filePath) |> fun s -> s.Replace(".", "_").Replace("-", "_") |> Fable.Naming.applyCaseRule Fable.Core.CaseRules.SnakeCase + |> limitErlangAtomLength // ---------------------------------------------------------------------------------- // Qualified module names @@ -411,6 +457,7 @@ module Naming = |> fun s -> Regex.Replace(s, "_+", "_") |> fun s -> s.Trim('_') |> checkErlKeywords + |> limitErlangAtomLength /// Number of leading segments `a` and `b` have in common. let private commonPrefixLength (a: string[]) (b: string[]) = @@ -494,7 +541,24 @@ module Naming = else snakeName - checkErlKeywords disambiguated + checkErlKeywords disambiguated |> limitErlangAtomLength + + /// The atom a class or `val` field is stored under in the instance map. Codegen and the + /// collision diagnostic must both derive it here, or a collision is emitted unreported. + let classFieldAtomName (name: string) = + limitErlangAtomLength ("field_" + sanitizeErlangName name) + + /// Mark a generated accessor before atom sanitization, including runtime replacement calls. + let accessorFunctionName name = name + Fable.Naming.beamAccessorSuffix + + /// Derive the map key shared by interface implementations, dispatch, and collision diagnostics. + let interfaceMemberKey isSetter (name: string) = + let memberName = sanitizeErlangName name + + if isSetter then + limitErlangAtomLength ("set_" + memberName) + else + memberName /// Quote an Erlang atom if it needs quoting (starts with uppercase, contains special chars, etc.) /// @@ -548,6 +612,65 @@ module Chars = | Some _ -> Char | None -> e.Type +/// Statically resolved implementations of virtual System.Object members. +/// +/// Records and unions are bare maps/tuples/atoms on Beam, so they cannot carry an instance method +/// table. Keep the generated function naming and lookup contract in one place: declaration emission +/// uses the same name that call sites import. +module ObjectOverrides = + open Fable + open Fable.AST + open Fable.AST.Fable + open Fable.Transforms + + let isSystemObjectOverride (memb: MemberDecl) = + match memb.ImplementedSignatureRef with + | Some(MemberRef(entityRef, _)) -> entityRef.FullName = Types.object + | _ -> false + + let functionName entityName memberName = + let entityName = Naming.sanitizeErlangName entityName + Naming.limitErlangAtomLength $"%s{entityName}_%s{Naming.sanitizeErlangName memberName}" + + let tryCallZeroArg + (com: FSharp2Fable.IFableCompiler) + (r: SourceLocation option) + (returnType: Type) + memberName + (arg: Expr) + = + match arg.Type with + | DeclaredType(entRef, _) -> + let memberInfo = + { + IsInstance = true + CompiledName = memberName + NonCurriedArgTypes = Some [] + AttributeFullNames = [] + } + + com.TryGetEntity(entRef) + |> Option.bind (fun ent -> ent.TryFindMember(memberInfo)) + |> Option.filter (fun memb -> memb.IsOverrideOrExplicitInterfaceImplementation) + |> Option.bind (fun memb -> + entRef.SourcePath + |> Option.map (fun sourcePath -> + let entityName = FSharp2Fable.Helpers.getEntityDeclarationName com entRef + let calleeName = functionName entityName memb.CompiledName + let calleeType = LambdaType(arg.Type, returnType) + let memberRef = MemberRef(entRef, memberInfo) + + let callee = + if sourcePath = com.CurrentFile && not com.IsPrecompilingInlineFunction then + makeTypedIdentExpr calleeType calleeName + else + makeInternalMemberImport com calleeType memberRef calleeName sourcePath r + + makeCall r returnType (makeCallInfo None [ arg ] [ arg.Type ]) callee + ) + ) + | _ -> None + /// Conversion of a value to its `string` form, dispatched on the static type. module ToString = open Fable.AST @@ -568,18 +691,31 @@ module ToString = let toStringByType (com: ICompiler) (r: SourceLocation option) (t: Type) (arg: Expr) = let arg = Chars.tryAsChar arg |> Option.defaultValue arg - match arg.Type with - | Type.String -> Some arg - | Type.Char -> emitExpr r t [ arg ] "<<($0)/utf8>>" |> Some - | Type.Number(kind, _) -> - match kind with - | Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", t, [ arg ], ?loc = r) |> Some - | Float16 - | Float32 - | Float64 -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some - | _ -> emitExpr r t [ arg ] "integer_to_binary($0)" |> Some - | Type.Boolean -> emitExpr r t [ arg ] "atom_to_binary($0)" |> Some - | _ -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some + match ObjectOverrides.tryCallZeroArg com r t "ToString" arg with + | Some call -> Some call + | None -> + match arg.Type with + | Type.String -> Some arg + | Type.Char -> emitExpr r t [ arg ] "<<($0)/utf8>>" |> Some + | Type.Number(kind, _) -> + match kind with + | Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", t, [ arg ], ?loc = r) |> Some + | Float16 + | Float32 + | Float64 -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some + | _ -> emitExpr r t [ arg ] "integer_to_binary($0)" |> Some + | Type.Boolean -> emitExpr r t [ arg ] "atom_to_binary($0)" |> Some + | DeclaredType(ent, _) when ent.FullName = Types.timespan -> + Helper.LibCall(com, "fable_timespan", "to_string", t, [ arg ], ?loc = r) |> Some + | DeclaredType(ent, _) when ent.FullName = Types.datetime -> + Helper.LibCall(com, "fable_date", "to_string", t, [ arg ], ?loc = r) |> Some + | DeclaredType(ent, _) when ent.FullName = "System.Uri" -> + Helper.LibCall(com, "fable_uri", "to_string", t, [ arg ], ?loc = r) |> Some + | DeclaredType(ent, _) when ent.FullName = "System.Text.StringBuilder" -> + // StringBuilder.ToString() → iolist_to_binary(get(maps:get(field_buf, get(Sb)))) + emitExpr r t [ arg ] "iolist_to_binary(get(maps:get(field_buf, get($0))))" + |> Some + | _ -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some /// Fixed-width integer semantics. Erlang integers are arbitrary precision and never /// overflow, so operations that can leave the width of a .NET sized integer are routed diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 33738383c2..429ef885da 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -23,6 +23,32 @@ let private wrapToIntType (com: ICompiler) r (t: Type) (sourceType: Type) (expr: Helper.LibCall(com, "fable_int", Integers.wrapFunctionName info, t, [ expr ], ?loc = r) | _ -> expr +let private integerRangeArgs (bits, signed) = + [ makeIntConst bits; makeBoolConst signed ] + +let private sizedIntInfoForEntity = + function + | Types.int8 -> Some(8, true) + | Types.uint8 -> Some(8, false) + | Types.int16 -> Some(16, true) + | Types.uint16 -> Some(16, false) + | Types.int32 -> Some(32, true) + | Types.uint32 -> Some(32, false) + | Types.int64 -> Some(64, true) + | Types.uint64 -> Some(64, false) + | _ -> None + +let private checkedIntegerCall (com: ICompiler) r t funcName args rangeInfo = + let args = + match rangeInfo with + | Some info -> args @ integerRangeArgs info + | None -> args + + Helper.LibCall(com, "fable_convert", funcName, t, args, ?loc = r) + +let private checkedIntegerCallForType com r t funcName args = + checkedIntegerCall com r t funcName args (Integers.sizedIntInfo t) + /// Convert an expression *into* a BigInteger. Erlang integers are already arbitrary-precision, so /// this is the identity for an integral source. A float must be truncated toward zero, the way /// `BigInteger(double)` does: leaving it as a float would hand a non-integer to the integer @@ -38,6 +64,31 @@ let private isFunctionType (t: Type) = | DelegateType _ -> true | _ -> false +let private printfArgumentTypes (t: Type) = + match t.Generics |> List.tryHead with + | Some(Fable.Transforms.AST.NestedLambdaType(argTypes, _)) -> argTypes + | _ -> [] + +let private printfFormatters (com: ICompiler) r (t: Type) = + let formatters, hasCustomFormatter = + (([], false), printfArgumentTypes t) + ||> List.fold (fun (formatters, hasCustomFormatter) argType -> + let arg = makeTypedIdent argType $"printf_arg_%d{com.IncrementCounter()}" + + match ObjectOverrides.tryCallZeroArg com r String "ToString" (IdentExpr arg) with + | Some call -> Lambda(arg, call, None) :: formatters, true + | None -> Value(Null Any, None) :: formatters, hasCustomFormatter + ) + + if hasCustomFormatter then + // Printf applies values later through a curried runtime function. Preserve the static type + // knowledge as a parallel list of converters; `undefined` keeps the ordinary runtime path. + (Value(NewList(None, Any), None), formatters) + ||> List.fold (fun tail formatter -> Value(NewList(Some(formatter, tail), Any), None)) + |> Some + else + None + let private physicalEquals r (left: Expr) (right: Expr) = // Reference identity. For function values, the same F# value can be represented as either an // uncurried fun or a re-curried nested adapter at different sites, which Erlang's `=:=` sees as @@ -123,6 +174,31 @@ let private compare (com: ICompiler) r (left: Expr) (right: Expr) = Helper.LibCall(com, modName, "compare", Number(Int32, NumberInfo.Empty), [ left; right ], ?loc = r) +let private makeComparerFunction (com: ICompiler) ctx typArg = + let x = makeUniqueIdent com ctx typArg "x" + let y = makeUniqueIdent com ctx typArg "y" + let body = compare com None (IdentExpr x) (IdentExpr y) + Delegate([ x; y ], body, None, Tags.empty) + +let private tryUnionComparerFunction (com: ICompiler) ctx typ = + // decision: Keep native ordering for ordinary values and inject declaration ordering only + // when the collection operation retains a statically known union type. + if isFableUnion com typ then + makeComparerFunction com ctx typ |> Some + else + None + +let private tryGenericUnionComparerFunction (com: ICompiler) ctx index (info: CallInfo) = + info.GenericArgs + |> List.tryItem index + |> Option.bind (tryUnionComparerFunction com ctx) + +let private tryCollectionUnionComparerFunction (com: ICompiler) ctx typ = + match typ with + | List elemType + | Array(elemType, _) -> tryUnionComparerFunction com ctx elemType + | _ -> None + /// Relational operator (`<`, `<=`, `>`, `>=`): native Erlang term ordering for most /// types, but structural `compare_union` ordering for F# unions and instant ordering /// for dates. @@ -155,6 +231,31 @@ let private asArrayRef (com: ICompiler) r (t: Type) (expr: Expr) = | Array _ -> wrapArr com r t expr | _ -> Helper.LibCall(com, "fable_utils", "new_ref", t, [ expr ], ?loc = r) +let private captureExprOnce (com: ICompiler) name (expr: Expr) build = + let ident = makeTypedIdent expr.Type $"%s{name}_%d{com.IncrementCounter()}" + + // invariant: A captured expression is evaluated once before every use of its generated identifier. + Let(ident, expr, build (IdentExpr ident)) + +let private captureExprs2Once com (name1, expr1) (name2, expr2) build = + captureExprOnce com name1 expr1 (fun expr1 -> captureExprOnce com name2 expr2 (build expr1)) + +let private captureExprs4Once com arg1 arg2 arg3 arg4 build = + captureExprs2Once com arg1 arg2 (fun expr1 expr2 -> captureExprs2Once com arg3 arg4 (build expr1 expr2)) + +let private captureExprs5Once com arg1 arg2 arg3 arg4 arg5 build = + captureExprs2Once + com + arg1 + arg2 + (fun expr1 expr2 -> + captureExprs2Once + com + arg3 + arg4 + (fun expr3 expr4 -> captureExprOnce com (fst arg5) (snd arg5) (build expr1 expr2 expr3 expr4)) + ) + let private getOne (com: ICompiler) (ctx: Context) (t: Type) = match t with | Boolean -> makeBoolConst true @@ -206,7 +307,12 @@ let private fsFormat Helper.LibCall(com, "fable_string", "interpolate", t, [ str; values ], i.SignatureArgTypes, ?loc = r) |> Some | ".ctor", _, arg :: _ -> - Helper.LibCall(com, "fable_string", "printf", t, [ arg ], i.SignatureArgTypes, ?loc = r) + let printfArgs = + match printfFormatters com r t with + | Some formatters -> [ arg; formatters ] + | None -> [ arg ] + + Helper.LibCall(com, "fable_string", "printf", t, printfArgs, i.SignatureArgTypes, ?loc = r) |> Some | _ -> None @@ -223,21 +329,55 @@ let private operators | ("DefaultArg" | "DefaultValueArg"), [ opt; defValue ] -> Helper.LibCall(com, "fable_option", "default_value", _t, [ opt; defValue ]) |> Some - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r _t msg |> Some + | "FailWith", [ msg ] -> makeThrow r _t msg |> Some + | "InvalidOp", [ msg ] -> + makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => invalid_operation_exception, message => $0}") + |> Some | "InvalidArg", [ argName; msg ] -> - let msg = - add msg (add (add (Value(StringConstant " (Parameter '", None)) argName) (Value(StringConstant "')", None))) - - makeThrow r _t msg |> Some + captureExprOnce + com + "param_name" + argName + (fun argName -> + let msg = + add + msg + (add + (add (Value(StringConstant " (Parameter '", None)) argName) + (Value(StringConstant "')", None))) + + makeThrow + r + _t + (emitExpr + r + Any + [ msg; argName ] + "#{exn_type => argument_exception, message => $0, param_name => $1}") + ) + |> Some | "Raise", [ arg ] -> makeThrow r _t arg |> Some | "NullArg", [ arg ] -> - let msg = - add - (Value(StringConstant "Value cannot be null. (Parameter '", None)) - (add arg (Value(StringConstant "')", None))) - - makeThrow r _t msg |> Some + captureExprOnce + com + "param_name" + arg + (fun arg -> + let msg = + add + (Value(StringConstant "Value cannot be null. (Parameter '", None)) + (add arg (Value(StringConstant "')", None))) + + makeThrow + r + _t + (emitExpr + r + Any + [ msg; arg ] + "#{exn_type => argument_null_exception, message => $0, param_name => $1}") + ) + |> Some | "IsNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =:= undefined)" |> Some | "IsNotNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =/= undefined)" |> Some // Nullable active patterns and helpers @@ -296,7 +436,13 @@ let private operators | _ -> emitExpr r _t [ arg ] "float(erlang:floor($0))" |> Some | "Log", [ arg ] -> emitExpr r _t [ arg ] "math:log($0)" |> Some | "Log10", [ arg ] -> emitExpr r _t [ arg ] "math:log10($0)" |> Some - | "Log2", [ arg ] -> emitExpr r _t [ arg ] "math:log2($0)" |> Some + | "Log2", [ arg ] -> + match arg.Type with + | Type.Number(Float16, _) + | Type.Number(Float32, _) + | Type.Number(Float64, _) -> emitExpr r _t [ arg ] "math:log2($0)" |> Some + | Type.Number _ -> Helper.LibCall(com, "fable_int", "log2", _t, [ arg ], ?loc = r) |> Some + | _ -> emitExpr r _t [ arg ] "math:log2($0)" |> Some | ("Pow" | "PowInteger" | "op_Exponentiation"), [ base_; exp_ ] -> match base_.Type with | Type.Number(Decimal, _) -> @@ -369,9 +515,7 @@ let private operators | ("ToSByte" | "ToByte" | "ToInt8" | "ToUInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToUInt" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr"), [ arg ] -> match arg.Type with - // TODO: `fable_convert:to_int` does not range-check against the target type, so - // out-of-range input yields the parsed value instead of raising the way .NET does. - | Type.String -> Helper.LibCall(com, "fable_convert", "to_int", _t, [ arg ], ?loc = r) |> Some + | Type.String -> checkedIntegerCallForType com r _t "to_int" [ arg ] |> Some | Type.Number(kind, _) -> match kind with | Decimal -> @@ -687,10 +831,8 @@ let private languagePrimitives | "DivideByInt", [ left; right ] -> makeBinOp r t left right BinaryDivide |> Some // IntrinsicFunctions within LanguagePrimitives | "UnboxFast", [ arg ] -> TypeCast(arg, t) |> Some - | ("ParseInt32" | "ParseUInt32"), [ arg ] -> - Helper.LibCall(com, "fable_convert", "to_int", t, [ arg ], ?loc = r) |> Some - | ("ParseInt64" | "ParseUInt64"), [ arg ] -> - Helper.LibCall(com, "fable_convert", "to_int", t, [ arg ], ?loc = r) |> Some + | ("ParseInt32" | "ParseUInt32"), [ arg ] -> checkedIntegerCallForType com r t "to_int" [ arg ] |> Some + | ("ParseInt64" | "ParseUInt64"), [ arg ] -> checkedIntegerCallForType com r t "to_int" [ arg ] |> Some | _ -> None let private unchecked @@ -708,7 +850,7 @@ let private unchecked match typ with | Boolean -> makeBoolConst false |> Some - | Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue None |> Some + | Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue r |> Some | Char -> CharConstant '\u0000' |> makeValue None |> Some | String -> makeStrConst "" |> Some | _ -> Value(Null typ, r) |> Some @@ -738,39 +880,13 @@ let private objects | _, Array _ -> physicalEquals r arg1 arg2 |> Some | _ -> equals com r true arg1 arg2 |> Some | "GetHashCode", Some thisObj, [] -> - Helper.LibCall(com, "fable_comparison", "hash", t, [ thisObj ], ?loc = r) - |> Some - | "GetType", Some arg, _ -> makeTypeInfo r arg.Type |> Some - | "ToString", Some thisObj, [] -> - match thisObj.Type with - | Type.Char -> emitExpr r t [ thisObj ] "<<($0)/utf8>>" |> Some - | Type.Number(kind, _) -> - match kind with - | Decimal -> - Helper.LibCall(com, "fable_decimal", "to_string", t, [ thisObj ], ?loc = r) - |> Some - | Float16 - | Float32 - | Float64 -> - Helper.LibCall(com, "fable_convert", "to_string", t, [ thisObj ], ?loc = r) - |> Some - | _ -> emitExpr r t [ thisObj ] "integer_to_binary($0)" |> Some - | Type.Boolean -> emitExpr r t [ thisObj ] "atom_to_binary($0)" |> Some - | Type.String -> Some thisObj - | DeclaredType(ent, _) when ent.FullName = Types.timespan -> - Helper.LibCall(com, "fable_timespan", "to_string", t, [ thisObj ], ?loc = r) - |> Some - | DeclaredType(ent, _) when ent.FullName = Types.datetime -> - Helper.LibCall(com, "fable_date", "to_string", t, [ thisObj ], ?loc = r) |> Some - | DeclaredType(ent, _) when ent.FullName = "System.Uri" -> - Helper.LibCall(com, "fable_uri", "to_string", t, [ thisObj ], ?loc = r) |> Some - | DeclaredType(ent, _) when ent.FullName = "System.Text.StringBuilder" -> - // StringBuilder.ToString() → iolist_to_binary(get(maps:get(field_buf, get(Sb)))) - emitExpr r t [ thisObj ] "iolist_to_binary(get(maps:get(field_buf, get($0))))" - |> Some - | _ -> - Helper.LibCall(com, "fable_convert", "to_string", t, [ thisObj ], ?loc = r) + ObjectOverrides.tryCallZeroArg com r t "GetHashCode" thisObj + |> Option.orElseWith (fun () -> + Helper.LibCall(com, "fable_comparison", "get_hash_code", t, [ thisObj ], ?loc = r) |> Some + ) + | "GetType", Some arg, _ -> makeTypeInfo r arg.Type |> Some + | "ToString", Some thisObj, [] -> ToString.toStringByType com r t thisObj | _ -> None /// Beam-specific System.ValueType replacements. @@ -1387,7 +1503,7 @@ let private conversions | ("ToSByte" | "ToByte" | "ToInt8" | "ToUInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToUInt" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr"), [ arg ] -> match arg.Type with - | Type.String -> Helper.LibCall(com, "fable_convert", "to_int", t, [ arg ], ?loc = r) |> Some + | Type.String -> checkedIntegerCallForType com r t "to_int" [ arg ] |> Some | Type.Number(kind, _) -> match kind with | Float16 @@ -1445,6 +1561,19 @@ let private numericTypes (args: Expr list) = match info.CompiledName, thisArg, args with + | ".ctor", None, [ arg ] when info.DeclaringEntityFullName = Types.decimal -> + match arg.Type with + | Type.Array(Type.Number(Int32, _), _) -> + Helper.LibCall(com, "fable_decimal", "from_bits", t, [ derefArr r arg ], ?loc = r) + |> Some + | _ -> None + | ".ctor", None, [ low; mid; high; isNegative; scale ] when info.DeclaringEntityFullName = Types.decimal -> + Helper.LibCall(com, "fable_decimal", "from_parts", t, [ low; mid; high; isNegative; scale ], ?loc = r) + |> Some + | "GetBits", None, [ arg ] when info.DeclaringEntityFullName = Types.decimal -> + Helper.LibCall(com, "fable_decimal", "get_bits", t, [ arg ], ?loc = r) + |> wrapArr com r t + |> Some | "Parse", None, [ arg ] -> match info.DeclaringEntityFullName with | "System.Double" @@ -1452,10 +1581,11 @@ let private numericTypes | "System.Decimal" -> Helper.LibCall(com, "fable_decimal", "parse", t, [ arg ], ?loc = r) |> Some | _ -> // Int32, Int64, Byte, etc. — all parse to integer - emitExpr r t [ arg ] "binary_to_integer($0)" |> Some + checkedIntegerCallForType com r t "to_int" [ arg ] |> Some | "Parse", None, [ arg; _style ] -> // NumberStyles.HexNumber — parse hex string to integer - emitExpr r t [ arg ] "binary_to_integer($0, 16)" |> Some + checkedIntegerCallForType com r t "to_int_with_base" [ arg; makeIntConst 16 ] + |> Some | "ToString", Some c, [] -> match c.Type with | Type.Number(kind, _) -> @@ -1498,7 +1628,13 @@ let private numericTypes Helper.LibCall(com, "fable_decimal", "try_parse", t, [ str; outRef ], ?loc = r) |> Some | _ -> - Helper.LibCall(com, "fable_convert", "try_parse_int", t, [ str; outRef ], ?loc = r) + checkedIntegerCall + com + r + t + "try_parse_int" + [ str; outRef ] + (sizedIntInfoForEntity info.DeclaringEntityFullName) |> Some // Numeric type static methods that delegate to operators | ("Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Log2" | "DivRem"), _, _ -> @@ -1557,16 +1693,17 @@ let private convert = let toInt (arg: Expr) = match arg.Type with - | Type.String -> Helper.LibCall(com, "fable_convert", "to_int", t, [ arg ], ?loc = r) |> Some - | Type.Char -> Some arg - | Type.Number(kind, _) -> - match kind with - | Float16 - | Float32 - | Float64 - | Decimal -> emitExpr r t [ arg ] "trunc($0)" |> Some - | _ -> Some arg - | _ -> Some arg + | Type.String -> checkedIntegerCallForType com r t "to_int" [ arg ] |> Some + | Type.Number(Decimal, _) -> + let rounded = + Helper.LibCall(com, "fable_decimal", "round_decimal", arg.Type, [ arg ], ?loc = r) + + let integerValue = + Helper.LibCall(com, "fable_decimal", "to_int", t, [ rounded ], ?loc = r) + + checkedIntegerCallForType com r t "convert_to_int" [ integerValue ] |> Some + // decision: System.Convert is checked; F# casts keep their separate wrap/truncate path. + | _ -> checkedIntegerCallForType com r t "convert_to_int" [ arg ] |> Some let toFloat (arg: Expr) = match arg.Type with @@ -1584,14 +1721,12 @@ let private convert | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64"), [ arg ] -> toInt arg | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64"), - [ arg; baseArg ] -> - Helper.LibCall(com, "fable_convert", "to_int_with_base", t, [ arg; baseArg ], ?loc = r) - |> Some + [ arg; baseArg ] -> checkedIntegerCallForType com r t "to_int_with_base" [ arg; baseArg ] |> Some | ("ToSingle" | "ToDouble"), [ arg ] -> toFloat arg | "ToChar", [ arg ] -> match arg.Type with | Type.String -> emitExpr r t [ arg ] "binary:first($0)" |> Some - | _ -> Some arg + | _ -> toInt arg | "ToString", [ arg ] -> match arg.Type with | Type.String -> Some arg @@ -1633,13 +1768,21 @@ let private convert | "FromBase64String", [ arg ] -> Helper.LibCall(com, "fable_convert", "from_base64", t, [ arg ], ?loc = r) |> Some + | "ToHexString", ([ _ ] | [ _; _; _ ]) -> + Helper.LibCall(com, "fable_convert", "to_hex_string", t, args, ?loc = r) |> Some + | "ToHexStringLower", ([ _ ] | [ _; _; _ ]) -> + Helper.LibCall(com, "fable_convert", "to_hex_string_lower", t, args, ?loc = r) + |> Some + | "FromHexString", [ arg ] -> + Helper.LibCall(com, "fable_convert", "from_hex_string", t, [ arg ], ?loc = r) + |> Some | _ -> None /// Beam-specific List module replacements. /// Lists in Erlang are native linked lists, same as F#. let private listModule (com: ICompiler) - (_ctx: Context) + (ctx: Context) r (t: Type) (info: CallInfo) @@ -1678,7 +1821,10 @@ let private listModule "lists:sort(fun(A, B) -> fable_comparison:compare_union($0, A, B) =< 0 end, $1)" |> Some | None -> emitExpr r t [ list ] "lists:sort($0)" |> Some - | "SortBy", [ fn; list ] -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; list ]) |> Some + | "SortBy", [ fn; list ] -> + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; comparer; list ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; list ]) |> Some | "SortDescending", [ list ] -> match (match list.Type with @@ -1694,7 +1840,11 @@ let private listModule |> Some | None -> emitExpr r t [ list ] "lists:reverse(lists:sort($0))" |> Some | "SortByDescending", [ fn; list ] -> - Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; list ]) |> Some + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> + Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; comparer; list ]) + |> Some + | None -> Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; list ]) |> Some | "SortWith", [ fn; list ] -> Helper.LibCall(com, "fable_list", "sort_with", t, [ fn; list ]) |> Some | "Contains", [ item; list ] -> emitExpr r t [ item; list ] "lists:member($0, $1)" |> Some | "Exists", [ fn; list ] -> emitExpr r t [ fn; list ] "lists:any($0, $1)" |> Some @@ -1707,10 +1857,22 @@ let private listModule | "Partition", [ fn; list ] -> emitExpr r t [ fn; list ] "lists:partition($0, $1)" |> Some | "Zip", [ l1; l2 ] -> Helper.LibCall(com, "fable_list", "zip", t, [ l1; l2 ]) |> Some | "Unzip", [ list ] -> emitExpr r t [ list ] "lists:unzip($0)" |> Some - | "Min", [ list ] -> emitExpr r t [ list ] "lists:min($0)" |> Some - | "Max", [ list ] -> emitExpr r t [ list ] "lists:max($0)" |> Some - | "MinBy", [ fn; list ] -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; list ]) |> Some - | "MaxBy", [ fn; list ] -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; list ]) |> Some + | "Min", [ list ] -> + match tryCollectionUnionComparerFunction com ctx list.Type with + | Some comparer -> Helper.LibCall(com, "fable_list", "min_with", t, [ comparer; list ]) |> Some + | None -> emitExpr r t [ list ] "lists:min($0)" |> Some + | "Max", [ list ] -> + match tryCollectionUnionComparerFunction com ctx list.Type with + | Some comparer -> Helper.LibCall(com, "fable_list", "max_with", t, [ comparer; list ]) |> Some + | None -> emitExpr r t [ list ] "lists:max($0)" |> Some + | "MinBy", [ fn; list ] -> + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; comparer; list ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; list ]) |> Some + | "MaxBy", [ fn; list ] -> + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; comparer; list ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; list ]) |> Some | "Indexed", [ list ] -> Helper.LibCall(com, "fable_list", "indexed", t, [ list ]) |> Some | "ReduceBack", [ fn; list ] -> Helper.LibCall(com, "fable_list", "reduce_back", t, [ fn; list ]) |> Some | ("Init" | "Initialize"), [ count; fn ] -> Helper.LibCall(com, "fable_list", "init", t, [ count; fn ]) |> Some @@ -2017,15 +2179,29 @@ let private stringModule | "Filter", [ fn; str ] -> Helper.LibCall(com, "fable_string", "filter", t, [ fn; str ]) |> Some | _ -> None -let private makeComparerFunction (com: ICompiler) ctx typArg = - let x = makeUniqueIdent com ctx typArg "x" - let y = makeUniqueIdent com ctx typArg "y" - let body = compare com None (IdentExpr x) (IdentExpr y) - Delegate([ x; y ], body, None, Tags.empty) - let private makeComparer (com: ICompiler) ctx typArg = objExpr [ "Compare", makeComparerFunction com ctx typArg ] +/// BinarySearch of an array or ResizeArray `list`, where a range search goes to `rangeFuncName`. +/// A missing comparer, or a literal null one as .NET allows, means the default comparer. +let private binarySearch (com: ICompiler) ctx r t rangeFuncName (list: Expr) (args: Expr list) = + let call funcName searchArgs (value: Expr) comparer = + let comparer = + match comparer with + | None + | Some NullConst -> makeComparer com ctx value.Type + | Some comparer -> comparer + + Helper.LibCall(com, "fable_resize_array", funcName, t, list :: searchArgs @ [ comparer ], ?loc = r) + |> Some + + match args with + | [ value ] -> call "binary_search" [ value ] value None + | [ value; comparer ] -> call "binary_search" [ value ] value (Some comparer) + | [ index; count; value ] -> call rangeFuncName [ index; count; value ] value None + | [ index; count; value; comparer ] -> call rangeFuncName [ index; count; value ] value (Some comparer) + | _ -> None + let private makeEqualityFunction (com: ICompiler) ctx typArg = let x = makeUniqueIdent com ctx typArg "x" let y = makeUniqueIdent com ctx typArg "y" @@ -2272,6 +2448,9 @@ let private arrays | "IndexOf", None, [ arr; value ] -> let arr = derefArr r arr Helper.LibCall(com, "fable_list", "index_of_value", t, [ value; arr ]) |> Some + // System.Array.BinarySearch, with or without a range and a comparer + | "BinarySearch", None, (ExprType(Array _) as arr) :: args -> + binarySearch com _ctx r t "binary_search_range" (derefArr r arr) args // Iterating an array as a non-generic IEnumerable (e.g. `for v in Enum.GetValues t do`). // Deref via derefArr so byte arrays (atomics, not process-dict refs) are handled too; // the non-generic System.Array of Enum.GetValues stays a ref, which get_enumerator accepts. @@ -2285,7 +2464,7 @@ let private arrays /// Beam-specific Array module replacements. let private arrayModule (com: ICompiler) - (_ctx: Context) + (ctx: Context) r (t: Type) (info: CallInfo) @@ -2303,8 +2482,22 @@ let private arrayModule | "Tail", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "erlang:tl($0)" |> wrapArr com r t |> Some | "IsEmpty", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "($0 =:= [])" |> Some | "Sum", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "lists:sum($0)" |> Some - | "Min", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "lists:min($0)" |> Some - | "Max", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "lists:max($0)" |> Some + | "Min", [ arr ] -> + let comparer = tryCollectionUnionComparerFunction com ctx arr.Type + + let arr = derefArr r arr + + match comparer with + | Some comparer -> Helper.LibCall(com, "fable_list", "min_with", t, [ comparer; arr ]) |> Some + | None -> emitExpr r t [ arr ] "lists:min($0)" |> Some + | "Max", [ arr ] -> + let comparer = tryCollectionUnionComparerFunction com ctx arr.Type + + let arr = derefArr r arr + + match comparer with + | Some comparer -> Helper.LibCall(com, "fable_list", "max_with", t, [ comparer; arr ]) |> Some + | None -> emitExpr r t [ arr ] "lists:max($0)" |> Some | "Contains", [ value; arr ] -> let arr = derefArr r arr in emitExpr r t [ value; arr ] "lists:member($0, $1)" |> Some | "Exists", [ fn; arr ] -> let arr = derefArr r arr in emitExpr r t [ fn; arr ] "lists:any($0, $1)" |> Some @@ -2324,10 +2517,16 @@ let private arrayModule Helper.LibCall(com, "fable_list", "sum_by", t, [ fn; arr ]) |> Some | "MinBy", [ fn; arr ] -> let arr = derefArr r arr - Helper.LibCall(com, "fable_list", "min_by", t, [ fn; arr ]) |> Some + + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; comparer; arr ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; arr ]) |> Some | "MaxBy", [ fn; arr ] -> let arr = derefArr r arr - Helper.LibCall(com, "fable_list", "max_by", t, [ fn; arr ]) |> Some + + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; comparer; arr ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; arr ]) |> Some | "Find", [ fn; arr ] -> let arr = derefArr r arr Helper.LibCall(com, "fable_list", "find", t, [ fn; arr ]) |> Some @@ -2550,7 +2749,9 @@ let private arrayModule | "SortBy", [ fn; arr ] -> let arr = derefArr r arr - Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; arr ]) + (match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; comparer; arr ]) + | None -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; arr ])) |> wrapArr com r t |> Some | "SortWith", [ fn; arr ] -> @@ -2562,7 +2763,9 @@ let private arrayModule | "SortByDescending", [ fn; arr ] -> let arr = derefArr r arr - Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; arr ]) + (match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; comparer; arr ]) + | None -> Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; arr ])) |> wrapArr com r t |> Some | "Zip", [ arr1; arr2 ] -> @@ -2742,7 +2945,7 @@ let private arrayModule | ("Sub" | "GetSubArray"), [ arr; start; count ] -> let arr = derefArr r arr - emitExpr r t [ arr; start; count ] "lists:sublist($0, $1 + 1, $2)" + Helper.LibCall(com, "fable_list", "sub", t, [ arr; start; count ]) |> wrapArr com r t |> Some | "Except", [ excludeArr; arr ] -> @@ -2813,11 +3016,11 @@ let private arrayModule | "Exists2", [ fn; a1; a2 ] -> let a1 = derefArr r a1 let a2 = derefArr r a2 - Helper.LibCall(com, "fable_list", "exists2", t, [ fn; a1; a2 ]) |> Some + Helper.LibCall(com, "fable_list", "array_exists2", t, [ fn; a1; a2 ]) |> Some | "ForAll2", [ fn; a1; a2 ] -> let a1 = derefArr r a1 let a2 = derefArr r a2 - Helper.LibCall(com, "fable_list", "forall2", t, [ fn; a1; a2 ]) |> Some + Helper.LibCall(com, "fable_list", "array_forall2", t, [ fn; a1; a2 ]) |> Some | "Fold2", [ fn; state; a1; a2 ] -> let a1 = derefArr r a1 let a2 = derefArr r a2 @@ -2891,38 +3094,93 @@ let private arrayModule | "ToSeq", [ arr ] -> derefArr r arr |> Some // === In-place mutation: deref for computation, put result back === | "SortInPlace", [ arr ] -> - let derefed = derefArr r arr - let sorted = emitExpr r t [ derefed ] "lists:sort($0)" - Fable.Set(arr, ValueSet, Type.Unit, sorted, None) |> Some + let comparer = tryCollectionUnionComparerFunction com ctx arr.Type + + captureExprOnce + com + "array" + arr + (fun arr -> + let derefed = derefArr r arr + + let sorted = + match comparer with + | Some comparer -> + emitExpr r t [ comparer; derefed ] "lists:sort(fun(A, B) -> $0(A, B) =< 0 end, $1)" + | None -> emitExpr r t [ derefed ] "lists:sort($0)" + + Fable.Set(arr, ValueSet, Type.Unit, sorted, None) + ) + |> Some | "SortInPlaceBy", [ fn; arr ] -> - let derefed = derefArr r arr - let sorted = Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; derefed ]) - Fable.Set(arr, ValueSet, Type.Unit, sorted, None) |> Some + let comparer = tryGenericUnionComparerFunction com ctx 1 info + + captureExprs2Once + com + ("projection", fn) + ("array", arr) + (fun fn arr -> + let derefed = derefArr r arr + + let sorted = + match comparer with + | Some comparer -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; comparer; derefed ]) + | None -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; derefed ]) + + Fable.Set(arr, ValueSet, Type.Unit, sorted, None) + ) + |> Some | "SortInPlaceWith", [ fn; arr ] -> - let derefed = derefArr r arr - let sorted = Helper.LibCall(com, "fable_list", "sort_with", t, [ fn; derefed ]) - Fable.Set(arr, ValueSet, Type.Unit, sorted, None) |> Some + captureExprs2Once + com + ("comparer", fn) + ("array", arr) + (fun fn arr -> + let derefed = derefArr r arr + let sorted = Helper.LibCall(com, "fable_list", "sort_with", t, [ fn; derefed ]) + Fable.Set(arr, ValueSet, Type.Unit, sorted, None) + ) + |> Some | "Fill", [ arr; start; count; value ] -> - let derefed = derefArr r arr + captureExprs4Once + com + ("array", arr) + ("start", start) + ("count", count) + ("value", value) + (fun arr start count value -> + let derefed = derefArr r arr - let filled = - Helper.LibCall(com, "fable_resize_array", "fill", t, [ derefed; start; count; value ]) + let filled = + Helper.LibCall(com, "fable_resize_array", "fill", t, [ derefed; start; count; value ]) - Fable.Set(arr, ValueSet, Type.Unit, filled, None) |> Some + Fable.Set(arr, ValueSet, Type.Unit, filled, None) + ) + |> Some | "CopyTo", [ source; sourceIdx; target; targetIdx; count ] -> - let derefSource = derefArr r source - let derefTarget = derefArr r target - - let blitted = - Helper.LibCall( - com, - "fable_resize_array", - "blit", - t, - [ derefSource; sourceIdx; derefTarget; targetIdx; count ] + captureExprs5Once + com + ("source", source) + ("source_index", sourceIdx) + ("target", target) + ("target_index", targetIdx) + ("count", count) + (fun source sourceIdx target targetIdx count -> + let derefSource = derefArr r source + let derefTarget = derefArr r target + + let blitted = + Helper.LibCall( + com, + "fable_list", + "blit", + t, + [ derefSource; sourceIdx; derefTarget; targetIdx; count ] + ) + + Fable.Set(target, ValueSet, Type.Unit, blitted, None) ) - - Fable.Set(target, ValueSet, Type.Unit, blitted, None) |> Some + |> Some | _ -> None /// Beam-specific Array.Parallel module replacements — dispatches to fable_parallel. @@ -3090,7 +3348,12 @@ let makeRefFromMutableField (callee: Expr) (key: string) = - Get(callee, FieldInfo.Create(key, isMutable = true), t, r) + match callee.Type, key with + | DeclaredType(ent, _), "contents" when ent.FullName = Types.refCell -> + // A BEAM FSharpRef is itself a process-dictionary key, so its contents + // address is the ref expression rather than a field of a record/map. + callee + | _ -> Get(callee, FieldInfo.Create(key, isMutable = true), t, r) let asyncBuilder (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (_thisArg: Expr option) (args: Expr list) = match i.CompiledName with @@ -3134,6 +3397,18 @@ let asyncs (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (_thisArg: Expr op | "Catch" -> Helper.LibCall(com, "fable_async", "catch_async", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "RunSynchronously" -> + // decision: Keep timeout and token in separate slots; the legacy /2 runtime also accepts a token. + let omitted = Null Any |> makeValue r + + let args = + match args with + | [ computation ] -> [ computation; omitted; omitted ] + | [ computation; timeout ] -> [ computation; timeout; omitted ] + | _ -> args + + Helper.LibCall(com, "fable_async", "run_synchronously", t, args, ?loc = r) + |> Some | meth -> Helper.LibCall(com, "fable_async", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -3164,6 +3439,12 @@ let cancels (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr op ?loc = r ) |> Some + | "Dispose" when i.DeclaringEntityFullName = "System.Threading.CancellationTokenRegistration" -> + match thisArg with + | Some registration -> + Helper.LibCall(com, "fable_utils", "safe_dispose", t, [ registration ], ?loc = r) + |> Some + | None -> None | "Dispose" -> Null Type.Unit |> makeValue r |> Some | "Register" -> let args, argTypes = @@ -3273,13 +3554,25 @@ let mailbox (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr op let private regex (com: ICompiler) - (_ctx: Context) + (ctx: Context) r (t: Type) (info: CallInfo) (thisArg: Expr option) (args: Expr list) = + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + match info.CompiledName, thisArg, args with // Constructor | ".ctor", _, [ pattern ] -> Helper.LibCall(com, "fable_regex", "create", t, [ pattern ], ?loc = r) |> Some @@ -3480,6 +3773,9 @@ let private resizeArrays ?loc = r ) |> Some + // BinarySearch + | "BinarySearch", Some callee, args -> + binarySearch com _ctx r t "binary_search_range_count" (emitExpr r (List Any) [ callee ] "get($0)") args // Remove | "Remove", Some callee, [ arg ] -> let listExpr = emitExpr r (List Any) [ callee ] "get($0)" @@ -4345,6 +4641,7 @@ let private randoms match args with | [ seed ] -> Helper.LibCall(com, "fable_random", "new_seeded", t, [ seed ], ?loc = r) |> Some | _ -> Helper.LibCall(com, "fable_random", "new", t, [], ?loc = r) |> Some + | "get_Shared", None -> Helper.LibCall(com, "fable_random", "shared", t, [], ?loc = r) |> Some | "Next", Some thisArg -> match args with | [] -> Helper.LibCall(com, "fable_random", "next", t, [ thisArg ], ?loc = r) |> Some @@ -4816,6 +5113,9 @@ let private dateTimeOffsets (args: Expr list) = match info.CompiledName with + | "op_Implicit" -> + Helper.LibCall(com, "fable_date_offset", "from_date", t, args @ [ makeIntConst 0 ], ?loc = r) + |> Some | ".ctor" -> match args with | [] -> Helper.LibCall(com, "fable_date_offset", "min_value", t, [], ?loc = r) |> Some @@ -5392,18 +5692,30 @@ let private getMangledNames (i: CallInfo) (thisArg: Expr option) = let mangledName = Naming.buildNameWithoutSanitationFrom entityName isStatic memberName i.OverloadSuffix + let mangledName = + match i.CompiledName with + // decision: Match the runtime property explicitly; get_Chars is an ordinary F# method despite its BCL accessor spelling. + // invariant: StringBuilder.Length calls use the same accessor marker as its F# runtime declaration. + | "get_Length" -> accessorFunctionName mangledName + | _ -> mangledName + moduleName, mangledName let private bclType (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName, mangledName = getMangledNames i thisArg + match i.CompiledName, thisArg, args with + | "ToString", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "GetHashCode", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "Equals", Some callee, [ other ] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | _ -> + let moduleName, mangledName = getMangledNames i thisArg - let args = - match thisArg with - | Some callee -> callee :: args - | _ -> args + let args = + match thisArg with + | Some callee -> callee :: args + | _ -> args - Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) - |> Some + Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some let tryType (_t: Type) = None @@ -5563,6 +5875,7 @@ let tryCall match info.DeclaringEntityFullName with // Implicit cast for erased unions (U2, U3...) via `!^`; the cast is erased. | _ when info.CompiledName = "op_ErasedCast" -> List.tryHead args + | Naming.StartsWith "Fable.Core.U" _ when info.CompiledName = "op_Implicit" -> List.tryHead args | "Microsoft.FSharp.Core.Operators" | "Microsoft.FSharp.Core.Operators.Checked" | "Microsoft.FSharp.Core.ExtraTopLevelOperators" @@ -5676,7 +5989,12 @@ let tryCall | "Microsoft.FSharp.Core.NumericLiterals.NumericLiteralI" -> // Erlang has native arbitrary-precision integers, so BigInt ops map directly match info.CompiledName, thisArg, args with - | ".ctor", None, [ arg ] -> toBigIntFrom r t arg |> Some // bigint(x) = x in Erlang, bar a float + | ".ctor", None, [ arg ] -> + match arg.Type with + | Array(Number(UInt8, _), _) -> + Helper.LibCall(com, "fable_int", "bigint_from_byte_array", t, [ arg ], ?loc = r) + |> Some + | _ -> toBigIntFrom r t arg |> Some // bigint(x) = x in Erlang, bar a float | "op_Addition", None, [ left; right ] -> makeBinOp r t left right BinaryPlus |> Some | "op_Subtraction", None, [ left; right ] -> makeBinOp r t left right BinaryMinus |> Some | "op_Multiply", None, [ left; right ] -> makeBinOp r t left right BinaryMultiply |> Some @@ -5709,11 +6027,19 @@ let tryCall | Number(BigInt, _) -> toBigIntFrom r t arg |> Some | _ -> arg |> wrapToIntType com r t arg.Type |> Some | "Parse", None, [ str ] -> emitExpr r t [ str ] "binary_to_integer($0)" |> Some - | "Pow", None, [ base_; exp_ ] -> emitExpr r t [ base_; exp_ ] "math:pow($0, $1)" |> Some + | "ToByteArray", Some value, [] -> + Helper.LibCall(com, "fable_int", "bigint_to_byte_array", t, [ value ], ?loc = r) + |> Some + | "Pow", None, [ base_; exp_ ] -> + Helper.LibCall(com, "fable_int", "bigint_pow", t, [ base_; exp_ ], ?loc = r) + |> Some + | "GreatestCommonDivisor", None, [ left; right ] -> + Helper.LibCall(com, "fable_int", "bigint_gcd", t, [ left; right ], ?loc = r) + |> Some | "Log", None, [ arg ] -> emitExpr r t [ arg ] "math:log(float($0))" |> Some | "Log", None, [ arg; base_ ] -> emitExpr r t [ arg; base_ ] "(math:log(float($0)) / math:log($1))" |> Some | "Log10", None, [ arg ] -> emitExpr r t [ arg ] "math:log10(float($0))" |> Some - | "Log2", None, [ arg ] -> emitExpr r t [ arg ] "math:log2(float($0))" |> Some + | "Log2", None, [ arg ] -> Helper.LibCall(com, "fable_int", "log2", t, [ arg ], ?loc = r) |> Some | "DivRem", None, [ x; y ] -> emitExpr r t [ x; y ] "{$0 div $1, $0 rem $1}" |> Some | "DivRem", None, [ x; y; refRem ] -> Helper.LibCall(com, "fable_utils", "div_rem", t, [ x; y; refRem ], ?loc = r) @@ -5777,7 +6103,8 @@ let tryCall | Types.printfModule | Naming.StartsWith Types.printfFormat _ -> fsFormat com ctx r t info thisArg args | "System.Threading.CancellationToken" - | "System.Threading.CancellationTokenSource" -> cancels com ctx r t info thisArg args + | "System.Threading.CancellationTokenSource" + | "System.Threading.CancellationTokenRegistration" -> cancels com ctx r t info thisArg args | "Microsoft.FSharp.Control.CommonExtensions" -> controlExtensions com ctx r t info thisArg args | "Microsoft.FSharp.Control.FSharpEvent`1" | "Microsoft.FSharp.Control.FSharpEvent`2" @@ -5824,7 +6151,7 @@ let tryCall | "System.Diagnostics.Stopwatch" -> stopwatch com ctx r t info thisArg args | Types.nullable -> nullables com ctx r t info thisArg args | Types.guid -> guids com ctx r t info thisArg args - | "System.Random" -> randoms com ctx r t info thisArg args + | Types.random -> randoms com ctx r t info thisArg args | "System.Environment" -> match info.CompiledName with | "get_NewLine" -> makeStrConst "\n" |> Some @@ -5928,20 +6255,39 @@ let tryCall | "Capture", _, [ arg ] -> Some arg | "Throw", Some arg, _ -> makeThrow r t arg |> Some | _ -> None + | "System.AggregateException" -> + let makeAggregateException message exceptions = + Helper.LibCall(com, "fable_utils", "aggregate_exception", t, [ message; exceptions ], ?loc = r) + |> Some + + match info.CompiledName, thisArg, args, info.SignatureArgTypes with + | ".ctor", None, [], _ -> + makeAggregateException (makeStrConst "One or more errors occurred.") (Value(NewList(None, Any), None)) + | ".ctor", None, [ message ], [ String ] -> makeAggregateException message (Value(NewList(None, Any), None)) + | ".ctor", None, [ exceptions ], [ _ ] -> + makeAggregateException (makeStrConst "One or more errors occurred.") exceptions + | ".ctor", None, [ message; exceptions ], [ String; _ ] -> makeAggregateException message exceptions + | "get_Message", Some aggregate, _, _ -> emitExpr r t [ aggregate ] "maps:get(message, $0, $0)" |> Some + | "get_InnerException", Some aggregate, _, _ -> + emitExpr r t [ aggregate ] "maps:get(inner_exception, $0, undefined)" |> Some + | "get_InnerExceptions", Some aggregate, _, _ -> + emitExpr r t [ aggregate ] "maps:get(inner_exceptions, $0)" |> Some + | _ -> None | "System.Exception" -> match info.CompiledName, thisArg, args with - | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some - | ".ctor", None, [ msg; inner ] -> emitExpr r t [ msg; inner ] "#{message => $0, inner_exception => $1}" |> Some - | ".ctor", None, [] -> - emitExpr r t [] "#{message => <<\"Exception of type 'System.Exception' was thrown.\">>}" + | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] "#{exn_type => exception, message => $0}" |> Some + | ".ctor", None, [ msg; inner ] -> + emitExpr r t [ msg; inner ] "#{exn_type => exception, message => $0, inner_exception => $1}" |> Some - | "get_Message", Some c, _ -> - // Handle both map exceptions and reference-based class exceptions + | ".ctor", None, [] -> emitExpr r t - [ c ] - "case erlang:is_reference($0) of true -> maps:get(message, erlang:get($0), $0); false -> maps:get(message, $0, $0) end" + [] + "#{exn_type => exception, message => <<\"Exception of type 'System.Exception' was thrown.\">>}" + |> Some + | "get_Message", Some c, _ -> + Helper.LibCall(com, "fable_utils", "exception_message", t, [ c ], ?loc = r) |> Some | "get_InnerException", Some c, _ -> // Handle both map exceptions and reference-based class exceptions @@ -5956,23 +6302,74 @@ let tryCall | BuiltinSystemException _ | "System.Collections.Generic.KeyNotFoundException" | "System.OperationCanceledException" -> + let typeName = info.DeclaringEntityFullName + + let typeTag = + typeName.Substring(typeName.LastIndexOf('.') + 1) |> sanitizeErlangName + + let make msg inner = + match inner with + | None -> emitExpr r t [ msg ] $"#{{exn_type => %s{typeTag}, message => $0}}" + | Some inner -> + emitExpr r t [ msg; inner ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" + + let isArgumentException = typeName = "System.ArgumentException" + + // ArgumentNullException and ArgumentOutOfRangeException take (paramName, message), the others (message, paramName) + let paramNameFirst = + typeName = "System.ArgumentNullException" + || typeName = "System.ArgumentOutOfRangeException" + + // message and paramName are evaluated once, in source order + let makeArg msg paramName inner = + let build msg paramName = + let msg = + Helper.LibCall(com, "fable_utils", "argument_exception_message", String, [ msg; paramName ]) + + match inner with + | None -> + emitExpr r t [ msg; paramName ] $"#{{exn_type => %s{typeTag}, message => $0, param_name => $1}}" + | Some inner -> + emitExpr + r + t + [ msg; paramName; inner ] + $"#{{exn_type => %s{typeTag}, message => $0, param_name => $1, inner_exception => $2}}" + + if paramNameFirst then + captureExprs2Once + com + ("param_name", paramName) + ("message", msg) + (fun paramName msg -> build msg paramName) + else + captureExprs2Once com ("message", msg) ("param_name", paramName) build + + let defaultMessage = + match typeName with + | "System.ArgumentException" -> "Value does not fall within the expected range." + | "System.ArgumentNullException" -> "Value cannot be null." + | "System.ArgumentOutOfRangeException" -> "Specified argument was out of the range of valid values." + | _ -> $"Exception of type '%s{typeName}' was thrown." + match info.CompiledName, thisArg, args with - | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some - | ".ctor", None, [ msg; second ] -> + | ".ctor", None, [] -> make (makeStrConst defaultMessage) None |> Some + | ".ctor", None, [ paramName ] when paramNameFirst -> + makeArg (makeStrConst defaultMessage) paramName None |> Some + | ".ctor", None, [ msg ] -> make msg None |> Some + | ".ctor", None, [ first; second ] -> match info.SignatureArgTypes with - // (message, paramName): paramName is not modelled, only the message is kept - | [ _; String ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some - // (message, innerException) - | _ -> emitExpr r t [ msg; second ] "#{message => $0, inner_exception => $1}" |> Some - // (message, paramName, innerException) - | ".ctor", None, [ msg; _paramName; inner ] -> - emitExpr r t [ msg; inner ] "#{message => $0, inner_exception => $1}" |> Some - | ".ctor", None, [] -> - let typeName = info.DeclaringEntityFullName - let msg = $"Exception of type '%s{typeName}' was thrown." - emitExpr r t [] $"#{{message => <<\"%s{msg}\">>}}" |> Some + | [ _; String ] when paramNameFirst -> makeArg second first None |> Some + | [ _; String ] when isArgumentException -> makeArg first second None |> Some + | [ _; String ] -> make first None |> Some + | _ -> make first (Some second) |> Some + | ".ctor", None, [ msg; paramName; inner ] when isArgumentException -> + makeArg msg paramName (Some inner) |> Some + | ".ctor", None, [ paramName; _; msg ] when paramNameFirst -> makeArg msg paramName None |> Some + | ".ctor", None, [ msg; _; inner ] -> make msg (Some inner) |> Some | "get_Message", Some c, _ -> emitExpr r t [ c ] "maps:get(message, $0, $0)" |> Some | "get_InnerException", Some c, _ -> emitExpr r t [ c ] "maps:get(inner_exception, $0, undefined)" |> Some + | "get_ParamName", Some c, _ -> emitExpr r t [ c ] "maps:get(param_name, $0, undefined)" |> Some | _ -> None // System.Type (reflection) — type info is a map #{fullname => ..., generics => [...]} | "System.Type" -> diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs index 15943a8a5d..4b9f9196c1 100644 --- a/src/Fable.Transforms/Dart/Fable2Dart.fs +++ b/src/Fable.Transforms/Dart/Fable2Dart.fs @@ -44,6 +44,7 @@ type Context = /// Vars declared in current function scope VarsDeclaredInScope: HashSet ConstIdents: Set + ShadowingMemberNames: Set } member this.AddToScope(name) = @@ -219,7 +220,8 @@ module Util = Type.reference (makeImmutableIdent MetaType "MapEntry", [ key; value ]) match entRef.FullName, genArgs with - | Types.enum_, _ -> Integer + | Types.enum_, _ + | Types.rune, _ -> Integer // F# Quotation types are modelled dynamically by the quotation runtime, // so map them to `dynamic` (the runtime returns plain tagged objects). | Types.fsharpExpr, _ @@ -241,7 +243,7 @@ module Util = | "Fable.Core.Dart.DartNullable`1", [ genArg ] -> Nullable genArg | Types.regexGroup, _ -> Nullable String | Types.regexMatch, _ -> makeTypeRefFromName "Match" [] - | "System.Random", _ -> libTypeRef com ctx "Random" "Random" [] + | Types.random, _ -> libTypeRef com ctx "Random" "Random" [] // We use `dynamic` for Exception because there is no single type that catches all errors in Dart // (except when inherited as base class, then it's getExceptionType, see transformInheritedClass) | Types.exception_, _ -> Dynamic // getExceptionType com ctx @@ -291,6 +293,44 @@ module Util = | _ -> "_") name + /// In Dart, class members shadow the file scope within the class body, see https://github.com/fable-compiler/Fable/issues/4979 + let isShadowedByClassMember (ctx: Context) (name: string) = + let isLocalName = + let locals = ctx.UsedNames.CurrentDeclarationScope + not (isNull locals) && locals.Contains(name) + + ctx.ShadowingMemberNames.Contains(name) + && ctx.File.UsedNamesInRootScope.Contains(name) + && not isLocalName + + let getShadowingMemberNames (ent: Fable.Entity) (decl: Fable.ClassDecl) = + let dispatchSlots = + ent.MembersFunctionsAndValues + |> Seq.choose (fun memb -> + if memb.IsDispatchSlot then + Some memb.DisplayName + else + None + ) + + set + [ + yield! dispatchSlots + yield! ent.FSharpFields |> List.map (fun f -> sanitizeMember f.Name) + yield! decl.AttachedMembers |> List.map (fun memb -> sanitizeMember memb.Name) + if ent.IsFSharpUnion then + "tag" + + yield! + ent.UnionCases + |> List.collect (fun uci -> uci.UnionCaseFields |> List.map (fun f -> sanitizeMember f.Name)) + ] + + /// A Dart library can import itself + let getSelfImportPrefix (com: IDartCompiler) ctx = + let selfModule = com.GetImportIdent(ctx, "*", com.CurrentFile, Fable.MetaType) + selfModule.Name + let getUniqueNameInRootScope (ctx: Context) name = let name = (name, Naming.NoMemberPart) @@ -704,7 +744,12 @@ module Util = makeIdent isMutable typ name let transformIdent (com: IDartCompiler) ctx (id: Fable.Ident) : Ident = - transformIdentWith com ctx id.IsMutable id.Type id.Name + let ident = transformIdentWith com ctx id.IsMutable id.Type id.Name + + if isShadowedByClassMember ctx id.Name then + { ident with ImportModule = Some(getSelfImportPrefix com ctx) } + else + ident let transformIdentAsExpr (com: IDartCompiler) ctx (id: Fable.Ident) = transformIdent com ctx id |> Expression.identExpression @@ -1993,8 +2038,8 @@ module Util = args | Some(Fable.Value _ as e) -> - $"Ignoring base call for %s{classDecl.Entity.FullName}" - |> addWarning com [] e.Range + WarningCodes.baseCallIgnored classDecl.Entity.FullName + |> addWarningWithCode com [] e.Range [] | Some e -> @@ -2158,6 +2203,19 @@ module Util = bindings @ body, captureExpr | Fable.Sequential exprs -> + // Fable appends a unit value after a throw, which Dart cannot type + let exprs = + match + exprs + |> List.tryFindIndex (fun e -> + match e with + | Fable.Extended(Fable.Throw _, _) -> true + | _ -> false + ) + with + | Some i -> List.truncate (i + 1) exprs + | None -> exprs + let exprs, lastExpr = List.splitLast exprs let statements1 = exprs |> List.collect (transform com ctx Ignore >> fst) @@ -3022,6 +3080,8 @@ module Util = let entRef = decl.Entity let ent = com.GetEntity(entRef) + let ctx = { ctx with ShadowingMemberNames = getShadowingMemberNames ent decl } + if ent.IsInterface then transformInterfaceDeclaration com ctx decl ent else @@ -3148,9 +3208,10 @@ module Compiler = member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = DartCompiler(com) @@ -3182,6 +3243,7 @@ module Compiler = OptimizeTailCall = fun () -> () VarsDeclaredInScope = HashSet() ConstIdents = Set.empty + ShadowingMemberNames = Set.empty } let rootDecls = List.collect (transformDeclaration com ctx) file.Declarations diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 3b25868286..01dcf7b65f 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -50,6 +50,7 @@ let coreModFor = | FSharpReference _ -> "Types" | BclHashSet _ -> "MutableSet" | BclDictionary _ -> "MutableMap" + | BclRune | BclKeyValuePair _ -> FableError "Cannot decide core module" |> raise let makeLongInt com r t signed (x: uint64) = @@ -231,6 +232,7 @@ let toChar com (ctx: Context) r (arg: Expr) = // TODO: Check length | Char -> arg | String -> Helper.InstanceCall(arg, "codeUnitAt", Char, [ makeIntConst 0 ]) + | Number((Float16 | Float32 | Float64), _) -> Helper.InstanceCall(arg, "toInt", Char, []) | _ -> TypeCast(arg, Char) let charToString = @@ -247,6 +249,7 @@ let toString com (ctx: Context) r (args: Expr list) = match head.Type with | String -> head | Char -> charToString head + | DeclaredType({ FullName = Types.rune }, _) -> charToString head // | Builtin BclGuid when tail.IsEmpty -> head // | Builtin (BclGuid|BclTimeSpan|BclTimeOnly|BclDateOnly as bt) -> // Helper.LibCall(com, coreModFor bt, "toString", String, args) @@ -632,6 +635,13 @@ let tryEntityIdent (com: Compiler) entFullName = match entFullName with | "Fable.Core.Dart.Future`1" -> makeIdentExpr "Future" |> Some | "Fable.Core.Dart.Stream`1" -> makeIdentExpr "Stream" |> Some + | "Microsoft.FSharp.Control.FSharpAsync`1" -> makeImportLib com MetaType "Async" "AsyncBuilder" |> Some + | "System.Threading.CancellationToken" + | "System.Threading.CancellationTokenSource" -> + makeImportLib com MetaType "CancellationToken" "AsyncBuilder" |> Some + | "System.Threading.CancellationTokenRegistration" -> makeImportLib com MetaType "IDisposable" "Types" |> Some + | "System.OperationCanceledException" -> + makeImportLib com MetaType "OperationCanceledException" "AsyncBuilder" |> Some | BuiltinDefinition BclDateOnly | BuiltinDefinition BclDateTime | BuiltinDefinition BclDateTimeOffset -> makeIdentExpr "DateTime" |> Some @@ -715,6 +725,8 @@ let tryCoreOp com r t coreModule coreMember args = let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.DeclaringEntityFullName, i.CompiledName with | _, UniversalFableCoreHelpers com ctx r t i args error expr -> Some expr + | _, "Async.AwaitFuture.Static" -> Helper.LibCall(com, "Async", "awaitFuture", t, args, ?loc = r) |> Some + | _, "Async.StartAsFuture.Static" -> Helper.LibCall(com, "Async", "startAsFuture", t, args, ?loc = r) |> Some | "Fable.Core.Reflection", meth -> Helper.LibCall(com, "Reflection", meth, t, args, ?loc = r) |> Some | "Fable.Core.Compiler", meth -> match meth with @@ -854,15 +866,20 @@ let getMangledNames (i: CallInfo) (thisArg: Expr option) = moduleName, mangledName let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName, mangledName = getMangledNames i thisArg + match i.CompiledName, thisArg, args with + | "ToString", Some callee, [] -> Helper.InstanceCall(callee, "toString", t, args, ?loc = r) |> Some + | "GetHashCode", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "Equals", Some callee, [ other ] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | _ -> + let moduleName, mangledName = getMangledNames i thisArg - let args = - match thisArg with - | Some callee -> callee :: args - | _ -> args + let args = + match thisArg with + | Some callee -> callee :: args + | _ -> args - Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) - |> Some + Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some let fsharpModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let moduleName, mangledName = getMangledNames i thisArg @@ -1020,16 +1037,12 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Strings | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _ -> fsFormat com ctx r t i thisArg args - | ("Failure" | "FailurePattern" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> + | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r t (error com msg) |> Some - | "InvalidArg", [ argName; msg ] -> - let msg = add msg (add (add (str " (Parameter '") argName) (str "')")) - makeThrow r t (error com msg) |> Some + | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some | "Raise", [ arg ] -> makeThrow r t arg |> Some | "Reraise", _ -> Extended(Throw(None, t), r) |> Some // Math functions @@ -1392,10 +1405,11 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "String", "compareWith", Int32.Number, [ x; y; kind ]) makeEqOp r left (makeIntConst 0) BinaryEqual |> Some + | "EnumerateRunes", Some c, _ -> Helper.LibCall(com, "String", "enumerateRunes", t, [ c ], ?loc = r) |> Some | "GetEnumerator", Some c, _ -> stringToCharSeq c |> getEnumerator com r t |> Some | ("Contains" | "StartsWith" | "EndsWith" as meth), Some c, arg :: _ -> if List.isMultiple args then - addWarning com ctx.InlinePath r $"String.%s{meth}: second argument is ignored" + WarningCodes.stringComparisonIgnored |> addWarningWithCode com ctx.InlinePath r Helper.InstanceCall(c, Naming.lowerFirst meth, t, [ arg ], ?loc = r) |> Some | ReplaceName [ "ToUpper", "toUpperCase" @@ -1548,6 +1562,17 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt |> Some | _ -> None +let runes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + match i.CompiledName, thisArg, args with + | "GetRuneAt", None, [ input; index ] -> + Helper.LibCall(com, "String", "getRuneAt", t, [ input; index ], i.SignatureArgTypes, ?loc = r) + |> Some + | "get_Value", Some rune, [] -> Some rune + | "get_Utf16SequenceLength", Some runes, [] -> + let isBmp = makeEqOp r runes (makeIntConst 0xFFFF) BinaryLessOrEqual + IfThenElse(isBmp, makeIntConst 1, makeIntConst 2, r) |> Some + | _ -> None + let stringModule (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with | "Length", [ arg ] -> getLength arg |> Some @@ -1625,7 +1650,7 @@ let formattableString |} ) - printJsTaggedTemplate str holes (fun i -> "$" + string (i + offset)) + printJsTaggedTemplate str holes (fun i -> "$" + string(i + offset)) emitExpr r t args (callMacro + jsTaggedTemplate) |> Some | "get_Format", Some x, _ -> Helper.LibCall(com, "String", "getFormat", t, [ x ], ?loc = r) |> Some @@ -1672,6 +1697,25 @@ let seqModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg ) |> Some +/// BinarySearch without a comparer, or with a null one, uses the default comparer, like ResizeArray.Sort() +let binarySearch (com: ICompiler) ctx r t (i: CallInfo) rangeMeth (ar: Expr) args = + let call meth args comparer = + let comparer = + match comparer with + | None + | Some NullConst -> makeComparer com ctx (getElementType ar.Type) + | Some comparer -> comparer + + Helper.LibCall(com, "Array", meth, t, ar :: args @ [ comparer ], genArgs = i.GenericArgs, ?loc = r) + |> Some + + match args with + | [ value ] -> call "binarySearch" [ value ] None + | [ value; comparer ] -> call "binarySearch" [ value ] (Some comparer) + | [ index; length; value ] -> call rangeMeth [ index; length; value ] None + | [ index; length; value; comparer ] -> call rangeMeth [ index; length; value ] (Some comparer) + | _ -> None + let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", _, [] -> makeResizeArray (getElementType t) [] |> Some @@ -1717,6 +1761,7 @@ let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this Helper.LibCall(com, "Array", "getSubArray", t, [ ar; idx; cnt ], genArgs = i.GenericArgs, ?loc = r) |> Some | "IndexOf", Some ar, args -> Helper.InstanceCall(ar, "indexOf", t, args, ?loc = r) |> Some + | "BinarySearch", Some ar, args -> binarySearch com ctx r t i "binarySearchRangeCount" ar args | "Insert", Some ar, [ idx; arg ] -> Helper.InstanceCall(ar, "insert", t, [ idx; arg ], ?loc = r) |> Some | "InsertRange", Some ar, [ idx; arg ] -> Helper.InstanceCall(ar, "insertAll", t, [ idx; arg ], ?loc = r) |> Some | "RemoveRange", Some ar, [ startIdx; count ] -> @@ -1799,6 +1844,7 @@ let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E | "IndexOf", None, args -> Helper.LibCall(com, "Array", "indexOf", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some + | "BinarySearch", None, ar :: args -> binarySearch com ctx r t i "binarySearchRange" ar args | "GetEnumerator", Some arg, _ -> getEnumerator com r t arg |> Some | _ -> None @@ -1831,6 +1877,10 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex | "IsEmpty", [ ar ] -> getFieldWith r t ar "isEmpty" |> Some | "Contains", [ value; ar ] -> Helper.LibCall(com, "Array", "contains", t, [ value; ar ], ?loc = r) |> Some | "CopyTo", args -> copyToArray com r t i args + // getSubArray is shared with ResizeArray.GetRange, which must stay unchecked + | "GetSubArray", args -> + Helper.LibCall(com, "Array", "sub", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy" as meth), args -> let meth = Naming.lowerFirst meth let args = injectArg com ctx r "Seq2" meth i.GenericArgs args @@ -1850,14 +1900,24 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use methods for Head and Tail (instead of Get(ListHead) for example) to check for empty lists - | ReplaceName [ "get_Head", "head" - "get_Tail", "tail" - "get_Item", "item" - "get_Length", "length" - "GetSlice", "getSlice" ] methName, - Some x, - _ -> + // list.Head and list.Tail raise InvalidOperationException, List.head and List.tail ArgumentException + | ("get_Head" | "get_Tail"), Some x, _ -> + let mangledName = + Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" + + Helper.LibCall( + com, + "List", + mangledName, + t, + [], + i.SignatureArgTypes, + genArgs = i.GenericArgs, + thisArg = x, + ?loc = r + ) + |> Some + | ReplaceName [ "get_Item", "item"; "get_Length", "length"; "GetSlice", "getSlice" ] methName, Some x, _ -> let args = match args with | [ ExprType Unit ] -> [ x ] @@ -1999,6 +2059,43 @@ let nullables (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: | "get_HasValue", Some c -> makeEqOp r c (makeNull ()) BinaryUnequal |> Some | _ -> None +let nullableModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = + let convert (targetType: Type) (value: Expr) = + match targetType with + | Char -> toChar com ctx r value + | Number((Float16 | Float32 | Float64), _) -> toFloat com ctx r targetType [ value ] + | Number(Decimal, _) -> toDecimal com ctx r targetType [ value ] + | Number _ -> toInt com ctx r targetType [ value ] + | _ -> TypeCast(value, targetType) + + match i.CompiledName.Replace("$W", ""), List.tryLast args, t with + | ("ToByte" | "ToUInt8" | "ToSByte" | "ToInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToInt32" | "ToUInt" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr" | "ToSingle" | "ToFloat32" | "ToDouble" | "ToFloat" | "ToDecimal" | "ToChar" | "ToEnum"), + Some value, + Nullable(targetType, _) -> + match value with + | Value(Null _, _) -> makeNull () |> Some + | Value((NumberConstant _ | CharConstant _), _) -> convert targetType value |> Some + | _ -> + let sourceType = + match value.Type with + | Nullable(sourceType, _) -> sourceType + | sourceType -> sourceType + + let v = makeUniqueIdent com ctx value.Type "v" + + Let( + v, + value, + IfThenElse( + makeEqOp None (IdentExpr v) (makeNull ()) BinaryUnequal, + TypeCast(convert targetType (TypeCast(IdentExpr v, sourceType)), t), + makeNull (), + r + ) + ) + |> Some + | _ -> None + let options isStruct (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with | "Some", _ -> NewOption(List.tryHead args, t.Generics.Head, isStruct) |> makeValue r |> Some @@ -2085,8 +2182,8 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let intConst = int System.Globalization.NumberStyles.Integer if style <> hexConst && style <> intConst then - $"%s{i.DeclaringEntityFullName}.%s{meth}(): NumberStyle %d{style} is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.numberStylesIgnored style + |> addWarningWithCode com ctx.InlinePath r let acceptedArgs = if meth = "Parse" then @@ -2094,10 +2191,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 3 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, style, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r parseCall meth str args style | ("Parse" | "TryParse") as meth, str :: _ -> @@ -2107,10 +2207,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 2 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r let style = int System.Globalization.NumberStyles.Any parseCall meth str args style @@ -2609,12 +2712,16 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr match i.DeclaringEntityFullName with | "System.Collections.Generic.KeyNotFoundException" | BuiltinSystemException _ -> bclType com ctx r t i thisArg args + | "System.OperationCanceledException" -> + let e = makeImportLib com Any "OperationCanceledException" "AsyncBuilder" + Helper.ConstructorCall(e, t, args, ?loc = r) |> Some | _ -> let e = makeImportLib com Any "ExceptionBase" "Types" Helper.ConstructorCall(e, t, args, ?loc = r) |> Some | "get_Message", Some e -> Helper.InstanceCall(e, "toString", t, [], ?loc = r) |> Some // | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some | "get_InnerException", Some e -> getFieldWith r t e "innerException" |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = @@ -2708,8 +2815,7 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( | "ToBase64String" | "FromBase64String" -> if not (List.isSingle args) then - $"Convert.%s{Naming.upperFirst i.CompiledName} only accepts one single argument" - |> addWarning com ctx.InlinePath r + WarningCodes.base64ArgumentsIgnored |> addWarningWithCode com ctx.InlinePath r Helper.LibCall( com, @@ -2722,13 +2828,20 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( ?loc = r ) |> Some + | "ToHexString" + | "ToHexStringLower" -> + Helper.LibCall(com, "String", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "FromHexString" when List.isSingle args -> + Helper.LibCall(com, "String", "fromHexString", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> None let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with | "get_Out" -> typedObjExpr t [] |> Some // empty object | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | _ -> None @@ -2736,7 +2849,7 @@ let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | "Break" -> makeDebugger r |> Some @@ -2858,11 +2971,33 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio Helper.InstanceCall(thisArg.Value, meth, t, args, ?loc = r) |> Some | meth -> + // Drops the IFormatProvider (and DateTimeStyles, where present), warning once per + // discarded argument. `Parse arg` with no extra argument discards nothing. let args = + // Passing InvariantCulture asks for exactly what Fable does, so nothing to report. + let warnProvider culture = + match culture with + | InvariantCulture -> () + | _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + + let warnStyles styles = + match styles with + | NumberConst(NumberValue.Int32 0, _) -> () // DateTimeStyles.None: no special handling + | _ -> WarningCodes.dateTimeStylesIgnored |> addWarningWithCode com ctx.InlinePath r + match meth, args with - // Ignore IFormatProvider + | "Parse", arg :: culture :: styles :: _ -> + warnProvider culture + warnStyles styles + [ arg ] + | "Parse", arg :: culture :: _ -> + warnProvider culture + [ arg ] | "Parse", arg :: _ -> [ arg ] - | "TryParse", input :: _culture :: _styles :: defVal :: _ -> [ input; defVal ] + | "TryParse", input :: culture :: styles :: defVal :: _ -> + warnProvider culture + warnStyles styles + [ input; defVal ] | _ -> args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst @@ -3162,6 +3297,7 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti ?loc = r ) |> Some + | "get_Shared", None -> Helper.LibCall(com, "Random", "shared", t, [], ?loc = r) |> Some // Not yet supported | ("NextInt64" | "NextSingle"), _ -> None | meth, Some thisArg -> @@ -3224,6 +3360,18 @@ let regexMatchToSeq com t e = Helper.LibCall(com, "RegExp", "GroupIterable", t, [ e ]) let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + let isGroup = function | ExprType(DeclaredTypeFullName Types.regexGroup) -> true @@ -3493,7 +3641,8 @@ let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr lis match i.CompiledName with // TODO: Throw error for RunSynchronously | "Start" -> - "Async.Start will behave as StartImmediate" |> addWarning com ctx.InlinePath r + WarningCodes.asyncStartBehavesAsStartImmediate + |> addWarningWithCode com ctx.InlinePath r Helper.LibCall(com, "Async", "start", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some @@ -3923,6 +4072,7 @@ let private replacedModules = "System.Math", operators "System.MathF", operators "Microsoft.FSharp.Core.Operators", operators + "Microsoft.FSharp.Linq.NullableModule", nullableModule "Microsoft.FSharp.Core.Operators.Checked", operators "Microsoft.FSharp.Core.Operators.Unchecked", unchecked "Microsoft.FSharp.Core.Operators.OperatorIntrinsics", intrinsicFunctions @@ -3935,6 +4085,7 @@ let private replacedModules = "System.Runtime.ExceptionServices.ExceptionDispatchInfo", exceptionDispatchInfo Types.char, chars Types.string, strings + Types.rune, runes "Microsoft.FSharp.Core.StringModule", stringModule "System.FormattableString", formattableString "System.Runtime.CompilerServices.FormattableStringFactory", formattableString @@ -4019,12 +4170,13 @@ let private replacedModules = Types.dateOnly, dateOnly Types.timeOnly, timeOnly Types.timespan, timeSpans - "System.Timers.Timer", timers + Types.timer, timers "System.Environment", systemEnv "System.Globalization.CultureInfo", globalization - "System.Random", random + Types.random, random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels + "System.Threading.CancellationTokenRegistration", disposables "System.Threading.Monitor", monitor "System.Activator", activator "System.Text.Encoding", encoding @@ -4191,12 +4343,13 @@ let tryType typ = | List genArg -> Some(Types.list, lists, [ genArg ]) | Builtin kind -> match kind with + | BclRune -> None | BclGuid -> Some(Types.guid, guids, []) | BclDateTime -> Some(Types.datetime, dates, []) | BclDateTimeOffset -> Some(Types.datetimeOffset, dates, []) | BclDateOnly -> Some(Types.dateOnly, dateOnly, []) | BclTimeOnly -> Some(Types.timeOnly, timeOnly, []) - | BclTimer -> Some("System.Timers.Timer", timers, []) + | BclTimer -> Some(Types.timer, timers, []) | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclHashSet genArg -> Some(Types.hashset, hashSets, [ genArg ]) | BclDictionary(key, value) -> Some(Types.dictionary, dictionaries, [ key; value ]) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 389bd21190..6eb9b17333 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -12,7 +12,7 @@ open Fable.Transforms open Fable.Transforms.FSharp2Fable module Extensions = - let areParamTypesEqual genArgs (args1: Fable.Type array) (args2: IList>) = + let areParamTypesEqual strict genArgs (args1: Fable.Type array) (args2: IList>) = // Not entirely sure why, but it seems members with a single unit argument sometimes have this parameter // and sometimes none, so just to be sure always remove single unit arguments let args2 = @@ -24,7 +24,7 @@ module Extensions = if args1.Length = args2.Length then let args2 = args2 |> Array.map (fun p -> TypeHelpers.makeType genArgs p.Type) - Array.forall2 (typeEquals false) args1 args2 + Array.forall2 (typeEquals strict) args1 args2 else false @@ -138,9 +138,11 @@ type FsUnionCase(uci: FSharpUnionCase) = | _ -> None ) - // static member HasNamedFields(uci: FSharpUnionCase) = - // not (uci.Fields.Count = 1 && uci.Fields[0].Name = "Item") - // true + /// The macro of a CaseTestAttribute, an extra test for a TypeScriptTaggedUnion case + static member CaseTest(uci: FSharpUnionCase) = + uci.Attributes + |> Helpers.tryFindAttrib Atts.caseTest + |> Option.map (fun (att: FSharpAttribute) -> att.ConstructorArguments[0] |> snd |> string) interface Fable.UnionCase with member _.Name = uci.Name @@ -432,19 +434,23 @@ type FsEnt(maybeAbbrevEnt: FSharpEntity) = ) // entity.EnumerateMembersFunctionsAndValues(?includeHierarchy=searchHierarchy) - members.Force() - |> Seq.tryFind (fun m -> - if + let candidates = + members.Force() + |> Seq.filter (fun m -> m.CompiledName = compiledName && m.IsInstanceMember = isInstance && (doNotRequireDispatchSlot || m.IsDispatchSlot) - then - match argTypes with - | Some argTypes -> Extensions.areParamTypesEqual genArgs argTypes m.CurriedParameterGroups - | None -> true - else - false - ) + ) + + match argTypes with + | None -> Seq.tryHead candidates + | Some argTypes -> + let hasParamTypes strict = + candidates + |> Seq.tryFind (fun m -> Extensions.areParamTypesEqual strict genArgs argTypes m.CurriedParameterGroups) + + // A loose comparison accepts a generic parameter as equal to any type + hasParamTypes true |> Option.orElseWith (fun () -> hasParamTypes false) interface Fable.Entity with member _.Ref = FsEnt.Ref ent @@ -789,6 +795,11 @@ module Helpers = |> Fable.Py.Naming.toPythonNaming | Rust -> Naming.buildNameWithoutSanitation name part | Dart -> Naming.sanitizeDartIdent (fun _ -> false) name part + | Beam when memb.IsProperty || memb.IsPropertyGetterMethod || memb.IsPropertySetterMethod -> + // decision: BEAM accessors carry a kind marker because snake_case merges `set_Foo` with `SetFoo` (and `get_Foo` with `GetFoo`). + // invariant: A CLI event's property symbol and its getter method receive the same marker. + // invariant: Declarations and local, cross-file, and referenced-library calls derive the accessor marker from the same member metadata. + Naming.sanitizeJsIdent (fun _ -> false) name part + Naming.beamAccessorSuffix | _ -> Naming.sanitizeJsIdent (fun _ -> false) name part let hasOverloadSuffix = not (String.IsNullOrEmpty(part.OverloadSuffix)) @@ -806,6 +817,13 @@ module Helpers = || ctx.UsedNamesInDeclarationScope.Contains name let getIdentUniqueName (com: Compiler) (ctx: Context) name = + // Reserve the emitted Python name so tuple-match temporaries cannot shadow + // existing bindings when camelCase is converted to snake_case. + let name = + match com.Options.Language with + | Python -> Fable.Py.Naming.toPythonNaming name + | _ -> name + let sanitizeIdent = match com.Options.Language with | Python -> Fable.Py.Naming.sanitizeIdent @@ -832,6 +850,9 @@ module Helpers = // typ.TypeDefinition.DisplayName = "inref" || // typ.TypeDefinition.DisplayName = "outref") + let isInRefType (typ: FSharpType) = + typ.HasTypeDefinition && typ.TypeDefinition.DisplayName = "inref" + let isByRefValue (value: FSharpMemberOrFunctionOrValue) = // Value type "this" is passed as inref, so it has to be excluded // (Note: the non-abbreviated type of inref and outref is byref) @@ -1071,7 +1092,7 @@ module Helpers = let getCaseRule (att: FSharpAttribute) = match Seq.tryHead att.ConstructorArguments with - | Some(_, (:? int as rule)) -> enum (rule) + | Some(_, (:? int as rule)) -> enum(rule) | _ -> CaseRules.LowerFirst unionCase.Attributes @@ -1099,7 +1120,7 @@ module Helpers = | Some(_, (:? string as name)), None -> Some(TypeScriptTaggedUnion(tdef, typ.GenericArguments, name, CaseRules.LowerFirst)) | Some(_, (:? string as name)), Some(_, (:? int as rule)) -> - Some(TypeScriptTaggedUnion(tdef, typ.GenericArguments, name, enum (rule))) + Some(TypeScriptTaggedUnion(tdef, typ.GenericArguments, name, enum(rule))) | _ -> failwith "Invalid TypeScriptTaggedUnion attribute" | _ -> None ) @@ -1980,6 +2001,36 @@ module Identifiers = let tryGetIdentFromScope (ctx: Context) r typ (fsRef: FSharpMemberOrFunctionOrValue) = tryGetIdentFromScopeIf ctx r typ fsRef.Equals + let getDeclaredIdentNames (expr: Fable.Expr) = + let names = HashSet() + + let rec loop (e: Fable.Expr) = + let declared: Fable.Ident list = + match e with + | Fable.Let(i, _, _) + | Fable.Lambda(i, _, _) + | Fable.ForLoop(i, _, _, _, _, _) + | Fable.TryCatch(_, Some(i, _), _, _) -> [ i ] + | Fable.LetRec(bindings, _) -> List.map fst bindings + | Fable.Delegate(args, _, _, _) -> args + | Fable.DecisionTree(_, targets) -> List.collect fst targets + | Fable.ObjectExpr(members, _, _) -> List.collect (fun (m: Fable.ObjectExprMember) -> m.Args) members + | _ -> [] + + for i in declared do + names.Add(i.Name) |> ignore + + match e with + | Fable.Unresolved(Fable.UnresolvedTraitCall(argExprs = argExprs), _, _) -> argExprs + | Fable.Unresolved(Fable.UnresolvedInlineCall(_, _, callee, info), _, _) -> + Option.toList callee @ Option.toList info.ThisArg @ info.Args + | Fable.Unresolved(Fable.UnresolvedReplaceCall(thisArg, args, _, _), _, _) -> Option.toList thisArg @ args + | e -> getSubExpressions e + |> List.iter loop + + loop expr + set names + module Util = open Helpers open Patterns @@ -2403,6 +2454,52 @@ module Util = let getGenParamNames (types: Fable.Type list) : string list = getGenParams types |> List.map fst let getGenParamTypes (types: Fable.Type list) : Fable.Type list = getGenParams types |> List.map snd + let supportsCompilerGeneratedBaseCall (com: Compiler) = + match com.Options.Language with + | JavaScript + | TypeScript + | Python -> true + | _ -> false + + /// A class without a primary constructor gets a compiler-generated one that takes its fields + let hasCompilerGeneratedConstructor (ent: FSharpEntity) = + ent.IsClass + && not ent.IsFSharpExceptionDeclaration + && not ent.IsValueType + && not (isGlobalOrImportedFSharpEntity ent) + && (FsEnt.Ref ent).SourcePath.IsSome + && not (ent.MembersFunctionsAndValues |> Seq.exists _.IsImplicitConstructor) + + /// Base classes with compiler-generated constructors, root first, or None when a base has a primary constructor + let rec tryGetCompilerGeneratedBases (ent: FSharpEntity) : FSharpEntity list option = + match tryGetBaseEntity ent with + | None -> Some [] + | Some(baseEnt, _) when hasCompilerGeneratedConstructor baseEnt -> + tryGetCompilerGeneratedBases baseEnt + |> Option.map (fun bases -> bases @ [ baseEnt ]) + | Some _ -> None + + /// Inherited val fields root first, with the base generic arguments resolved through the chain + let getInheritedValFields (ent: FSharpEntity) (genArgs: Map) = + let rec collect (ent: FSharpEntity) genArgs acc = + match tryGetBaseEntity ent with + | Some(baseEnt, baseGenArgs) when hasCompilerGeneratedConstructor baseEnt -> + let baseGenArgs = + Seq.zip baseEnt.GenericParameters baseGenArgs + |> Seq.map (fun (param, typ) -> genParamName param, makeType genArgs typ) + |> Map.ofSeq + + let fields = + baseEnt.FSharpFields + |> Seq.filter (fun field -> not field.IsStatic) + |> Seq.map (fun field -> field, makeType baseGenArgs field.FieldType) + |> List.ofSeq + + collect baseEnt baseGenArgs (fields @ acc) + | _ -> acc + + collect ent genArgs [] + /// We can add a suffix to the entity name for special methods, like reflection declaration let entityIdentWithSuffix (com: Compiler) (ent: Fable.EntityRef) suffix = let error msg = @@ -2416,10 +2513,32 @@ module Util = if not com.IsPrecompilingInlineFunction && file = com.CurrentFile then makeIdentExpr entityName else + com.AddSignatureDependency(file) makeInternalClassImport com ent entityName file let entityIdent (com: Compiler) (ent: Fable.EntityRef) = entityIdentWithSuffix com ent "" + /// Compiler-generated constructors of derived classes receive the inherited fields and pass them to the base + let tryMakeCompilerGeneratedBaseCall (com: Compiler) (ent: FSharpEntity) = + if supportsCompilerGeneratedBaseCall com && hasCompilerGeneratedConstructor ent then + match tryGetCompilerGeneratedBases ent with + | Some(_ :: _ as bases) -> + let baseEnt = List.last bases + + let args = + getInheritedValFields ent Map.empty + |> List.map (fun (field, typ) -> makeTypedIdentExpr typ field.Name) + + let baseExpr = + match tryGlobalOrImportedFSharpEntity com baseEnt with + | Some baseExpr -> baseExpr + | None -> FsEnt.Ref baseEnt |> entityIdent com + + Some(Fable.Call(baseExpr, Fable.CallInfo.Create(args = args), Fable.Unit, None)) + | _ -> None + else + None + /// In Python a union is emitted as a public type alias (`type Demo = Demo_A | Demo_B`) /// plus a private base class (`_Demo`) that holds the runtime members: attached static /// members and the `cases()` method used by reflection. The type alias is typing-only, @@ -2445,6 +2564,11 @@ module Util = else Some(entityIdent com ent.Ref) + let private isInlinedInDeclaringFileOnly (ctx: Context) = + match ctx.PrecompilingInlineFunction with + | Some inlineMemb -> not (isNotPrivate inlineMemb) + | None -> false + let memberIdent (com: Compiler) (ctx: Context) r typ (memb: FSharpMemberOrFunctionOrValue) membRef = let r = r |> Option.map (fun r -> { r with identifierName = Some memb.DisplayName }) @@ -2493,13 +2617,19 @@ module Util = if hasOverloadSuffix then com.AddWatchDependency(file) + com.AddSignatureDependency(file) + // Private values are not exported so they can't be imported by call sites in other files. // We need to handle it manually because Fable handle resolve inline function itself - if com.IsPrecompilingInlineFunction && memb.Accessibility.IsPrivate then + if + com.IsPrecompilingInlineFunction + && not (isNotPrivate memb) + && not (isInlinedInDeclaringFileOnly ctx) + then $"The value '%s{memb.DisplayName}' was marked inline but its implementation makes use of an internal or private function which is not sufficiently accessible" |> addError com [] r - makeInternalMemberImport com typ membRef memberName file + makeInternalMemberImport com typ membRef memberName file r let getFunctionMemberRef (memb: FSharpMemberOrFunctionOrValue) = match memb.DeclaringEntity with @@ -2640,6 +2770,11 @@ module Util = || isPojoDefinedByConsArgsFSharpEntity ent ) + let isMangledAbstractFableEntity (com: Compiler) (ent: Fable.Entity) = + match ent with + | :? FsEnt as ent -> isMangledAbstractEntity com ent.FSharpEntity + | _ -> false + let getMangledAbstractMemberName (ent: FSharpEntity) memberName overloadHash = // TODO: Error if entity doesn't have fullName? let entityName = defaultArg ent.TryFullName "" @@ -3269,3 +3404,405 @@ module Util = | Imported com ctx r typ None imported -> imported | Try (tryGetIdentFromScope ctx r (Some typ)) expr, _ -> expr | _ -> getValueMemberRef v |> memberIdent com ctx r typ v + + /// Runtime checks that tell apart union cases the tag or type test alone cannot + [] + type DerivedCaseTest = + | HasField of name: string + | LacksField of name: string + | FieldOfType of name: string * typ: Fable.Type + | NotOfType of typ: Fable.Type + | NotCaseTest of macro: string + + let private tryTaggedUnionTagKey (rule: CaseRules) (unionCase: FSharpUnionCase) = + match transformStringEnum rule unionCase with + | Fable.Value(Fable.StringConstant s, _) -> Some("s:" + s) + | Fable.Value(Fable.BoolConstant b, _) -> Some("b:" + string b) + | Fable.Value(Fable.NumberConstant(v, _), _) -> Some("n:" + string v) + | _ -> None + + let taggedUnionTagDisplay (rule: CaseRules) (unionCase: FSharpUnionCase) = + match FsUnionCase.CompiledValue unionCase with + | Some(CompiledValue.Integer i) -> string i + | Some(CompiledValue.Float f) -> string f + | Some(CompiledValue.Boolean b) -> (string b).ToLowerInvariant() + | None -> + match transformStringEnum rule unionCase with + | Fable.Value(Fable.StringConstant s, _) -> $"\"%s{s}\"" + | _ -> "the same tag" + + type private TaggedUnionField = + { + Name: string + Type: Fable.Type + Display: string + } + + /// Keep in sync with Fable2Babel.Reflection.transformTypeTest: equal keys are the same runtime test, None is untestable + let rec private tryRuntimeTypeTestKey (com: Compiler) (typ: Fable.Type) : string option = + match typ with + | Fable.String + | Fable.Char -> Some "string" + | Fable.Boolean -> Some "boolean" + | Fable.Number(Decimal, _) -> Some "decimal" + | Fable.Number((Int64 | UInt64 | Int128 | UInt128 | BigInt | NativeInt | UNativeInt), _) -> Some "bigint" + | Fable.Number _ -> Some "number" + | Fable.Regex -> Some "regexp" + | Fable.LambdaType _ + | Fable.DelegateType _ -> Some "function" + | Fable.Array _ + | Fable.Tuple _ -> Some "array" + | Fable.List _ -> Some "list" + | Fable.Nullable(genArg, _) -> tryRuntimeTypeTestKey com genArg + | Fable.DeclaredType(entRef, _) -> + match com.TryGetEntity entRef with + | None -> None + | Some _ when isReplacementCandidate entRef -> None + | Some ent when hasAttribute Atts.stringEnum ent -> Some "string" + | Some ent when hasAttribute Atts.erase ent && ent.IsFSharpUnion -> + match ent.UnionCases with + | [ uci ] when List.isSingle uci.UnionCaseFields -> + tryRuntimeTypeTestKey com uci.UnionCaseFields[0].FieldType + | cases when cases |> List.forall (fun c -> List.isEmpty c.UnionCaseFields) -> Some "string" + | _ -> None + | Some ent when + ent.IsMeasure + || isErasedOrStringEnumEntity ent + || isPojoDefinedByConsArgsEntity ent + -> + None + | Some ent when ent.IsInterface && not (isGlobalOrImportedEntity ent) -> None + | Some ent -> Some("instanceof:" + ent.FullName) + | _ -> None + + let rec private isSubtypeOf (com: Compiler) (baseFullName: string) (entRef: Fable.EntityRef) = + entRef.FullName = baseFullName + || match com.TryGetEntity entRef with + | None -> false + | Some ent -> + ent.AllInterfaces |> Seq.exists (fun ifc -> ifc.Entity.FullName = baseFullName) + || ( + match ent.BaseType with + | Some baseType -> isSubtypeOf com baseFullName baseType.Entity + | None -> false + ) + + /// Whether every value of `sub` also passes the runtime test of `super` + let private passesTestOf (com: Compiler) (super: Fable.Type) (sub: Fable.Type) = + match super, sub with + | Fable.DeclaredType(superRef, _), Fable.DeclaredType(subRef, _) -> isSubtypeOf com superRef.FullName subRef + | _ -> false + + /// A required field that only one side has + let private tryDistinguishByPresence (mine: TaggedUnionField list) (other: TaggedUnionField list) = + let isRequired (f: TaggedUnionField) = + match f.Type with + | Fable.Option _ -> false + | _ -> true + + let hasName name (fields: TaggedUnionField list) = + fields |> List.exists (fun f -> f.Name = name) + + let byPresence = + mine + |> List.tryFind (fun f -> isRequired f && not (hasName f.Name other)) + |> Option.map (fun f -> DerivedCaseTest.HasField f.Name) + + let byAbsence () = + other + |> List.tryFind (fun f -> isRequired f && not (hasName f.Name mine)) + |> Option.map (fun f -> DerivedCaseTest.LacksField f.Name) + + byPresence |> Option.orElseWith byAbsence + + let private tryDistinguishTaggedUnionCase + (com: Compiler) + (mine: TaggedUnionField list) + (other: TaggedUnionField list) + = + let byType () = + mine + |> List.tryPick (fun f -> + match List.tryFind (fun (o: TaggedUnionField) -> o.Name = f.Name) other with + | None -> None + | Some o -> + match tryRuntimeTypeTestKey com f.Type, tryRuntimeTypeTestKey com o.Type with + | Some myKey, Some otherKey when + myKey <> otherKey + && not (passesTestOf com f.Type o.Type) + && not (passesTestOf com o.Type f.Type) + -> + Some(DerivedCaseTest.FieldOfType(f.Name, f.Type)) + | _ -> None + ) + + tryDistinguishByPresence mine other |> Option.orElseWith byType + + /// Why a type is known to have no runtime test, None when it has one or when this is unknown + let private tryUntestableKind (com: Compiler) (typ: Fable.Type) = + match typ with + | Fable.Any -> Some "which can hold any value" + | Fable.GenericParam _ -> Some "a generic parameter unknown at runtime" + | Fable.DeclaredType(entRef, _) when (tryRuntimeTypeTestKey com typ).IsNone -> + match com.TryGetEntity entRef with + | Some ent when isPojoDefinedByConsArgsEntity ent -> Some "a Pojo that is a plain object at runtime" + | Some ent when ent.IsInterface -> Some "an interface that does not exist at runtime" + | Some ent when isErasedOrStringEnumEntity ent -> Some "an erased type" + | _ -> None + | _ -> None + + let isUntestableType (com: Compiler) (typ: Fable.Type) = (tryUntestableKind com typ).IsSome + + let private describeUntestableType (com: Compiler) (f: TaggedUnionField) = + let kind = + tryUntestableKind com f.Type + |> Option.defaultValue "a type that cannot be tested at runtime" + + $"field '%s{f.Name}' has type '%s{f.Display}', %s{kind}" + + /// Why no field of `mine` tells it apart from `other` at runtime + let private explainIndistinguishable (com: Compiler) (mine: TaggedUnionField list) (other: TaggedUnionField list) = + let shared = + mine + |> List.choose (fun f -> other |> List.tryFind (fun o -> o.Name = f.Name) |> Option.map (fun o -> f, o)) + + match shared |> List.tryFind (fun (f, o) -> f.Type <> o.Type) with + | None when List.length shared = List.length mine && List.length shared = List.length other -> + "have the same fields" + | None -> "differ only by optional fields" + | Some(f, o) -> + let reason = + match tryRuntimeTypeTestKey com f.Type, tryRuntimeTypeTestKey com o.Type with + | None, _ -> describeUntestableType com f + | _, None -> describeUntestableType com o + | Some myKey, Some otherKey when myKey = otherKey -> + $"field '%s{f.Name}' has types '%s{f.Display}' and '%s{o.Display}', which look the same at runtime" + | _ -> + let sub, super = + if passesTestOf com f.Type o.Type then + o, f + else + f, o + + $"field '%s{f.Name}' has types '%s{f.Display}' and '%s{o.Display}', and '%s{sub.Display}' is a subtype of '%s{super.Display}'" + + "cannot be told apart at runtime: " + reason + + /// Required members of an interface and the interfaces it inherits, with the names they have at runtime + let private tryInterfaceMembers (com: Compiler) (typ: Fable.Type) : TaggedUnionField list option = + let hasRuntimeName (m: Fable.MemberFunctionOrValue) = + m.Attributes + |> Seq.forall (fun att -> + match att.Entity.FullName with + | Naming.StartsWith Atts.emit _ + | Naming.StartsWith Atts.import _ + | Atts.global_ -> false + | _ -> true + ) + + let isPlainInterface (ent: Fable.Entity) = + ent.IsInterface + && not (hasAttribute Atts.mangle ent) + && not (ent.FullName.StartsWith("System.", StringComparison.Ordinal)) + + let membersOf (ent: Fable.Entity) = + ent.MembersFunctionsAndValues + |> Seq.filter (fun m -> + m.IsInstance + && not m.IsConstructor + && not m.IsGetter + && not m.IsSetter + && hasRuntimeName m + ) + |> Seq.map (fun m -> + { + Name = + if m.IsProperty then + m.DisplayName + else + m.CompiledName + Type = + if m.IsProperty then + m.ReturnParameter.Type + else + Fable.Any + Display = m.DisplayName + } + ) + + match typ with + | Fable.DeclaredType(entRef, _) -> + match com.TryGetEntity entRef with + | Some ent when isPlainInterface ent -> + ent.AllInterfaces + |> Seq.choose (fun i -> com.TryGetEntity i.Entity) + |> Seq.filter isPlainInterface + |> Seq.collect membersOf + |> Seq.append (membersOf ent) + |> Seq.distinctBy (fun f -> f.Name) + |> List.ofSeq + |> Some + | _ -> None + | _ -> None + + /// The interface type a TypeScriptTaggedUnion case compiles to: a single unnamed field of an interface type + let tryInlinedTaggedUnionCase (com: Compiler) (unionCase: FSharpUnionCase) = + tryInlinedTaggedUnionCaseType com (FsUnionCase unionCase) + + /// Whether the interface of an inlined TypeScriptTaggedUnion case declares the tag member, None when the case is not inlined or the members are unknown + let inlinedTaggedUnionCaseHasTag (com: Compiler) (tagName: string) (unionCase: FSharpUnionCase) = + tryInlinedTaggedUnionCase com unionCase + |> Option.bind (tryInterfaceMembers com) + |> Option.map (List.exists (fun m -> m.Name = tagName)) + + /// One check per other case with the same tag value, or the case the fields cannot exclude + let deriveDerivedCaseTests + (com: Compiler) + (tdef: FSharpEntity) + (rule: CaseRules) + (unionCase: FSharpUnionCase) + : Result + = + let fieldsOf (uc: FSharpUnionCase) = + match tryInlinedTaggedUnionCase com uc with + | Some typ -> tryInterfaceMembers com typ |> Option.defaultValue [] + | None -> + uc.Fields + |> Seq.map (fun fi -> + { + Name = FsField.FSharpFieldName fi + Type = makeType Map.empty fi.FieldType + Display = fi.FieldType.Format(FSharpDisplayContext.Empty.WithShortTypeNames(true)) + } + ) + |> List.ofSeq + + match tryTaggedUnionTagKey rule unionCase with + | None -> Ok [] + | Some myTag -> + let myFields = fieldsOf unionCase + + tdef.UnionCases + |> Seq.filter (fun uc -> uc.Name <> unionCase.Name && tryTaggedUnionTagKey rule uc = Some myTag) + |> Seq.fold + (fun acc other -> + match acc with + | Error _ -> acc + | Ok tests -> + let otherFields = fieldsOf other + + let byCaseTest () = + FsUnionCase.CaseTest other |> Option.map DerivedCaseTest.NotCaseTest + + match + tryDistinguishTaggedUnionCase com myFields otherFields + |> Option.orElseWith byCaseTest + with + | Some test -> Ok(tests @ [ test ]) + | None -> Error(other, explainIndistinguishable com myFields otherFields) + ) + (Ok []) + |> Result.map List.distinct + + /// Checks added to the type test of an erased union case, or why the type test is not enough + [] + type ErasedUnionCaseTests = + | Tests of DerivedCaseTest list + | Untestable of reason: string + | Ambiguous of other: FSharpUnionCase * reason: string + + /// Checks that exclude the other cases from this one: negated type tests, interface members, or negated CaseTests + let deriveErasedUnionCaseTests + (com: Compiler) + (tdef: FSharpEntity) + (unionCase: FSharpUnionCase) + (payloadType: FSharpUnionCase -> Fable.Type option) + : ErasedUnionCaseTests + = + let display (uc: FSharpUnionCase) = + uc.Fields[0].FieldType.Format(FSharpDisplayContext.Empty.WithShortTypeNames(true)) + + let describeUntestable (uc: FSharpUnionCase) (typ: Fable.Type) kind = + match typ with + | Fable.Any -> $"type '%s{display uc}' can hold any value" + | _ -> $"type '%s{display uc}' is %s{kind}" + + let byCaseTest (other: FSharpUnionCase) = + FsUnionCase.CaseTest other |> Option.map DerivedCaseTest.NotCaseTest + + match payloadType unionCase with + | None -> ErasedUnionCaseTests.Tests [] + | Some myType -> + let myKind = tryUntestableKind com myType + + let myMembers = + if myKind.IsSome then + tryInterfaceMembers com myType + else + None + + match myKind, myMembers with + | Some kind, None -> ErasedUnionCaseTests.Untestable(describeUntestable unionCase myType kind) + | _ -> + let myKey = tryRuntimeTypeTestKey com myType + + let excludeByType (other: FSharpUnionCase) (otherType: Fable.Type) = + let isSubtype = passesTestOf com myType otherType + + let sameKey = myKey.IsSome && myKey = tryRuntimeTypeTestKey com otherType + + if not isSubtype && not sameKey then + Ok [] + else + match byCaseTest other with + | Some test -> Ok [ test ] + | None when isSubtype -> Error $"'%s{display other}' is a subtype of '%s{display unionCase}'" + | None when display unionCase = display other -> Error $"both have type '%s{display unionCase}'" + | None -> + Error $"types '%s{display unionCase}' and '%s{display other}' look the same at runtime" + + let excludeByMembers (mine: TaggedUnionField list) (other: FSharpUnionCase) (otherType: Fable.Type) = + match tryUntestableKind com otherType with + | None -> Ok [ DerivedCaseTest.NotOfType otherType ] + | Some otherKind -> + let theirs = tryInterfaceMembers com otherType + + match + theirs + |> Option.bind (tryDistinguishByPresence mine) + |> Option.orElseWith (fun () -> byCaseTest other) + with + | Some test -> Ok [ test ] + | None when theirs.IsSome -> + Error + $"interfaces '%s{display unionCase}' and '%s{display other}' have no required member that tells them apart" + | None -> Error(describeUntestable other otherType otherKind) + + let exclude = + match myMembers with + | Some mine -> excludeByMembers mine + | None -> excludeByType + + tdef.UnionCases + |> Seq.filter (fun uc -> uc.Name <> unionCase.Name) + |> Seq.fold + (fun acc other -> + match acc, payloadType other with + | ErasedUnionCaseTests.Tests tests, Some otherType -> + match exclude other otherType with + | Ok more -> ErasedUnionCaseTests.Tests(tests @ more) + | Error reason -> ErasedUnionCaseTests.Ambiguous(other, reason) + | _ -> acc + ) + (ErasedUnionCaseTests.Tests []) + |> function + | ErasedUnionCaseTests.Tests tests -> + tests + |> List.distinct + |> List.sortBy ( + function + | DerivedCaseTest.NotOfType _ -> 0 + | _ -> 1 + ) + |> ErasedUnionCaseTests.Tests + | result -> result diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 2e21c18583..1ff1c704cf 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -43,7 +43,7 @@ let private transformBaseConsCall makeCall r Fable.Unit callInfo baseRef | None -> if not baseCons.IsImplicitConstructor then - "Only inheriting from primary constructors is supported" |> addWarning com [] r + WarningCodes.nonPrimaryBaseConstructor |> addWarningWithCode com [] r match makeCallFrom com ctx r Fable.Unit genArgs None baseArgs baseCons with | Fable.Call(_baseExpr, info, t, r) when not (com.Options.Language = Rust) -> @@ -110,8 +110,8 @@ let private transformNewUnion com ctx r fsType (unionCase: FSharpUnionCase) (arg | argExprs -> makeTuple r false argExprs | TypeScriptTaggedUnion(_, _, tagName, rule) -> - match argExprs with - // | [ argExpr ] when not (FsUnionCase.HasNamedFields unionCase) -> argExpr + match tryInlinedTaggedUnionCase com unionCase, argExprs with + | Some _, [ argExpr ] -> Fable.TypeCast(argExpr, makeType ctx.GenericArgs fsType) | _ -> let isCompiledValue, tagExpr = match FsUnionCase.CompiledValue unionCase with @@ -525,6 +525,7 @@ let private transformObjExpr match baseCall.DeclaringEntity with | Some baseEnt when baseEnt.TryFullName <> Some Types.object -> let r = makeRangeFrom baseCallExpr + checkSelfConstructorBase com ctx r baseEnt let genArgs = genArgs1 @ genArgs2 return transformBaseConsCall com ctx r baseEnt baseCall genArgs baseArgs |> Some @@ -566,6 +567,18 @@ let private transformDelegate com ctx (delegateType: FSharpType) expr = | _ -> return expr } +let private makeDerivedCaseTest r (unionExpr: Fable.Expr) (test: DerivedCaseTest) = + match test with + | DerivedCaseTest.HasField name -> emitExpr r Fable.Boolean [ makeStrConst name; unionExpr ] "$0 in $1" + | DerivedCaseTest.LacksField name -> emitExpr r Fable.Boolean [ makeStrConst name; unionExpr ] "!($0 in $1)" + | DerivedCaseTest.FieldOfType(name, typ) -> + let field = + Fable.Get(unionExpr, Fable.FieldInfo.Create(name, fieldType = typ), typ, r) + + Fable.Test(field, Fable.TypeTest typ, r) + | DerivedCaseTest.NotOfType typ -> makeUnOp r Fable.Boolean (Fable.Test(unionExpr, Fable.TypeTest typ, r)) UnaryNot + | DerivedCaseTest.NotCaseTest macro -> emitExpr r Fable.Boolean [ unionExpr ] $"!(%s{macro})" + let private transformUnionCaseTest (com: IFableCompiler) (ctx: Context) @@ -576,44 +589,100 @@ let private transformUnionCaseTest = trampoline { let! unionExpr = transformExpr com ctx [] unionExpr + let unionPattern = getUnionPattern fsType unionCase - match getUnionPattern fsType unionCase with + match unionPattern with | ErasedUnionCase -> return "Cannot test erased union cases" |> addErrorAndReturnNull com ctx.InlinePath r | ErasedUnion(tdef, genArgs, rule) -> + let payloadType (uc: FSharpUnionCase) = + match uc.Fields.Count with + | 1 -> + let fi = uc.Fields[0] + + let typ = + if fi.FieldType.IsGenericParameter then + let name = genParamName fi.FieldType.GenericParameter + + let index = + tdef.GenericParameters |> Seq.findIndex (fun arg -> genParamName arg = name) + + genArgs[index] + else + fi.FieldType + + makeType ctx.GenericArgs typ |> Some + | _ -> None + + let supportsCaseTest = + match com.Options.Language with + | JavaScript + | TypeScript -> not (isReplacementCandidate (FsEnt.Ref tdef)) + | _ -> false + match unionCase.Fields.Count with | 0 -> return makeEqOp r unionExpr (transformStringEnum rule unionCase) BinaryEqual | 1 -> - let fi = unionCase.Fields[0] - - let typ = - if fi.FieldType.IsGenericParameter then - let name = genParamName fi.FieldType.GenericParameter + let fableType = (payloadType unionCase).Value - let index = - tdef.GenericParameters |> Seq.findIndex (fun arg -> genParamName arg = name) + let typeTest unionExpr = + Fable.Test(unionExpr, Fable.TypeTest fableType, r) - genArgs[index] - else - fi.FieldType + let anyError = + $"Erased union case '%s{unionCase.Name}' is typed as 'obj' which cannot be tested at runtime (type test always evaluates to true). Use a more specific type or TypeScriptTaggedUnion instead." - let fableType = makeType ctx.GenericArgs typ + let tests: Result<(Fable.Expr -> Fable.Expr) list, string> = + match FsUnionCase.CaseTest unionCase, fableType with + | _, Fable.Any when not supportsCaseTest -> Error anyError + | _, _ when not supportsCaseTest -> Ok [ typeTest ] + | Some macro, _ -> + let caseTest unionExpr = + emitExpr r Fable.Boolean [ unionExpr ] macro - match fableType with - | Fable.Any -> - return - $"Erased union case '%s{unionCase.Name}' is typed as 'obj' which cannot be tested at runtime (type test always evaluates to true). Use a more specific type or TypeScriptTaggedUnion instead." - |> addErrorAndReturnNull com ctx.InlinePath r - | _ -> - let kind = fableType |> Fable.TypeTest - return Fable.Test(unionExpr, kind, r) + if isUntestableType com fableType then + Ok [ caseTest ] + else + Ok [ typeTest; caseTest ] + | None, Fable.Any -> Error anyError + | None, _ -> + match deriveErasedUnionCaseTests com tdef unionCase payloadType with + | ErasedUnionCaseTests.Untestable reason -> + Error + $"Case '%s{unionCase.Name}' of '%s{tdef.DisplayName}' cannot be tested at runtime: %s{reason}. Add a CaseTest attribute to '%s{unionCase.Name}'." + | ErasedUnionCaseTests.Ambiguous(other, reason) -> + Error + $"Cases '%s{unionCase.Name}' and '%s{other.Name}' of '%s{tdef.DisplayName}' cannot be told apart at runtime: %s{reason}. Add a CaseTest attribute to '%s{unionCase.Name}' or '%s{other.Name}'." + | ErasedUnionCaseTests.Tests derived -> + let derived = + derived |> List.map (fun test unionExpr -> makeDerivedCaseTest r unionExpr test) + + if isUntestableType com fableType then + Ok derived + else + Ok(typeTest :: derived) + + match tests with + | Error msg -> return addErrorAndReturnNull com ctx.InlinePath r msg + | Ok [] -> return makeBoolConst true + | Ok [ test ] -> return test unionExpr + | Ok tests -> + let makeTest unionExpr = + tests + |> List.map (fun test -> test unionExpr) + |> List.reduce (fun acc test -> makeLogOp r acc test LogicalAnd) + + if canHaveSideEffects com unionExpr then + let ident = getIdentUniqueName com ctx "unionValue" |> makeTypedIdent unionExpr.Type + return Fable.Let(ident, unionExpr, makeTest (Fable.IdentExpr ident)) + else + return makeTest unionExpr | _ -> return "Erased unions with multiple cases cannot have more than one field: " + (getFsTypeFullName fsType) |> addErrorAndReturnNull com ctx.InlinePath r - | TypeScriptTaggedUnion(_, _, tagName, rule) -> + | TypeScriptTaggedUnion(tdef, _, tagName, rule) -> let isCompiledValue, value = match FsUnionCase.CompiledValue unionCase with | None -> false, transformStringEnum rule unionCase @@ -627,9 +696,34 @@ let private transformUnionCaseTest "CompileValue attribute is not supported in TypeScript" |> addErrorAndReturnNull com ctx.InlinePath r | _ -> - let getTag = Fable.Get(unionExpr, Fable.FieldInfo.Create(tagName), value.Type, r) + let makeTagTest unionExpr = + let getTag = Fable.Get(unionExpr, Fable.FieldInfo.Create(tagName), value.Type, r) + makeEqOp r getTag value BinaryEqual + + let caseTests: (Fable.Expr -> Fable.Expr) list = + match FsUnionCase.CaseTest unionCase with + | Some macro -> [ fun unionExpr -> emitExpr r Fable.Boolean [ unionExpr ] macro ] + | None -> + match deriveDerivedCaseTests com tdef rule unionCase with + | Ok tests -> tests |> List.map (fun test unionExpr -> makeDerivedCaseTest r unionExpr test) + | Error(other, reason) -> + let tag = taggedUnionTagDisplay rule unionCase + + $"Cases '%s{unionCase.Name}' and '%s{other.Name}' of '%s{tdef.DisplayName}' share the tag value %s{tag} and %s{reason}. Add a CaseTest attribute to '%s{unionCase.Name}' or '%s{other.Name}'." + |> addError com ctx.InlinePath r - return makeEqOp r getTag value BinaryEqual + [] + + let makeTest unionExpr = + (makeTagTest unionExpr, caseTests) + ||> List.fold (fun acc test -> makeLogOp r acc (test unionExpr) LogicalAnd) + + // The union value is read once per test, so bind it first if it is not a plain value + if canHaveSideEffects com unionExpr && not (List.isEmpty caseTests) then + let ident = getIdentUniqueName com ctx "unionValue" |> makeTypedIdent unionExpr.Type + return Fable.Let(ident, unionExpr, makeTest (Fable.IdentExpr ident)) + else + return makeTest unionExpr | OptionUnion _ -> let kind = @@ -648,6 +742,55 @@ let private transformUnionCaseTest return Fable.Test(unionExpr, Fable.UnionCaseTest(tag), r) } +let private withDefaultValueFields (com: IFableCompiler) (ctx: Context) (fsType: FSharpType) (args: Fable.Expr list) = + let ent = fsType.TypeDefinition + + let isDefaultValueField (field: FSharpField) = + not field.IsStatic && hasAttrib Atts.defaultValue field.FieldAttributes + + let fields = + ent.FSharpFields |> Seq.filter (fun field -> not field.IsStatic) |> List.ofSeq + + if not ctx.CapturingQuotation && fields |> List.exists isDefaultValueField then + let genArgs = + Seq.zip ent.GenericParameters (getGenericArguments fsType) + |> Seq.map (fun (param, typ) -> genParamName param, makeType ctx.GenericArgs typ) + |> Map.ofSeq + + let rec fill (fields: FSharpField list) args = + match fields, args with + | field :: fields, _ when isDefaultValueField field -> + let value = + makeType genArgs field.FieldType |> Replacements.Api.defaultof com ctx None + + value :: fill fields args + | _ :: fields, arg :: args -> arg :: fill fields args + | _, args -> args + + fill fields args + else + args + +let private derefByRefCallee + (com: IFableCompiler) + (ctx: Context) + r + (callee: FSharpExpr) + (calleeType: FSharpType) + (transformedCallee: Fable.Expr) + = + match callee with + | FSharpExprPatterns.Value var when + com.Options.Language <> Rust + && isByRefType callee.Type + && isByRefValue var + // An inref argument is passed as the value itself when its source is immutable + && not (isInRefType var.FullType) + && not (var.IsCompilerGenerated && var.CompiledName = "inputRecord") + -> + Replacements.Api.getRefCell com r (makeType ctx.GenericArgs calleeType) transformedCallee + | _ -> transformedCallee + let rec private transformDecisionTargets (com: IFableCompiler) (ctx: Context) @@ -671,9 +814,147 @@ let rec private transformDecisionTargets } +// F# omits the `init@` field it adds for `new() as x` from the record expression of the constructor +let private withOmittedFieldDefaults (com: IFableCompiler) (ctx: Context) (fsType: FSharpType) (args: Fable.Expr list) = + let ent = fsType.TypeDefinition + + if hasCompilerGeneratedConstructor ent then + let genArgs = + getGenericArguments fsType |> Seq.map (makeType ctx.GenericArgs) |> List.ofSeq + + let genArgs = + Seq.zip ent.GenericParameters genArgs + |> Seq.map (fun (param, typ) -> genParamName param, typ) + |> Map.ofSeq + + let fields = + ent.FSharpFields |> Seq.filter (fun field -> not field.IsStatic) |> List.ofSeq + + let omitted = + List.skip (min args.Length fields.Length) fields + |> List.map (fun field -> makeType genArgs field.FieldType |> Replacements.Api.defaultof com ctx None) + + args @ omitted + else + args + +let private (|LoadSelf|_|) (e: FSharpExpr) = + match e with + | FSharpExprPatterns.ILAsm(("[I_ldarg 0us]" | "[I_ldarg 0]"), [], []) -> Some() + | _ -> None + +// FCS writes `init@` on self in every constructor of a class with a self identifier +let rec private loadsSelf skipInitWrite (e: FSharpExpr) = + match e with + | LoadSelf -> true + | FSharpExprPatterns.FSharpFieldSet(Some LoadSelf, _, field, value) when + skipInitWrite && field.Name.StartsWith("init@") + -> + loadsSelf skipInitWrite value + | e -> e.ImmediateSubExpressions |> List.exists (loadsSelf skipInitWrite) + +let private hasSelfConstructor (com: IFableCompiler) (ent: FSharpEntity) = + let constructors = + ent.MembersFunctionsAndValues |> Seq.filter (fun memb -> memb.IsConstructor) + + let hasPrimaryConstructor = + constructors |> Seq.exists (fun memb -> memb.IsImplicitConstructor) + + let hasExplicitConstructor = + constructors |> Seq.exists (fun memb -> not memb.IsImplicitConstructor) + + if + (FsEnt.Ref ent).SourcePath.IsNone + || isGlobalOrImportedFSharpEntity ent + || not hasExplicitConstructor + then + false + else + let file = Path.normalizePathAndEnsureFsExtension ent.DeclarationLocation.FileName + + let rec exists decls = + decls + |> List.exists ( + function + | FSharpImplementationFileDeclaration.Entity(_, sub) -> exists sub + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(memb, _, body) -> + memb.IsConstructor + && not memb.IsImplicitConstructor + && memb.DeclaringEntity + |> Option.exists (fun declaring -> declaring.TryFullName = ent.TryFullName) + && loadsSelf hasPrimaryConstructor body + | FSharpImplementationFileDeclaration.InitAction _ -> false + ) + + let decls = + try + Some(com.GetImplementationFile(file)) + with _ -> + None + + match decls with + | Some decls -> + com.AddWatchDependency(file) + exists decls + // Precompiled libraries keep the `init@` field of such a class, but not the constructor bodies + | None -> + ent.FSharpFields + |> Seq.exists (fun field -> not field.IsStatic && field.Name.StartsWith("init@")) + +let private checkSelfConstructorBase (com: IFableCompiler) (ctx: Context) r (baseEnt: FSharpEntity) = + if hasSelfConstructor com baseEnt then + $"%s{baseEnt.FullName} has a constructor with a self identifier (`new() as x`), which Fable does not support inheriting from." + |> addError com ctx.InlinePath r + +// F# emits `new() = { ... } then body` as sequences nested after the created object +let private (|ExplicitConstructorThen|_|) com (ctx: Context) (fsExpr: FSharpExpr) = + match ctx.EnclosingMember with + | Some m when + m.IsConstructor + && not m.IsImplicitConstructor + && supportsCompilerGeneratedBaseCall com + && m.DeclaringEntity + |> Option.exists (fun ent -> (tryGetBaseEntity ent).IsNone && not (isAttachMembersEntity com ent)) + -> + let isCreated (e: FSharpExpr) = + e.Type.HasTypeDefinition + && m.DeclaringEntity |> Option.exists (fun ent -> ent = e.Type.TypeDefinition) + + let rec split (e: FSharpExpr) = + match e with + | FSharpExprPatterns.Sequential(first, second) when isCreated first -> + let created, rest = split first + created, rest @ [ second ] + | _ -> e, [] + + match fsExpr with + | FSharpExprPatterns.Sequential(first, _) when isCreated fsExpr && isCreated first -> Some(split fsExpr) + | _ -> None + | _ -> None + let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fsExpr = trampoline { match fsExpr with + // TryGetValue-style out args: pass a value cell and read it back, instead of a cell + // wrapping the local in a getter and a setter closure + | ByrefArgToTuple(callee, memb, ownerGenArgs, membGenArgs, membArgs) when + com.Options.Language = JavaScript || com.Options.Language = TypeScript + -> + let r = makeRangeFrom fsExpr + let! callee = transformExprOpt com ctx callee + let! args = transformExprList com ctx membArgs + let args, def = List.splitLast args + let cellValue = Replacements.Api.makeRefCellFromValue com r def + let cellIdent = getIdentUniqueName com ctx "cell" |> makeTypedIdent cellValue.Type + let cellExpr = Fable.IdentExpr cellIdent + let genArgs = ownerGenArgs @ membGenArgs |> List.map (makeType ctx.GenericArgs) + let returnType = makeType ctx.GenericArgs fsExpr.Type.GenericArguments[0] + + let callExpr = + makeCallFrom com ctx r returnType genArgs callee (args @ [ cellExpr ]) memb + + let readBack = Replacements.Api.getRefCell com r def.Type cellExpr + return Fable.Let(cellIdent, cellValue, Fable.Value(Fable.NewTuple([ callExpr; readBack ], false), r)) // | ByrefArgToTuple (callee, memb, ownerGenArgs, membGenArgs, membArgs) -> // let! callee = transformExprOpt com ctx callee // let! args = transformExprList com ctx membArgs @@ -813,6 +1094,14 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | FSharpExprPatterns.Const(value, typ) -> let typ = makeType ctx.GenericArgs typ + + // Enum cases reach Fable as their numeric value, so files using them must follow the enum declaration + do + match typ with + | Fable.Number(_, Fable.NumberInfo.IsEnum entRef) -> + entRef.SourcePath |> Option.iter com.AddSignatureDependency + | _ -> () + let expr = makeTypeConst (makeRangeFrom fsExpr) typ value return expr @@ -837,6 +1126,17 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | _, Some i -> identWithRange r i |> Fable.IdentExpr | _, None -> Fable.Value(makeType Map.empty typ |> Fable.ThisValue, r) + // The `x` of `new() as x` is read through an IL `this` load, printed `0us` by FCS on .NET and `0` by FCS on JS + | FSharpExprPatterns.ILAsm(("[I_ldarg 0us]" | "[I_ldarg 0]"), [], []) -> + let r = makeRangeFrom fsExpr + + return + match ctx.BoundConstructorThis with + | Some i -> identWithRange r i |> Fable.IdentExpr + | None -> + "This explicit constructor uses a self identifier (`new() as x`), which Fable only supports on JavaScript, TypeScript and Python, in a class with no base class and without []." + |> addErrorAndReturnNull com ctx.InlinePath r + | FSharpExprPatterns.Value var -> let r = makeRangeFrom fsExpr @@ -950,6 +1250,17 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs return (expr, bindings) ||> List.fold (fun e (i, v) -> Fable.Let(i, v, e)) + | FSharpExprPatterns.AddressOf inner when + var.IsCompilerGenerated + && var.CompiledName = "inputRecord" + && com.Options.Language <> Rust + -> + let! value = transformExpr com ctx [] inner + let ctx, ident = putIdentInScope com ctx var (Some value) + let ident = { ident with Type = value.Type } + let! body = transformExpr com ctx [] body + return Fable.Let(ident, value, body) + | _ when isInline var -> let ctx = { ctx with ScopeInlineValues = (var, value) :: ctx.ScopeInlineValues } @@ -1252,7 +1563,14 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs // Getters and Setters | FSharpExprPatterns.AnonRecordGet(callee, calleeType, fieldIndex) -> let r = makeRangeFrom fsExpr - let! callee = transformExpr com ctx [] callee + + let callee = + match callee with + | IgnoreAddressOf inner when com.Options.Language <> Rust -> inner + | _ -> callee + + let! transformedCallee = transformExpr com ctx [] callee + let callee = derefByRefCallee com ctx r callee calleeType transformedCallee let fieldName = calleeType.AnonRecordTypeDetails.SortedFieldNames[fieldIndex] @@ -1264,7 +1582,19 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | FSharpExprPatterns.FSharpFieldGet(callee, calleeType, field) -> let r = makeRangeFrom fsExpr - let! callee = transformCallee com ctx callee calleeType + + let callee = + match callee with + | Some(IgnoreAddressOf inner) when com.Options.Language <> Rust -> Some inner + | callee -> callee + + let! transformedCallee = transformCallee com ctx callee calleeType + + let callee = + match callee with + | Some callee -> derefByRefCallee com ctx r callee calleeType transformedCallee + | None -> transformedCallee + // let typ = makeType ctx.GenericArgs fsExpr.Type // Doesn't always work let typ = resolveFieldType ctx calleeType field.FieldType @@ -1316,25 +1646,28 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | ErasedUnionCase -> let index = unionCase.Fields |> Seq.findIndex (fun x -> x.Name = field.Name) - return Fable.Get(unionExpr, Fable.TupleIndex(index), makeType ctx.GenericArgs unionType, r) + return Fable.Get(unionExpr, Fable.TupleIndex(index), fieldType, r) | ErasedUnion _ -> if unionCase.Fields.Count = 1 then - return unionExpr + if unionExpr.Type = fieldType then + return unionExpr + else + return Fable.TypeCast(unionExpr, fieldType) else let index = unionCase.Fields |> Seq.findIndex (fun x -> x.Name = field.Name) return Fable.Get(unionExpr, Fable.TupleIndex index, fieldType, r) | TypeScriptTaggedUnion _ -> - // if not (FsUnionCase.HasNamedFields unionCase) then - // return unionExpr - // else - let kind = - Fable.FieldInfo.Create( - FsField.FSharpFieldName field, - fieldType = makeType Map.empty field.FieldType - ) + match tryInlinedTaggedUnionCase com unionCase with + | Some _ -> return Fable.TypeCast(unionExpr, fieldType) + | None -> + let kind = + Fable.FieldInfo.Create( + FsField.FSharpFieldName field, + fieldType = makeType Map.empty field.FieldType + ) - return Fable.Get(unionExpr, kind, fieldType, r) + return Fable.Get(unionExpr, kind, fieldType, r) | StringEnum _ -> return "StringEnum types cannot have fields" @@ -1454,6 +1787,48 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs return makeCallFrom com ctx (makeRangeFrom fsExpr) typ genArgs None args memb + | FSharpExprPatterns.Sequential(ConstructorCall(baseCons, _, _) as first, + FSharpExprPatterns.NewRecord(fsType, ownArgs)) when + ctx.CaptureBaseConsCall.IsNone + && supportsCompilerGeneratedBaseCall com + && fsType.HasTypeDefinition + && ( + match baseCons.DeclaringEntity, tryGetBaseEntity fsType.TypeDefinition with + | Some callee, Some(baseEnt, _) -> callee = baseEnt + | _ -> false + ) + && (tryGetCompilerGeneratedBases fsType.TypeDefinition + |> Option.exists (List.isEmpty >> not)) + -> + let r = makeRangeFrom fsExpr + let! baseValue = transformExpr com ctx [] first + let! ownArgs = transformExprList com ctx ownArgs + let ownArgs = withDefaultValueFields com ctx fsType ownArgs + + let baseIdent = getIdentUniqueName com ctx "base" |> makeTypedIdent baseValue.Type + + let inheritedArgs = + getInheritedValFields fsType.TypeDefinition ctx.GenericArgs + |> List.map (fun (field, typ) -> getFieldWith None typ (Fable.IdentExpr baseIdent) field.Name) + + let genArgs = makeTypeGenArgs ctx.GenericArgs (getGenericArguments fsType) + let entRef = FsEnt.Ref fsType.TypeDefinition + entRef.SourcePath |> Option.iter com.AddSignatureDependency + + let newRecord = + Fable.NewRecord(inheritedArgs @ ownArgs, entRef, genArgs) |> makeValue r + + return Fable.Let(baseIdent, baseValue, newRecord) + + | ExplicitConstructorThen com ctx (created, rest) when ctx.BoundConstructorThis.IsNone -> + let! created = transformExpr com ctx [] created + let thisIdent = getIdentUniqueName com ctx "this" |> makeTypedIdent created.Type + + let! rest = + transformExprList com { ctx with BoundConstructorThis = Some thisIdent } rest + + return Fable.Let(thisIdent, created, Fable.Sequential(rest @ [ Fable.IdentExpr thisIdent ])) + | FSharpExprPatterns.Sequential(first, second) -> let exprs = match ctx.CaptureBaseConsCall with @@ -1481,11 +1856,16 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs let r = makeRangeFrom fsExpr let! argExprs = transformExprList com ctx argExprs + let argExprs = + withDefaultValueFields com ctx fsType argExprs + |> withOmittedFieldDefaults com ctx fsType + let genArgs = makeTypeGenArgs ctx.GenericArgs (getGenericArguments fsType) + let entRef = FsEnt.Ref fsType.TypeDefinition + // Record fields are passed by position, so a field reordering in the declaring file changes this call + entRef.SourcePath |> Option.iter com.AddSignatureDependency - return - Fable.NewRecord(argExprs, FsEnt.Ref fsType.TypeDefinition, genArgs) - |> makeValue r + return Fable.NewRecord(argExprs, entRef, genArgs) |> makeValue r | FSharpExprPatterns.NewAnonRecord(fsType, argExprs) -> let r = makeRangeFrom fsExpr @@ -2185,11 +2565,95 @@ let rec private getUsedRootNames (com: Compiler) (usedNames: Set) decls | FSharpImplementationFileDeclaration.InitAction _ -> usedNames ) +let private warnIgnoredCaseTests (com: FableCompiler) ctx (ent: FSharpEntity) = + let reason = + if not ent.IsFSharpUnion || hasAttrib Atts.tsTaggedUnion ent.Attributes then + None + elif not (hasAttrib Atts.erase ent.Attributes) then + Some "it is only supported on TypeScriptTaggedUnion and Erase union cases" + else + match (com :> Compiler).Options.Language with + | JavaScript + | TypeScript -> None + | _ -> Some "on Erase union cases it only applies when targeting JavaScript or TypeScript" + + match reason with + | None -> () + | Some reason -> + for uci in ent.UnionCases do + if (FsUnionCase.CaseTest uci).IsSome then + WarningCodes.caseTestIgnored uci.Name reason + |> addWarningWithCode (com :> Compiler) ctx.InlinePath (makeRange uci.DeclarationLocation |> Some) + +/// Values built in F# only carry the tag if the interface of an inlined case declares the tag member +let private checkInlinedTaggedUnionCases (com: FableCompiler) ctx (ent: FSharpEntity) = + let tagName = + if ent.IsFSharpUnion then + ent.Attributes + |> tryFindAttrib Atts.tsTaggedUnion + |> Option.bind (fun att -> + match Seq.tryHead att.ConstructorArguments with + | Some(_, (:? string as name)) -> Some name + | _ -> None + ) + else + None + + match tagName with + | None -> () + | Some tagName -> + for uci in ent.UnionCases do + match inlinedTaggedUnionCaseHasTag com tagName uci with + | Some false -> + let typeName = + uci.Fields[0].FieldType.Format(FSharpDisplayContext.Empty.WithShortTypeNames(true)) + + $"Case '%s{uci.Name}' of '%s{ent.DisplayName}' compiles to its '%s{typeName}' value, so the interface must declare a '%s{tagName}' member to carry the tag. Add the member, or name the field to compile the case as an object holding the tag and the field." + |> addError (com :> Compiler) ctx.InlinePath (makeRange uci.DeclarationLocation |> Some) + | _ -> () + +let private warnDuplicateCompiledNames (com: FableCompiler) ctx (ent: FSharpEntity) = + let compiledName (uci: FSharpUnionCase) = + let stringEnumName rule = + match transformStringEnum rule uci with + | Fable.Value(Fable.StringConstant name, _) -> Some name + | _ -> None + + match getUnionPattern (ent.AsType()) uci with + | StringEnum(_, rule) -> stringEnumName rule + | ErasedUnion(_, _, rule) when uci.Fields.Count = 0 -> stringEnumName rule + | DiscriminatedUnion _ -> FsUnionCase.CompiledName uci |> Option.defaultValue uci.Name |> Some + // TypeScriptTaggedUnion cases sharing a tag are supported and checked separately + | _ -> None + + // Beam reports collisions on the Erlang atom itself, see `reportUnionCaseTagCollisions` + if ent.IsFSharpUnion && (com :> Compiler).Options.Language <> Beam then + ent.UnionCases + |> Seq.choose (fun uci -> compiledName uci |> Option.map (fun name -> name, uci)) + |> Seq.groupBy fst + |> Seq.iter (fun (name, cases) -> + let _, first = Seq.head cases + + for _, uci in Seq.tail cases do + WarningCodes.duplicateCompiledName ent.DisplayName uci.Name first.Name name + |> addWarningWithCode com ctx.InlinePath (makeRange uci.DeclarationLocation |> Some) + ) + let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = fsDecls |> List.collect (fun fsDecl -> match fsDecl with | FSharpImplementationFileDeclaration.Entity(fsEnt, sub) -> + warnIgnoredCaseTests com ctx fsEnt + checkInlinedTaggedUnionCases com ctx fsEnt + warnDuplicateCompiledNames com ctx fsEnt + + if not (isGlobalOrImportedFSharpEntity fsEnt) then + tryGetBaseEntity fsEnt + |> Option.iter (fun (baseEnt, _) -> + checkSelfConstructorBase com ctx (Some(makeRange fsEnt.DeclarationLocation)) baseEnt + ) + match sub with | [] when isIgnoredLeafEntity fsEnt -> [] | [] -> @@ -2215,7 +2679,7 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = Name = name Entity = entRef Constructor = None - BaseCall = None + BaseCall = tryMakeCompilerGeneratedBaseCall com fsEnt AttachedMembers = [] Tags = Fable.Tags.empty XmlDoc = tryGetXmlDoc fsEnt.XmlDoc @@ -2301,6 +2765,7 @@ type InlineExprInfo = FileName: string ScopeIdents: Set ResolvedIdents: Dictionary + IsFileLocal: bool } let resolveInlineIdent (ctx: Context) (info: InlineExprInfo) (ident: Fable.Ident) = @@ -2322,6 +2787,27 @@ let resolveInlineIdent (ctx: Context) (info: InlineExprInfo) (ident: Fable.Ident else ident +let checkInlinedPrivateAccess + (com: IFableCompiler) + (ctx: Context) + (info: InlineExprInfo) + (importInfo: Fable.ImportInfo) + r + = + match importInfo.Kind, info.IsFileLocal with + | Fable.MemberImport membRef, true -> + match com.TryGetMember(membRef) with + | Some memb when not memb.IsPublic -> + let r = + match r, ctx.InlinePath with + | None, { ToRange = toRange } :: _ -> toRange + | _ -> r + + $"The value '%s{memb.DisplayName}' was marked inline but its implementation makes use of an internal or private function which is not sufficiently accessible" + |> addError com ctx.InlinePath r + | _ -> () + | _ -> () + let resolveInlinedCallInfo com (ctx: Context) info (callInfo: Fable.CallInfo) = { callInfo with ThisArg = Option.map (resolveInlineExpr com ctx info) callInfo.ThisArg @@ -2513,6 +2999,8 @@ let resolveInlineExpr (com: IFableCompiler) ctx info expr = if isImportToSameFile then Fable.IdentExpr { makeTypedIdent t importInfo.Selector with Range = r } else + checkInlinedPrivateAccess com ctx info importInfo r + let path = fixImportedRelativePath com importInfo.Path info.FileName Fable.Import({ importInfo with Path = path }, t, r) @@ -2616,13 +3104,15 @@ let resolveInlineExpr (com: IFableCompiler) ctx info expr = | None -> transformTraitCall com ctx r t sourceTypes traitName isInstance argTypes argExprs | Some w -> // As witnesses come from the context, idents may be duplicated, see #2855 + // The same witness can be inlined several times in one scope, see #3921 let info = { info with + ScopeIdents = Set.union info.ScopeIdents (getDeclaredIdentNames w.Expr) ResolvedIdents = Dictionary() FileName = w.FileName } - let callee = resolveInlineExpr com ctx { info with FileName = w.FileName } w.Expr + let callee = resolveInlineExpr com ctx info w.Expr let callInfo = makeCallInfo None argExprs argTypes makeCall r t callInfo callee @@ -2729,6 +3219,7 @@ type FableCompiler(com: Compiler) = FileName = inExpr.FileName ScopeIdents = inExpr.ScopeIdents ResolvedIdents = Dictionary() + IsFileLocal = inExpr.IsFileLocal } let ctx, bindings, forceInlineMap = @@ -2873,9 +3364,8 @@ type FableCompiler(com: Compiler) = |> List.filter (fun (i, v) -> if ctx.CapturedBindings.Contains(i.Name) && canHaveSideEffects com v then if isIdentUsed i.Name resolved then - $"Inlined argument %s{i.Name} is being captured but is also used somewhere else. " - + "There's a risk of double evaluation." - |> addWarning com [] i.Range + WarningCodes.inlinedArgumentDoubleEvaluation i.Name + |> addWarningWithCode com [] i.Range true else @@ -2919,9 +3409,10 @@ type FableCompiler(com: Compiler) = member _.TryGetEntity(entRef) = com.TryGetEntity(entRef) member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let rec attachClassMembers (com: FableCompiler) = @@ -2934,7 +3425,7 @@ let rec attachClassMembers (com: FableCompiler) = |> Option.map (fun members -> { decl with Constructor = members.Cons - BaseCall = members.BaseCall + BaseCall = members.BaseCall |> Option.orElse decl.BaseCall AttachedMembers = members.Members.ToArray() |> List.ofArray } |> Fable.ClassDeclaration @@ -3002,6 +3493,7 @@ let getInlineExprs fileName (declarations: FSharpImplementationFileDeclaration l FileName = fileName GenericArgs = genArgs ScopeIdents = set ctx.UsedNamesInDeclarationScope + IsFileLocal = not (isNotPrivate memb) } ) ] diff --git a/src/Fable.Transforms/Fable.Transforms.fsproj b/src/Fable.Transforms/Fable.Transforms.fsproj index 738cab2bb8..86cbef389d 100644 --- a/src/Fable.Transforms/Fable.Transforms.fsproj +++ b/src/Fable.Transforms/Fable.Transforms.fsproj @@ -5,6 +5,8 @@ + + diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index d638737185..70730b2074 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -44,16 +44,56 @@ let getCapturedNames expr = let isIdentCaptured identName expr = walkCapturedIdents (fun candidate -> candidate = identName) expr -let isTailRecursive identName expr = +let isUnitArg (ident: Ident) = + ident.IsCompilerGenerated + && ident.Type = Unit + && (ident.DisplayName.StartsWith("unitVar", System.StringComparison.Ordinal) + || ident.DisplayName.Contains("@")) + +let discardUnitArg (genArgs: Type list) (args: Ident list) = + match genArgs, args with + | [ Unit ], [ arg ] -> args // don't drop unit arg when generic arg is unit + | _ -> + match args with + | [] -> [] + | [ arg ] when isUnitArg arg -> [] + | [ thisArg; arg ] when thisArg.IsThisArgument && isUnitArg arg -> [ thisArg ] + | args -> args + +let discardThisArg (args: Ident list) = + match args with + | thisArg :: args when thisArg.IsThisArgument -> args + | _ -> args + +let isTailRecursive (com: Compiler) name (args: Ident list) body = let mutable isTailRec = true let mutable isRecursive = false - let rec loop inTailPos = - function - | CurriedApply(IdentExpr i, _, _, _) - | Call(IdentExpr i, _, _, _) as e when i.Name = identName -> - isRecursive <- true - isTailRec <- isTailRec && inTailPos + let args = args |> discardUnitArg [] |> discardThisArg + let argTypes = args |> List.map (fun arg -> arg.Type) + + let rec loop inTailPos e = + match e with + | CurriedApply(IdentExpr id, callArgs, _, _) -> + let callArgTypes = callArgs |> List.map (fun arg -> arg.Type) + + if name = id.Name && argTypes = callArgTypes then + isRecursive <- true + isTailRec <- isTailRec && inTailPos + + getSubExpressions e |> List.iter (loop false) + | Call(IdentExpr id, info, _, _) -> + let callArgTypes = info.Args |> List.map (fun arg -> arg.Type) + + let isNameEqual = + match Option.bind com.TryGetMember info.MemberRef with + | Some memb -> name = memb.FullName + | None -> name = id.Name + + if isNameEqual && argTypes = callArgTypes then + isRecursive <- true + isTailRec <- isTailRec && inTailPos + getSubExpressions e |> List.iter (loop false) | Sequential exprs -> let lastIndex = (List.length exprs) - 1 @@ -73,7 +113,7 @@ let isTailRecursive identName expr = List.map snd targets |> List.iter (loop inTailPos) | e -> getSubExpressions e |> List.iter (loop false) - loop true expr + loop true body isTailRec <- isTailRec && isRecursive isRecursive, isTailRec @@ -255,40 +295,56 @@ let noSideEffectBeforeIdent identName expr = findIdentOrSideEffect expr && not sideEffect +/// A binding referenced from inside a quotation must not be inlined. `visit` +/// deliberately does not rewrite quoted expressions, so the substitution would +/// never reach that reference, and removing the binding would leave the captured +/// value dangling. +let isReferencedInsideQuote identName body = + body + |> deepExists ( + function + | Quote(quotedExpr, _, _) -> countReferencesUntil 1 identName quotedExpr >= 1 + | _ -> false + ) + let canInlineArg (com: Compiler) identName value body = - match value with - | Value((Null _ | UnitConstant | TypeInfo _ | BoolConstant _ | NumberConstant _ | CharConstant _), _) -> true - | Value(StringConstant s, _) -> - match com.Options.Language with - | Python -> - // Only inline short strings if they're referenced at most once, - // to avoid duplicating the literal in generated code (which can cause - // issues like property access on string literals in Python) - s.Length < 100 && countReferencesUntil 2 identName body <= 1 - | _ -> s.Length < 100 - | _ -> - let refCount = countReferencesUntil 2 identName body + if isReferencedInsideQuote identName body then + false + else - // Don't inline values that create new mutable state (e.g. ResizeArray(), mutable arrays) - // into closures: even though creation is side-effect-free, inlining into a closure - // called multiple times would create a new instance per call instead of sharing the - // single captured instance - let createsMutableState = - match value with - | Value(NewArray(_, _, kind), _) -> - match kind with - | MutableArray - | ResizeArray -> true - | ImmutableArray -> false - | _ -> false + match value with + | Value((Null _ | UnitConstant | TypeInfo _ | BoolConstant _ | NumberConstant _ | CharConstant _), _) -> true + | Value(StringConstant s, _) -> + match com.Options.Language with + | Python -> + // Only inline short strings if they're referenced at most once, + // to avoid duplicating the literal in generated code (which can cause + // issues like property access on string literals in Python) + s.Length < 100 && countReferencesUntil 2 identName body <= 1 + | _ -> s.Length < 100 + | _ -> + let refCount = countReferencesUntil 2 identName body + + // Don't inline values that create new mutable state (e.g. ResizeArray(), mutable arrays) + // into closures: even though creation is side-effect-free, inlining into a closure + // called multiple times would create a new instance per call instead of sharing the + // single captured instance + let createsMutableState = + match value with + | Value(NewArray(_, _, kind), _) -> + match kind with + | MutableArray + | ResizeArray -> true + | ImmutableArray -> false + | _ -> false - (refCount <= 1 - && not (canHaveSideEffects com value) - && not (createsMutableState && isIdentCaptured identName body)) - // If it can have side effects, make sure is at least referenced once so the expression is not erased - || (refCount = 1 - && noSideEffectBeforeIdent identName body - && not (isIdentCaptured identName body)) + (refCount <= 1 + && not (canHaveSideEffects com value) + && not (createsMutableState && isIdentCaptured identName body)) + // If it can have side effects, make sure is at least referenced once so the expression is not erased + || (refCount = 1 + && noSideEffectBeforeIdent identName body + && not (isIdentCaptured identName body)) /// Returns arity of lambda (or lambda option) types let (|Arity|) typ = @@ -353,11 +409,21 @@ module private Transforms = | _ -> ValueNone let tryInlineBinding (com: Compiler) (ident: Ident) value letBody = + let readsMutableValue callee = + match callee with + | IdentExpr id -> id.IsMutable + | Import({ Kind = MemberImport memberRef }, _, _) -> + com.TryGetMember(memberRef) |> Option.exists (fun memb -> memb.IsMutable) + | _ -> false + let canInlineBinding = match value with | Import(i, _, _) -> i.IsCompilerGenerated | Call(callee, info, _, _) when List.isEmpty info.Args && List.contains "value" info.Tags -> - canInlineArg com ident.Name callee letBody + if readsMutableValue callee then + canInlineArg com ident.Name value letBody + else + canInlineArg com ident.Name callee letBody // Replace non-recursive lambda bindings | NestedLambda(_args, lambdaBody, _name) -> match lambdaBody with @@ -450,6 +516,7 @@ module private Transforms = && isErasingCandidate ident && countReferencesUntil 1 ident.Name letBody = 0 && canHaveSideEffects com value + && not (com.Options.Language = Rust) // Rust emits warnings on missing bindings -> // The binding is never read but its value may have side effects (e.g. residue from // inlining CE builder methods like `Combine`/`Run` that discard their argument). @@ -751,8 +818,8 @@ module private Transforms = Delegate(args, body, name, tags) // Uncurry getters for Rust - | Call(Get(_callee, FieldGet _, _, _), m, _, r) when com.Options.Language = Rust -> - match Option.bind com.TryGetMember m.MemberRef with + | Call(Get(_callee, FieldGet _, _, _), info, _, r) when com.Options.Language = Rust -> + match Option.bind com.TryGetMember info.MemberRef with | Some memb when isGetterOrValueWithoutGenerics memb -> match memb.ReturnParameter.Type with // It may happen the arity of the abstract signature is smaller than actual arity @@ -963,7 +1030,7 @@ let rec transformDeclaration transformations (com: Compiler) file decl = let cons, baseCall = match decl.Constructor, decl.BaseCall with - | None, _ -> None, None + | None, baseCall -> None, baseCall | Some cons, None -> uncurryMemberArgs cons |> transformMemberBody com |> Some, None | Some cons, Some baseCall -> // In order to uncurry correctly the baseCall arguments, diff --git a/src/Fable.Transforms/FableTransforms.fsi b/src/Fable.Transforms/FableTransforms.fsi index cc54d8948b..714c978340 100644 --- a/src/Fable.Transforms/FableTransforms.fsi +++ b/src/Fable.Transforms/FableTransforms.fsi @@ -4,8 +4,11 @@ open Fable open Fable.AST.Fable val isIdentCaptured: identName: string -> expr: Expr -> bool +val isUnitArg: ident: Ident -> bool +val discardUnitArg: genArgs: Type list -> args: Ident list -> Ident list +val discardThisArg: args: Ident list -> Ident list val getCapturedNames: expr: Expr -> string list -val isTailRecursive: identName: string -> expr: Expr -> bool * bool +val isTailRecursive: com: Compiler -> name: string -> args: Ident list -> body: Expr -> bool * bool val countReferencesUntil: limit: int -> identName: string -> body: Expr -> int val replaceValues: replacements: Map -> expr: Expr -> Expr val uncurryType: typ: Type -> Type diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 39937961a3..91ab47a3f9 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.15.0" + let VERSION = "5.27.0" [] - let JS_LIBRARY_VERSION = "2.5.1" + let JS_LIBRARY_VERSION = "2.13.0" type CompilerOptionsHelper = static member Make @@ -18,7 +18,8 @@ type CompilerOptionsHelper = ?verbosity, ?fileExtension, ?clampByteArrays, - ?noReflection + ?noReflection, + ?jsTemporal ) = { @@ -32,6 +33,7 @@ type CompilerOptionsHelper = ClampByteArrays = defaultArg clampByteArrays false NoReflection = defaultArg noReflection false TriggeredByDependency = false + JsTemporal = defaultArg jsTemporal false } [] @@ -58,6 +60,7 @@ type InlineExpr = FileName: string GenericArgs: string list ScopeIdents: Set + IsFileLocal: bool } type CompilerPlugins = { MemberDeclarationPlugins: Map } @@ -84,9 +87,11 @@ type Compiler = abstract TryGetEntity: Fable.EntityRef -> Fable.Entity option abstract GetInlineExpr: string -> InlineExpr abstract AddWatchDependency: file: string -> unit + abstract AddSignatureDependency: file: string -> unit abstract AddLog: - msg: string * severity: Severity * ?range: SourceLocation * ?fileName: string * ?tag: string -> unit + msg: string * severity: Severity * ?range: SourceLocation * ?fileName: string * ?tag: string * ?code: string -> + unit type InlineExprLazy(f: Compiler -> InlineExpr) = let mutable value: InlineExpr voption = ValueNone diff --git a/src/Fable.Transforms/Global/Naming.fs b/src/Fable.Transforms/Global/Naming.fs index 83b0ece01e..abec6b2d0c 100644 --- a/src/Fable.Transforms/Global/Naming.fs +++ b/src/Fable.Transforms/Global/Naming.fs @@ -449,6 +449,8 @@ module Naming = let reflectionSuffix = "_$reflection" + let beamAccessorSuffix = "_accessor" + let private printPart sanitize separator part overloadSuffix = (if String.IsNullOrEmpty(part) then "" @@ -534,3 +536,13 @@ module Naming = | c -> sb.Append(c) |> ignore sb.ToString() + + // F# hash function gives different results in different runs + // Taken from fable-library-ts/Util.ts. Possible variant in https://stackoverflow.com/a/1660613 + let stringHash (s: string) = + let mutable h = 5381 + + for i = 0 to s.Length - 1 do + h <- (h * 33) ^^^ (int s[i]) + + h diff --git a/src/Fable.Transforms/Global/WarningCodes.fs b/src/Fable.Transforms/Global/WarningCodes.fs new file mode 100644 index 0000000000..c2d67bb1b5 --- /dev/null +++ b/src/Fable.Transforms/Global/WarningCodes.fs @@ -0,0 +1,169 @@ +(* + Central registry for Fable warnings codes + + - `FABLE0001`-`FABLE0099` describe Fable's own suppression directivers. They are never suppressible. + - `FABLE0100` and up describe code from compilation result. Those can be suppressed +*) +module Fable.Transforms.WarningCodes + +/// Can a `// fable-disable` comment silence a warning carrying this code? Codes below +/// `FABLE0100` describe the directives themselves and never can - see the band rules above. +/// This is what makes the banding an invariant rather than a naming convention: it holds +/// wherever a diagnostic is raised from, not just on the path that happens to raise them today. +/// +/// `None` is a warning with no code at all, which a bare `// fable-disable-line` still catches. +let isSuppressible (code: string option) = + match code with + | None -> true + // Every code is `FABLE` plus four digits, so ordinal comparison orders them numerically. + | Some code -> System.String.CompareOrdinal(code, "FABLE0100") >= 0 + +(* + FABLE0001-0099: about Fable's own directives. Never suppressible. +*) + +/// A `fable-disable*` directive named something that isn't in this registry - most likely a typo. +let unknownSuppressionCode (code: string) = + "FABLE0001", $"Unknown warning code '%s{code}' in a fable-disable directive" + +/// A `fable-disable*` directive that never suppressed anything, so it can be deleted. +let unusedSuppressionDirective = + "FABLE0002", "This fable-disable directive doesn't suppress anything" + +/// A bare `// fable-disable` block, which would silence every Fable warning up to end of file. +let suppressionBlockWithoutCode = + "FABLE0003", + "A 'fable-disable' block must list the warning codes it suppresses, otherwise it silences every Fable warning until the end of the file" + +(* + FABLE0100 and up: about the compiled code. Suppressible. +*) + +/// `String.StartsWith`/`EndsWith` with a `CultureInfo` argument: the comparison always runs +/// with the target's default culture rules, the argument is accepted but has no effect. +/// Used in both the JS/TS and Python replacements. +let cultureInfoIgnored = "FABLE0100", "CultureInfo argument is ignored" + +/// Dart's `contains`/`startsWith`/`endsWith` are ordinal and case-sensitive, so a +/// `StringComparison` or `CultureInfo` asking for anything else has no effect. +let stringComparisonIgnored = "FABLE0101", "String comparison argument is ignored" + +/// An `IFormatProvider`/`CultureInfo` passed to a `Parse`/`TryParse`, `String.Format` or +/// `StringBuilder.AppendFormat` overload. Fable is culture-independent here, so a culture that +/// changes the decimal separator or the day/month order silently changes the result. +let formatProviderIgnored = "FABLE0102", "Format provider argument is ignored" + +/// A `NumberStyles` value that isn't `Integer` or `HexNumber` passed to a numeric `Parse`. The +/// value is interpolated because the range can't tell you which style was discarded. +let numberStylesIgnored (style: int) = + "FABLE0103", $"NumberStyles argument %d{style} is ignored" + +/// A `DateTimeStyles` value passed to a date/time `Parse`. +let dateTimeStylesIgnored = "FABLE0104", "DateTimeStyles argument is ignored" + +/// A `TimeSpan` constructed with a microseconds argument. The runtime representation only carries +/// milliseconds, so the finer component is dropped rather than rounded. +let timeSpanPrecisionIgnored = + "FABLE0105", "TimeSpan precision is limited to milliseconds, microsecond arguments are ignored" + +/// `FSharpType.IsUnion(t, allowAccessToPrivateRepresentation)` and friends. Fable's reflection +/// has no notion of a private representation, so the flag never restricts anything. +let privateRepresentationFlagIgnored = + "FABLE0106", "Private representation flag is ignored" + +/// `Convert.ToBase64String(bytes, offset, length)` / `(bytes, options)`. Only the array is used, +/// so a slice is encoded whole and line-break options have no effect. +let base64ArgumentsIgnored = + "FABLE0107", "Base64 offset, length and formatting arguments are ignored" + +/// `Console.Write` and `Debug.Write`: the target cannot print without a line break. +let writeBehavesAsWriteLine = "FABLE0108", "Write will behave as WriteLine" + +/// `Async.Start`: there is no thread pool, so the computation starts on the current thread. +let asyncStartBehavesAsStartImmediate = + "FABLE0109", "Async.Start will behave as StartImmediate" + +/// `Task.Result` or `GetAwaiter().GetResult()` on JS/TS, where a task is a promise and nothing can block on it. +let taskResultReturnsTask = + "FABLE0110", + "Task.Result and GetAwaiter().GetResult() return the task itself in JS/TS, as there's no way to block until it completes. Use let! in task { } or Async.AwaitTask instead." + +/// `GetType()` on a value typed `obj`: Fable resolves types at compile time only. +let getTypeOnObj = + "FABLE0111", "Types can only be resolved at compile time. At runtime this will be same as `typeof`" + +/// A `:?` test against a type with no runtime representation (interface, erased type, Pojo class). +let typeTestEvalsToFalse (typeName: string) = + "FABLE0112", $"Cannot type test (evals to false): %s{typeName}" + +/// An `inherit` call whose arguments the target cannot forward to the base constructor. +let baseCallIgnored (entityFullName: string) = + "FABLE0113", $"Ignoring base call for %s{entityFullName}" + +/// An `inherit` through a secondary constructor (`new(...) = ...`). +let nonPrimaryBaseConstructor = + "FABLE0114", "Only inheriting from primary constructors is supported" + +/// An argument of an inlined function that is captured by a closure and also used elsewhere in the body. +let inlinedArgumentDoubleEvaluation (argName: string) = + "FABLE0115", + $"Inlined argument %s{argName} is being captured but is also used somewhere else. There's a risk of double evaluation." + +/// A `CaseTest` attribute on a union case where Fable does not use it. +let caseTestIgnored (caseName: string) (reason: string) = + "FABLE0116", $"CaseTest attribute on '%s{caseName}' is ignored: %s{reason}" + +/// Rust: `String.Compare` and `String.CompareTo` are compiled as ordinal comparisons. +let stringCompareIsOrdinal (methodName: string) = + "FABLE0117", $"String.%s{methodName} will be compiled as String.CompareOrdinal" + +/// Rust: a `char` is a Unicode scalar value, so it can never be a surrogate. +let charSurrogateTestsAreFalse = + "FABLE0118", "Rust chars are Unicode scalar values, so surrogate tests will be false." + +/// Rust: static and module `do` bindings are skipped unless the `static_do_bindings` feature is enabled. +let staticDoBindingsDisabled = + "FABLE0119", + "For Rust, support for F# static and module do bindings is disabled by default. It can be enabled with the 'static_do_bindings' feature. Use at your own risk!" + +/// PHP: a regex flag with no PCRE modifier, dropped from the pattern. +let regexFlagUnsupported (flag: string) = + "FABLE0120", $"Regex %s{flag} flag is not supported in Php" + +/// Two cases of a union compile to the same name, so the output cannot tell them apart. +let duplicateCompiledName (unionName: string) (caseName: string) (otherCaseName: string) (compiledName: string) = + "FABLE0121", $"Cases '%s{caseName}' and '%s{otherCaseName}' of '%s{unionName}' both compile to '%s{compiledName}'" + +/// Every code the compiler can emit, both bands. A directive naming anything else is reported as +/// a typo, so a new warning MUST be added to this list as well as defined above. +let knownCodes = + [ + // The two parameterised warnings get a throwaway argument; only the code is read. + unknownSuppressionCode "" + unusedSuppressionDirective + suppressionBlockWithoutCode + cultureInfoIgnored + stringComparisonIgnored + formatProviderIgnored + numberStylesIgnored 0 + dateTimeStylesIgnored + timeSpanPrecisionIgnored + privateRepresentationFlagIgnored + base64ArgumentsIgnored + writeBehavesAsWriteLine + asyncStartBehavesAsStartImmediate + taskResultReturnsTask + getTypeOnObj + typeTestEvalsToFalse "" + baseCallIgnored "" + nonPrimaryBaseConstructor + inlinedArgumentDoubleEvaluation "" + caseTestIgnored "" "" + stringCompareIsOrdinal "" + charSurrogateTestsAreFalse + staticDoBindingsDisabled + regexFlagUnsupported "" + duplicateCompiledName "" "" "" "" + ] + |> List.map fst + |> Set.ofList diff --git a/src/Fable.Transforms/Global/WarningSuppression.fs b/src/Fable.Transforms/Global/WarningSuppression.fs new file mode 100644 index 0000000000..fa3c32182e --- /dev/null +++ b/src/Fable.Transforms/Global/WarningSuppression.fs @@ -0,0 +1,437 @@ +(* + Computes which diagnostics `// fable-disable/-enable...` comments suppress (ESLint's + disable-line/next-line/block model), via real comment tokens - not raw text matching. +*) +module Fable.Transforms.WarningSuppression + +open Fable +open System.Collections.Concurrent +open System.Text.RegularExpressions +open FSharp.Compiler.Tokenization + +[] +type private DirectiveKind = + | DisableLine + | DisableNextLine + | Disable + | Enable + +[] +type private Directive = + { + Kind: DirectiveKind + /// `None` means "every code" + Codes: Set option + Line: int + /// Set the first time this directive suppresses something. + mutable Used: bool + /// Set when the directive was already reported for something else (typo'd code, bare + /// block): no point telling the user it is unused on top of that. + mutable Reported: bool + } + +type private BlockState = + | NoneDisabled + /// Blanket disable opened by this directive, minus the codes since re-enabled. + | AllDisabledExcept of opener: Directive * enabled: Set + /// code -> the directive that disabled it + | SpecificDisabled of Map + +/// A problem with a directive itself (typo'd code, directive that suppresses nothing, ...), +/// reported once the file it belongs to has finished compiling. +type DirectiveDiagnostic = + { + Code: string + Message: string + Line: int + } + +// The name must be followed by end-of-comment, whitespace or `:` so that `fable-disabled` or +// `fable-disable-lines` aren't mistaken for a bare directive. `:` is then optionally consumed +let private directiveRegex = + Regex(@"^fable-(disable-next-line|disable-line|disable|enable)(?=$|\s|:)\s*:?\s*(.*)$", RegexOptions.Compiled) + +let private codeSeparators = [| ' '; ','; '\t' |] + +/// Cheap pre-filter: tokenizing a source file to look for directives that aren't there is pure +/// waste, and the vast majority of files contain none. +let private mayContainDirectives (source: string) = + source.IndexOf("fable-disable", System.StringComparison.Ordinal) >= 0 + || source.IndexOf("fable-enable", System.StringComparison.Ordinal) >= 0 + +/// Splits a directive's argument text into codes, dropping the ESLint-style ` -- justification` +/// tail. Codes are upper-cased so `fable0001` matches, and anything not in the registry is +/// returned separately so the caller can report the typo instead of silently ignoring it. +let private parseCodes (raw: string) : Set option * string list = + let raw = + match raw.IndexOf("--", System.StringComparison.Ordinal) with + | -1 -> raw + | i -> raw.Substring(0, i) + + let tokens = + raw.Split(codeSeparators, System.StringSplitOptions.RemoveEmptyEntries) + |> Array.map (fun t -> t.ToUpperInvariant()) + + let known, unknown = tokens |> Array.partition WarningCodes.knownCodes.Contains + + let codes = + if Array.isEmpty tokens then + None + else + Some(Set.ofArray known) + + codes, List.ofArray unknown + +let private stripCommentMarkers (raw: string) = + let raw = + if raw.StartsWith("//", System.StringComparison.Ordinal) then + raw.Substring(2) + elif raw.StartsWith("(*", System.StringComparison.Ordinal) then + raw.Substring(2) + else + raw + + let raw = + if raw.EndsWith("*)", System.StringComparison.Ordinal) then + raw.Substring(0, raw.Length - 2) + else + raw + + raw.Trim() + +let private tryParseDirective (line: int) (commentText: string) : (Directive * string list) option = + let text = stripCommentMarkers commentText + let m = directiveRegex.Match(text) + + if not m.Success then + None + else + let codes, unknown = parseCodes m.Groups[2].Value + + let kind = + match m.Groups[1].Value with + | "disable-line" -> Some DisableLine + | "disable-next-line" -> Some DisableNextLine + | "disable" -> Some Disable + | "enable" -> Some Enable + | _ -> None + + kind + |> Option.map (fun kind -> + { + Kind = kind + Codes = codes + Line = line + Used = false + Reported = false + }, + unknown + ) + +/// Gathers each line's comment token runs as plain text (a line can have more than one, e.g. a +/// block comment then a trailing line comment); also returns the lexer state to carry into the +/// next line, needed to resume correctly inside multi-line block comments/strings. +let private scanLineComments + (tokenizer: FSharpLineTokenizer) + (initialState: FSharpTokenizerLexState) + (line: string) + : string list * FSharpTokenizerLexState + = + // Each finished run is one comment on the line; `current` is the run being built. + let runs = ResizeArray() + let mutable current: System.Text.StringBuilder option = None + + // Pull tokens one at a time, threading the lexer state (needed across lines too). + let rec loop state = + match tokenizer.ScanToken(state) with + | Some(tok: FSharpTokenInfo), state2 -> + if tok.ColorClass = FSharpTokenColorKind.Comment then + let text = line.Substring(tok.LeftColumn, tok.RightColumn - tok.LeftColumn + 1) + + match current with + // Still inside the same comment: glue this token onto the current run. + | Some sb -> sb.Append(text) |> ignore + // First comment token after non-comment text: start a new run. + | None -> + let sb = System.Text.StringBuilder(text: string) + current <- Some sb + runs.Add(sb) + else + // Non-comment token: close the current run, if any (e.g. code between two comments). + current <- None + + loop state2 + // No more tokens on this line: return the final state for the next line. + | None, state2 -> state2 + + let endState = loop initialState + // Materialize each run's text; endState lets the caller resume correctly on the next line. + (runs |> Seq.map _.ToString() |> List.ofSeq), endState + +/// Computed, queryable suppression info for a single source file. +type FileSuppressions = + private + { + /// Every parsed directive, in source order. + Directives: Directive array + /// 1-based line -> the line-scoped directives landing on it + LineOnly: Map + /// index (line - 1) -> block-disable state as of (and including) that line + BlockAtLine: BlockState array + /// Problems found while parsing (typo'd codes, bare block disables) + ParseDiagnostics: DirectiveDiagnostic list + } + + /// Is a diagnostic with the given code (None = no code assigned to it) suppressed anywhere in + /// `[startLine, endLine]`? Line-scoped directives match on any line of the range, so a + /// trailing `// fable-disable-line` still works on a multi-line expression. The block state is + /// read at `startLine` only: a `fable-disable` buried inside a large expression shouldn't + /// retroactively silence a warning anchored above it. + member this.IsSuppressed(startLine: int, endLine: int, code: string option) = + let matches (d: Directive) = + match d.Codes with + | None -> true + | Some codes -> code |> Option.map codes.Contains |> Option.defaultValue false + + let mutable suppressed = false + + for line in startLine .. max startLine endLine do + match Map.tryFind line this.LineOnly with + | Some directives -> + for d in directives do + if matches d then + d.Used <- true + suppressed <- true + | None -> () + + if not suppressed && startLine >= 1 && startLine <= this.BlockAtLine.Length then + match this.BlockAtLine[startLine - 1] with + | NoneDisabled -> () + | AllDisabledExcept(opener, enabled) -> + let hit = + match code with + | None -> true + | Some c -> not (Set.contains c enabled) + + if hit then + opener.Used <- true + suppressed <- true + | SpecificDisabled disabled -> + match code |> Option.bind (fun c -> Map.tryFind c disabled) with + | Some opener -> + opener.Used <- true + suppressed <- true + | None -> () + + suppressed + + member this.IsSuppressed(line: int, code: string option) = this.IsSuppressed(line, line, code) + + /// Parse problems plus every directive that never suppressed anything. Only meaningful once + /// the whole compilation is over: an inlined call can suppress through a directive in a file + /// other than the one currently being compiled. + member this.GetDiagnostics() = + let unused = + this.Directives + |> Array.choose (fun d -> + // `fable-enable` re-opens warnings rather than suppressing them, so "unused" has + // no meaning for it - ESLint doesn't report those either. A directive whose codes + // are already reported as typos is obviously unused too; one message is enough. + if d.Kind = Enable || d.Used || d.Reported then + None + else + let code, message = WarningCodes.unusedSuppressionDirective + + Some + { + Code = code + Message = message + Line = d.Line + } + ) + |> List.ofArray + + this.ParseDiagnostics @ unused + + static member Empty = + { + Directives = [||] + LineOnly = Map.empty + BlockAtLine = [||] + ParseDiagnostics = [] + } + +/// Folds one `fable-disable`/`fable-enable` into the running block state. The directive itself is +/// carried through so that whatever it ends up suppressing can mark it used. +let private transition (state: BlockState) (d: Directive) = + match d.Kind = Disable, d.Codes, state with + | true, None, _ -> AllDisabledExcept(d, Set.empty) + | true, Some codes, NoneDisabled -> SpecificDisabled(codes |> Seq.map (fun c -> c, d) |> Map.ofSeq) + | true, Some codes, SpecificDisabled m -> SpecificDisabled((m, codes) ||> Set.fold (fun m c -> Map.add c d m)) + | true, Some codes, AllDisabledExcept(opener, ex) -> AllDisabledExcept(opener, Set.difference ex codes) + | false, None, _ -> NoneDisabled + | false, Some _, NoneDisabled -> NoneDisabled + | false, Some codes, SpecificDisabled m -> SpecificDisabled((m, codes) ||> Set.fold (fun m c -> Map.remove c m)) + | false, Some codes, AllDisabledExcept(opener, ex) -> AllDisabledExcept(opener, Set.union ex codes) + +let private computeDirectives (defines: string list) (source: string) = + let lines = source.Replace("\r\n", "\n").Split('\n') + let sourceTok = FSharpSourceTokenizer(defines, None, None, None) + let directives = ResizeArray() + let parseDiagnostics = ResizeArray() + let mutable state = FSharpTokenizerLexState.Initial + + for i in 0 .. lines.Length - 1 do + let lineNo = i + 1 + let tokenizer = sourceTok.CreateLineTokenizer(lines[i]) + let runs, newState = scanLineComments tokenizer state lines[i] + state <- newState + + for run in runs do + match tryParseDirective lineNo run with + | None -> () + | Some(directive, unknownCodes) -> + let flag (code, message) = + directive.Reported <- true + + parseDiagnostics.Add + { + Code = code + Message = message + Line = lineNo + } + + for unknown in unknownCodes do + flag (WarningCodes.unknownSuppressionCode unknown) + + // A bare `fable-disable` block silences every Fable warning to end of file, which + // is almost never what people mean - flag it, but honour it. + if directive.Kind = Disable && Option.isNone directive.Codes then + flag WarningCodes.suppressionBlockWithoutCode + + directives.Add directive + + let lineOnly = + (Map.empty, directives) + ||> Seq.fold (fun acc d -> + let add line = + let existing = Map.tryFind line acc |> Option.defaultValue [] + Map.add line (d :: existing) acc + + match d.Kind with + | DisableLine -> add d.Line + | DisableNextLine -> add (d.Line + 1) + | Disable + | Enable -> acc + ) + + let blockDirectivesByLine = + directives + |> Seq.filter (fun d -> + match d.Kind with + | Disable + | Enable -> true + | DisableLine + | DisableNextLine -> false + ) + |> Seq.groupBy (fun d -> d.Line) + |> Seq.map (fun (line, ds) -> line, List.ofSeq ds) + |> Map.ofSeq + + // Filling this with hundreds/thousands of NoneDisabled is nearly free: F# compiles a + // nullary DU case to a singleton, so every slot is a pointer to the same object. + let blockAtLine = Array.create lines.Length NoneDisabled + let mutable current = NoneDisabled + + for lineNo in 1 .. lines.Length do + match Map.tryFind lineNo blockDirectivesByLine with + | Some ds -> + for d in ds do + current <- transition current d + | None -> () + + blockAtLine[lineNo - 1] <- current + + { + Directives = Seq.toArray directives + LineOnly = lineOnly + BlockAtLine = blockAtLine + ParseDiagnostics = List.ofSeq parseDiagnostics + } + +/// Scans the given source text for `fable-disable*`/`fable-enable*` comments and builds a +/// queryable `FileSuppressions` snapshot. Meant to be computed once per file and cached. +/// +/// `defines` must be the same conditional-compilation symbols FCS was given (`FABLE_COMPILER`, +/// the project's `DefineConstants`, ...). Without them the tokenizer reports everything inside +/// an `#if` block as inactive code rather than comments, so directives sitting next to the very +/// warnings they're meant to suppress would be invisible. +let compute (defines: string list) (source: string) : FileSuppressions = + if mayContainDirectives source then + computeDirectives defines source + else + FileSuppressions.Empty + +/// Suppression snapshots shared by every file of a project: computing them is per-file work, but +/// warnings can point at any file (inlined calls) and files compile in parallel. Keyed by the +/// content hash `SourceReader` already returns, so a file edited under watch mode recomputes +/// instead of being scanned against stale text. +type private Cache() = + let entries = ConcurrentDictionary() + + /// May throw `KeyNotFoundException` for a file outside the project (e.g. the source of a + /// precompiled dll) - that's the caller's cue that there's nothing to suppress with. + member _.GetOrCompute(fileName: string, defines: string list, read: SourceReader) = + let hash, source = read fileName + + // GetOrAdd rather than a TryGetValue/assign pair: two threads racing on the same file + // would otherwise end up with separate snapshots, splitting the "directive was used" + // flags between them and producing bogus unused-directive reports. + let cachedHash, suppressions = + entries.GetOrAdd(fileName, fun _ -> hash, compute defines source.Value) + + if cachedHash = hash then + suppressions + else + // The file changed under us, so the snapshot is against the wrong text. + let fresh = hash, compute defines source.Value + entries[fileName] <- fresh + snd fresh + +/// Extracts the conditional-compilation symbols out of the option list FCS is given +/// (`FSharpProjectOptions.OtherOptions`), which is the only place the project's `DefineConstants` +/// and Fable's own `FABLE_COMPILER*` symbols are both present. +let definesFromCompilerOptions (otherOptions: string seq) = + otherOptions + |> Seq.choose (fun opt -> + if opt.StartsWith("--define:", System.StringComparison.Ordinal) then + Some(opt.Substring("--define:".Length)) + else + None + ) + |> List.ofSeq + +/// Project-wide entry point: resolves a file name to its suppressions, computing and caching on +/// first use. One instance per compiled project - it is shared across the per-file compilers. +type Resolver(defines: string list, read: SourceReader) = + let cache = Cache() + + static member FromCompilerOptions(otherOptions: string seq, read: SourceReader) = + Resolver(definesFromCompilerOptions otherOptions, read) + + member _.For(fileName: string) = + try + cache.GetOrCompute(fileName, defines, read) + with :? System.Collections.Generic.KeyNotFoundException -> + // Not a file of this project (e.g. the source of a precompiled dll): nothing to read, + // so nothing can be suppressed. Any other failure is a real problem and must surface. + FileSuppressions.Empty + + /// The directive problems of the given files. These are `FABLE0001`-band codes and so are + /// never themselves suppressible - they flag comment text that is wrong to keep, so the only + /// correct answer is to edit it. Call once the whole compilation is over: a warning raised + /// while compiling one file can be suppressed by a directive living in another. + member this.GetDiagnostics(fileNames: string seq) = + fileNames + |> Seq.collect (fun fileName -> this.For(fileName).GetDiagnostics() |> List.map (fun d -> fileName, d)) + |> List.ofSeq diff --git a/src/Fable.Transforms/OverloadSuffix.fs b/src/Fable.Transforms/OverloadSuffix.fs index 2f353aec58..323543785b 100644 --- a/src/Fable.Transforms/OverloadSuffix.fs +++ b/src/Fable.Transforms/OverloadSuffix.fs @@ -154,19 +154,9 @@ let private combineHashCodes (hashes: int seq) = else hashes |> Array.reduce (fun h1 h2 -> ((h1 <<< 5) + h1) ^^^ h2) -// F# hash function gives different results in different runs -// Taken from fable-library-ts/Util.ts. Possible variant in https://stackoverflow.com/a/1660613 -let private stringHash (s: string) = - let mutable h = 5381 - - for i = 0 to s.Length - 1 do - h <- (h * 33) ^^^ (int s[i]) - - h - let private getHashPrivate (paramTypes: ParamTypes) genParams = paramTypes - |> List.map (getTypeFastFullName genParams >> stringHash) + |> List.map (getTypeFastFullName genParams >> Naming.stringHash) |> combineHashCodes |> hashToString diff --git a/src/Fable.Transforms/Php/Fable2Php.fs b/src/Fable.Transforms/Php/Fable2Php.fs index 4afbb5ee5b..e91811f1e3 100644 --- a/src/Fable.Transforms/Php/Fable2Php.fs +++ b/src/Fable.Transforms/Php/Fable2Php.fs @@ -1509,11 +1509,11 @@ and convertValue (com: IPhpCompiler) (value: Fable.ValueKind) range = | RegexMultiline -> "m" | RegexSingleline -> "s" | RegexGlobal -> - addWarning com [] range "Regex global flag is not supported in Php" + WarningCodes.regexFlagUnsupported "global" |> addWarningWithCode com [] range "" | RegexSticky -> - addWarning com [] range "Regex sticky flag is not supported in Php" + WarningCodes.regexFlagUnsupported "sticky" |> addWarningWithCode com [] range "" ) @@ -2138,10 +2138,11 @@ type PhpCompiler(com: Fable.Compiler) = member this.WillPrecompileInlineFunction(file) = com.WillPrecompileInlineFunction(file) - member this.AddLog(msg, severity, rang, fileName, tag) = - com.AddLog(msg, severity, ?range = rang, ?fileName = fileName, ?tag = tag) + member this.AddLog(msg, severity, rang, fileName, tag, code) = + com.AddLog(msg, severity, ?range = rang, ?fileName = fileName, ?tag = tag, ?code = code) member this.AddWatchDependency(file) = com.AddWatchDependency(file) + member this.AddSignatureDependency(file) = com.AddSignatureDependency(file) member this.GetImplementationFile(fileName) = com.GetImplementationFile(fileName) diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index 4d8d0a6254..2148591fa7 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -104,30 +104,9 @@ let private transformUnionReflectionInfo com ctx r (ent: Fable.Entity) generics let py, stmts = pyConstructor com ctx ent - // Generate case constructors list for make_union - // Use full case class names (UnionName_CaseName) to match the generated classes, - // except for library types (Result, Choice) which use simple names - let usesSimpleNames = Util.usesSimpleCaseNames ent.FullName - - // Get the entity declaration name (with module scope) for consistent naming - let entityDeclName = FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref - let caseConstructors = ent.UnionCases - |> Seq.map (fun uci -> - let caseName = - match uci.CompiledName with - | Some cname -> cname - | None -> uci.Name - - let caseClassName = - if usesSimpleNames then - caseName - else - $"%s{entityDeclName}_%s{caseName}" - - com.GetIdentifierAsExpr(ctx, caseClassName) - ) + |> Seq.map (Util.getUnionCaseRef com ctx ent.Ref ent) |> Seq.toList |> Expression.list @@ -237,6 +216,7 @@ let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map genericEntity fullName [], [] | Replacements.Util.BclHashSet gen | Replacements.Util.FSharpSet gen -> @@ -278,21 +258,49 @@ let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map List.map (transformTypeInfo com ctx r genMap) |> Helpers.unzipArgs - // Check if the entity is actually declared in Python code - if - ent.IsInterface - || FSharp2Fable.Util.isErasedOrStringEnumEntity ent - || FSharp2Fable.Util.isGlobalOrImportedEntity ent - || FSharp2Fable.Util.isReplacementCandidate entRef - then - genericEntity ent.FullName generics, stmts - else - let reflectionMethodExpr = - FSharp2Fable.Util.entityIdentWithSuffix com entRef Naming.reflectionSuffix - let callee, stmts' = com.TransformAsExpr(ctx, reflectionMethodExpr) + let erasedFieldType = + if ent.IsFSharpUnion && FSharp2Fable.Util.hasAttribute Atts.erase ent then + match ent.UnionCases with + | [ uci ] when List.isSingle uci.UnionCaseFields -> Some uci.UnionCaseFields.Head.FieldType + | _ -> None + else + None + + match erasedFieldType with + | Some fieldType -> + let fieldGenMap = + Seq.zip (ent.GenericParameters |> Seq.map (fun p -> p.Name)) generics |> Map + + let expr, stmts' = transformTypeInfo com ctx r fieldGenMap fieldType + expr, stmts @ stmts' + | None -> + // Check if the entity is actually declared in Python code + if + ent.IsInterface + || FSharp2Fable.Util.isErasedOrStringEnumEntity ent + || FSharp2Fable.Util.isGlobalOrImportedEntity ent + || FSharp2Fable.Util.isReplacementCandidate entRef + then + genericEntity ent.FullName generics, stmts + else + let reflectionMethodExpr = + FSharp2Fable.Util.entityIdentWithSuffix com entRef Naming.reflectionSuffix + + let callee, stmts' = com.TransformAsExpr(ctx, reflectionMethodExpr) + + Expression.call (callee, generics), stmts @ stmts' + +let private getReflectionBaseType (com: IPythonCompiler) (ent: Fable.Entity) = + ent.BaseType + |> Option.bind (fun baseType -> + let baseEnt = com.GetEntity(baseType.Entity) - Expression.call (callee, generics), stmts @ stmts' + // decision: native exception bridges add Python ancestry without changing .NET reflection ancestry. + match (com :> Compiler), baseEnt.Attributes with + | FSharp2Fable.Util.ImportAtt(_, "fable_library.exception_bases") -> baseEnt.BaseType + | _ -> Some baseType + ) let transformReflectionInfo com ctx r (ent: Fable.Entity) generics = if ent.IsFSharpRecord then @@ -313,7 +321,7 @@ let transformReflectionInfo com ctx r (ent: Fable.Entity) generics = yield Expression.name (name.Name |> Naming.toPythonNaming), stmts | Some(cons, stmts) -> yield cons, stmts | None -> () - match ent.BaseType with + match getReflectionBaseType com ent with | Some d -> let genMap = Seq.zip ent.GenericParameters generics @@ -332,7 +340,7 @@ let transformReflectionInfo com ctx r (ent: Fable.Entity) generics = let transformTypeTest (com: IPythonCompiler) ctx range expr (typ: Fable.Type) : Expression * Statement list = let warnAndEvalToFalse msg = - "Cannot type test (evals to false): " + msg |> addWarning com [] range + WarningCodes.typeTestEvalsToFalse msg |> addWarningWithCode com [] range Expression.boolConstant false diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index aac00e39b3..c77d94d28c 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -111,65 +111,10 @@ let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable. args, body, returnType -let getUnionCaseName (uci: Fable.UnionCase) = - match uci.CompiledName with - | Some cname -> cname - | None -> uci.Name - /// Attribute holding the precomputed singleton instance of a zero-field union case (see transformUnion). let unionCaseSingletonAttrName = "singleton" let unionCaseSingletonAttr = Identifier unionCaseSingletonAttrName -/// Gets the unique case class name by prefixing with the union type name. -/// This prevents collisions when different union types have cases with the same name. -/// Library types (Result, Choice) use simple case names without prefix. -/// The optional entityName parameter should be the compiled entity name (with module scope). -let getUnionCaseClassName - (com: IPythonCompiler) - (ent: Fable.Entity) - (uci: Fable.UnionCase) - (entityName: string option) - = - let caseName = getUnionCaseName uci - // Library types use simple names (Ok, Error, Choice1Of2, etc.) for backwards compatibility - if usesSimpleCaseNames ent.FullName then - caseName - else - // Use provided entity name or compute from entity reference - let unionName = - match entityName with - | Some name -> name - | None -> FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref - - $"%s{unionName}_%s{caseName}" - -/// Resolves the expression referring to a union case's class (constructor / type), -/// handling both library types (Result, Choice - simple names from fable_library) -/// and user-defined unions (full case class name, imported from another module if needed). -let getUnionCaseRef (com: IPythonCompiler) ctx (entRef: Fable.EntityRef) (ent: Fable.Entity) (uci: Fable.UnionCase) = - if isLibraryUnionType entRef.FullName then - let caseName = getUnionCaseName uci - // Result uses "result" module, Choice uses "choice" module - let moduleName = - if entRef.FullName = Types.result then - "result" - else - "choice" - - libValue com ctx moduleName caseName - else - // User-defined union - use full case class name (UnionName_CaseName) - let caseClassName = getUnionCaseClassName com ent uci None - - match entRef.SourcePath with - | Some path when path <> com.CurrentFile -> - // Import from another module - let importPath = Path.getRelativeFileOrDirPath false com.CurrentFile false path - com.GetImportExpr(ctx, importPath, caseClassName) - | _ -> - // Local - just get identifier - com.GetIdentifierAsExpr(ctx, caseClassName) - let getUnionExprTag (com: IPythonCompiler) ctx r (fableExpr: Fable.Expr) = Expression.withStmts { let! expr = com.TransformAsExpr(ctx, fableExpr) @@ -437,6 +382,14 @@ let transformCast (com: IPythonCompiler) (ctx: Context) t e : Expression * State let value, stmts = com.TransformAsExpr(ctx, e) libCall com ctx None "core" "int32" [ value ], stmts | _ -> com.TransformAsExpr(ctx, e) + // The other integer widths are wrapper classes too, so `uint32 (x: byte)` needs the same explicit constructor call + | Fable.Number((Int8 | UInt8 | Int16 | UInt16 | UInt32) as kind, _), _ -> + match e.Type with + | Fable.Number(sourceKind, _) when sourceKind = kind -> com.TransformAsExpr(ctx, e) + | Fable.Number _ -> + let value, stmts = com.TransformAsExpr(ctx, e) + libCall com ctx None "core" (Py.Replacements.getIntTypeName kind) [ value ], stmts + | _ -> com.TransformAsExpr(ctx, e) // Float64 is a plain Python `float`, and the same reasoning applies: // `Replacements.toFloat` emits a bare `TypeCast` for every numeric source that is // not bigint/decimal/int64, so `float (x: float32)` would otherwise stay a @@ -516,10 +469,6 @@ let transformValue (com: IPythonCompiler) (ctx: Context) r value : Expression * | Fable.NumberValue.Float64 x when x = -infinity -> Expression.unaryOp (UnaryMinus, com.GetImportExpr(ctx, "math", "inf")), [] | Fable.NumberValue.Float64 x when Double.IsNaN(x) -> com.GetImportExpr(ctx, "math", "nan"), [] - | Fable.NumberValue.Float32 x when Single.IsNaN(x) -> - libCall com ctx r "core" "float32" [ Expression.stringConstant "nan" ], [] - | Fable.NumberValue.Float16 x when Single.IsNaN(x) -> - libCall com ctx r "core" "float32" [ Expression.stringConstant "nan" ], [] | Fable.NumberValue.Float16 x -> makeFloat com ctx r value.Type "float32" (float x) | Fable.NumberValue.Float32 x -> makeFloat com ctx r value.Type "float32" (float x) | Fable.NumberValue.Float64 x -> Expression.floatConstant (float x, ?loc = r), [] @@ -613,7 +562,7 @@ let extractBaseExprFromBaseCall (com: IPythonCompiler) (ctx: Context) (baseType: |> Option.map (fun t -> t.Entity.FullName) |> Option.defaultValue "unknown type" - $"Ignoring base call for %s{name}" |> addWarning com [] range + WarningCodes.baseCallIgnored name |> addWarningWithCode com [] range None | Some(Fable.Value _), Some baseType -> @@ -625,8 +574,8 @@ let extractBaseExprFromBaseCall (com: IPythonCompiler) (ctx: Context) (baseType: // Some (baseExpr, []) // default base constructor let range = baseCall |> Option.bind (fun x -> x.Range) - $"Ignoring base call for %s{baseType.Entity.FullName}" - |> addWarning com [] range + WarningCodes.baseCallIgnored baseType.Entity.FullName + |> addWarningWithCode com [] range None | Some _, _ -> @@ -816,11 +765,24 @@ let transformObjectExpr Expression.call (Expression.name name), thisCaptureStmts @ [ stmt ] @ stmts -let transformCallArgs +let transformArgWithAdapters (com: IPythonCompiler) ctx (sigType: Fable.Type option) (argExpr: Fable.Expr) = + let expr, stmts = com.TransformAsExpr(ctx, argExpr) + + if needsOptionWidenForArg sigType argExpr then + let widen = com.TransformImport(ctx, "widen", getLibPath com "option") + Expression.call (widen, [ expr ]), stmts + elif needsUnitCallableEraseForArg sigType argExpr then + let erase = com.TransformImport(ctx, "erase_unit_callable", getLibPath com "util") + Expression.call (erase, [ expr ]), stmts + else + expr, stmts + +let transformCallArgsWith (com: IPythonCompiler) ctx (callInfo: Fable.CallInfo) (isBaseConstructorCall: bool) + (fallbackSigTypes: Fable.Type list) : Expression list * Keyword list * Statement list = @@ -913,15 +875,15 @@ let transformCallArgs let hasSpread = paramsInfo |> Option.map (fun i -> i.HasSpread) |> Option.defaultValue false - // Helper to transform an arg and wrap with widen() if needed - let transformArgWithWiden (sigType: Fable.Type option) (argExpr: Fable.Expr) = - let expr, stmts = com.TransformAsExpr(ctx, argExpr) + let transformArgs args = + args + |> List.mapi (fun i e -> + let sigType = + List.tryItem i callInfo.SignatureArgTypes + |> Option.orElse (List.tryItem i fallbackSigTypes) - if needsOptionWidenForArg sigType argExpr then - let widen = com.TransformImport(ctx, "widen", getLibPath com "option") - Expression.call (widen, [ expr ]), stmts - else - expr, stmts + transformArgWithAdapters com ctx sigType e + ) let args, stmts' = match args with @@ -930,31 +892,30 @@ let transformCallArgs match List.rev args with | [] -> [], [] | Replacements.Util.ArrayOrListLiteral(spreadArgs, _) :: rest -> - let rest = List.rev rest |> List.map (fun e -> com.TransformAsExpr(ctx, e)) + let rest = List.rev rest |> transformArgs rest @ List.map (fun e -> com.TransformAsExpr(ctx, e)) spreadArgs |> Helpers.unzipArgs | last :: rest -> - let rest, stmts = - List.rev rest - |> List.map (fun e -> com.TransformAsExpr(ctx, e)) - |> Helpers.unzipArgs + let rest, stmts = List.rev rest |> transformArgs |> Helpers.unzipArgs let expr, stmts' = com.TransformAsExpr(ctx, last) rest @ [ Expression.starred expr ], stmts @ stmts' - | args -> - // Transform args with widen() where needed based on signature types - args - |> List.mapi (fun i e -> - let sigType = List.tryItem i callInfo.SignatureArgTypes - transformArgWithWiden sigType e - ) - |> Helpers.unzipArgs + | args -> args |> transformArgs |> Helpers.unzipArgs match objArg with | None -> args, [], stmts @ stmts' | Some objArg -> args, objArg, stmts @ stmts' +let transformCallArgs + (com: IPythonCompiler) + ctx + (callInfo: Fable.CallInfo) + (isBaseConstructorCall: bool) + : Expression list * Keyword list * Statement list + = + transformCallArgsWith com ctx callInfo isBaseConstructorCall [] + let resolveExpr (ctx: Context) _t strategy pyExpr : Statement list = // printfn "resolveExpr: %A" (pyExpr, strategy) match strategy with @@ -1340,7 +1301,8 @@ let transformCall (com: IPythonCompiler) ctx range callee (callInfo: Fable.CallI let callee', stmts = com.TransformAsExpr(ctx, callee) - let args, kw, stmts' = transformCallArgs com ctx callInfo false + let args, kw, stmts' = + transformCallArgsWith com ctx callInfo false (calleeSignatureArgTypes ctx callee callInfo) match callee, callInfo.ThisArg with | Fable.Get(expr, Fable.FieldGet { Name = "Dispose" }, _, _), _ -> @@ -1363,9 +1325,17 @@ let transformCall (com: IPythonCompiler) ctx range callee (callInfo: Fable.CallI Expression.call (callee', args, kw, ?loc = range), stmts @ stmts' | _, None -> callFunction range callee' args kw, stmts @ stmts' -let transformCurriedApply com ctx range (TransformExpr com ctx (applied, stmts)) args = - ((applied, stmts), args) - ||> List.fold (fun (applied, stmts) arg -> +let transformCurriedApply (com: IPythonCompiler) ctx range (callee: Fable.Expr) args = + let rec expectedArgTypes typ args = + match typ, args with + | Fable.LambdaType(argType, returnType), _ :: rest -> Some argType :: expectedArgTypes returnType rest + | _, _ :: rest -> None :: expectedArgTypes typ rest + | _, [] -> [] + + let applied, stmts = com.TransformAsExpr(ctx, callee) + + ((applied, stmts), List.zip (expectedArgTypes callee.Type args) args) + ||> List.fold (fun (applied, stmts) (sigType, arg) -> let args, argStmts = match arg with // TODO: If arg type is unit but it's an expression with potential @@ -1375,7 +1345,7 @@ let transformCurriedApply com ctx range (TransformExpr com ctx (applied, stmts)) | Fable.Value(Fable.UnitConstant, _) -> [], [] | Fable.IdentExpr ident when ident.Type = Fable.Unit -> [], [] | arg -> - let argExpr, transformStmts = com.TransformAsExpr(ctx, arg) + let argExpr, transformStmts = transformArgWithAdapters com ctx sigType arg // When a Call or CurriedApply result is passed as an argument, // check if we need to erase Option[T] to T | None let argExpr = @@ -1637,7 +1607,7 @@ let transformGet (com: IPythonCompiler) ctx range typ (fableExpr: Fable.Expr) ki // Use effective type for field naming (considers type refinement) let narrowedType = match fableExpr with - | Fable.IdentExpr ident -> getNarrowedType ctx ident + | Fable.IdentExpr ident -> getNarrowedType ctx ident |> Replacements.Util.getIdentValueType com ident | _ -> fableExpr.Type let fieldName = Util.applyFieldNaming com narrowedType i.Name true @@ -1710,7 +1680,10 @@ let transformGet (com: IPythonCompiler) ctx range typ (fableExpr: Fable.Expr) ki let caseRef = getUnionCaseRef com ctx i.Entity ent uci let narrow = libValue com ctx "union" "narrow" let narrowedExpr = Expression.call (narrow, [ caseRef; baseExpr ]) - let! finalExpr = getExpr com ctx range narrowedExpr (Expression.stringConstant fieldName) + + let! finalExpr = + getExpr com ctx range narrowedExpr (Expression.stringConstant fieldName) + return finalExpr } @@ -1745,7 +1718,12 @@ let transformSet (com: IPythonCompiler) ctx range fableExpr typ (value: Fable.Ex let transformBindingExprBody (com: IPythonCompiler) (ctx: Context) (var: Fable.Ident) (value: Fable.Expr) = match value with | Function(args, body) -> - let name = Some var.Name + // decision: mutable callbacks keep their Callable annotation instead of inheriting a def's parameter names + let name = + if var.IsMutable then + None + else + Some var.Name let args', body', returnType, typeParams = Annotation.transformFunctionWithAnnotations com ctx name args body @@ -1761,6 +1739,7 @@ let transformBindingAsExpr (com: IPythonCompiler) ctx (var: Fable.Ident) (value: expr |> assign None (identAsExpr com ctx var), stmts let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) (value: Fable.Expr) = + let varType = Replacements.Util.getIdentValueType com var var.Type let shouldTreatAsStatement = isPyStatement ctx false value let needsErase = needsOptionEraseForBinding value var.Type // Skip type annotation to avoid Option[T] vs T | None mismatch issues with Pyright: @@ -1780,7 +1759,7 @@ let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) ( let body = com.TransformAsStatements(ctx, Some(Assign varExpr), value) body else - let ta, stmts = Annotation.typeAnnotation com ctx None var.Type + let ta, stmts = Annotation.typeAnnotation com ctx None varType let decl = Statement.assign (varName, ta) let body = com.TransformAsStatements(ctx, Some(Assign varExpr), value) stmts @ [ decl ] @ body @@ -1793,7 +1772,7 @@ let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) ( let decl = varDeclaration ctx varName None expr stmts @ decl else - let ta, stmts' = Annotation.typeAnnotation com ctx None var.Type + let ta, stmts' = Annotation.typeAnnotation com ctx None varType // Erase Option wrapper if needed (zero runtime overhead, just for type checker) let expr' = if needsErase then @@ -2884,6 +2863,8 @@ let rec transformAsExpr (com: IPythonCompiler) ctx (expr: Fable.Expr) : Expressi iife com ctx expr | Fable.LetRec(bindings, body) -> + let ctx = withDeclaredFunctionTypes ctx bindings + if ctx.HoistVars(List.map fst bindings) then Expression.withStmts { let values, stmts = @@ -3085,10 +3066,13 @@ let rec transformAsStatements (com: IPythonCompiler) ctx returnStrategy (expr: F | Fable.Let(ident, value, body) -> let binding = transformBindingAsStatements com ctx ident value + let ctx = withDeclaredFunctionTypes ctx [ ident, value ] List.append binding (transformAsStatements com ctx returnStrategy body) | Fable.LetRec(bindings, body) -> + let ctx = withDeclaredFunctionTypes ctx bindings + let bindings = bindings |> Seq.collect (fun (i, v) -> transformBindingAsStatements com ctx i v) @@ -3429,8 +3413,15 @@ let transformFunction args = [ Arg.arg (Identifier("__unit"), annotation = unitType) ], defaults = [ unitDefault ] ) - // No args but has tail-call args: skip __unit, tcArgs are sufficient - | [], _, _ -> Arguments.arguments (args = tcArgs, defaults = tcDefaults) + // invariant: a runtime unit argument never overwrites a captured TCO default + | [], _, _ -> + let unitType = com.GetImportExpr(ctx, getLibPath com "util", "Unit") + let unitName = getUniqueNameInDeclarationScope ctx "__unit" + + Arguments.arguments ( + args = Arg.arg (Identifier unitName, annotation = unitType) :: tcArgs, + defaults = unitDefault :: tcDefaults + ) // Single generic/unit arg with no tail-call args: keep it with () default | [ arg ], true, [] -> Arguments.arguments (args = args, defaults = [ unitDefault ]) // Single generic/unit arg with tail-call args: keep arg (body may reference it) @@ -3541,6 +3532,18 @@ let declareDataClassType = let name = com.GetIdentifier(ctx, entName) + // Record slots follow Python naming conventions, but structured formatting must + // retain the original F# field names. Keep the names on the generated class so + // the runtime can pair them with the slots without attempting a lossy conversion. + let fsharpFieldNames = + ent.FSharpFields + |> List.filter (fun field -> not field.IsStatic) + |> List.map (fun field -> Expression.stringConstant field.Name) + |> Expression.tuple + + let fsharpFieldNamesMember = + Statement.assign ([ Expression.name ("__fable_field_names__", Store) ], fsharpFieldNames) + // Generate field annotations from entity's FSharpFields to properly uncurry function types. // Record/dataclass fields store functions uncurried at runtime, so we need to uncurry // lambda types in the type annotations to match. @@ -3664,6 +3667,7 @@ let declareDataClassType let classBody = let body = [ + yield fsharpFieldNamesMember yield! staticFieldAnnotations yield! props yield! classMembers @@ -4293,6 +4297,7 @@ let transformAttachedMethod (com: IPythonCompiler) ctx (info: Fable.MemberFuncti ] let transformUnion (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: string) classMembers = + let classMembers = withReferenceEquality com ctx ent classMembers // Get generic type parameters for the union let typeParams = makeEntityTypeParams com ctx ent @@ -4493,10 +4498,12 @@ let transformClassWithCompilerGeneratedConstructor ctx (ent: Fable.Entity) (entName: string) + (baseCall: Fable.Expr option) classMembers (classAttributes: ClassAttributes) = // printfn "transformClassWithCompilerGeneratedConstructor" + let classMembers = withReferenceEquality com ctx ent classMembers let fieldIds = getEntityFieldsAsIdents com ent let args = @@ -4506,18 +4513,34 @@ let transformClassWithCompilerGeneratedConstructor let isOptional = Helpers.isOptional fieldIds || ent.IsFSharpRecord || ent.IsValueType - let baseExpr = + let baseExpr, superArgs, baseFieldIds, baseStmts = if ent.IsFSharpExceptionDeclaration then - libValue com ctx "exceptions" "FSharpException" |> Some + libValue com ctx "exceptions" "FSharpException" |> Some, [], [], [] elif ent.IsFSharpRecord || ent.IsValueType then - libValue com ctx "record" "Record" |> Some + libValue com ctx "record" "Record" |> Some, [], [], [] else - None + match extractBaseExprFromBaseCall com ctx ent.BaseType baseCall with + | Some(baseExpr, (superArgs, _keywords, stmts)) -> + let baseFieldIds = + match baseCall with + | Some(Fable.Call(_, info, _, _)) -> + info.Args + |> List.choose ( + function + | Fable.IdentExpr id -> Some id + | _ -> None + ) + | _ -> [] + + Some baseExpr, superArgs, baseFieldIds, stmts + | None -> None, [], [], [] let body = [ + yield! baseStmts + if Option.isSome baseExpr then - yield callSuperAsStatement [] + yield callSuperAsStatement superArgs // For attributes style, use direct attribute names and proper parameter handling if classAttributes.Style = ClassStyle.Attributes then @@ -4556,8 +4579,8 @@ let transformClassWithCompilerGeneratedConstructor ] let args = - fieldIds - |> Array.mapToList (fun id -> + baseFieldIds @ Array.toList fieldIds + |> List.map (fun id -> let ta, _ = Annotation.typeAnnotation com ctx None id.Type Arg.arg (ident com ctx id, annotation = ta) ) @@ -5382,7 +5405,14 @@ let rec transformDeclaration (com: IPythonCompiler) ctx (decl: Fable.Declaration allStatements @ classStmts | _, None -> let classStmts = - transformClassWithCompilerGeneratedConstructor com ctx ent decl.Name allClassMembers classAttributes + transformClassWithCompilerGeneratedConstructor + com + ctx + ent + decl.Name + decl.BaseCall + allClassMembers + classAttributes allStatements @ classStmts @@ -5501,6 +5531,7 @@ let transformFile (com: IPythonCompiler) (file: Fable.File) = ScopedTypeParams = Set.empty TypeParamsScope = 0 NarrowedTypes = Map.empty + DeclaredFunctionTypes = Map.empty EnclosingUnionBaseClass = None InEmitMacroArgument = false } diff --git a/src/Fable.Transforms/Python/Fable2Python.Types.fs b/src/Fable.Transforms/Python/Fable2Python.Types.fs index 2e9536484a..93dad4224e 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Types.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Types.fs @@ -124,6 +124,7 @@ type Context = ScopedTypeParams: Set TypeParamsScope: int NarrowedTypes: Map + DeclaredFunctionTypes: Map /// When inside a union base class definition, this holds the entity name. /// Used to determine whether to use base class name or type alias for annotations. EnclosingUnionBaseClass: string option diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index d71adf1a28..23bfd37303 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -53,6 +53,53 @@ module Util = | "FSharp.Core.FSharpResult`2" -> true | _ -> false + let getUnionCaseName (uci: Fable.UnionCase) = + match uci.CompiledName with + | Some cname -> cname + | None -> uci.Name + + let getUnionCaseClassName + (com: IPythonCompiler) + (ent: Fable.Entity) + (uci: Fable.UnionCase) + (entityName: string option) + = + let caseName = getUnionCaseName uci + + if usesSimpleCaseNames ent.FullName then + caseName + else + let unionName = + match entityName with + | Some name -> name + | None -> FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref + + $"%s{unionName}_%s{caseName}" + + let getUnionCaseRef + (com: IPythonCompiler) + ctx + (entRef: Fable.EntityRef) + (ent: Fable.Entity) + (uci: Fable.UnionCase) + = + if isLibraryUnionType entRef.FullName then + let moduleName = + if entRef.FullName = Types.result then + "result" + else + "choice" + + libValue com ctx moduleName (getUnionCaseName uci) + else + let caseClassName = getUnionCaseClassName com ent uci None + + match entRef.SourcePath with + | Some path when path <> com.CurrentFile -> + let importPath = Path.getRelativeFileOrDirPath false com.CurrentFile false path + com.GetImportExpr(ctx, importPath, caseClassName) + | _ -> com.GetIdentifierAsExpr(ctx, caseClassName) + /// Ensures a statement list is non-empty by adding Pass if needed. /// Python requires at least one statement in function/class/match bodies. let ensureNonEmptyBody stmts = @@ -272,6 +319,50 @@ module Util = | _ -> false | _ -> false + let needsUnitCallableEraseForArg (expectedType: Fable.Type option) (argExpr: Fable.Expr) = + let rec mismatches expected actual = + match FableTransforms.uncurryType expected, FableTransforms.uncurryType actual with + | Fable.DelegateType([ Fable.GenericParam _ ], _), Fable.DelegateType([ Fable.Unit ], _) -> true + | Fable.DelegateType(expectedArgs, _), Fable.DelegateType(actualArgs, _) + | Fable.Tuple(expectedArgs, _), Fable.Tuple(actualArgs, _) -> + List.sameLength expectedArgs actualArgs + && List.exists2 mismatches expectedArgs actualArgs + | _ -> false + + // Generic-to-unit callback inputs need the typing adapter both directly and inside callbacks. + expectedType + |> Option.exists (fun expected -> + match FableTransforms.uncurryType expected, FableTransforms.uncurryType argExpr.Type with + | Fable.DelegateType _, Fable.DelegateType _ -> mismatches expected argExpr.Type + | _ -> false + ) + + // Identifiers at call sites carry the instantiated type, not the declared generic one + let withDeclaredFunctionTypes (ctx: Context) (bindings: (Fable.Ident * Fable.Expr) list) = + let declared = + (ctx.DeclaredFunctionTypes, bindings) + ||> List.fold (fun declared (ident, value) -> + match value with + | Fable.Lambda _ + | Fable.Delegate _ -> Map.add ident.Name ident.Type declared + | _ -> Map.remove ident.Name declared + ) + + { ctx with DeclaredFunctionTypes = declared } + + // uncurryApplications emits an empty SignatureArgTypes for local function calls + let calleeSignatureArgTypes (ctx: Context) (callee: Fable.Expr) (callInfo: Fable.CallInfo) = + let calleeType = + match callee with + | Fable.IdentExpr ident -> + Map.tryFind ident.Name ctx.DeclaredFunctionTypes + |> Option.defaultValue callee.Type + | _ -> callee.Type + + match callInfo.SignatureArgTypes, FableTransforms.uncurryType calleeType with + | [], Fable.DelegateType(argTypes, _) when List.sameLength argTypes callInfo.Args -> argTypes + | _ -> [] + /// Wraps None values in cast(type, None) for type safety. /// Skips if type annotation is also None (unit type). let wrapNoneInCast (com: IPythonCompiler) ctx (value: Expression) (typeAnnotation: Expression) : Expression = @@ -603,6 +694,46 @@ module Util = let thisExpr = Expression.name "self" + let hasClassMethod name (classMembers: Statement list) = + classMembers + |> List.exists ( + function + | FunctionDef { Name = Identifier key } -> key = name + | _ -> false + ) + + let withReferenceEquality (com: IPythonCompiler) ctx (ent: Fable.Entity) (classMembers: Statement list) = + let isReferenceEqualityType = + (ent.IsFSharpRecord || ent.IsFSharpUnion) + && not ent.IsValueType + && not (FSharp2Fable.Util.hasStructuralEquality ent) + + if not isReferenceEqualityType then + classMembers + else + let makeMethod name args returnExpr (returnType: string) = + Statement.functionDef ( + Identifier name, + Arguments.arguments (args = Arg.arg "self" :: args), + body = [ Statement.return' returnExpr ], + returns = Expression.name returnType + ) + + [ + yield! classMembers + if not (hasClassMethod "Equals" classMembers) then + let other = + Arg.arg ("other", annotation = com.TransformImport(ctx, "Any", "typing")) + + makeMethod + "Equals" + [ other ] + (Expression.compare (thisExpr, [ Is ], [ Expression.name "other" ])) + "bool" + if not (hasClassMethod "GetHashCode" classMembers) then + makeMethod "GetHashCode" [] (libCall com ctx None "util" "physical_hash" [ thisExpr ]) "int" + ] + let ofInt (_com: IPythonCompiler) (_ctx: Context) (i: int) = Expression.intConstant (int i) let ofString (s: string) = Expression.stringConstant s @@ -1037,10 +1168,16 @@ module Util = | _, (:? uint64 as i) when i = 64UL -> makeFieldGet cons "SIXTY_FOUR" | _ -> Expression.call (cons, [ value ], ?loc = r), [] - let makeFloat (com: IPythonCompiler) (ctx: Context) r _t floatName x = + let makeFloat (com: IPythonCompiler) (ctx: Context) r _t floatName (x: float) = let cons = libValue com ctx "core" floatName - let value = Expression.floatConstant (x, ?loc = r) - Expression.call (cons, [ value ], ?loc = r), [] + + match x with + | x when Double.IsPositiveInfinity x -> makeFieldGet cons "infinity" + | x when Double.IsNegativeInfinity x -> makeFieldGet cons "negative_infinity" + | x when Double.IsNaN x -> makeFieldGet cons "nan" + | _ -> + let value = Expression.floatConstant (x, ?loc = r) + Expression.call (cons, [ value ], ?loc = r), [] // --------------------------------------------------------------------------- // Int32 normalization diff --git a/src/Fable.Transforms/Python/Prelude.fs b/src/Fable.Transforms/Python/Prelude.fs index d14f71f8a6..133c90ed69 100644 --- a/src/Fable.Transforms/Python/Prelude.fs +++ b/src/Fable.Transforms/Python/Prelude.fs @@ -173,7 +173,7 @@ module Naming = let reflectionSuffix = "_reflection" - let mutable uniqueIndex = 0 + let mutable private uniqueIndex = 0 let getUniqueIndex () = let idx = uniqueIndex diff --git a/src/Fable.Transforms/Python/PythonCompiler.fs b/src/Fable.Transforms/Python/PythonCompiler.fs index 63563c02bf..dff28d2dc0 100644 --- a/src/Fable.Transforms/Python/PythonCompiler.fs +++ b/src/Fable.Transforms/Python/PythonCompiler.fs @@ -136,9 +136,10 @@ type PythonCompiler(com: Compiler) = member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = PythonCompiler(com) diff --git a/src/Fable.Transforms/Python/PythonPrinter.fs b/src/Fable.Transforms/Python/PythonPrinter.fs index aa89f08ed4..234c149811 100644 --- a/src/Fable.Transforms/Python/PythonPrinter.fs +++ b/src/Fable.Transforms/Python/PythonPrinter.fs @@ -792,6 +792,14 @@ module PrinterExtensions = printer.Print("\"") printer.Print(Naming.escapeString (fun _ -> false) value) printer.Print("\"") + | Constant(value = FloatLiteral value) when Double.IsNaN value || Double.IsInfinity value -> + // .NET prints these as `Infinity`, `-Infinity` and `NaN`, which are not Python names + if Double.IsNaN value then + printer.Print("float(\"nan\")") + elif Double.IsPositiveInfinity value then + printer.Print("float(\"inf\")") + else + printer.Print("float(\"-inf\")") | Constant(value = FloatLiteral value) -> let value = string value printer.Print(value) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 997d42d43d..66d15d3596 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -90,6 +90,7 @@ let coreModFor = | BclDictionary _ -> "mutable_map" | BclDateOnly -> "date_only" | BclTimeOnly -> "time_only" + | BclRune | BclKeyValuePair _ -> FableError "Cannot decide core module" |> raise let makeDecimal com r t (x: decimal) = @@ -781,6 +782,8 @@ let tryEntityIdent (com: Compiler) entFullName = // | BuiltinDefinition FSharpMap _ -> fail "Map" // TODO: | Types.matchFail -> makeImportLib com Any "MatchFailureException" "Types" |> Some | Types.exception_ -> makeIdentExpr "Exception" |> Some + | "System.Threading.Tasks.TaskCanceledException" -> + makeImportLib com Any "TaskCanceledError" "async_builder" |> Some // | "System.Collections.Generic.KeyNotFoundException" -> // makeImportLib com Any "KeyNotFoundException" "System.Collections.Generic" |> Some | BuiltinSystemException entName -> makeImportLib com Any entName "System" |> Some @@ -1072,15 +1075,20 @@ let getMangledNames (i: CallInfo) (thisArg: Expr option) = moduleName, mangledName let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName, mangledName = getMangledNames i thisArg + match i.CompiledName, thisArg, args with + | "ToString", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "GetHashCode", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "Equals", Some callee, [ other ] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | _ -> + let moduleName, mangledName = getMangledNames i thisArg - let args = - match thisArg with - | Some callee -> callee :: args - | _ -> args + let args = + match thisArg with + | Some callee -> callee :: args + | _ -> args - Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, ?loc = r) + |> Some let fsharpModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let moduleName, mangledName = getMangledNames i thisArg @@ -1208,18 +1216,15 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Strings | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // bprintf _ -> fsFormat com ctx r t i thisArg args - | ("Failure" | "FailurePattern" | "LazyPattern" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args + | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "NullArg" | "Using"), _ -> + fsharpModule com ctx r t i thisArg args | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args | "Lock", _ -> Helper.LibCall(com, "util", "lock", t, args, i.SignatureArgTypes, ?loc = r) |> Some // Exceptions - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r t (error com msg) |> Some - | "InvalidArg", [ argName; msg ] -> - let msg = add msg (add (add (str " (Parameter '") argName) (str "')")) - makeThrow r t (error com msg) |> Some + | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some | "Raise", [ arg ] -> makeThrow r t arg |> Some | "Reraise", _ -> match ctx.CaughtException with @@ -1403,8 +1408,7 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "GetHashCode", Some arg, _ -> identityHash com r arg |> Some | "GetType", Some arg, _ -> if arg.Type = Any then - "Types can only be resolved at compile time. At runtime this will be same as `typeof`" - |> addWarning com ctx.InlinePath r + WarningCodes.getTypeOnObj |> addWarningWithCode com ctx.InlinePath r makeTypeInfo r arg.Type |> Some | _ -> None @@ -1530,7 +1534,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "string", "starts_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "StartsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "string", "starts_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -1542,7 +1546,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "string", "ends_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "EndsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "string", "ends_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -1846,6 +1850,26 @@ let injectIndexOfArgs com ctx r genArgs args = injectArg com ctx r "Array" "indexOf" genArgs args +/// resize_array's binary search indexes arrays and ResizeArrays alike. +/// As in .NET, an omitted or null comparer means the default one. +let binarySearch (com: ICompiler) ctx r t rangeMeth (ar: Expr) args = + let call meth args comparer = + let comparer = + match comparer with + | None + | Some NullConst -> makeComparer com ctx (getElementType ar.Type) + | Some comparer -> comparer + + Helper.LibCall(com, "resize_array", meth, t, ar :: args @ [ comparer ], ?loc = r) + |> Some + + match args with + | [ value ] -> call "binary_search" [ value ] None + | [ value; comparer ] -> call "binary_search" [ value ] (Some comparer) + | [ index; length; value ] -> call rangeMeth [ index; length; value ] None + | [ index; length; value; comparer ] -> call rangeMeth [ index; length; value ] (Some comparer) + | _ -> None + let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", _, [] -> makeResizeArray (getElementType t) [] |> Some @@ -1924,6 +1948,7 @@ let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this Helper.LibCall(com, "resize_array", "index_of", t, args @ [ ar ], ?loc = r) |> Some + | "BinarySearch", Some ar, args -> binarySearch com ctx r t "binary_search_range_count" ar args | "Insert", Some ar, [ idx; arg ] -> Helper.InstanceCall(ar, "insert", t, [ idx; arg ], ?loc = r) |> Some | "InsertRange", Some ar, [ idx; arg ] -> Helper.LibCall(com, "resize_array", "insert_range_in_place", t, [ idx; arg; ar ], ?loc = r) @@ -2030,6 +2055,7 @@ let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E Helper.LibCall(com, "array", "index_of", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "BinarySearch", None, ar :: args -> binarySearch com ctx r t "binary_search_range" ar args | "GetEnumerator", Some arg, _ -> getEnumerator com r t arg |> Some | "Resize", None, args -> let args = @@ -2057,15 +2083,7 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex match i.CompiledName, args with | "ToSeq", [ arg ] -> Some arg - | "OfSeq", [ arg ] -> - let meth = Naming.lowerFirst i.CompiledName - let args = injectArg com ctx r "Array" meth i.GenericArgs args - - Helper.LibCall(com, "array", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "OfList", [ arg ] -> - Helper.LibCall(com, "list", "to_array", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | ("OfSeq" | "OfList"), [ arg ] -> makeArrayFrom (getElementType t) arg |> Some | "ToList", args -> Helper.LibCall(com, "list", "of_array", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -2111,14 +2129,13 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use methods for Head and Tail (instead of Get(ListHead) for example) to check for empty lists - | ReplaceName [ "get_Head", "head" - "get_Tail", "tail" - "get_Item", "item" - "get_Length", "length" - "GetSlice", "getSlice" ] methName, - Some x, - _ -> + // list.Head and list.Tail raise InvalidOperationException, List.head and List.tail ArgumentException + | ("get_Head" | "get_Tail"), Some x, _ -> + let mangledName = + Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" + + Helper.LibCall(com, "list", mangledName, t, [], thisArg = x, ?loc = r) |> Some + | ReplaceName [ "get_Item", "item"; "get_Length", "length"; "GetSlice", "getSlice" ] methName, Some x, _ -> let args = match args with | [ ExprType Unit ] -> [ x ] @@ -2151,6 +2168,7 @@ let listModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Exp Helper.LibCall(com, "seq2", "List_" + meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "ToArray", [ arg ] -> makeArrayFrom (getElementType t) arg |> Some | meth, _ -> let meth = Naming.lowerFirst meth let args = injectArg com ctx r "List" meth i.GenericArgs args @@ -2229,6 +2247,45 @@ let nullables (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: | "get_HasValue", Some c -> Test(c, OptionTest true, r) |> Some | _ -> None +let nullableModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = + let convert (targetType: Type) (value: Expr) = + match targetType with + | Char -> toChar com ctx r value + | Number(UInt64, _) -> toLong com ctx r true targetType [ value ] + | Number(Int64, _) -> toLong com ctx r false targetType [ value ] + | Number((Float16 | Float32 | Float64), _) -> toFloat com ctx r targetType [ value ] + | Number(Decimal, _) -> toDecimal com ctx r targetType [ value ] + | Number _ -> toInt com ctx r targetType [ value ] + | _ -> TypeCast(value, targetType) + + match i.CompiledName.Replace("$W", ""), List.tryLast args, t with + | ("ToByte" | "ToUInt8" | "ToSByte" | "ToInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToInt32" | "ToUInt" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr" | "ToSingle" | "ToFloat32" | "ToDouble" | "ToFloat" | "ToDecimal" | "ToChar" | "ToEnum"), + Some value, + Nullable(targetType, _) -> + match value with + | Value(Null _, _) -> Value(Null t, None) |> Some + | Value((NumberConstant _ | CharConstant _), _) -> convert targetType value |> Some + | _ -> + let sourceType = + match value.Type with + | Nullable(sourceType, _) -> sourceType + | sourceType -> sourceType + + let v = makeUniqueIdent com ctx value.Type "v" + + Let( + v, + value, + IfThenElse( + Test(IdentExpr v, OptionTest true, None), + TypeCast(convert targetType (TypeCast(IdentExpr v, sourceType)), t), + Value(Null t, None), + r + ) + ) + |> Some + | _ -> None + // See fable-library-ts/Option.ts for more info on how options behave in Fable runtime let options isStruct (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with @@ -2347,8 +2404,8 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let intConst = int System.Globalization.NumberStyles.Integer if style <> hexConst && style <> intConst then - $"%s{i.DeclaringEntityFullName}.%s{meth}(): NumberStyle %d{style} is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.numberStylesIgnored style + |> addWarningWithCode com ctx.InlinePath r let acceptedArgs = if meth = "Parse" then @@ -2356,10 +2413,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 3 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, style, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r parseCall meth str args style | ("Parse" | "TryParse") as meth, str :: _ -> @@ -2369,10 +2429,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 2 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r let style = int System.Globalization.NumberStyles.Any parseCall meth str args style @@ -2613,10 +2676,26 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr | "GetArray", _, [ ar; idx ] -> getExpr r t ar idx |> Some | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some | ("GetArraySlice" | "GetStringSlice"), None, [ ar; lower; upper ] -> + // Python slicing counts a negative index from the end, .NET clamps it to 0 + let clampToZero (index: Expr) = + match index with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> index + | _ -> Helper.LibCall(com, "util", "slice_index", Int32.Number, [ index ], ?loc = r) + + let lower = + match lower with + | Value(NewOption(None, _, _), _) -> makeIntConst 0 + | Value(NewOption(Some lower, _, _), _) + | lower -> clampToZero lower + let upper = match upper with | Value(NewOption(None, _, _), _) -> Helper.GlobalCall("len", t, [ ar ], [ t ], ?loc = r) - | _ -> add upper (makeIntConst 1) + | Value(NewOption(Some upper, _, _), _) + | upper -> + match upper with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> makeIntConst (n + 1) + | upper -> add upper (makeIntConst 1) |> clampToZero Helper.InstanceCall(ar, "slice", t, [ lower; upper ], ?loc = r) |> Some | "SetArraySlice", None, args -> @@ -2845,14 +2924,21 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr match i.CompiledName, thisArg with | ".ctor", _ -> match i.DeclaringEntityFullName with + | "System.Threading.Tasks.TaskCanceledException" -> + Helper.LibCall(com, "async_builder", "TaskCanceledError", t, args, isConstructor = true, ?loc = r) + |> Some // | "System.Collections.Generic.KeyNotFoundException" | BuiltinSystemException _ -> bclType com ctx r t i thisArg args | _ -> - let e = makeImportLib com Any "ExceptionBase" "Types" + let e = makeImportLib com Any "ExceptionBase" "exception_bases" Helper.ConstructorCall(e, t, args, ?loc = r) |> Some | "get_Message", Some e -> Helper.GlobalCall("str", t, [ thisArg.Value ], ?loc = r) |> Some | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some - | "get_InnerException", Some e -> getFieldWith r t e "inner_exception" |> Some + | "get_InnerException", Some e -> + // Caught exceptions are typed as Python's built-in Exception, which lacks this attribute. + Helper.GlobalCall("getattr", t, [ e; makeStrConst "inner_exception" ], ?loc = r) + |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = @@ -2952,11 +3038,17 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( | "ToBase64String" | "FromBase64String" -> if not (List.isSingle args) then - $"Convert.%s{Naming.upperFirst i.CompiledName} only accepts one single argument" - |> addWarning com ctx.InlinePath r + WarningCodes.base64ArgumentsIgnored |> addWarningWithCode com ctx.InlinePath r Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "ToHexString" + | "ToHexStringLower" -> + Helper.LibCall(com, "Convert", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "FromHexString" when List.isSingle args -> + Helper.LibCall(com, "Convert", "fromHexString", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> None let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -2964,7 +3056,7 @@ let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "get_Out" -> Helper.ImportedValue(com, "sys", "stdout", t) |> Some | "get_Error" -> Helper.ImportedValue(com, "sys", "stderr", t) |> Some | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "ReadLine" -> Helper.GlobalCall("input", t, args, ?loc = r) |> Some | "WriteLine" -> log com r t i thisArg args |> Some @@ -2997,7 +3089,7 @@ let stopwatch (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | "Break" -> makeDebugger r |> Some @@ -3016,24 +3108,35 @@ let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio IfThenElse(arg, makeDebugger r, unit, r) |> Some | _ -> None -let private ignoreFormatProvider com (ctx: Context) r (moduleName: string) meth args = - match meth, args with - // Ignore IFormatProvider - | "Parse", arg :: _culture :: _styles :: _ -> - addWarning com ctx.InlinePath r $"%s{moduleName}.Parse will ignore culture and styles" +/// Drops the `IFormatProvider` (and `DateTimeStyles`, where present) a date/time `Parse` was +/// given, warning once per discarded argument. Two codes rather than one bundled message so a +/// user who accepts invariant parsing can still be told about a discarded style, and vice versa. +let private ignoreFormatProvider com (ctx: Context) r meth args = + // Passing InvariantCulture asks for exactly what Fable does, so there is nothing to report. + let warnProvider culture = + match culture with + | InvariantCulture -> () + | _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + + let warnStyles styles = + match styles with + | NumberConst(NumberValue.Int32 0, _) -> () // DateTimeStyles.None: no special handling + | _ -> WarningCodes.dateTimeStylesIgnored |> addWarningWithCode com ctx.InlinePath r + match meth, args with + | "Parse", arg :: culture :: styles :: _ -> + warnProvider culture + warnStyles styles [ arg ] - | "Parse", arg :: _culture :: _ -> - addWarning com ctx.InlinePath r $"%s{moduleName}.Parse will ignore culture" - + | "Parse", arg :: culture :: _ -> + warnProvider culture [ arg ] - | "TryParse", input :: _culture :: _styles :: defVal :: _ -> - addWarning com ctx.InlinePath r $"%s{moduleName}.TryParse will ignore culture and styles" - + | "TryParse", input :: culture :: styles :: defVal :: _ -> + warnProvider culture + warnStyles styles [ input; defVal ] - | "TryParse", input :: _culture :: defVal :: _ -> - addWarning com ctx.InlinePath r $"%s{moduleName}.TryParse will ignore culture" - + | "TryParse", input :: culture :: defVal :: _ -> + warnProvider culture [ input; defVal ] | _ -> args @@ -3084,7 +3187,7 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op Helper.LibCall(com, "DateOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | meth -> - let args = ignoreFormatProvider com ctx r i.DeclaringEntityFullName meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, "DateOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3141,7 +3244,7 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op |> Some | _ -> None | meth -> - let args = ignoreFormatProvider com ctx r i.DeclaringEntityFullName meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst Helper.LibCall(com, "TimeOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3172,6 +3275,9 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio "DateOffset" match i.CompiledName with + | "op_Implicit" -> + Helper.LibCall(com, "DateOffset", "fromDate", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | ".ctor" -> match args with | [] -> Helper.LibCall(com, moduleName, "minValue", t, [], [], ?loc = r) |> Some @@ -3283,8 +3389,7 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio Helper.LibCall(com, "DateOffset", "compareTo", t, [ thisArg; args.Head ], ?loc = r) ) | "TryParse" -> - let args = - ignoreFormatProvider com ctx r i.DeclaringEntityFullName i.CompiledName args + let args = ignoreFormatProvider com ctx r i.CompiledName args Helper.LibCall(com, moduleName, "tryParse", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -3311,7 +3416,7 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio |> Some | _ -> None | meth -> - let args = ignoreFormatProvider com ctx r i.DeclaringEntityFullName meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3392,17 +3497,18 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti | ".ctor", _ -> Helper.LibCall(com, "util", "create_random", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "get_Shared", None -> Helper.LibCall(com, "util", "random_shared", t, [], ?loc = r) |> Some | "Next", Some thisArg -> - let min, max = + let moduleName, meth, args = match args with - | [] -> makeIntConst 0, makeIntConst System.Int32.MaxValue - | [ max ] -> makeIntConst 0, max - | [ min; max ] -> min, max + | [] -> "util", "random_int", [ thisArg; makeIntConst 0; makeIntConst System.Int32.MaxValue ] + // These check the arguments like .NET + | [ _ ] -> "random_", "next_max", thisArg :: args + | [ _; _ ] -> "random_", "next_range", thisArg :: args | _ -> FableError "Unexpected arg count for Random.Next" |> raise - let args = [ thisArg; min; max ] - let argTypes = [ thisArg.Type; min.Type; max.Type ] - Helper.LibCall(com, "util", "random_int", t, args, argTypes, ?loc = r) |> Some + let argTypes = args |> List.map (fun a -> a.Type) + Helper.LibCall(com, moduleName, meth, t, args, argTypes, ?loc = r) |> Some | "NextDouble", Some thisArg -> let args = [ thisArg ] let argTypes = [ thisArg.Type ] @@ -3471,6 +3577,18 @@ let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Exp let propInt p callee = getExpr r t callee (makeIntConst p) let propStr p callee = getExpr r t callee (makeStrConst p) + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + let isGroup = match thisArg with | Some(ExprType(DeclaredTypeFullName Types.regexGroup)) -> true @@ -3673,6 +3791,16 @@ let asyncBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with + | "AwaitTask" -> + Helper.LibCall(com, "async_", "await_task_with_cancellation", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "FromContinuations" when + (match t with + | Fable.DeclaredType(_, [ Fable.Unit ]) -> true + | _ -> false) + -> + Helper.LibCall(com, "async_", "from_continuations_unit", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | "Start" -> Helper.LibCall(com, "async_", "start", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -4129,6 +4257,7 @@ let private replacedModules = "System.Math", operators "System.MathF", operators "Microsoft.FSharp.Core.Operators", operators + "Microsoft.FSharp.Linq.NullableModule", nullableModule "Microsoft.FSharp.Core.Operators.Checked", operators "Microsoft.FSharp.Core.Operators.Unchecked", unchecked "Microsoft.FSharp.Core.Operators.OperatorIntrinsics", intrinsicFunctions @@ -4219,14 +4348,14 @@ let private replacedModules = Types.dateOnly, dateOnly Types.timeOnly, timeOnly Types.timespan, timeSpans - "System.Timers.Timer", timers + Types.timer, timers "System.Environment", systemEnv "System.Globalization.CultureInfo", globalization "System.IO.File", files "System.IO.Directory", directories "System.IO.Path", paths "System.IO.TextWriter", textWriter - "System.Random", random + Types.random, random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels "System.Threading.Monitor", monitor @@ -4330,7 +4459,7 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genArgs args = match ent.FullName with - | Types.exception_ -> Some(makeImportLib com Any "ExceptionBase" "Types", args) + | Types.exception_ -> Some(makeImportLib com Any "ExceptionBase" "exception_bases", args) | Types.attribute -> Some(makeImportLib com Any "Attribute" "Types", args) | Types.dictionary -> let args = @@ -4382,11 +4511,12 @@ let tryType typ = | List genArg -> Some(Types.list, lists, [ genArg ]) | Builtin kind -> match kind with + | BclRune -> None | BclGuid -> Some(Types.guid, guids, []) | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclDateTime -> Some(Types.datetime, dates, []) | BclDateTimeOffset -> Some(Types.datetimeOffset, dates, []) - | BclTimer -> Some("System.Timers.Timer", timers, []) + | BclTimer -> Some(Types.timer, timers, []) | BclHashSet genArg -> Some(Types.hashset, hashSets, [ genArg ]) | BclDictionary(key, value) -> Some(Types.dictionary, dictionaries, [ key; value ]) | BclKeyValuePair(key, value) -> Some(Types.keyValuePair, keyValuePairs, [ key; value ]) diff --git a/src/Fable.Transforms/QuotationEmitter.fs b/src/Fable.Transforms/QuotationEmitter.fs index 48ea46f98f..dcc3d22d4a 100644 --- a/src/Fable.Transforms/QuotationEmitter.fs +++ b/src/Fable.Transforms/QuotationEmitter.fs @@ -10,14 +10,20 @@ open Replacements.Util /// to construct a quotation AST. The input is the Fable.Expr captured /// inside a Quote node; the output is a Fable.Expr that calls the /// quotation runtime library to build the AST at runtime. -let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = +let rec private emitQuotedExprIn (com: Compiler) (bound: Set) (expr: Expr) : Expr = match expr with - | Value(kind, r) -> emitQuotedValue com kind r + | Value(kind, r) -> emitQuotedValue com bound kind r + + | IdentExpr ident when not (Set.contains ident.Name bound) -> + // Free in the quotation, so it is a local captured from the enclosing + // scope rather than a quotation variable. .NET splices the captured + // *value* in as a Value node; emitting a Var here instead left consumers + // with a name and no value to bind -- which is precisely what a query + // translator needs in order to turn a captured local into a parameter. + Helper.LibCall(com, "quotation", "mkValue", Any, [ IdentExpr ident; makeStrConst (typeToString ident.Type) ]) | IdentExpr ident -> - // Reference to a variable already introduced by a lambda/let in the quotation. - // Emit: quotation.mkVar(Var) - // We need a var reference. Create a var and then wrap it. + // Bound by a lambda or let inside the quotation: a genuine Var. let varExpr = Helper.LibCall( com, @@ -47,14 +53,14 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ] ) - let bodyExpr = emitQuotedExpr com body + let bodyExpr = emitQuotedExprIn com (Set.add arg.Name bound) body Helper.LibCall(com, "quotation", "mkLambda", Any, [ varExpr; bodyExpr ]) | Delegate(args, body, _name, _tags) -> // Multi-arg delegate: nest as curried lambdas - let rec nestLambdas args body = + let rec nestLambdas bound args body = match args with - | [] -> emitQuotedExpr com body + | [] -> emitQuotedExprIn com bound body | (arg: Ident) :: rest -> let varExpr = Helper.LibCall( @@ -69,10 +75,10 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ] ) - let innerBody = nestLambdas rest body + let innerBody = nestLambdas (Set.add arg.Name bound) rest body Helper.LibCall(com, "quotation", "mkLambda", Any, [ varExpr; innerBody ]) - nestLambdas args body + nestLambdas bound args body | Let(ident, value, body) -> let varExpr = @@ -88,25 +94,25 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ] ) - let valueExpr = emitQuotedExpr com value - let bodyExpr = emitQuotedExpr com body + let valueExpr = emitQuotedExprIn com bound value + let bodyExpr = emitQuotedExprIn com (Set.add ident.Name bound) body Helper.LibCall(com, "quotation", "mkLet", Any, [ varExpr; valueExpr; bodyExpr ]) | IfThenElse(guardExpr, thenExpr, elseExpr, _r) -> - let guard = emitQuotedExpr com guardExpr - let thenE = emitQuotedExpr com thenExpr - let elseE = emitQuotedExpr com elseExpr + let guard = emitQuotedExprIn com bound guardExpr + let thenE = emitQuotedExprIn com bound thenExpr + let elseE = emitQuotedExprIn com bound elseExpr Helper.LibCall(com, "quotation", "mkIfThenElse", Any, [ guard; thenE; elseE ]) | CurriedApply(applied, args, _typ, _r) -> // Emit nested applications: Application(Application(f, a1), a2) - let appliedExpr = emitQuotedExpr com applied + let appliedExpr = emitQuotedExprIn com bound applied args |> List.fold (fun acc arg -> - let argExpr = emitQuotedExpr com arg + let argExpr = emitQuotedExprIn com bound arg Helper.LibCall(com, "quotation", "mkApplication", Any, [ acc; argExpr ]) ) appliedExpr @@ -114,7 +120,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | Call(callee, info, _typ, _r) -> let instanceExpr = match info.ThisArg with - | Some thisArg -> emitQuotedExpr com thisArg + | Some thisArg -> emitQuotedExprIn com bound thisArg // Static/operator call: no instance. | None -> mkNoInstanceExpr com @@ -135,16 +141,16 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = else let methodExpr = makeStrConst methodName let declTypeExpr = makeStrConst declaringType - let argExprs = mkExprArray com (info.Args |> List.map (emitQuotedExpr com)) + let argExprs = mkExprArray com (info.Args |> List.map (emitQuotedExprIn com bound)) Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs; declTypeExpr ]) | Sequential exprs -> match exprs with - | [] -> emitQuotedExpr com (Value(UnitConstant, None)) - | [ single ] -> emitQuotedExpr com single + | [] -> emitQuotedExprIn com bound (Value(UnitConstant, None)) + | [ single ] -> emitQuotedExprIn com bound single | first :: rest -> - let restExpr = emitQuotedExpr com (Sequential rest) - let firstExpr = emitQuotedExpr com first + let restExpr = emitQuotedExprIn com bound (Sequential rest) + let firstExpr = emitQuotedExprIn com bound first Helper.LibCall(com, "quotation", "mkSequential", Any, [ firstExpr; restExpr ]) | Operation(kind, _tags, _typ, _r) -> @@ -195,13 +201,13 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let methodExpr = makeStrConst opName let instanceExpr = mkNoInstanceExpr com - let argExprs = mkExprArray com (args |> List.map (emitQuotedExpr com)) + let argExprs = mkExprArray com (args |> List.map (emitQuotedExprIn com bound)) // Operators have no declaring type; pass an empty string. Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs; makeStrConst "" ]) | Get(expr, kind, _typ, _r) -> - let target = emitQuotedExpr com expr + let target = emitQuotedExprIn com bound expr match kind with | TupleIndex index -> Helper.LibCall(com, "quotation", "mkTupleGet", Any, [ target; makeIntConst index ]) @@ -227,8 +233,8 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | Set(expr, kind, _typ, value, _r) -> - let target = emitQuotedExpr com expr - let valueExpr = emitQuotedExpr com value + let target = emitQuotedExprIn com bound expr + let valueExpr = emitQuotedExprIn com bound value match kind with | ValueSet -> @@ -242,7 +248,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | TypeCast(innerExpr, _typ) -> // Coerce/cast: just emit the inner expression for now - emitQuotedExpr com innerExpr + emitQuotedExprIn com bound innerExpr | DecisionTree(decisionExpr, targets) -> // Inline each DecisionTreeSuccess leaf into its target body via nested Lets, turning @@ -261,18 +267,18 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | e -> e ) - emitQuotedExpr com inlined + emitQuotedExprIn com bound inlined | DecisionTreeSuccess(idx, boundValues, _typ) -> match boundValues with - | [] -> emitQuotedExpr com (Value(UnitConstant, None)) - | [ single ] -> emitQuotedExpr com single + | [] -> emitQuotedExprIn com bound (Value(UnitConstant, None)) + | [ single ] -> emitQuotedExprIn com bound single | _ -> let msg = "Unsupported quotation node: DecisionTreeSuccess" Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | Test(testExpr, kind, _r) -> - let target = emitQuotedExpr com testExpr + let target = emitQuotedExprIn com bound testExpr // Note: real .NET quotations use dedicated UnionCaseTest/TypeTest node kinds here, not // Call — this emitter renders all of these as synthetic Calls instead (e.g. "get_IsCons", @@ -282,7 +288,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | UnionCaseTest tag -> // Represent as: (unionTag target) = tag let tagExpr = Helper.LibCall(com, "quotation", "mkUnionTag", Any, [ target ]) - let tagConst = emitQuotedExpr com (makeIntConst tag) + let tagConst = emitQuotedExprIn com bound (makeIntConst tag) Helper.LibCall( com, @@ -364,7 +370,7 @@ and private mkNoInstanceExpr (com: Compiler) : Expr = // so isCall/isFieldGet don't conflate the two. mkNullExpr com "novalue" -and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocation option) : Expr = +and private emitQuotedValue (com: Compiler) (bound: Set) (kind: ValueKind) (_r: SourceLocation option) : Expr = match kind with | BoolConstant b -> Helper.LibCall(com, "quotation", "mkValue", Any, [ makeBoolConst b; makeStrConst "bool" ]) @@ -388,7 +394,8 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(CharConstant c, None); makeStrConst "char" ]) | NewTuple(values, _isStruct) -> - let emittedValues = mkExprArray com (values |> List.map (emitQuotedExpr com)) + let emittedValues = + mkExprArray com (values |> List.map (emitQuotedExprIn com bound)) Helper.LibCall(com, "quotation", "mkNewTuple", Any, [ emittedValues ]) @@ -398,7 +405,8 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio | Some ent -> ent.FullName | None -> entRef.FullName - let emittedValues = mkExprArray com (values |> List.map (emitQuotedExpr com)) + let emittedValues = + mkExprArray com (values |> List.map (emitQuotedExprIn com bound)) if com.Options.Language = Rust then // Rust needs the case name to build a real UnionCaseInfo; other targets keep (name, tag, fields). @@ -426,10 +434,15 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio | NewRecord(values, entRef, _genArgs) -> let fieldNames = match com.TryGetEntity(entRef) with - | Some ent -> ent.FSharpFields |> List.map (fun f -> makeStrConst f.Name) |> makeArray Any + | Some ent -> + ent.FSharpFields + |> List.filter (fun f -> not f.IsStatic && not f.HasDefaultValueAttribute) + |> List.map (fun f -> makeStrConst f.Name) + |> makeArray Any | None -> makeArray Any [] - let emittedValues = mkExprArray com (values |> List.map (emitQuotedExpr com)) + let emittedValues = + mkExprArray com (values |> List.map (emitQuotedExprIn com bound)) if com.Options.Language = Rust then // Rust needs the record type name; other targets keep (fieldNames, values). @@ -449,7 +462,7 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio match value with | Some v -> - let emitted = mkExprArray com [ emitQuotedExpr com v ] + let emitted = mkExprArray com [ emitQuotedExprIn com bound v ] Helper.LibCall( com, @@ -472,15 +485,15 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio | NewOption(value, _typ, _isStruct) -> match value with | Some v -> - let emitted = emitQuotedExpr com v + let emitted = emitQuotedExprIn com bound v Helper.LibCall(com, "quotation", "mkValue", Any, [ emitted; makeStrConst "option" ]) | None -> mkNullExpr com "option" | NewList(headAndTail, _typ) -> match headAndTail with | Some(head, tail) -> - let headExpr = emitQuotedExpr com head - let tailExpr = emitQuotedExpr com tail + let headExpr = emitQuotedExprIn com bound head + let tailExpr = emitQuotedExprIn com bound tail Helper.LibCall(com, "quotation", "mkNewList", Any, [ headExpr; tailExpr ]) | None -> mkNullExpr com "list" @@ -549,3 +562,7 @@ and private mkExprArray (com: Compiler) (elements: Expr list) : Expr = match elements with | [] when com.Options.Language = Rust -> Helper.LibCall(com, "quotation", "emptyExprArray", Any, []) | _ -> makeArray Any elements + +/// Entry point. Nothing is bound at the top of a quotation, so any identifier +/// still free by the time it is reached is a captured local. +let emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = emitQuotedExprIn com Set.empty expr diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index 04989077c5..2c30d27c82 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -294,6 +294,7 @@ type BuiltinType = | BclDateTimeOffset | BclDateOnly | BclTimeOnly + | BclRune | BclTimer | BclHashSet of Type | BclDictionary of key: Type * value: Type @@ -313,7 +314,8 @@ let (|BuiltinDefinition|_|) = | Types.datetimeOffset -> ValueSome BclDateTimeOffset | Types.dateOnly -> ValueSome BclDateOnly | Types.timeOnly -> ValueSome BclTimeOnly - | "System.Timers.Timer" -> ValueSome BclTimer + | Types.rune -> ValueSome BclRune + | Types.timer -> ValueSome BclTimer | Types.fsharpSet -> ValueSome(FSharpSet(Any)) | Types.fsharpMap -> ValueSome(FSharpMap(Any, Any)) | Types.hashset -> ValueSome(BclHashSet(Any)) @@ -981,6 +983,16 @@ let (|IsByRefType|_|) (com: Compiler) = | _ -> ValueNone | _ -> ValueNone +/// F# types the locals it generates for copy-and-update expressions (`inputRecord`, `copyOfStruct`) +/// as byrefs, but Fable binds the struct value itself to them. +let getIdentValueType (com: Compiler) (ident: Ident) (typ: Type) = + if ident.IsCompilerGenerated then + match typ with + | IsByRefType com innerType -> innerType + | typ -> typ + else + typ + [] let (|IsInRefType|_|) (com: Compiler) = function @@ -1105,6 +1117,15 @@ let (|ArrayOrListLiteral|_|) = | MaybeCasted(Value((NewArray(ArrayValues vals, t, _) | ListLiteral(vals, t)), _)) -> ValueSome(vals, t) | _ -> ValueNone +/// `CultureInfo.InvariantCulture`, which every target replaces with an empty object literal (see +/// `globalization`). Matching it keeps "culture is ignored" warnings quiet when the culture asked +/// for is the one Fable uses anyway. +[] +let (|InvariantCulture|_|) = + function + | MaybeCasted(ObjectExpr([], DeclaredType(entRef, _), None)) when entRef.FullName = Types.cultureInfo -> ValueSome() + | _ -> ValueNone + [] let (|IsEntity|_|) fullName = function @@ -1261,17 +1282,63 @@ let (|CustomOp|_|) (com: ICompiler) (ctx: Context) r t opName (argExprs: Expr li | Some x -> ValueSome x | None -> ValueNone +module RegexOption = + [] + let IgnoreCase = 1 + + [] + let Multiline = 2 + + [] + let ExplicitCapture = 4 + + [] + let Compiled = 8 + + [] + let Singleline = 16 + + [] + let IgnorePatternWhitespace = 32 + + [] + let RightToLeft = 64 + + [] + let ECMAScript = 256 + + [] + let CultureInvariant = 512 + + [] + let NonBacktracking = 1024 + + let names = + [ + IgnoreCase, "IgnoreCase" + Multiline, "Multiline" + ExplicitCapture, "ExplicitCapture" + Compiled, "Compiled" + Singleline, "Singleline" + IgnorePatternWhitespace, "IgnorePatternWhitespace" + RightToLeft, "RightToLeft" + ECMAScript, "ECMAScript" + CultureInvariant, "CultureInvariant" + NonBacktracking, "NonBacktracking" + ] + [] let (|RegexFlags|_|) e = let rec getFlags = function | NumberConst(NumberValue.Int32 value, _) -> match value with - | 1 -> Some [ RegexIgnoreCase ] - | 2 -> Some [ RegexMultiline ] - | 8 -> Some [] // Compiled flag (ignored) - | 16 -> Some [ RegexSingleline ] - | 256 -> Some [] // ECMAScript flag (ignored) + | RegexOption.IgnoreCase -> Some [ RegexIgnoreCase ] + | RegexOption.Multiline -> Some [ RegexMultiline ] + | RegexOption.Compiled -> Some [] + | RegexOption.Singleline -> Some [ RegexSingleline ] + | RegexOption.ECMAScript -> Some [] + | RegexOption.NonBacktracking -> Some [] | _ -> None | Operation(Binary(BinaryOrBitwise, flags1, flags2), _, _, _) -> match getFlags flags1, getFlags flags2 with @@ -1283,10 +1350,53 @@ let (|RegexFlags|_|) e = | Some x -> ValueSome x | None -> ValueNone +[] +let (|RegexOptionsConst|_|) (e: Expr) = + let rec fold = + function + | NumberConst(NumberValue.Int32 value, _) -> Some value + | Operation(Binary(BinaryOrBitwise, left, right), _, _, _) -> + match fold left, fold right with + | Some left, Some right -> Some(left ||| right) + | _ -> None + | _ -> None + + match e.Type, fold e with + | Number(Int32, NumberInfo.IsEnum ent), Some value when ent.FullName = Types.regexOptions -> ValueSome value + | _ -> ValueNone + +let checkRegexOptions (com: ICompiler) (ctx: Context) r (supported: int) (args: Expr list) = + for arg in args do + match arg with + | RegexOptionsConst options when options &&& ~~~supported <> 0 -> + let unsupported = options &&& ~~~supported + + let names = + RegexOption.names + |> List.choose (fun (bit, name) -> + if unsupported &&& bit <> 0 then + Some $"RegexOptions.%s{name}" + else + None + ) + + let names = + if List.isEmpty names then + [ $"RegexOptions value 0x%X{unsupported}" ] + else + names + + let names = String.concat ", " names + + $"%s{names} is not supported by Fable on this target" + |> addError com ctx.InlinePath r + | _ -> () + [] let (|UniversalFableCoreHelpers|_|) (com: ICompiler) (ctx: Context) r t (i: CallInfo) args error = function - | "op_ErasedCast" -> + | "op_ErasedCast" + | "op_Implicit" -> match List.tryHead args with | Some x -> ValueSome x | None -> ValueNone @@ -1837,6 +1947,51 @@ module Quotations = Helper.LibCall(com, libModule, "isFieldGet", t, [ expr ], ?loc = r) |> Some | _ -> None + /// DerivedPatterns are the ones F# defines on top of Patterns. AndAlso and + /// OrElse matter because `&&` and `||` desugar to IfThenElse, so without them + /// a consumer has to re-derive that shape by hand; SpecificCall is how + /// operators are matched by identity rather than by compiled name. + let quotationDerivedPatterns + (libModule: string) + (com: ICompiler) + (ctx: Context) + r + (t: Type) + (i: CallInfo) + (_thisArg: Expr option) + (args: Expr list) + = + match i.CompiledName, args with + | ("AndAlsoPattern" | "|AndAlso|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isAndAlso", t, [ expr ], ?loc = r) |> Some + | ("OrElsePattern" | "|OrElse|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isOrElse", t, [ expr ], ?loc = r) |> Some + // SpecificCall is curried in FSharp.Core: applying it to the template + // returns the function that then tests the expression. Rather than have + // each runtime return a closure -- which the Rust backend uncurries back + // into a two-argument function -- the runtimes take both arguments and + // the partial application is built here. + | ("SpecificCallPattern" | "|SpecificCall|_|"), [ template ] -> + let exprType, resultType = + match t with + | LambdaType(argType, returnType) -> argType, returnType + | _ -> Any, Any + + let exprIdent = makeUniqueIdent com ctx exprType "expr" + + let body = + Helper.LibCall( + com, + libModule, + "isSpecificCall", + resultType, + [ template; IdentExpr exprIdent ], + ?loc = r + ) + + Lambda(exprIdent, body, None) |> Some + | _ -> None + let tryQuotationCall (libModule: string) (com: ICompiler) @@ -1853,6 +2008,7 @@ module Quotations = | Types.fsharpExprGeneric -> quotationExprs libModule com ctx r t info thisArg args | Types.fsharpVar -> quotationVars libModule com ctx r t info thisArg args | Types.patternsModule -> quotationPatterns libModule com ctx r t info thisArg args + | Types.derivedPatternsModule -> quotationDerivedPatterns libModule com ctx r t info thisArg args | "Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter" -> match info.CompiledName, args with | "EvaluateQuotation", [ expr ] -> Helper.LibCall(com, libModule, "evaluate", t, [ expr ], ?loc = r) |> Some diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 769a1dba58..074a619f71 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -94,8 +94,57 @@ let coreModFor = | FSharpReference _ -> "Types" | BclHashSet _ -> "MutableSet" | BclDictionary _ -> "MutableMap" + | BclRune | BclKeyValuePair _ -> FableError "Cannot decide core module" |> raise +let dateOnlyModule (com: Compiler) = + if com.Options.JsTemporal then + "DateOnlyTemporal" + else + "DateOnly" + +let timeOnlyModule (com: Compiler) = + if com.Options.JsTemporal then + "TimeOnlyTemporal" + else + "TimeOnly" + +let timeSpanModule (com: Compiler) = + if com.Options.JsTemporal then + "TimeSpanTemporal" + else + "TimeSpan" + +let dateTimeModule (com: Compiler) = + if com.Options.JsTemporal then + "DateTimeTemporal" + else + "Date" + +let dateTimeOffsetModule (com: Compiler) = + if com.Options.JsTemporal then + "DateTimeOffsetTemporal" + else + "DateOffset" + +let private temporalSubtractionName opName (argTypes: Type list) = + if opName <> Operators.subtraction then + opName + else + match List.tryLast argTypes with + // DateTime - DateTime and DateTimeOffset - DateTimeOffset yield a TimeSpan + | Some(Builtin(BclDateTime | BclDateTimeOffset)) -> "subtractDate" + | _ -> "subtractTimeSpan" + +let private temporalDivisionName opName (argTypes: Type list) = + if opName <> Operators.division then + opName + else + match List.tryLast argTypes with + // TimeSpan / TimeSpan yields a ratio, TimeSpan / float another TimeSpan + | Some(Builtin BclTimeSpan) -> "divideByTimeSpan" + | _ -> "divide" + let makeDecimal com r t (x: decimal) = let str = x.ToString(System.Globalization.CultureInfo.InvariantCulture) Helper.LibCall(com, "Decimal", "default", t, [ makeStrConst str ], isConstructor = true, ?loc = r) @@ -215,31 +264,54 @@ let needToCast fromKind toKind = let h = kindIndex toKind // return type (horizontal) ((v > h) || (v < 4 && h > 3)) && (h < 8) || (h <> v && (h = 11 || v = 11)) +let roundToFloat32 r t (expr: Expr) = + match t with + | Number(Float32, _) -> emitExpr r t [ expr ] "Math.fround($0)" + | _ -> expr + /// Conversions to floating point let toFloat com (ctx: Context) r targetType (args: Expr list) : Expr = - match args.Head.Type with - | Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ]) - | String -> Helper.LibCall(com, "Double", "parse", targetType, args) - | Number(kind, _) -> - match kind with - | Decimal -> Helper.LibCall(com, "Decimal", "toFloat64", targetType, args) - | BigIntegers _ -> Helper.LibCall(com, "BigInt", "toFloat64", targetType, args) - | _ -> TypeCast(args.Head, targetType) - | _ -> - addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown" + let value = + match args.Head.Type with + | Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ]) + | String -> + let style = + int ( + System.Globalization.NumberStyles.Float + ||| System.Globalization.NumberStyles.AllowThousands + ) - TypeCast(args.Head, targetType) + Helper.LibCall(com, "Double", "parse", targetType, [ args.Head; makeIntConst style ]) + | Number(kind, _) -> + match kind with + | Decimal -> Helper.LibCall(com, "Decimal", "toFloat64", targetType, args) + | BigIntegers _ -> Helper.LibCall(com, "BigInt", "toFloat64", targetType, args) + | _ -> TypeCast(args.Head, targetType) + | _ -> + addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown" + + TypeCast(args.Head, targetType) + + match args.Head.Type with + | Char + | Number((Float32 | Int8 | UInt8 | Int16 | UInt16), _) -> value + | _ -> roundToFloat32 r targetType value let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr = match args.Head.Type with | Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ]) |> makeDecimalFromExpr com r targetType - | String -> makeDecimalFromExpr com r targetType args.Head + // FSharp.Core's decimal parses with NumberStyles.Float + | String -> + let style = int System.Globalization.NumberStyles.Float + Helper.LibCall(com, "Decimal", "parse", targetType, [ args.Head; makeIntConst style ], ?loc = r) | Number(kind, _) -> match kind with | Decimal -> args.Head | BigIntegers _ -> Helper.LibCall(com, "BigInt", "toDecimal", targetType, args) + | Float32 -> Helper.LibCall(com, "Decimal", "fromFloat32", targetType, args, ?loc = r) + | Float64 -> Helper.LibCall(com, "Decimal", "fromFloat64", targetType, args, ?loc = r) | _ -> makeDecimalFromExpr com r targetType args.Head | _ -> addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown" @@ -301,11 +373,12 @@ let toLong com (ctx: Context) r targetType (args: Expr list) : Expr = let emitIntCast toKind arg = match toKind with - | Int8 -> emitExpr None Int8.Number [ arg ] "($0 + 0x80 & 0xFF) - 0x80" - | Int16 -> emitExpr None Int16.Number [ arg ] "($0 + 0x8000 & 0xFFFF) - 0x8000" + // `~~` truncates toward zero before the wrap, so negative fractions round like .NET + | Int8 -> emitExpr None Int8.Number [ arg ] "(~~$0 + 0x80 & 0xFF) - 0x80" + | Int16 -> emitExpr None Int16.Number [ arg ] "(~~$0 + 0x8000 & 0xFFFF) - 0x8000" | Int32 -> fastIntFloor arg - | UInt8 -> emitExpr None UInt8.Number [ arg ] "$0 & 0xFF" - | UInt16 -> emitExpr None UInt16.Number [ arg ] "$0 & 0xFFFF" + | UInt8 -> emitExpr None UInt8.Number [ arg ] "~~$0 & 0xFF" + | UInt16 -> emitExpr None UInt16.Number [ arg ] "~~$0 & 0xFFFF" | UInt32 -> emitExpr None UInt32.Number [ arg ] "$0 >>> 0" | _ -> FableError $"Unexpected non-integer type %A{toKind}" |> raise @@ -348,17 +421,29 @@ let toChar com (ctx: Context) r (arg: Expr) = let code = toInt com ctx r UInt16.Number [ arg ] Helper.GlobalCall("String", Char, [ code ], memb = "fromCharCode") -let toString com (ctx: Context) r (args: Expr list) = +let private toStringWith nullAsEmpty com (ctx: Context) r (args: Expr list) = match args with | [] -> "toString is called with empty args" |> addErrorAndReturnNull com ctx.InlinePath r | head :: tail -> match head.Type with - | String -> head + | String -> + match head with + | Value(StringConstant _, _) -> head + | _ when nullAsEmpty -> Operation(Logical(LogicalOr, head, makeStrConst ""), Tags.empty, String, r) + | _ -> head | Char -> TypeCast(head, String) | Builtin BclGuid when tail.IsEmpty -> head - | Builtin(BclGuid | BclTimeSpan | BclTimeOnly | BclDateOnly | BclDateTime | BclDateTimeOffset as bt) -> + | Builtin BclDateOnly -> Helper.LibCall(com, dateOnlyModule com, "toString", String, args) + | Builtin BclTimeOnly -> Helper.LibCall(com, timeOnlyModule com, "toString", String, args) + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, timeSpanModule com, "toString", String, args) + | Builtin BclDateTime when com.Options.JsTemporal -> + Helper.LibCall(com, dateTimeModule com, "toString", String, args) + | Builtin BclDateTimeOffset when com.Options.JsTemporal -> + Helper.LibCall(com, dateTimeOffsetModule com, "toString", String, args) + | Builtin(BclGuid | BclTimeSpan | BclDateTime | BclDateTimeOffset as bt) -> Helper.LibCall(com, coreModFor bt, "toString", String, args) | Number(Int16, _) -> Helper.LibCall(com, "Util", "int16ToString", String, args) | Number(Int32, _) -> Helper.LibCall(com, "Util", "int32ToString", String, args) @@ -366,13 +451,27 @@ let toString com (ctx: Context) r (args: Expr list) = | Number(NativeInt, _) -> Helper.LibCall(com, "Util", "int64ToString", String, args) | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toString", String, args) | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toString", String, args) + | Number(Float64, _) -> Helper.LibCall(com, "String", "float64ToString", String, [ head ]) + | Number(Float32, _) -> Helper.LibCall(com, "String", "float32ToString", String, [ head ]) | Number _ -> Helper.InstanceCall(head, "toString", String, tail) | Array _ | List _ -> Helper.LibCall(com, "Types", "seqToString", String, [ head ], ?loc = r) // | DeclaredType(ent, _) when ent.IsFSharpUnion || ent.IsFSharpRecord || ent.IsValueType -> // Helper.InstanceCall(head, "toString", String, [], ?loc=r) // | DeclaredType(ent, _) -> - | _ -> Helper.LibCall(com, "Types", "toString", String, [ head ], ?loc = r) + | _ -> + let funcName = + if nullAsEmpty then + "toStringOrEmpty" + else + "toString" + + Helper.LibCall(com, "Types", funcName, String, [ head ], ?loc = r) + +let toString com ctx r args = toStringWith false com ctx r args + +/// F#'s `string` operator returns an empty string for null +let stringOperator com ctx r args = toStringWith true com ctx r args let round com (args: Expr list) = match args.Head.Type with @@ -384,6 +483,21 @@ let round com (args: Expr list) = rounded :: args.Tail | _ -> args +let roundCall (com: ICompiler) r t moduleName (i: CallInfo) (args: Expr list) = + let isMidpointRounding = + match List.tryLast i.SignatureArgTypes with + | Some(Number(_, NumberInfo.IsEnum ent)) -> ent.FullName = "System.MidpointRounding" + | _ -> false + + match args with + | [ _; _ ] when isMidpointRounding -> Helper.LibCall(com, moduleName, "roundMode", t, args, ?loc = r) + | [ ExprType(Number(Float32, _)); _; _ ] when isMidpointRounding -> + Helper.LibCall(com, moduleName, "roundToModeFloat32", t, args, ?loc = r) + | [ _; _; _ ] when isMidpointRounding -> Helper.LibCall(com, moduleName, "roundToMode", t, args, ?loc = r) + | [ ExprType(Number(Float32, _)); _ ] -> + Helper.LibCall(com, moduleName, "roundFloat32", t, args, i.SignatureArgTypes, ?loc = r) + | _ -> Helper.LibCall(com, moduleName, "round", t, args, i.SignatureArgTypes, ?loc = r) + let toList com returnType expr = Helper.LibCall(com, "List", "ofSeq", returnType, [ expr ]) @@ -468,6 +582,18 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = op else wrapLong com ctx r t op + | Builtin BclDateTime :: _ when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeTemporal", temporalSubtractionName opName argTypes, t, args, argTypes, ?loc = r) + | Builtin BclDateTimeOffset :: _ when com.Options.JsTemporal -> + Helper.LibCall( + com, + "DateTimeOffsetTemporal", + temporalSubtractionName opName argTypes, + t, + args, + argTypes, + ?loc = r + ) | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly as bt) :: _ -> Helper.LibCall(com, coreModFor bt, opName, t, args, argTypes, ?loc = r) | Builtin(FSharpSet _) :: _ -> @@ -477,13 +603,17 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = // | Builtin (FSharpMap _)::_ -> // let mangledName = Naming.buildNameWithoutSanitationFrom "FSharpMap" true opName overloadSuffix.Value // Helper.LibCall(com, "Map", mangledName, t, args, argTypes, ?loc=r) + | Builtin BclTimeSpan :: _ when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", temporalDivisionName opName argTypes, t, args, argTypes, ?loc = r) | Builtin BclTimeSpan :: _ -> nativeOp opName argTypes args | CustomOp com ctx r t opName args e -> e | _ -> nativeOp opName argTypes args -let isCompatibleWithNativeComparison = - function - | Builtin(BclGuid | BclTimeSpan | BclTimeOnly) +let isCompatibleWithNativeComparison (com: Compiler) typ = + match typ with + // Temporal.PlainTime and Temporal.Duration cannot be compared natively + | Builtin(BclTimeOnly | BclTimeSpan) -> not com.Options.JsTemporal + | Builtin BclGuid | Boolean | Char | String @@ -497,55 +627,74 @@ let isCompatibleWithNativeComparison = // * `.GetHashCode` called directly defaults to identity hash (for reference types except string) if not implemented. // * `LanguagePrimitive.PhysicalHash` creates an identity hash no matter whether GetHashCode is implemented or not. -let identityHash com r (arg: Expr) = - let methodName = - match arg.Type with - // These are the same for identity/structural hashing - | Char - | String - | Builtin BclGuid -> "stringHash" - | Number(Decimal, _) -> "safeHash" - | Number(BigIntegers _, _) -> "bigintHash" - | Number(Numbers _, _) -> "numberHash" - | Builtin BclTimeSpan - | Builtin BclTimeOnly -> "numberHash" - | List _ -> "safeHash" - | Tuple _ -> "arrayHash" // F# tuples must use structural hashing - // These are only used for structural hashing - // | Array _ -> "arrayHash" - // | Builtin (BclDateTime|BclDateTimeOffset) -> "dateHash" - | DeclaredType _ -> "safeHash" - | _ -> "identityHash" - - Helper.LibCall(com, "Util", methodName, Int32.Number, [ arg ], ?loc = r) +let identityHash (com: ICompiler) r (arg: Expr) = + match arg.Type with + | Builtin BclTimeOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeOnlyTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | _ -> + let methodName = + match arg.Type with + // These are the same for identity/structural hashing + | Char + | String + | Builtin BclGuid -> "stringHash" + | Number(Decimal, _) -> "safeHash" + | Number(BigIntegers _, _) -> "bigintHash" + | Number(Numbers _, _) -> "numberHash" + | Builtin BclTimeSpan + | Builtin BclTimeOnly -> "numberHash" + | List _ -> "safeHash" + | Tuple _ -> "arrayHash" // F# tuples must use structural hashing + // These are only used for structural hashing + // | Array _ -> "arrayHash" + // | Builtin (BclDateTime|BclDateTimeOffset) -> "dateHash" + | DeclaredType _ -> "safeHash" + | _ -> "identityHash" + + Helper.LibCall(com, "Util", methodName, Int32.Number, [ arg ], ?loc = r) let structuralHash (com: ICompiler) r (arg: Expr) = - let methodName = - match arg.Type with - | Char - | String - | Builtin BclGuid -> "stringHash" - | Number(Decimal, _) -> "fastStructuralHash" - | Number(BigIntegers _, _) -> "bigintHash" - | Number(Numbers _, _) -> "numberHash" - | Builtin BclTimeSpan - | Builtin BclTimeOnly -> "numberHash" - | List _ -> "safeHash" - // TODO: Get hash functions of the generic arguments - // for better performance when using tuples as map keys - | Tuple _ - | Array _ -> "arrayHash" - | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly) -> "dateHash" - | DeclaredType(ent, _) -> - let ent = com.GetEntity(ent) + match arg.Type with + | Builtin BclDateOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "DateOnlyTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclTimeOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeOnlyTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclDateTime when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclDateTimeOffset when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeOffsetTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | _ -> - if not ent.IsInterface then - "safeHash" - else - "structuralHash" - | _ -> "structuralHash" + let methodName = + match arg.Type with + | Char + | String + | Builtin BclGuid -> "stringHash" + | Number(Decimal, _) -> "safeHash" + | Number(BigIntegers _, _) -> "bigintHash" + | Number(Numbers _, _) -> "numberHash" + | Builtin BclTimeSpan + | Builtin BclTimeOnly -> "numberHash" + | List _ -> "safeHash" + // TODO: Get hash functions of the generic arguments + // for better performance when using tuples as map keys + | Tuple _ + | Array _ -> "arrayHash" + | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly) -> "dateHash" + | DeclaredType(ent, _) -> + let ent = com.GetEntity(ent) + + if not ent.IsInterface then + "safeHash" + else + "structuralHash" + | _ -> "structuralHash" - Helper.LibCall(com, "Util", methodName, Int32.Number, [ arg ], ?loc = r) + Helper.LibCall(com, "Util", methodName, Int32.Number, [ arg ], ?loc = r) let rec equals (com: ICompiler) ctx r equal (left: Expr) (right: Expr) = let is equal expr = @@ -561,6 +710,12 @@ let rec equals (com: ICompiler) ctx r equal (left: Expr) (right: Expr) = | Number(BigIntegers _, _) -> Helper.LibCall(com, "BigInt", "equals", Boolean, [ left; right ], ?loc = r) |> is equal + | Builtin BclTimeOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeOnlyTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal | Builtin(BclGuid | BclTimeSpan | BclTimeOnly) | Boolean | Char @@ -576,6 +731,15 @@ let rec equals (com: ICompiler) ctx r equal (left: Expr) (right: Expr) = // Use BinaryEquals for MetaType to have a change of optimization in FableTransforms.operationReduction // We will call Reflection.equals in the Fable2Babel step //| MetaType -> Helper.LibCall(com, "Reflection", "equals", Boolean, [left; right], ?loc=r) |> is equal + | Builtin BclDateOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "DateOnlyTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal + | Builtin BclDateTime when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal + | Builtin BclDateTimeOffset when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeOffsetTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly) -> Helper.LibCall(com, "Date", "equals", Boolean, [ left; right ], ?loc = r) |> is equal @@ -611,11 +775,21 @@ and compare (com: ICompiler) ctx r (left: Expr) (right: Expr) = match left.Type with | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "compare", t, [ left; right ], ?loc = r) | Number(BigIntegers _, _) -> Helper.LibCall(com, "BigInt", "compare", t, [ left; right ], ?loc = r) + | Builtin BclTimeOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeOnlyTemporal", "compare", t, [ left; right ], ?loc = r) + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "compare", t, [ left; right ], ?loc = r) | Builtin(BclGuid | BclTimeSpan | BclTimeOnly) | Boolean | Char | String | Number _ -> Helper.LibCall(com, "Util", "comparePrimitives", t, [ left; right ], ?loc = r) + | Builtin BclDateOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "DateOnlyTemporal", "compare", t, [ left; right ], ?loc = r) + | Builtin BclDateTime when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeTemporal", "compare", t, [ left; right ], ?loc = r) + | Builtin BclDateTimeOffset when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeOffsetTemporal", "compare", t, [ left; right ], ?loc = r) | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly) -> Helper.LibCall(com, "Date", "compare", t, [ left; right ], ?loc = r) | DeclaredType _ -> Helper.LibCall(com, "Util", "compare", t, [ left; right ], ?loc = r) @@ -629,7 +803,7 @@ and compare (com: ICompiler) ctx r (left: Expr) (right: Expr) = /// Boolean comparison operators like <, >, <=, >= and booleanCompare (com: ICompiler) ctx r (left: Expr) (right: Expr) op = - if isCompatibleWithNativeComparison left.Type then + if isCompatibleWithNativeComparison com left.Type then makeEqOp r left right op else let comparison = compare com ctx r left right @@ -662,6 +836,23 @@ let makeEqualityComparer (com: ICompiler) ctx typArg = "GetHashCode", Delegate([ x ], structuralHash com None (IdentExpr x), None, Tags.empty) ] +/// Matches EqualityComparer.Default, which compares arrays and List by reference in .NET +let makeDefaultEqualityComparer (com: ICompiler) ctx typArg = + match typArg with + | Array _ -> + let x = makeUniqueIdent com ctx typArg "x" + let y = makeUniqueIdent com ctx typArg "y" + + let hash = + Helper.LibCall(com, "Util", "physicalHash", Int32.Number, [ IdentExpr x ]) + + objExpr + [ + "Equals", Delegate([ x; y ], makeEqOp None (IdentExpr x) (IdentExpr y) BinaryEqual, None, Tags.empty) + "GetHashCode", Delegate([ x ], hash, None, Tags.empty) + ] + | _ -> makeEqualityComparer com ctx typArg + // TODO: Try to detect at compile-time if the object already implements `Compare`? let inline makeComparerFromEqualityComparer e = e // leave it as is, if implementation supports it // Helper.LibCall(com, "Util", "comparerFromEqualityComparer", Any, [e]) @@ -679,25 +870,42 @@ let makeMap (com: ICompiler) ctx r t methName args genArg = let makeDictionaryWithComparer com r t sourceSeq comparer = Helper.LibCall(com, "MutableMap", "Dictionary", t, [ sourceSeq; comparer ], isConstructor = true, ?loc = r) -let makeDictionary (com: ICompiler) ctx r t sourceSeq = +let private makeDictionaryWithKeyComparer makeKeyComparer (com: ICompiler) ctx r t sourceSeq = match t with - | DeclaredType(_, [ key; _ ]) when not (isCompatibleWithNativeComparison key) -> + | DeclaredType(_, [ key; _ ]) when not (isCompatibleWithNativeComparison com key) -> // makeComparer com ctx key - makeEqualityComparer com ctx key |> makeDictionaryWithComparer com r t sourceSeq + makeKeyComparer com ctx key |> makeDictionaryWithComparer com r t sourceSeq | _ -> Helper.GlobalCall("Map", t, [ sourceSeq ], isConstructor = true, ?loc = r) +let makeDictionary (com: ICompiler) ctx r t sourceSeq = + makeDictionaryWithKeyComparer makeEqualityComparer com ctx r t sourceSeq + +let makeBclDictionary (com: ICompiler) ctx r t sourceSeq = + makeDictionaryWithKeyComparer makeDefaultEqualityComparer com ctx r t sourceSeq + let makeHashSetWithComparer com r t sourceSeq comparer = Helper.LibCall(com, "MutableSet", "HashSet", t, [ sourceSeq; comparer ], isConstructor = true, ?loc = r) let makeHashSet (com: ICompiler) ctx r t sourceSeq = match t with - | DeclaredType(_, [ key ]) when not (isCompatibleWithNativeComparison key) -> + | DeclaredType(_, [ key ]) when not (isCompatibleWithNativeComparison com key) -> // makeComparer com ctx key - makeEqualityComparer com ctx key |> makeHashSetWithComparer com r t sourceSeq + makeDefaultEqualityComparer com ctx key + |> makeHashSetWithComparer com r t sourceSeq | _ -> Helper.GlobalCall("Set", t, [ sourceSeq ], isConstructor = true, ?loc = r) let tryEntityIdent (com: Compiler) entFullName = match entFullName with + | BuiltinDefinition BclDateOnly when com.Options.JsTemporal -> + makeImportLib com Any "PlainDate" "DateOnlyTemporal" |> Some + | BuiltinDefinition BclTimeOnly when com.Options.JsTemporal -> + makeImportLib com Any "PlainTime" "TimeOnlyTemporal" |> Some + | BuiltinDefinition BclTimeSpan when com.Options.JsTemporal -> + makeImportLib com Any "Duration" "TimeSpanTemporal" |> Some + | BuiltinDefinition BclDateTime when com.Options.JsTemporal -> + makeImportLib com Any "PlainDateTime" "DateTimeTemporal" |> Some + | BuiltinDefinition BclDateTimeOffset when com.Options.JsTemporal -> + makeImportLib com Any "ZonedDateTime" "DateTimeOffsetTemporal" |> Some | BuiltinDefinition BclDateOnly | BuiltinDefinition BclDateTime | BuiltinDefinition BclDateTimeOffset -> makeIdentExpr "Date" |> Some @@ -724,6 +932,7 @@ let tryEntityIdent (com: Compiler) entFullName = // | Naming.EndsWith "Exception" _ -> makeImportLib com Any "Exception" "Util" |> Some | Types.attribute -> makeImportLib com Any "Attribute" "Types" |> Some | "System.Uri" -> makeImportLib com Any "Uri" "Uri" |> Some + | "System.Diagnostics.Stopwatch" -> makeImportLib com Any "Stopwatch" "Stopwatch" |> Some | "Microsoft.FSharp.Control.FSharpAsyncReplyChannel`1" -> makeImportLib com Any "AsyncReplyChannel" "AsyncBuilder" |> Some | "Microsoft.FSharp.Control.FSharpEvent`1" -> makeImportLib com Any "Event" "Event" |> Some @@ -772,10 +981,12 @@ and private getZero (com: ICompiler) (ctx: Context) (t: Type) = | Char -> makeCharConst '\000' | String -> makeStrConst "" // TODO: Use null for string? | Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue None + | Builtin BclTimeOnly when com.Options.JsTemporal -> Helper.LibCall(com, "TimeOnlyTemporal", "minValue", t, []) + | Builtin BclTimeSpan when com.Options.JsTemporal -> Helper.LibCall(com, "TimeSpanTemporal", "zero", t, []) | Builtin(BclTimeSpan | BclTimeOnly) -> TypeCast(makeIntConst 0, t) - | Builtin BclDateTime as t -> Helper.LibCall(com, "Date", "minValue", t, []) - | Builtin BclDateTimeOffset as t -> Helper.LibCall(com, "DateOffset", "minValue", t, []) - | Builtin BclDateOnly as t -> Helper.LibCall(com, "DateOnly", "minValue", t, []) + | Builtin BclDateTime as t -> Helper.LibCall(com, dateTimeModule com, "minValue", t, []) + | Builtin BclDateTimeOffset as t -> Helper.LibCall(com, dateTimeOffsetModule com, "minValue", t, []) + | Builtin BclDateOnly as t -> Helper.LibCall(com, dateOnlyModule com, "minValue", t, []) | Builtin(FSharpSet genArg) as t -> makeSet com ctx None t "Empty" [] genArg | Builtin(BclKeyValuePair(k, v)) -> makeTuple None true [ getZero com ctx k; getZero com ctx v ] | ListSingleton(CustomOp com ctx None t "get_Zero" [] e) -> e @@ -1237,6 +1448,10 @@ let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp | "Fable.Core.JSX", meth -> match meth with | "create" -> Helper.LibCall(com, "JSX", "create", t, args, ?loc = r) |> withTag "jsx" |> Some + | "spread" -> + Helper.LibCall(com, "JSX", "spread", t, args, ?loc = r) + |> withTag "jsx-spread" + |> Some | "html" | "jsx" -> Helper.LibCall(com, "JSX", "html", t, args, ?loc = r) @@ -1272,15 +1487,20 @@ let getMangledNames (i: CallInfo) (thisArg: Expr option) = moduleName, mangledName let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName, mangledName = getMangledNames i thisArg + match i.CompiledName, thisArg, args with + | "ToString", Some callee, [] -> Helper.InstanceCall(callee, "toString", t, args, ?loc = r) |> Some + | "GetHashCode", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "Equals", Some callee, [ other ] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | _ -> + let moduleName, mangledName = getMangledNames i thisArg - let args = - match thisArg with - | Some callee -> callee :: args - | _ -> args + let args = + match thisArg with + | Some callee -> callee :: args + | _ -> args - Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) - |> Some + Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some let fsharpModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let moduleName, mangledName = getMangledNames i thisArg @@ -1298,8 +1518,38 @@ let (|PrintfNoFormatSpecifiers|_|) (arg: Expr) = | _ -> None | _ -> None +// The fallback only runs for a nullish read, and every out-of-bounds read is nullish +let readAtCallSite (com: ICompiler) r t container idx (fallback: Expr) = + let rec canRepeat e = + match e with + | IdentExpr _ + | Value((StringConstant _ | NumberConstant _ | CharConstant _ | BoolConstant _), _) -> true + | TypeCast(e, _) -> canRepeat e + | Get(e, FieldGet info, _, _) -> not info.CanHaveSideEffects && canRepeat e + | _ -> false + + if canRepeat container && canRepeat idx then + emitExpr r t [ getExpr None t container idx; fallback ] "$0 ?? $1" + else + fallback + +let (|InterpolatedPrintf|_|) (arg: Expr) = + match arg with + | MaybeCasted(Call(Import({ Selector = "interpolatedPrintf" }, _, _), callInfo, _, _)) -> + match callInfo.Args with + | [ _; text ] -> Some text + | _ -> None + | _ -> None + let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let unwrapInterpolated = + function + | InterpolatedPrintf text -> text + | arg -> arg + let consoleCall consoleMember libMember args = + let args = List.map unwrapInterpolated args + match args with | [ PrintfNoFormatSpecifiers str ] -> Helper.GlobalCall("console", t, [ makeStrConst str ], memb = consoleMember, ?loc = r) @@ -1315,10 +1565,11 @@ let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | [ _ ] -> Helper.LibCall(com, "String", "toText", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | [ cont; InterpolatedPrintf text ] -> curriedApply r t cont [ text ] |> Some | [ cont; fmt ] -> Helper.InstanceCall(fmt, "cont", t, [ cont ]) |> Some | _ -> None | "PrintFormatToString", _, _ -> - match args with + match List.map unwrapInterpolated args with | [ template ] when template.Type = String -> Some template | _ -> Helper.LibCall(com, "String", "toText", t, args, i.SignatureArgTypes, ?loc = r) @@ -1331,25 +1582,31 @@ let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | "PrintFormat", _, _ -> // addWarning com ctx.FileName r "Printf will behave as printfn" consoleCall "log" "toConsole" args + | "PrintFormatThen", _, arg :: InterpolatedPrintf text :: _ -> curriedApply r t arg [ text ] |> Some | "PrintFormatThen", _, arg :: callee :: _ -> Helper.InstanceCall(callee, "cont", t, [ arg ]) |> Some | "PrintFormatToStringThenFail", _, _ -> + let args = List.map unwrapInterpolated args + Helper.LibCall(com, "String", "toFail", t, args, i.SignatureArgTypes, ?loc = r) |> Some | ("PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _, _ -> fsharpModule com ctx r t i thisArg args | ".ctor", _, str :: (Value(NewArray(ArrayValues templateArgs, _, _), _) as values) :: _ -> - match makeStringTemplateFrom com [| "%s"; "%i" |] templateArgs str with - | Some v -> makeValue r v |> Some - | None -> - // Try to build a StringTemplate where formatted values are individually wrapped - // in String.interpolate calls. This keeps the AST as a StringTemplate (rather than - // a single runtime interpolate call) so that JSX templates can still use it. - match makeStringTemplateFromAllowingFormat com [| "%s"; "%i" |] templateArgs str with - | Some v -> makeValue r v |> Some + let text = + match makeStringTemplateFrom com [| "%s"; "%i" |] templateArgs str with + | Some v -> makeValue r v | None -> - Helper.LibCall(com, "String", "interpolate", t, [ str; values ], i.SignatureArgTypes, ?loc = r) - |> Some + // Try to build a StringTemplate where formatted values are individually wrapped + // in String.interpolate calls. This keeps the AST as a StringTemplate (rather than + // a single runtime interpolate call) so that JSX templates can still use it. + match makeStringTemplateFromAllowingFormat com [| "%s"; "%i" |] templateArgs str with + | Some v -> makeValue r v + | None -> + Helper.LibCall(com, "String", "interpolate", String, [ str; values ], i.SignatureArgTypes, ?loc = r) + + Helper.LibCall(com, "String", "interpolatedPrintf", t, [ str; text ], ?loc = r) + |> Some | ".ctor", _, arg :: _ -> Helper.LibCall(com, "String", "printf", t, [ arg ], i.SignatureArgTypes, ?loc = r) |> Some @@ -1382,10 +1639,15 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | ("ToSByte" | "ToByte" | "ToInt8" | "ToUInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToUInt" | "ToInt32" | "ToUInt32"), _ -> toInt com ctx r t args |> Some | ("ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr"), _ -> toLong com ctx r t args |> Some + // Unlike Double.Parse, FSharp.Core's float and float32 remove underscores + | ("ToSingle" | "ToDouble"), [ ExprType String as str ] -> + Helper.LibCall(com, "Double", "parseIgnoringUnderscores", t, [ str ], ?loc = r) + |> roundToFloat32 r t + |> Some | ("ToSingle" | "ToDouble"), _ -> toFloat com ctx r t args |> Some | "ToDecimal", _ -> toDecimal com ctx r t args |> Some | "ToChar", _ -> toChar com ctx r args.Head |> Some - | "ToString", _ -> toString com ctx r args |> Some + | "ToString", _ -> stringOperator com ctx r args |> Some | "CreateSequence", [ xs ] -> TypeCast(xs, t) |> Some | ("CreateDictionary" | "CreateReadOnlyDictionary"), [ arg ] -> makeDictionary com ctx r t arg |> Some | "CreateSet", _ -> (genArg com ctx r 0 i.GenericArgs) |> makeSet com ctx r t "OfSeq" args |> Some @@ -1423,14 +1685,12 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o _ -> fsFormat com ctx r t i thisArg args | ("Failure" | "FailurePattern" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args - | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), + // Thrown at the call site so V8 sees a non-returning branch rather than a call + | ("InvalidArg" | "InvalidOp"), _ -> fsharpModule com ctx r t i thisArg args |> Option.map (makeThrow r t) + | ("IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r t (error com msg) |> Some - | "InvalidArg", [ argName; msg ] -> - let msg = add msg (add (add (str " (Parameter '") argName) (str "')")) - makeThrow r t (error com msg) |> Some + | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some | "Raise", [ arg ] -> makeThrow r t arg |> Some | "Reraise", _ -> match ctx.CaughtException with @@ -1491,12 +1751,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | _ -> applyOp com ctx r t i.CompiledName args |> Some | "Round", _ -> match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", "round", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | _ -> - Helper.LibCall(com, "Util", "round", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> roundCall com r t "Decimal" i args |> Some + | _ -> roundCall com r t "Util" i args |> Some | "Truncate", _ -> match args with | ExprType(Number(Decimal, _)) :: _ -> @@ -1541,6 +1797,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | (Operators.inequality | "Neq"), [ left; right ] -> equals com ctx r false left right |> Some | (Operators.equality | "Eq"), [ left; right ] -> equals com ctx r true left right |> Some | "IsNull", [ arg ] -> nullCheck r true arg |> Some + | "IsNotNull", [ arg ] -> nullCheck r false arg |> Some | "Hash", [ arg ] -> structuralHash com r arg |> Some // Comparison | "Compare", [ left; right ] -> compare com ctx r left right |> Some @@ -1596,8 +1853,7 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "GetHashCode", Some arg, _ -> identityHash com r arg |> Some | "GetType", Some arg, _ -> if arg.Type = Any then - "Types can only be resolved at compile time. At runtime this will be same as `typeof`" - |> addWarning com ctx.InlinePath r + WarningCodes.getTypeOnObj |> addWarningWithCode com ctx.InlinePath r makeTypeInfo (changeRangeToCallSite ctx.InlinePath r) arg.Type |> Some | _ -> None @@ -1660,6 +1916,17 @@ let chars (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | ("Compare" | "CompareTo" | "Equals" | "GetHashCode") -> valueTypes com ctx r t i thisArg args | _ -> None +// JS can hold an int as -0, which runtime number formatting would print with its sign +let rec private normalizeIntegerFormatArg (arg: Expr) = + match arg with + | TypeCast(e, t) -> TypeCast(normalizeIntegerFormatArg e, t) + | Value(NewArray(ArrayValues values, typ, kind), r) -> + Value(NewArray(ArrayValues(List.map normalizeIntegerFormatArg values), typ, kind), r) + | e -> + match e.Type with + | Number(Integers _, _) -> makeBinOp None e.Type e (makeIntConst 0) BinaryPlus + | _ -> e + let implementedStringFunctions = set [| @@ -1701,8 +1968,9 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt |> Some | _ -> fsFormat com ctx r t i thisArg args | "get_Length", Some c, _ -> getFieldWith r t c "length" |> Some - | "get_Chars", Some c, _ -> + | "get_Chars", Some c, [ idx ] -> Helper.LibCall(com, "String", "getCharAtIndex", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> readAtCallSite com r t c idx |> Some | "Equals", Some x, [ y ] | "Equals", None, [ x; y ] -> makeEqOp r x y BinaryEqual |> Some @@ -1723,7 +1991,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "String", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "StartsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "String", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -1733,7 +2001,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "String", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "EndsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "String", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -1882,6 +2150,11 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt |> addErrorAndReturnNull com ctx.InlinePath r |> Some | Patterns.SetContains implementedStringFunctions, thisArg, args -> + let args = + match i.CompiledName, args with + | "Format", format :: formatArgs -> format :: List.map normalizeIntegerFormatArg formatArgs + | _ -> args + Helper.LibCall( com, "String", @@ -2019,6 +2292,37 @@ let copyToArray (com: ICompiler) r t (i: CallInfo) args = Helper.LibCall(com, "Util", "copyToArray", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some +let binarySearch (com: ICompiler) ctx r t rangeMeth (ar: Expr) args = + let call meth args comparer = + let comparer = + match comparer with + // .NET uses the default comparer when the comparer is null. The null literal + // gets here as Util.defaultOf() unless its type is nullable (see defaultof) + | None + | Some NullConst + | Some(MaybeCasted(Call(Import({ Selector = "defaultOf" }, _, _), _, _, _))) -> + makeComparer com ctx (getElementType ar.Type) + | Some comparer -> comparer + + Helper.LibCall(com, "Array", meth, t, ar :: args @ [ comparer ], ?loc = r) + |> Some + + match args with + | [ value ] -> call "binarySearch" [ value ] None + | [ value; comparer ] -> call "binarySearch" [ value ] (Some comparer) + | [ index; length; value ] -> call rangeMeth [ index; length; value ] None + | [ index; length; value; comparer ] -> call rangeMeth [ index; length; value ] (Some comparer) + | _ -> None + +let arrayGetItem (com: ICompiler) r (t: Type) (args: Expr list) (signatureArgTypes: Type list) = + let item = + Helper.LibCall(com, "Array", "item", t, args, signatureArgTypes, ?loc = r) + + match args with + // V8 cannot specialise the read inside the shared `item` once it sees many array kinds + | [ idx; ar ] -> readAtCallSite com r t ar idx item |> Some + | _ -> Some item + let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", _, [] -> makeResizeArray (getElementType t) [] |> Some @@ -2030,7 +2334,7 @@ let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this Helper.GlobalCall("Array", t, args, memb = "from", ?loc = r) |> withTag "array" |> Some - | "get_Item", Some ar, [ idx ] -> Helper.LibCall(com, "Array", "item", t, [ idx; ar ], ?loc = r) |> Some + | "get_Item", Some ar, [ idx ] -> arrayGetItem com r t [ idx; ar ] [] | "set_Item", Some ar, [ idx; value ] -> Helper.LibCall(com, "Array", "setItem", t, [ ar; idx; value ], ?loc = r) |> Some | "CopyTo", Some ar, [ target ] -> @@ -2097,6 +2401,7 @@ let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this | "IndexOf", Some ar, args -> let args = injectIndexOfArgs com ctx r i.GenericArgs (ar :: args) Helper.LibCall(com, "Array", "indexOf", t, args, ?loc = r) |> Some + | "BinarySearch", Some ar, args -> binarySearch com ctx r t "binarySearchRangeCount" ar args | "Insert", Some ar, [ idx; arg ] -> Helper.InstanceCall(ar, "splice", t, [ idx; makeIntConst 0; arg ], ?loc = r) |> Some @@ -2147,8 +2452,6 @@ let nativeArrayFunctions = "Filter", "filter" "ForAll", "every" "Iterate", "forEach" - "Reduce", "reduce" - "ReduceBack", "reduceRight" "SortInPlaceWith", "sort" |] @@ -2178,14 +2481,26 @@ let tuples (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E | "ToTuple", _ -> changeKind false args | _ -> None +let arrayCopy (com: ICompiler) (ctx: Context) r t source sourceIndex target targetIndex count = + let toInt32 (arg: Expr) = + match arg.Type with + | Number(Int32, _) -> arg + | _ -> toInt com ctx r Int32.Number [ arg ] + + let args = + [ source; toInt32 sourceIndex; target; toInt32 targetIndex; toInt32 count ] + + Helper.LibCall(com, "Array", "copyTo", t, args, ?loc = r) |> Some + let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | "get_Length", Some arg, _ -> getFieldWith r t arg "length" |> Some | "get_Item", Some arg, [ idx ] -> getExpr r t arg idx |> Some | "set_Item", Some arg, [ idx; value ] -> setExpr r arg idx value |> Some - | "Copy", None, [ _source; _sourceIndex; _target; _targetIndex; _count ] -> copyToArray com r t i args + | "Copy", None, [ source; sourceIndex; target; targetIndex; count ] -> + arrayCopy com ctx r t source sourceIndex target targetIndex count | "Copy", None, [ source; target; count ] -> - copyToArray com r t i [ source; makeIntConst 0; target; makeIntConst 0; count ] + arrayCopy com ctx r t source (makeIntConst 0) target (makeIntConst 0) count | "ConvertAll", None, [ source; mapping ] -> Helper.LibCall(com, "Array", "map", t, [ mapping; source ], ?loc = r) |> Some | "IndexOf", None, args -> @@ -2193,6 +2508,7 @@ let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E Helper.LibCall(com, "Array", "indexOf", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some + | "BinarySearch", None, ar :: args -> binarySearch com ctx r t "binarySearchRange" ar args | "GetEnumerator", Some arg, _ -> getEnumerator com r t arg |> Some | "Resize", None, args -> let args = @@ -2203,21 +2519,25 @@ let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E |> Some | _ -> None -let arrayGetItem (com: ICompiler) r (t: Type) (args: Expr list) (signatureArgTypes: Type list) = - Helper.LibCall(com, "Array", "item", t, args, signatureArgTypes, ?loc = r) - |> Some - let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = let newArrayAlloc size t = Value(NewArray(ArrayAlloc size, t, MutableArray), None) let createArray size value = match t, value with - | Array(Number _ as t2, _), None when com.Options.TypedArrays -> newArrayAlloc size t2 + | Array(TypedArrayCompatible com MutableArray _ as t2, _), None -> newArrayAlloc size t2 | Array(t2, _), value -> let value = value |> Option.defaultWith (fun () -> getZero com ctx t2) // If we don't fill the array some operations may behave unexpectedly, like Array.prototype.reduce - Helper.LibCall(com, "Array", "fill", t, [ newArrayAlloc size t2; makeIntConst 0; size; value ]) + let fill size = + Helper.LibCall(com, "Array", "fill", t, [ newArrayAlloc size t2; makeIntConst 0; size; value ]) + + match size with + | IdentExpr _ + | Value(NumberConstant _, _) -> fill size + | _ -> + let n = makeUniqueIdent com ctx size.Type "n" + Let(n, size, fill (IdentExpr n)) | _ -> $"Expecting an array type but got %A{t}" |> addErrorAndReturnNull com ctx.InlinePath r @@ -2267,7 +2587,13 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use methods for Head and Tail (instead of Get(ListHead) for example) to check for empty lists + // list.Head and list.Tail raise InvalidOperationException, List.head and List.tail ArgumentException + // PHP's FSharpList has no such members, so PHP keeps the head/tail functions below. + | ("get_Head" | "get_Tail"), Some x, _ when com.Options.Language <> Php -> + let mangledName = + Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" + + Helper.LibCall(com, "List", mangledName, t, [], thisArg = x, ?loc = r) |> Some | ReplaceName [ "get_Head", "head" "get_Tail", "tail" "get_Item", "item" @@ -2398,6 +2724,35 @@ let nullables (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: | "get_HasValue", Some c -> Test(c, OptionTest true, r) |> Some | _ -> None +let nullableModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = + let convert (targetType: Type) (value: Expr) = + match targetType with + | Char -> toChar com ctx r value + | Number(BigIntegers _, _) -> toLong com ctx r targetType [ value ] + | Number(Integers _, _) -> toInt com ctx r targetType [ value ] + | Number(Floats _, _) -> toFloat com ctx r targetType [ value ] + | Number(Decimal, _) -> toDecimal com ctx r targetType [ value ] + | _ -> TypeCast(value, targetType) + + match i.CompiledName.Replace("$W", ""), List.tryLast args, t with + | ("ToByte" | "ToUInt8" | "ToSByte" | "ToInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToInt32" | "ToUInt" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr" | "ToSingle" | "ToFloat32" | "ToDouble" | "ToFloat" | "ToDecimal" | "ToChar" | "ToEnum"), + Some value, + Nullable(targetType, _) -> + match value with + | Value(Null _, _) -> Value(Null t, None) |> Some + | Value((NumberConstant _ | CharConstant _), _) -> convert targetType value |> Some + | _ -> + let sourceType = + match value.Type with + | Nullable(sourceType, _) -> sourceType + | sourceType -> sourceType + + let v = makeUniqueIdent com ctx sourceType "v" + let mapping = Delegate([ v ], convert targetType (IdentExpr v), None, Tags.empty) + + Helper.LibCall(com, "Option", "map", t, [ mapping; value ], ?loc = r) |> Some + | _ -> None + // See fable-library-ts/Option.ts for more info on how options behave in Fable runtime let options isStruct (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with @@ -2475,18 +2830,19 @@ let parseBool (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | _ -> None let numericStringFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) format = - let libCallFormat () = + let libCallFormat (token: string option) = let format = emitExpr r String [ format ] "'{0:' + $0 + '}'" + let thisArg = thisArg.Value - Helper.LibCall( - com, - "String", - "format", - t, - [ format; thisArg.Value ], - [ format.Type; thisArg.Value.Type ], - ?loc = r - ) + let meth, thisArg = + match thisArg.Type, token |> Option.map (fun token -> token.ToLower()) with + | Number(Float32, _), (None | Some("g" | "r")) -> "formatSingle", thisArg + // "G" and "R" would print the sign of an int that JS holds as -0 + | Number(Integers _, _), (None | Some("g" | "r")) -> + "format", makeBinOp r thisArg.Type thisArg (makeIntConst 0) BinaryPlus + | _ -> "format", thisArg + + Helper.LibCall(com, "String", meth, t, [ format; thisArg ], [ format.Type; thisArg.Type ], ?loc = r) |> Some match format with @@ -2517,13 +2873,15 @@ let numericStringFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisA | _ -> "does not support decimal format specifier" |> Some | "e" -> match numberKind with + | Int64 + | UInt64 -> None | BigIntegers _ -> "does not support exponential format specifier" |> Some | _ -> None | "f" | "g" | "n" - | "p" -> None - | "r" -> "with round-trip format specifier is not support by Fable" |> Some + | "p" + | "r" -> None | "x" -> match numberKind with | Integers _ @@ -2537,15 +2895,15 @@ let numericStringFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisA |> addError com ctx.InlinePath r None - | None -> libCallFormat () + | None -> libCallFormat (Some token) else - libCallFormat () + libCallFormat (Some format) - | _ -> libCallFormat () + | _ -> libCallFormat None let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let parseCall meth str args style = + let parseCall meth str args (style: Expr) = let kind = match i.DeclaringEntityFullName with | Patterns.DicContains FSharp2Fable.TypeHelpers.numberTypes kind -> kind @@ -2561,10 +2919,14 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let args = if isFloatOrDecimal then - [ str ] @ outValue + [ str; style ] @ outValue else - [ str; makeIntConst style; makeBoolConst unsigned; makeIntConst bitsize ] - @ outValue + [ str; style; makeBoolConst unsigned; makeIntConst bitsize ] @ outValue + + let meth = + match kind with + | Float32 -> meth + "Single" + | _ -> meth let callExpr = Helper.LibCall(com, numberModule, Naming.lowerFirst meth, t, args, ?loc = r) @@ -2573,6 +2935,36 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | Number(BigIntegers _, _) -> wrapLong com ctx r t callExpr | _ -> callExpr + let isFloatOrDecimalType = + match i.DeclaringEntityFullName with + | Types.decimal + | Types.float32 + | Types.float64 -> true + | _ -> false + + let supportedFloatStyles = + int ( + System.Globalization.NumberStyles.Any + &&& ~~~System.Globalization.NumberStyles.AllowCurrencySymbol + ) + + let checkFormatProvider meth hasStyle = + let acceptedArgs = + (if meth = "Parse" then + 1 + else + 2) + + (if hasStyle then + 1 + else + 0) + + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + let isFloat = match i.SignatureArgTypes with | Number(Floats _, _) :: _ -> true @@ -2605,47 +2997,44 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op Helper.LibCall(com, "Double", "isInfinity", t, args, i.SignatureArgTypes, ?loc = r) |> Some | ("Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp"), _ -> operators com ctx r t i thisArg args - | ("Parse" | "TryParse") as meth, str :: NumberConst(NumberValue.Int32 style, _) :: _ -> - let hexConst = int System.Globalization.NumberStyles.HexNumber - let intConst = int System.Globalization.NumberStyles.Integer - - if style <> hexConst && style <> intConst then - $"%s{i.DeclaringEntityFullName}.%s{meth}(): NumberStyle %d{style} is ignored" - |> addWarning com ctx.InlinePath r - - let acceptedArgs = - if meth = "Parse" then - 2 - else - 3 - - if List.length args > acceptedArgs then - // e.g. Double.Parse(string, style, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r - + | ("Parse" | "TryParse") as meth, str :: (ExprType(Number(Int32, _)) as style) :: _ -> + match style with + | NumberConst(NumberValue.Int32 style, _) when + (if isFloatOrDecimalType then + style &&& ~~~supportedFloatStyles <> 0 + else + style <> int System.Globalization.NumberStyles.HexNumber + && style <> int System.Globalization.NumberStyles.Integer) + -> + WarningCodes.numberStylesIgnored style + |> addWarningWithCode com ctx.InlinePath r + | _ -> () + + checkFormatProvider meth true parseCall meth str args style |> Some | ("Parse" | "TryParse") as meth, str :: _ -> - let acceptedArgs = - if meth = "Parse" then - 1 - else - 2 - - if List.length args > acceptedArgs then - // e.g. Double.Parse(string, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + checkFormatProvider meth false - let style = int System.Globalization.NumberStyles.Any - parseCall meth str args style |> Some + let style = + match i.DeclaringEntityFullName with + | Types.decimal -> System.Globalization.NumberStyles.Number + | Types.float32 + | Types.float64 -> + System.Globalization.NumberStyles.Float + ||| System.Globalization.NumberStyles.AllowThousands + | _ -> System.Globalization.NumberStyles.Any + + parseCall meth str args (makeIntConst (int style)) |> Some | "Pow", _ -> Helper.GlobalCall("Math", t, args, i.SignatureArgTypes, memb = "pow", ?loc = r) |> Some | "ToString", [ ExprTypeAs(String, format) ] | "ToString", [ ExprTypeAs(String, format); _ (* Culture info *) ] -> numericStringFormat com ctx r t i thisArg format - | "ToString", _ -> Helper.GlobalCall("String", String, [ thisArg.Value ], ?loc = r) |> Some + | "ToString", _ -> + match thisArg.Value.Type with + | Number((Float32 | Float64), _) -> toString com ctx r [ thisArg.Value ] |> Some + | _ -> Helper.GlobalCall("String", String, [ thisArg.Value ], ?loc = r) |> Some | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args | _ -> None @@ -2684,12 +3073,20 @@ let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: | Decimal -> toDecimal com ctx r t args |> Some | _ -> None | _ -> None - | ("Ceiling" | "Floor" | "Round" | "Truncate" | "Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Add" | "Subtract" | "Multiply" | "Divide" | "Remainder" | "Negate" as meth), + | "Round", _ -> roundCall com r t "Decimal" i args |> Some + | ("Ceiling" | "Floor" | "Truncate" | "Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Add" | "Subtract" | "Multiply" | "Divide" | "Remainder" | "Negate" as meth), _ -> let meth = Naming.lowerFirst meth Helper.LibCall(com, "Decimal", meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + | ("ToByte" | "ToSByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToSingle" | "ToDouble"), + [ _ ] -> + match t with + | Number(BigIntegers _, _) -> toLong com ctx r t args |> Some + | Number(Integers _, _) -> toInt com ctx r t args |> Some + | Number(Floats _, _) -> toFloat com ctx r t args |> Some + | _ -> None | "ToString", [ ExprTypeAs(String, format) ] | "ToString", [ ExprTypeAs(String, format); _ (* Culture info *) ] -> numericStringFormat com ctx r t i thisArg format @@ -2817,14 +3214,33 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr |> withTag "downcast" |> Some | "MakeDecimal", _, _ -> decimals com ctx r t i thisArg args - | "GetString", _, [ ar; idx ] -> getExpr r t ar idx |> Some + | "GetString", _, [ str; idx ] -> + Helper.LibCall(com, "String", "getCharAtIndex", t, [ str; idx ], i.SignatureArgTypes, ?loc = r) + |> readAtCallSite com r t str idx + |> Some | "GetArray", _, [ ar; idx ] -> arrayGetItem com r t [ idx; ar ] (i.SignatureArgTypes |> List.rev) | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some | ("GetArraySlice" | "GetStringSlice"), None, [ ar; lower; upper ] -> + // JS slice counts a negative index from the end, .NET clamps it to 0 + let clampToZero (index: Expr) = + match index with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> index + | _ -> Helper.GlobalCall("Math", Int32.Number, [ index; makeIntConst 0 ], memb = "max") + + let lower = + match lower with + | Value(NewOption(None, _, _), _) -> makeIntConst 0 + | Value(NewOption(Some lower, _, _), _) + | lower -> clampToZero lower + let upper = match upper with | Value(NewOption(None, _, _), _) -> getExpr None (Int32.Number) ar (makeStrConst "length") - | _ -> add upper (makeIntConst 1) + | Value(NewOption(Some upper, _, _), _) + | upper -> + match upper with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> makeIntConst (n + 1) + | upper -> add upper (makeIntConst 1) |> clampToZero Helper.InstanceCall(ar, "slice", t, [ lower; upper ], ?loc = r) |> Some | "SetArraySlice", None, args -> @@ -2905,13 +3321,13 @@ let dictionaries (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp match i.CompiledName, thisArg with | ".ctor", _ -> match i.SignatureArgTypes, args with - | ([] | [ Number _ ]), _ -> makeDictionary com ctx r t (makeArray Any []) |> Some - | [ IDictionary ], [ arg ] -> makeDictionary com ctx r t arg |> Some + | ([] | [ Number _ ]), _ -> makeBclDictionary com ctx r t (makeArray Any []) |> Some + | [ IDictionary ], [ arg ] -> makeBclDictionary com ctx r t arg |> Some | [ IDictionary; IEqualityComparer ], [ arg; eqComp ] -> makeComparerFromEqualityComparer eqComp |> makeDictionaryWithComparer com r t arg |> Some - | [ IEnumerable ], [ arg ] -> makeDictionary com ctx r t arg |> Some + | [ IEnumerable ], [ arg ] -> makeBclDictionary com ctx r t arg |> Some | [ IEnumerable; IEqualityComparer ], [ arg; eqComp ] -> makeComparerFromEqualityComparer eqComp |> makeDictionaryWithComparer com r t arg @@ -3077,6 +3493,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | "get_Message", Some e -> getFieldWith r t e "message" |> Some | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some | "get_InnerException", Some e -> getFieldWith r t e "innerException" |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = @@ -3120,7 +3537,9 @@ let log (com: ICompiler) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match args with | [] -> [] | [ v ] -> [ v ] - | (StringConst _) :: _ -> [ Helper.LibCall(com, "String", "format", t, args, i.SignatureArgTypes) ] + | (StringConst _ as format) :: formatArgs -> + let args = format :: List.map normalizeIntegerFormatArg formatArgs + [ Helper.LibCall(com, "String", "format", t, args, i.SignatureArgTypes) ] | _ -> [ args.Head ] Helper.GlobalCall("console", t, args, memb = "log", ?loc = r) @@ -3156,6 +3575,13 @@ let bitConvert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option Helper.LibCall(com, "BitConverter", memberName, t, args, i.SignatureArgTypes, ?loc = r) |> Some +// The library decodes into a Uint8Array regardless of the typedArrays option +let private decodedBytes (com: ICompiler) r t expr = + if com.Options.TypedArrays then + expr + else + toArray r t expr + let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with | "ToSByte" @@ -3168,16 +3594,31 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( | "ToUInt64" -> round com args |> toLong com ctx r t |> Some | "ToSingle" | "ToDouble" -> toFloat com ctx r t args |> Some - | "ToDecimal" -> toDecimal com ctx r t args |> Some + | "ToDecimal" -> + match args with + // Convert.ToDecimal(string) parses with Decimal.Parse's default style + | ExprType String :: _ -> Helper.LibCall(com, "Decimal", "parse", t, [ args.Head ], ?loc = r) |> Some + | _ -> toDecimal com ctx r t args |> Some | "ToChar" -> toChar com ctx r args.Head |> Some | "ToString" -> toString com ctx r args |> Some | "ToBase64String" | "FromBase64String" -> if not (List.isSingle args) then - $"Convert.%s{Naming.upperFirst i.CompiledName} only accepts one single argument" - |> addWarning com ctx.InlinePath r + WarningCodes.base64ArgumentsIgnored |> addWarningWithCode com ctx.InlinePath r + + let expr = + Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r) - Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r) + match i.CompiledName with + | "FromBase64String" -> decodedBytes com r t expr |> Some + | _ -> Some expr + | "ToHexString" + | "ToHexStringLower" -> + Helper.LibCall(com, "String", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "FromHexString" when List.isSingle args -> + Helper.LibCall(com, "String", "fromHexString", t, args, i.SignatureArgTypes, ?loc = r) + |> decodedBytes com r t |> Some | _ -> None @@ -3186,7 +3627,7 @@ let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "get_Out" -> Helper.GlobalIdent("console", "log", t) |> Some | "get_Error" -> Helper.GlobalIdent("console", "error", t) |> Some | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | _ -> None @@ -3201,7 +3642,7 @@ let textWriter (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | "Break" -> makeDebugger r |> Some @@ -3218,16 +3659,41 @@ let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio IfThenElse(cond, makeDebugger r, unit, r) |> Some | _ -> None -let ignoreFormatProvider meth args = +/// Drops the `IFormatProvider` (and `DateTimeStyles`, where present) a date/time `Parse` was +/// given, warning once per discarded argument. `Parse arg` with no extra argument discards +/// nothing, so it must not warn. +let ignoreFormatProvider com (ctx: Context) r meth args = + // Passing InvariantCulture asks for exactly what Fable does, so there is nothing to report. + let warnProvider culture = + match culture with + | InvariantCulture -> () + | _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + + let warnStyles styles = + match styles with + | NumberConst(NumberValue.Int32 0, _) -> () // DateTimeStyles.None: no special handling + | _ -> WarningCodes.dateTimeStylesIgnored |> addWarningWithCode com ctx.InlinePath r + match meth, args with - // Ignore IFormatProvider + | "Parse", arg :: culture :: styles :: _ -> + warnProvider culture + warnStyles styles + [ arg ] + | "Parse", arg :: culture :: _ -> + warnProvider culture + [ arg ] | "Parse", arg :: _ -> [ arg ] - | "TryParse", input :: _culture :: _styles :: defVal :: _ -> [ input; defVal ] - | "TryParse", input :: _culture :: defVal :: _ -> [ input; defVal ] + | "TryParse", input :: culture :: styles :: defVal :: _ -> + warnProvider culture + warnStyles styles + [ input; defVal ] + | "TryParse", input :: culture :: defVal :: _ -> + warnProvider culture + [ input; defVal ] | _ -> args let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName = "Date" + let moduleName = dateTimeModule com match i.CompiledName with | ".ctor" -> @@ -3237,8 +3703,12 @@ let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op Helper.LibCall(com, moduleName, "fromTicks", t, args, i.SignatureArgTypes, ?loc = r) |> Some | ExprType(DeclaredType(ent, [])) :: _ when ent.FullName = Types.dateOnly -> - Helper.LibCall(com, moduleName, "fromDateTime", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + if com.Options.JsTemporal then + Helper.LibCall(com, "DateOnlyTemporal", "toDateTime", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + else + Helper.LibCall(com, moduleName, "fromDateTime", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> let last = List.last args @@ -3248,7 +3718,7 @@ let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let argTypes = (List.take 6 i.SignatureArgTypes) @ [ Int32.Number; last.Type ] - Helper.LibCall(com, "Date", "create", t, args, argTypes, ?loc = r) |> Some + Helper.LibCall(com, moduleName, "create", t, args, argTypes, ?loc = r) |> Some // JavaScript Date doesn't support microseconds precision | 8, Number(Int32, NumberInfo.Empty) -> @@ -3264,16 +3734,37 @@ let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op Helper.LibCall(com, moduleName, "create", t, args, i.SignatureArgTypes, ?loc = r) |> Some | "ToString" -> - Helper.LibCall(com, "Date", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | "Subtract" when com.Options.JsTemporal -> + // Overloaded on the argument: DateTime -> TimeSpan, TimeSpan -> DateTime + let meth = + match i.SignatureArgTypes with + | [ Builtin BclDateTime ] -> "subtractDate" + | _ -> "subtractTimeSpan" + + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | "get_Kind" -> - Helper.LibCall(com, "Date", "getKind", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + Helper.LibCall(com, moduleName, "getKind", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some | "get_Ticks" -> - Helper.LibCall(com, "Date", "getTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + Helper.LibCall(com, moduleName, "getTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some + | "get_Year" + | "get_Month" + | "get_Day" + | "get_Hour" + | "get_Minute" + | "get_Second" + | "get_Millisecond" + | "get_DayOfYear" when com.Options.JsTemporal -> + // These map one-to-one onto the equivalent Temporal.PlainDateTime properties + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some | meth -> - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3281,7 +3772,7 @@ let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName = "DateOffset" + let moduleName = dateTimeOffsetModule com match i.CompiledName with | ".ctor" -> @@ -3294,8 +3785,12 @@ let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: E Helper.LibCall(com, moduleName, "fromDate", t, args, i.SignatureArgTypes, ?loc = r) |> Some | ExprType(DeclaredType(ent, [])) :: _ when ent.FullName = Types.dateOnly -> - Helper.LibCall(com, moduleName, "fromDateTime", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + if com.Options.JsTemporal then + Helper.LibCall(com, "DateOnlyTemporal", "toDateTimeOffset", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + else + Helper.LibCall(com, moduleName, "fromDateTime", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> match args.Length with | 7 @@ -3311,14 +3806,36 @@ let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: E Helper.LibCall(com, moduleName, "fromDate", t, args, i.SignatureArgTypes, ?loc = r) |> Some | "ToString" -> - Helper.LibCall(com, "Date", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + // The Temporal module formats itself; the JS-Date one delegates to Date.toString (dispatches on offset) + let toStringModule = + if com.Options.JsTemporal then + moduleName + else + "Date" + + Helper.LibCall(com, toStringModule, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | "get_Offset" -> Helper.LibCall(com, moduleName, "offset", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some + | "Subtract" when com.Options.JsTemporal -> + // Overloaded on the argument: DateTimeOffset -> TimeSpan, TimeSpan -> DateTimeOffset + let meth = + match i.SignatureArgTypes with + | [ Builtin BclDateTimeOffset ] -> "subtractDate" + | _ -> "subtractTimeSpan" + + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | "get_UtcDateTime" when com.Options.JsTemporal -> + Helper.LibCall(com, moduleName, "utcDateTime", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + |> Some | "get_UtcDateTime" -> Helper.LibCall(com, "DateOffset", "toUniversalTime", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some + | "get_DateTime" when com.Options.JsTemporal -> + Helper.LibCall(com, moduleName, "dateTimeProp", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + |> Some | "get_DateTime" -> let kind = DateTimeKind.Unspecified |> int |> makeIntConst @@ -3333,13 +3850,32 @@ let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: E ) |> Some | "get_Ticks" -> - Helper.LibCall(com, "Date", "getTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + // Non-temporal DateTimeOffset reuses Date.getTicks; the Temporal module has its own + let ticksModule = + if com.Options.JsTemporal then + moduleName + else + "Date" + + Helper.LibCall(com, ticksModule, "getTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some | "get_UtcTicks" -> - Helper.LibCall(com, "DateOffset", "getUtcTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + Helper.LibCall(com, moduleName, "getUtcTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some + | "get_Year" + | "get_Month" + | "get_Day" + | "get_Hour" + | "get_Minute" + | "get_Second" + | "get_Millisecond" + | "get_DayOfYear" when com.Options.JsTemporal -> + // These map one-to-one onto the equivalent Temporal.ZonedDateTime properties + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some | meth -> - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3347,6 +3883,8 @@ let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: E let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let moduleName = dateOnlyModule com + match i.CompiledName with | ".ctor" when args.Length = 4 -> "DateOnly constructor with the calendar parameter is not supported." @@ -3354,7 +3892,7 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op None | ".ctor" -> - Helper.LibCall(com, "DateOnly", "create", t, args, i.SignatureArgTypes, ?loc = r) + Helper.LibCall(com, moduleName, "create", t, args, i.SignatureArgTypes, ?loc = r) |> Some | "ToString" -> match args with @@ -3365,7 +3903,7 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op None | [ StringConst("d" | "o" | "O"); _ ] -> - Helper.LibCall(com, "DateOnly", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | [ StringConst _; _ ] -> "DateOnly.ToString doesn't support custom format. It only handles \"d\", \"o\", \"O\" format, with CultureInfo.InvariantCulture." @@ -3375,7 +3913,7 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | [ _ ] -> Helper.LibCall( com, - "DateOnly", + moduleName, "toString", t, makeStrConst "d" :: args, @@ -3387,24 +3925,33 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | _ -> None | "AddDays" | "AddMonths" - | "AddYears" -> + | "AddYears" when not com.Options.JsTemporal -> let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst Helper.LibCall(com, "Date", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some + | "get_Year" + | "get_Month" + | "get_Day" + | "get_DayOfYear" when com.Options.JsTemporal -> + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some | meth -> - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst - Helper.LibCall(com, "DateOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let moduleName = timeSpanModule com + let timeSpanLibCall meth args = - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst - Helper.LibCall(com, "TimeSpan", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some let isNotDefaultInt64ZeroValue (expr: Expr) = @@ -3415,11 +3962,7 @@ let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg let limitArgsCountToMilliseconds (meth: string) (maxArgsCount: int) = if args.Length > maxArgsCount then if isNotDefaultInt64ZeroValue args.[maxArgsCount] then - addWarning - com - ctx.InlinePath - r - "TimeSpan precision is limited to milliseconds, microsecond arguments will be ignored" + WarningCodes.timeSpanPrecisionIgnored |> addWarningWithCode com ctx.InlinePath r args |> List.take maxArgsCount |> timeSpanLibCall meth @@ -3434,9 +3977,34 @@ let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg | [ _ticks ] -> "fromTicks" | _ -> "create" - Helper.LibCall(com, "TimeSpan", meth, t, args, i.SignatureArgTypes, ?loc = r) + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some - | "get_TotalMilliseconds" -> TypeCast(thisArg.Value, t) |> Some + | "get_TotalMilliseconds" when not com.Options.JsTemporal -> TypeCast(thisArg.Value, t) |> Some + | "op_Multiply" -> + // .NET overloads the operator on both sides: TimeSpan * float and float * TimeSpan + let args, argTypes = + match i.SignatureArgTypes with + | Builtin BclTimeSpan :: _ -> args, i.SignatureArgTypes + | _ -> List.rev args, List.rev i.SignatureArgTypes + + Helper.LibCall(com, moduleName, "multiply", t, args, argTypes, ?loc = r) |> Some + | "Divide" when com.Options.JsTemporal -> + // Overloaded on the argument: TimeSpan -> float ratio, float -> TimeSpan + let meth = + match i.SignatureArgTypes with + | [ Builtin BclTimeSpan ] -> "divideByTimeSpan" + | _ -> "divide" + + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | "get_Days" + | "get_Hours" + | "get_Minutes" + | "get_Seconds" + | "get_Milliseconds" when com.Options.JsTemporal -> + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some | "ToString" when (args.Length = 1) -> "TimeSpan.ToString with one argument is not supported, because it depends on local culture, please add CultureInfo.InvariantCulture" |> addError com ctx.InlinePath r @@ -3447,7 +4015,7 @@ let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg | StringConst "c" | StringConst "g" | StringConst "G" -> - Helper.LibCall(com, "TimeSpan", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | _ -> "TimeSpan.ToString don't support custom format. It only handles \"c\", \"g\" and \"G\" format, with CultureInfo.InvariantCulture." @@ -3463,26 +4031,37 @@ let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg | meth -> timeSpanLibCall meth args let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let moduleName = timeOnlyModule com + match i.CompiledName with | ".ctor" -> match args with | [ ExprType(Number(Int64, _)) ] -> - Helper.LibCall(com, "TimeOnly", "fromTicks", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "fromTicks", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | _ -> - Helper.LibCall(com, "TimeOnly", "create", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "create", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some - | "get_MinValue" -> makeIntConst 0 |> Some - | "ToTimeSpan" -> + | "get_MinValue" when not com.Options.JsTemporal -> makeIntConst 0 |> Some + | "ToTimeSpan" when not com.Options.JsTemporal -> // The representation is identical thisArg | "get_Hour" | "get_Minute" | "get_Second" + | "get_Millisecond" + | "get_Microsecond" + | "get_Nanosecond" when com.Options.JsTemporal -> + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some + | "get_Hour" + | "get_Minute" + | "get_Second" | "get_Millisecond" -> // Translate TimeOnly properties with a name in singular to the equivalent properties on TimeSpan timeSpans com ctx r t { i with CompiledName = i.CompiledName + "s" } thisArg args - | "get_Ticks" -> + | "get_Ticks" when not com.Options.JsTemporal -> Helper.LibCall(com, "TimeSpan", "ticks", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | "ToString" -> @@ -3494,7 +4073,7 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op None | [ StringConst("r" | "R" | "o" | "O" | "t" | "T"); _ ] -> - Helper.LibCall(com, "TimeOnly", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | [ StringConst _; _ ] -> "TimeOnly.ToString doesn't support custom format. It only handles \"r\", \"R\", \"o\", \"O\", \"t\", \"T\" format, with CultureInfo.InvariantCulture." @@ -3504,7 +4083,7 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | [ _ ] -> Helper.LibCall( com, - "TimeOnly", + moduleName, "toString", t, makeStrConst "t" :: args, @@ -3515,10 +4094,10 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op |> Some | _ -> None | meth -> - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst - Helper.LibCall(com, "TimeOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some let timers (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3531,6 +4110,33 @@ let timers (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti | meth, Some x, args -> Helper.InstanceCall(x, meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some | _ -> None +let stopwatch (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let toTimeSpan (ms: Expr) = + Helper.LibCall(com, timeSpanModule com, "fromMilliseconds", t, [ ms ], ?loc = r) + + let float64 = Number(Float64, NumberInfo.Empty) + + match i.CompiledName, thisArg with + | ".ctor", _ -> Helper.LibCall(com, "Stopwatch", "create", t, [], ?loc = r) |> Some + | ("StartNew" | "GetTimestamp" as meth), None -> + Helper.LibCall(com, "Stopwatch", Naming.lowerFirst meth, t, [], ?loc = r) + |> Some + | ("Start" | "Stop" | "Reset" | "Restart" as meth), Some x -> + Helper.LibCall(com, "Stopwatch", Naming.lowerFirst meth, t, [ x ], ?loc = r) + |> Some + | ("get_IsRunning" | "get_ElapsedMilliseconds" | "get_ElapsedTicks" as meth), Some x -> + Helper.LibCall(com, "Stopwatch", Naming.removeGetSetPrefix meth |> Naming.lowerFirst, t, [ x ], ?loc = r) + |> Some + | "get_Elapsed", Some x -> + Helper.LibCall(com, "Stopwatch", "elapsedTotalMilliseconds", float64, [ x ], ?loc = r) + |> toTimeSpan + |> Some + | "GetElapsedTime", None -> + Helper.LibCall(com, "Stopwatch", "getElapsedTotalMilliseconds", float64, args, ?loc = r) + |> toTimeSpan + |> Some + | _ -> None + let systemEnv (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with | "get_NewLine" -> Some(makeStrConst "\n") @@ -3639,6 +4245,7 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti | args -> Helper.LibCall(com, "Random", "seeded", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "get_Shared", None -> Helper.LibCall(com, "Random", "shared", t, [], ?loc = r) |> Some // Not yet supported | ("NextInt64" | "NextSingle"), _ -> None | meth, Some thisArg -> @@ -3701,6 +4308,18 @@ let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Exp let propInt p callee = getExpr r t callee (makeIntConst p) let propStr p callee = getExpr r t callee (makeStrConst p) + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + let isGroup = match thisArg with | Some(ExprType(DeclaredTypeFullName Types.regexGroup)) -> true @@ -3943,7 +4562,8 @@ let asyncBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with | "Start" -> - "Async.Start will behave as StartImmediate" |> addWarning com ctx.InlinePath r + WarningCodes.asyncStartBehavesAsStartImmediate + |> addWarningWithCode com ctx.InlinePath r Helper.LibCall(com, "Async", "start", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some @@ -4001,11 +4621,39 @@ let taskBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr ) |> Some -let tasks (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = +// .NET implements these overloads with Async.StartImmediateAsTask +let taskBuilderAsync (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let startAsPromise (computation: Expr) = + Helper.LibCall(com, "Async", "startAsPromise", Any, [ computation ], ?loc = r) + + match i.CompiledName, args with + | "TaskBuilderBase.Bind", [ computation; f ] -> taskBuilder com ctx r t i thisArg [ startAsPromise computation; f ] + | "TaskBuilderBase.ReturnFrom", [ computation ] -> taskBuilder com ctx r t i thisArg [ startAsPromise computation ] + | _ -> None + +let tasks (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match thisArg, i.CompiledName with - | Some x, ("GetAwaiter" | "GetResult" | "get_Result" | "Result") -> - // Task = Promise; return the promise - callers use Async.AwaitTask to extract the value + | Some x, "GetAwaiter" -> Some x + | Some x, ("GetResult" | "get_Result" | "Result") -> + WarningCodes.taskResultReturnsTask |> addWarningWithCode com ctx.InlinePath r + Some x + | None, "Delay" -> + // Without Temporal, a TimeSpan is already a number of milliseconds + let args = + match args with + | ExprType(Builtin BclTimeSpan) as delay :: rest when com.Options.JsTemporal -> + Helper.LibCall( + com, + timeSpanModule com, + "totalMilliseconds", + Number(Float64, NumberInfo.Empty), + [ delay ] + ) + :: rest + | args -> args + + Helper.LibCall(com, "Task", "delay", t, args, ?loc = r) |> Some | None, "FromResult" -> Helper.LibCall(com, "Task", "fromResult", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -4055,6 +4703,24 @@ let guids match i.CompiledName with | "NewGuid" -> Helper.LibCall(com, "Guid", "newGuid", t, []) |> Some + | "CreateVersion7" when com.Options.JsTemporal -> + // The Temporal DateTimeOffset is not a JS Date, so pass its Unix milliseconds instead + let args = + match args with + | [ dto ] -> + [ + Helper.LibCall( + com, + "DateTimeOffsetTemporal", + "toUnixTimeMilliseconds", + Int64.Number, + [ dto ], + [ dto.Type ] + ) + ] + | _ -> args + + Helper.LibCall(com, "Guid", "createVersion7", t, args) |> Some | "CreateVersion7" -> Helper.LibCall(com, "Guid", "createVersion7", t, args, i.SignatureArgTypes) |> Some @@ -4315,6 +4981,10 @@ let fsharpType com (ctx: Context) methName (r: SourceLocation option) t (i: Call // Prevent name clash with FSharpValue.GetRecordFields | "GetRecordFields" -> // Drop the trailing `allowAccessToPrivateRepresentation` flag (no meaning in JS/TS) + if List.length args > 1 then + WarningCodes.privateRepresentationFlagIgnored + |> addWarningWithCode com ctx.InlinePath r + Helper.LibCall(com, "Reflection", "getRecordElements", t, List.truncate 1 args, i.SignatureArgTypes, ?loc = r) |> Some | "GetUnionCases" @@ -4325,8 +4995,8 @@ let fsharpType com (ctx: Context) methName (r: SourceLocation option) t (i: Call | "IsTuple" | "IsFunction" -> if List.length args > 1 then - $"FSharpType.%s{methName}(): second argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.privateRepresentationFlagIgnored + |> addWarningWithCode com ctx.InlinePath r let args = [ List.head args ] @@ -4389,15 +5059,29 @@ let tryField com returnTyp ownerTyp fieldName = | Number(Decimal, _), _ -> Helper.LibValue(com, "Decimal", "get_" + fieldName, returnTyp) |> Some | String, "Empty" -> makeStrConst "" |> Some | Builtin BclGuid, "Empty" -> emptyGuid () |> Some + | Builtin BclTimeSpan, "Zero" when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "zero", returnTyp, []) |> Some | Builtin BclTimeSpan, "Zero" -> makeIntConst 0 |> Some - | Builtin(BclDateTime | BclDateTimeOffset | BclTimeOnly | BclDateOnly as t), ("MaxValue" | "MinValue") -> - Helper.LibCall(com, coreModFor t, Naming.lowerFirst fieldName, returnTyp, []) + | Builtin BclDateOnly, ("MaxValue" | "MinValue") -> + Helper.LibCall(com, dateOnlyModule com, Naming.lowerFirst fieldName, returnTyp, []) + |> Some + | Builtin BclTimeOnly, ("MaxValue" | "MinValue") -> + Helper.LibCall(com, timeOnlyModule com, Naming.lowerFirst fieldName, returnTyp, []) + |> Some + | Builtin BclDateTime, ("MaxValue" | "MinValue") -> + Helper.LibCall(com, dateTimeModule com, Naming.lowerFirst fieldName, returnTyp, []) + |> Some + | Builtin BclDateTimeOffset, ("MaxValue" | "MinValue") -> + Helper.LibCall(com, dateTimeOffsetModule com, Naming.lowerFirst fieldName, returnTyp, []) |> Some | DeclaredType(ent, genArgs), fieldName -> match ent.FullName with | "System.BitConverter" -> Helper.LibCall(com, "BitConverter", Naming.lowerFirst fieldName, returnTyp, []) |> Some + | "System.Diagnostics.Stopwatch" when fieldName = "Frequency" || fieldName = "IsHighResolution" -> + Helper.LibCall(com, "Stopwatch", Naming.lowerFirst fieldName, returnTyp, []) + |> Some | _ -> None | _ -> None @@ -4407,6 +5091,7 @@ let private replacedModules = "System.Math", operators "System.MathF", operators "Microsoft.FSharp.Core.Operators", operators + "Microsoft.FSharp.Linq.NullableModule", nullableModule "Microsoft.FSharp.Core.Operators.Checked", operators "Microsoft.FSharp.Core.Operators.Unchecked", unchecked "Microsoft.FSharp.Core.Operators.OperatorIntrinsics", intrinsicFunctions @@ -4496,20 +5181,21 @@ let private replacedModules = "System.Convert", convert "System.Console", console "System.Diagnostics.Debug", debug + "System.Diagnostics.Stopwatch", stopwatch "System.Diagnostics.Debugger", debug Types.datetime, dateTime Types.datetimeOffset, dateTimeOffset Types.dateOnly, dateOnly Types.timeOnly, timeOnly Types.timespan, timeSpans - "System.Timers.Timer", timers + Types.timer, timers "System.IO.File", files "System.IO.Directory", directories "System.IO.Path", paths "System.IO.TextWriter", textWriter "System.Environment", systemEnv "System.Globalization.CultureInfo", globalization - "System.Random", random + Types.random, random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels "System.Threading.Monitor", monitor @@ -4541,6 +5227,7 @@ let private replacedModules = "Microsoft.FSharp.Control.TaskBuilderBase", taskBuilder "Microsoft.FSharp.Control.TaskBuilderModule", taskBuilder "Microsoft.FSharp.Control.TaskBuilderExtensions.HighPriority", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderExtensions.MediumPriority", taskBuilderAsync "Microsoft.FSharp.Control.TaskBuilderExtensions.LowPriority", taskBuilder Types.guid, guids "System.Uri", uris @@ -4622,8 +5309,8 @@ let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genA | Types.dictionary -> let args = match argTypes.Value, args with - | ([] | [ Number _ ]), _ -> [ makeArray Any []; makeEqualityComparer com ctx (Seq.head genArgs) ] - | [ IDictionary ], [ arg ] -> [ arg; makeEqualityComparer com ctx (Seq.head genArgs) ] + | ([] | [ Number _ ]), _ -> [ makeArray Any []; makeDefaultEqualityComparer com ctx (Seq.head genArgs) ] + | [ IDictionary ], [ arg ] -> [ arg; makeDefaultEqualityComparer com ctx (Seq.head genArgs) ] | [ IDictionary; IEqualityComparer ], [ arg; eqComp ] -> [ arg; makeComparerFromEqualityComparer eqComp ] | [ IEqualityComparer ], [ eqComp ] | [ Number _; IEqualityComparer ], [ _; eqComp ] -> @@ -4636,8 +5323,8 @@ let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genA | Types.hashset -> let args = match argTypes.Value, args with - | ([] | [ Number _ ]), _ -> [ makeArray Any []; makeEqualityComparer com ctx (Seq.head genArgs) ] - | [ IEnumerable ], [ arg ] -> [ arg; makeEqualityComparer com ctx (Seq.head genArgs) ] + | ([] | [ Number _ ]), _ -> [ makeArray Any []; makeDefaultEqualityComparer com ctx (Seq.head genArgs) ] + | [ IEnumerable ], [ arg ] -> [ arg; makeDefaultEqualityComparer com ctx (Seq.head genArgs) ] | [ IEnumerable; IEqualityComparer ], [ arg; eqComp ] -> [ arg; makeComparerFromEqualityComparer eqComp ] | [ IEqualityComparer ], [ eqComp ] | [ Number _; IEqualityComparer ], [ _; eqComp ] -> @@ -4688,6 +5375,7 @@ let tryType typ = | List genArg -> Some(Types.list, lists, [ genArg ]) | Builtin kind -> match kind with + | BclRune -> None | BclGuid -> Some(Types.guid, guids, []) | BclDateTime -> Some(Types.datetime, dateTime, []) | BclDateTimeOffset -> Some(Types.datetimeOffset, dateTimeOffset, []) diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs index 7b6a4d5f56..ed695d9aa7 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs @@ -202,7 +202,7 @@ module Literals = let mkCharLit (value: char) : Lit = { - token = mkCharTokenLit ((string value).escape_debug ()) + token = mkCharTokenLit ((string value).escape_debug()) kind = LitKind.Char(value) span = DUMMY_SP } @@ -337,7 +337,47 @@ module Literals = span = DUMMY_SP } + let private normalizeStringLiteral (value: Symbol) = + let mutable builder: ValueOption = ValueNone + let mutable index = 0 + + while index < value.Length do + let current = value[index] + + if + System.Char.IsHighSurrogate(current) + && index + 1 < value.Length + && System.Char.IsLowSurrogate(value[index + 1]) + then + match builder with + | ValueSome builder -> builder.Append(value, index, 2) |> ignore + | ValueNone -> () + + index <- index + 2 + elif System.Char.IsSurrogate(current) then + match builder with + | ValueSome builder -> builder.Append('\uFFFD') |> ignore + | ValueNone -> + let newBuilder = System.Text.StringBuilder(value.Length) + newBuilder.Append(value, 0, index) |> ignore + newBuilder.Append('\uFFFD') |> ignore + builder <- ValueSome newBuilder + + index <- index + 1 + else + match builder with + | ValueSome builder -> builder.Append(current) |> ignore + | ValueNone -> () + + index <- index + 1 + + match builder with + | ValueSome builder -> builder.ToString() + | ValueNone -> value + let mkStrLit (value: Symbol) : Lit = + let value = normalizeStringLiteral value + { token = mkStrTokenLit (value.escape_debug ()) kind = LitKind.Str(value, StrStyle.Cooked) @@ -560,10 +600,11 @@ module MacroArgs = |> Seq.mapi (fun i tok -> let ttt = tok |> token.TokenTree.Token let sep = kind |> mkTokenTree - // if i < count - 1 then - [ (ttt, token.Spacing.Joint); (sep, token.Spacing.Alone) ] - // else - // [ (ttt, token.Spacing.Alone) ] + + if i < count - 1 then + [ (ttt, token.Spacing.Joint); (sep, token.Spacing.Alone) ] + else + [ (ttt, token.Spacing.Alone) ] ) |> Seq.concat |> mkVec @@ -890,7 +931,7 @@ module Exprs = value.StartsWith("\"", System.StringComparison.Ordinal) && value.EndsWith("\"", System.StringComparison.Ordinal) then - "\"" + value[1 .. (value.Length - 2)].escape_debug () + "\"" + "\"" + value[1 .. (value.Length - 2)].escape_debug() + "\"" else value @@ -1082,6 +1123,24 @@ module Types = let mkParenTy ty : Ty = TyKind.Paren(ty) |> mkTy + let mkPtrTy ty : Ty = + TyKind.Ptr( + { + ty = ty + mutbl = Mutability.Not + } + ) + |> mkTy + + let mkMutPtrTy ty : Ty = + TyKind.Ptr( + { + ty = ty + mutbl = Mutability.Mut + } + ) + |> mkTy + let mkRefTy (lifetimeOpt: Symbol option) ty : Ty = let opt_lifetime = lifetimeOpt |> Option.map mkLifetime diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Impl.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Impl.fs index 312811ee86..c0222bd999 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Impl.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Impl.fs @@ -374,7 +374,7 @@ type Expr with type MacCall with member self.span() : Span = - self.path.span.to_ (self.args.span().unwrap_or (self.path.span)) + self.path.span.to_ (self.args.span().unwrap_or(self.path.span)) type MacArgs with diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Parser.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Parser.fs index 293a73d302..8b48ed95d2 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Parser.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Parser.fs @@ -368,7 +368,7 @@ type ExprPrecedence with | ExprPrecedence.Range -> PREC_RANGE // Binop-like expr kinds, handled by `AssocOp`. - | ExprPrecedence.Binary(op) -> AssocOp.from_ast_binop(op).precedence () + | ExprPrecedence.Binary(op) -> AssocOp.from_ast_binop(op).precedence() | ExprPrecedence.Cast -> AssocOp.As.precedence () | ExprPrecedence.Type -> AssocOp.Colon.precedence () diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Pretty.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Pretty.fs index a07e989131..a63a99f7b6 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Pretty.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Pretty.fs @@ -421,11 +421,11 @@ type Printer with self.buf[self.right] <- entry self.scan_stack.push_front (self.right) - member self.scan_pop() : usize = self.scan_stack.pop_front().unwrap () + member self.scan_pop() : usize = self.scan_stack.pop_front().unwrap() - member self.scan_top() : usize = self.scan_stack.front().unwrap () + member self.scan_top() : usize = self.scan_stack.front().unwrap() - member self.scan_pop_bottom() : usize = self.scan_stack.pop_back().unwrap () + member self.scan_pop_bottom() : usize = self.scan_stack.pop_back().unwrap() member self.advance_right() = self.right <- self.right + 1 @@ -501,7 +501,7 @@ type Printer with member self.get_top() : PrintStackElem = self.print_stack .last() - .unwrap_or ( + .unwrap_or( { offset = 0 pbreak = PrintStackBreak.Broken(Breaks.Inconsistent) @@ -531,7 +531,7 @@ type Printer with member self.print_end() = debug ("print End -> pop End") - self.print_stack.pop().unwrap () |> ignore + self.print_stack.pop().unwrap() |> ignore member self.print_break(b: BreakToken, l: isize) = let top = self.get_top () @@ -573,7 +573,7 @@ type Printer with // self.out.reserve(self.pending_indentation) // self.out.extend(std.iter.repeat(' ').take(self.pending_indentation)) - self.out.push_str (" ".repeat (self.pending_indentation)) + self.out.push_str (" ".repeat(self.pending_indentation)) self.pending_indentation <- 0 self.out.push_str (s) @@ -637,7 +637,7 @@ type Printer with member self.hardbreak() = self.spaces (_SIZE_INFINITY) member self.is_beginning_of_line() : bool = - self.last_token().is_eof () || self.last_token().is_hardbreak_tok () + self.last_token().is_eof() || self.last_token().is_hardbreak_tok() static member hardbreak_tok_offset(off: isize) : Token = Token.Break( diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.State.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.State.fs index f4c59f6dae..ded92c2033 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.State.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.State.fs @@ -221,11 +221,11 @@ let literal_to_string (lit: token.Lit) : string = | token.LitKind.Char -> format ("'{0}'", symbol) | token.LitKind.Str -> format ("\"{0}\"", symbol) | token.LitKind.StrRaw(n) -> - let delim = "#".repeat (n |> usize) + let delim = "#".repeat(n |> usize) format ("r{0}\"{1}\"{2}", delim, symbol, delim) | token.LitKind.ByteStr -> format ("b\"{0}\"", symbol) | token.LitKind.ByteStrRaw(n) -> - let delim = "#".repeat (n |> usize) + let delim = "#".repeat(n |> usize) format ("br{0}\"{1}\"{2}", delim, symbol, delim) | token.LitKind.Integer | token.LitKind.Float @@ -238,7 +238,7 @@ let literal_to_string (lit: token.Lit) : string = // out.push_str(suffix.as_str()) let visibility_qualified (vis: ast.Visibility, s: string) : string = - let vis_str = State.new_().to_string (fun (s) -> s.print_visibility (vis)) + let vis_str = State.new_().to_string(fun (s) -> s.print_visibility (vis)) format ("{0}{1}", vis_str, s) // interface std.ops.Deref with // for State @@ -440,7 +440,7 @@ type State with | Some(cmnts) -> cmnts.current <- cmnts.current + 1 member self.next_comment() : Option = - self.comments().and_then (fun (c) -> c.next ()) + self.comments().and_then(fun (c) -> c.next ()) member self.print_literal(lit: ast.Lit) = self.maybe_print_comment (lit.span.lo ()) @@ -451,7 +451,7 @@ type State with match style with | ast.StrStyle.Cooked -> format ("\"{0}\"", st.escape_debug ()) | ast.StrStyle.Raw(n) -> - let delim = "#".repeat (n |> usize) + let delim = "#".repeat(n |> usize) format ("r{0}\"{1}\"{2}", delim, st, delim) self.s.word (st) @@ -701,7 +701,7 @@ type State with member self.break_offset_if_not_bol(n: usize, off: isize) = if not (self.s.is_beginning_of_line ()) then self.s.break_offset (n, off) - elif off <> 0 && self.s.last_token().is_hardbreak_tok () then + elif off <> 0 && self.s.last_token().is_hardbreak_tok() then // We do something pretty sketchy here: tuck the nonzero // offset-adjustment we were going to deposit along with the // break into the previous hardbreak. @@ -717,7 +717,7 @@ type State with | token.Nonterminal.NtBlock(e) -> self.block_to_string (e) | token.Nonterminal.NtStmt(e) -> self.stmt_to_string (e) | token.Nonterminal.NtPat(e) -> self.pat_to_string (e) - | token.Nonterminal.NtIdent(e, is_raw) -> IdentPrinter.for_ast_ident(e, is_raw).to_string () + | token.Nonterminal.NtIdent(e, is_raw) -> IdentPrinter.for_ast_ident(e, is_raw).to_string() | token.Nonterminal.NtLifetime(e) -> e.to_string () | token.Nonterminal.NtLiteral(e) -> self.expr_to_string (e) | token.Nonterminal.NtTT(tree) -> self.tt_to_string (tree) @@ -774,7 +774,7 @@ type State with | token.TokenKind.Literal(lit) -> literal_to_string (lit) // Name components - | token.TokenKind.Ident(s, is_raw) -> IdentPrinter.new_(s, is_raw, convert_dollar_crate).to_string () + | token.TokenKind.Ident(s, is_raw) -> IdentPrinter.new_(s, is_raw, convert_dollar_crate).to_string() | token.TokenKind.Lifetime(s) -> s.to_string () // Other @@ -858,7 +858,7 @@ type State with member self.comments() : Option = self.comments_ member self.print_ident(ident: Ident) = - self.s.word (IdentPrinter.for_ast_ident(ident, ident.is_raw_guess ()).to_string ()) + self.s.word (IdentPrinter.for_ast_ident(ident, ident.is_raw_guess ()).to_string()) self.ann.post (self, AnnNode.Ident(ident)) @@ -912,14 +912,14 @@ type State with let mutable i = 0 for elt in elts do - self.maybe_print_comment (get_span(elt).hi ()) + self.maybe_print_comment (get_span(elt).hi()) op (self, elt) i <- i + 1 if i < len then self.s.word (",") - self.maybe_print_trailing_comment (get_span (elt), Some(get_span(elts[i]).hi ())) + self.maybe_print_trailing_comment (get_span (elt), Some(get_span(elts[i]).hi())) self.s.space_if_not_bol () @@ -1593,7 +1593,7 @@ type State with self.print_expr_cond_paren ( scrutinee, self.cond_needs_par (scrutinee) - || parser.needs_par_as_let_scrutinee (scrutinee.precedence().order ()) + || parser.needs_par_as_let_scrutinee (scrutinee.precedence().order()) ) member self.print_else(els: Option) = @@ -1645,7 +1645,7 @@ type State with self.s.pclose () member self.print_expr_maybe_paren(expr: ast.Expr, prec: i8) = - self.print_expr_cond_paren (expr, expr.precedence().order () < prec) + self.print_expr_cond_paren (expr, expr.precedence().order() < prec) /// Prints an expr using syntax that's acceptable in a condition position, such as the `cond` in /// `if cond then ... }`. @@ -2766,7 +2766,7 @@ type State with member self.print_remaining_comments() = // If there aren't any remaining comments, then we need to manually // make sure there is a line break at the end_. - if self.next_comment().is_none () then + if self.next_comment().is_none() then self.s.hardbreak () let mutable cmnt = self.next_comment () diff --git a/src/Fable.Transforms/Rust/AST/Tests/Tests.fs b/src/Fable.Transforms/Rust/AST/Tests/Tests.fs index d80fc81537..0a3eaa9985 100644 --- a/src/Fable.Transforms/Rust/AST/Tests/Tests.fs +++ b/src/Fable.Transforms/Rust/AST/Tests/Tests.fs @@ -17,7 +17,7 @@ module Helpers = let fun_to_string (decl: FnDecl, header: FnHeader, name: Ident, generics: Generics) : string = State .new_() - .to_string (fun (s) -> + .to_string(fun (s) -> s.head ("") s.print_fn (decl, header, Some(name), generics) s.s.end_ () // Close the head box. @@ -25,7 +25,7 @@ module Helpers = ) let variant_to_string (var: Variant) : string = - State.new_().to_string (fun (s) -> s.print_variant (var)) + State.new_().to_string(fun (s) -> s.print_variant (var)) [] let test_fun_to_string () = diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 4bfad14e52..fa198834dd 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -55,7 +55,9 @@ type Context = ScopedSymbols: FSharp.Collections.Map // HasMultipleUses: bool //this could be a closure in a map, or a for loop. The point is anything leaving the scope cannot be assumed to be the only reference InferAnyType: bool + SkipRecordTypeRegistration: bool IsAssocMember: bool + IsFluentMember: bool IsLambda: bool IsParamByRefPreferred: bool RequiresSendSync: bool // a way to implicitly propagate Arc's down the hierarchy when it is not possible to explicitly tag @@ -626,6 +628,14 @@ module TypeInfo = let ent = com.GetEntity(entRef) (ent.IsFSharpRecord || ent.IsFSharpUnion) && not ent.IsValueType + let isReferenceClass (com: IRustCompiler) (entRef: Fable.EntityRef) = + let ent = com.GetEntity(entRef) + + not ent.IsInterface + && not ent.IsValueType + && not ent.IsFSharpRecord + && not ent.IsFSharpUnion + // Checks whether the type needs a ref counted wrapper // such as Rc (or Arc in a multithreaded context) let shouldBeRefCountWrapped (com: IRustCompiler) ctx typ = @@ -856,6 +866,8 @@ module TypeInfo = let transformTimeOnlyType com ctx : Rust.Ty = transformImportType com ctx [] "TimeOnly" "TimeOnly" + let transformRuneType _com _ctx : Rust.Ty = primitiveType "char" + let transformTimerType com ctx : Rust.Ty = transformImportType com ctx [] "DateTime" "Timer" @@ -889,7 +901,7 @@ module TypeInfo = | [ Fable.Unit ] -> [] | _ -> argTypes - let argCount = string (List.length argTypes) + let argCount = string(List.length argTypes) let genArgs = argTypes @ [ returnType ] transformImportType com ctx genArgs "Native" ("Func" + argCount) @@ -1020,10 +1032,14 @@ module TypeInfo = mkEmitTy value genArgs | ent when ent.IsInterface -> transformInterfaceType com ctx entRef genArgs | ent when ent.IsAbstractClass -> transformAbstractClassType com ctx entRef genArgs - | _ when entRef.FullName = "System.Random" -> + | _ when entRef.FullName = Types.random -> let entName = getLibraryImportName com ctx "Random" "Random" let genArgsOpt = transformGenArgs com ctx genArgs makeFullNamePathTy entName genArgsOpt + | _ when entRef.FullName = "System.Uri" -> + let entName = getLibraryImportName com ctx "Uri" "Uri" + let genArgsOpt = transformGenArgs com ctx genArgs + makeFullNamePathTy entName genArgsOpt | ent -> let entName = getEntityFullName com ctx entRef let genArgsOpt = transformGenArgs com ctx genArgs @@ -1033,7 +1049,7 @@ module TypeInfo = transformGenericType com ctx genArgs (rawIdent "Result") let transformChoiceType com ctx genArgs : Rust.Ty = - let argCount = string (List.length genArgs) + let argCount = string(List.length genArgs) transformImportType com ctx genArgs "Choice" ("Choice`" + argCount) let transformRefCellType com ctx genArg : Rust.Ty = @@ -1097,7 +1113,8 @@ module TypeInfo = // // primitiveType name |> makeLrcPtrTy com ctx // else if isInferredGenericParam com ctx name isMeasure then - mkInferTy () // mkNeverTy () + // mkNeverTy () // unstable + mkInferTy () else primitiveType name @@ -1115,6 +1132,7 @@ module TypeInfo = | Replacements.Util.BclDateTimeOffset -> transformDateTimeOffsetType com ctx | Replacements.Util.BclDateOnly -> transformDateOnlyType com ctx | Replacements.Util.BclTimeOnly -> transformTimeOnlyType com ctx + | Replacements.Util.BclRune -> transformRuneType com ctx | Replacements.Util.BclTimer -> transformTimerType com ctx | Replacements.Util.BclHashSet(genArg) -> transformHashSetType com ctx genArg | Replacements.Util.BclDictionary(k, v) -> transformHashMapType com ctx [ k; v ] @@ -1177,11 +1195,19 @@ module TypeInfo = // implemented random type | Replacements.Util.IsEntity (Types.random) (_, []) -> transformImportType com ctx [] "Random" "Random" + // implemented uri type + | Replacements.Util.IsEntity ("System.Uri") (_, []) -> transformImportType com ctx [] "Uri" "Uri" + // implemented event type (FSharpEvent`1); the module functions and // the .ctor are routed to Event_ by the events replacement, so the // concrete type must resolve to Event_::FSharpEvent`1 as well. | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.FSharpEvent`1") (_, [ t ]) -> transformImportType com ctx [ t ] "Event" "FSharpEvent`1" + | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.FSharpEvent`2") (_, [ _delegateType; argsType ]) -> + transformImportType com ctx [ argsType ] "Event" "FSharpEvent`2" + + | Replacements.Util.IsEntity ("Microsoft.FSharp.Core.CompilerServices.ResumableStateMachine`1") (_, [ t ]) -> + mkUnitTy () // spurious ResumableStateMachine type is erased to unit type // implemented regex types | Replacements.Util.IsEntity (Types.regexMatch) (_, []) -> transformImportType com ctx [] "RegExp" "Match" @@ -1355,22 +1381,6 @@ module Util = | Replacements.Util.IsEntity (Types.ienumerable) _ -> ValueSome(Fable.Any) | _ -> ValueNone - let isUnitArg (ident: Fable.Ident) = - ident.IsCompilerGenerated - && ident.Type = Fable.Unit - && (ident.DisplayName.StartsWith("unitVar", StringComparison.Ordinal) - || ident.DisplayName.Contains("@")) - - let discardUnitArg (genArgs: Fable.Type list) (args: Fable.Ident list) = - match genArgs, args with - | [ Fable.Unit ], [ arg ] -> args // don't drop unit arg when generic arg is unit - | _ -> - match args with - | [] -> [] - | [ arg ] when isUnitArg arg -> [] - | [ thisArg; arg ] when thisArg.IsThisArgument && isUnitArg arg -> [ thisArg ] - | args -> args - /// Fable doesn't currently sanitize attached members/fields so we do a simple sanitation here. /// Should this be done in FSharp2Fable step? let sanitizeMember (name: string) = @@ -1401,9 +1411,11 @@ module Util = ctx.UsedNames.CurrentDeclarationScope.Add(name) |> ignore name - type NamedTailCallOpportunity(_com: IRustCompiler, ctx, name, args: Fable.Ident list) = + type NamedTailCallOpportunity(com: IRustCompiler, ctx, name, args: Fable.Ident list) = let args = - args |> discardUnitArg [] |> List.filter (fun arg -> not (arg.IsThisArgument)) + args |> FableTransforms.discardUnitArg [] |> FableTransforms.discardThisArg + + let argTypes = args |> List.map (fun arg -> arg.Type) let label = Fable.Naming.splitLast name @@ -1413,7 +1425,15 @@ module Util = member _.IsRecursiveRef(e) = match e with - | Fable.IdentExpr ident -> name = ident.Name + | Fable.CurriedApply(Fable.IdentExpr id, callArgs, _, _) -> + let callArgTypes = callArgs |> List.map (fun arg -> arg.Type) + name = id.Name && argTypes = callArgTypes + | Fable.Call(Fable.IdentExpr id, info, _, _) -> + let callArgTypes = info.Args |> List.map (fun arg -> arg.Type) + + match Option.bind com.TryGetMember info.MemberRef with + | Some memb -> name = memb.FullName && argTypes = callArgTypes + | None -> name = id.Name && argTypes = callArgTypes | _ -> false let getDecisionTarget (ctx: Context) targetIndex = @@ -1449,6 +1469,14 @@ module Util = // transformIdent com ctx None id // | _ -> com.TransformExpr(ctx, expr) + let isTempLocal (ident: Fable.Ident) = + ident.IsCompilerGenerated + && (ident.Name = "copyOfStruct" || ident.Name = "inputRecord") + + // let isMutableOrByRef (ident: Fable.Ident) = + // (ident.IsMutable || isByRefType ident) + // && not (isTempLocal ident) + let transformIdentGet com ctx r (ident: Fable.Ident) = let expr = transformIdent com ctx r ident @@ -1467,16 +1495,22 @@ module Util = mutableSet expr value let transformIdentType com ctx isCaptured (ident: Fable.Ident) = - let ty = transformType com ctx ident.Type - - if isByRefType com ident.Type then - ty // already wrapped - elif ident.IsMutable && isCaptured then - ty |> makeMutTy com ctx |> makeLrcPtrTy com ctx - elif ident.IsMutable then - ty |> makeMutTy com ctx - else - ty + // let typ = Replacements.Util.getIdentValueType com ident ident.Type + match ident.Type with + | Replacements.Util.IsByRefType com typ when isTempLocal ident -> + // transform temporary local byref type to a reference type + transformType com ctx typ |> mkRefTy None + | _ -> + let ty = transformType com ctx ident.Type + + if isByRefType com ident.Type then + ty // already wrapped + elif ident.IsMutable && isCaptured then + ty |> makeMutTy com ctx |> makeLrcPtrTy com ctx + elif ident.IsMutable then + ty |> makeMutTy com ctx + else + ty let getField r (expr: Rust.Expr) (fieldName: string) = mkFieldExpr expr (fieldName |> sanitizeMember) // ?loc=r) @@ -1609,6 +1643,10 @@ module Util = | Fable.String, IEnumerable _ -> let chars = makeLibCall com ctx None "String" "toCharArray" [ expr ] makeLibCall com ctx None "Seq" "ofArray" [ chars ] + | Replacements.Util.Builtin(Replacements.Util.FSharpSet genArg), IEnumerable _ -> + makeLibCall com ctx None "Set" "toEnumerable" [ expr ] + | Replacements.Util.Builtin(Replacements.Util.FSharpMap(k, v)), IEnumerable _ -> + makeLibCall com ctx None "Map" "toEnumerable" [ expr ] | Replacements.Util.IsEntity (Types.hashset) _, IEnumerable _ | Replacements.Util.IsEntity (Types.iset) _, IEnumerable _ -> let ar = makeLibCall com ctx None "HashSet" "entries" [ expr ] @@ -1619,22 +1657,61 @@ module Util = let ar = makeLibCall com ctx None "HashMap" "entries" [ expr ] makeLibCall com ctx None "Seq" "ofArray" [ ar ] - // boxing value types or wrapped types - | t, Fable.Any when isValueType com t || isWrappedType com t -> expr |> boxValue com ctx + // Casting Enumerators to IDisposable + | Fable.Any, IDisposable when + (match nestedExpr.Type with + | Replacements.Util.IsEnumerator _ -> true + | _ -> false) + -> + expr + + // Boxing reads a value through an owned Rust expression. Clone first so fields + // read from reference-counted records are not moved out of their owner. + | t, Fable.Any when isValueType com t || isWrappedType com t -> expr |> makeClone |> boxValue com ctx // unboxing value types or wrapped types | Fable.Any, t when isValueType com t || isWrappedType com t -> expr |> unboxValue com ctx t - // boxing a reference-typed record/union (Lrc-wrapped) to obj: coerce the - // smart pointer to `dyn Any` so its concrete pointee stays the struct. - | Fable.DeclaredType(entRef, _), Fable.Any when isReferenceRecordOrUnion com entRef -> - [ expr ] |> makeLibCall com ctx None "Native" "box_lrc" + // Boxing a reference-typed record/union must use the same pointer representation + // as the source value. The LrcPtr case preserves identity; the explicit pointer + // attributes use a cloned underlying value so the object still carries its record + // TypeId even when Rc/Arc/Box cannot be unsized to the target object pointer. + | Fable.DeclaredType(entRef, genArgs), Fable.Any when isReferenceRecordOrUnion com entRef -> + let boxMethod = + match shouldBeRefCountWrapped com ctx (Fable.DeclaredType(entRef, genArgs)) with + | Some Lrc -> "box_lrc" + | Some Rc -> "box_rc" + | Some Arc -> "box_arc" + | Some Box -> "box_box" + | None -> "box_" + + [ expr |> makeClone ] |> makeLibCall com ctx None "Native" boxMethod + + | Fable.DeclaredType(entRef, genArgs), Fable.Any when + ctx.SkipRecordTypeRegistration && isReferenceClass com entRef + -> + [ expr |> makeClone ] |> makeLibCall com ctx None "Native" "box_" // unboxing obj back to a reference-typed record/union: downcast + re-wrap. | Fable.Any, Fable.DeclaredType(entRef, genArgs) when isReferenceRecordOrUnion com entRef -> let rawTy = transformEntityType com ctx entRef genArgs let genArgsOpt = [ rawTy ] |> mkTypesGenericArgs - [ expr ] |> makeLibCall com ctx genArgsOpt "Native" "unbox_lrc" + + let unboxMethod = + match shouldBeRefCountWrapped com ctx (Fable.DeclaredType(entRef, genArgs)) with + | Some Lrc -> "unbox_lrc" + | Some Rc -> "unbox_rc" + | Some Arc -> "unbox_arc" + | Some Box -> "unbox_box" + | None -> "unbox" + + [ expr ] |> makeLibCall com ctx genArgsOpt "Native" unboxMethod + + | Fable.Any, Fable.DeclaredType(entRef, genArgs) when + ctx.SkipRecordTypeRegistration && isReferenceClass com entRef + -> + let genArgsOpt = transformGenArgs com ctx [ Fable.DeclaredType(entRef, genArgs) ] + [ expr ] |> makeLibCall com ctx genArgsOpt "Native" "unbox" // casts to generic param | _, Fable.GenericParam(name, _isMeasure, _constraints) -> makeCall (name :: "from" :: []) None [ expr ] // e.g. T::from(value) @@ -1642,8 +1719,24 @@ module Util = // casts to IDictionary, for now does nothing // TODO: fix it | Replacements.Util.IsEntity (Types.dictionary) _, Replacements.Util.IsEntity (Types.idictionary) _ -> expr + // IEvent inherits IObservable in F#, but the generated Rust traits are flattened + // rather than related by Rust trait inheritance. Adapt the event view explicitly. + | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.IEvent`2") (_, [ _; eventType ]), + Replacements.Util.IsEntity ("System.IObservable`1") (_, [ observableType ]) when eventType = observableType -> + makeLibCall com ctx None "Event" "asObservable" [ expr ] + + // casts from interface to object + | t, Fable.Any when isInterface com t -> [ expr |> makeClone ] |> makeLibCall com ctx None "Native" "box_" + // casts from object to interface - | t1, t2 when not (isInterface com t1) && (isInterface com t2) -> makeInterfaceCast com ctx t2 expr + | t1, t2 when not (isInterface com t1) && (isInterface com t2) -> + match t1, nestedExpr with + | Fable.Any, Fable.ObjectExpr _ -> makeInterfaceCast com ctx t2 expr + | Fable.Any, _ -> + let genArgsOpt = transformGenArgs com ctx [ t2 ] + let boxedExpr = makeLibCall com ctx None "Native" "box_" [ expr ] + [ boxedExpr ] |> makeLibCall com ctx genArgsOpt "Native" "unbox" + | _ -> makeInterfaceCast com ctx t2 expr // casts from interface to interface | _, t when isInterface com t -> expr |> makeClone |> mkCastExpr ty //TODO: not working, implement @@ -1720,7 +1813,7 @@ module Util = | [ Fable.Unit ] -> [] | _ -> argTypes - let argCount = string (List.length argTypes) + let argCount = string(List.length argTypes) let funcWrap = getLibraryImportName com ctx "Native" ("Func" + argCount) let expr = transformIdent com ctx None ident @@ -1787,7 +1880,32 @@ module Util = let ctx = { ctx with IsParamByRefPreferred = isByRefPreferred || ctx.IsParamByRefPreferred } - transformLeaveContext com ctx argType arg + match argType, arg.Type with + | Some Fable.Any, actualType -> + match arg, actualType with + | MaybeCasted(Fable.IdentExpr ident), _ when ident.IsThisArgument && ctx.IsAssocMember -> + let expr = transformLeaveContext com ctx (Some Fable.Any) arg + [ expr |> makeClone ] |> makeLibCall com ctx None "Native" "box_lrc" + | _, Fable.DeclaredType(entRef, genArgs) when isReferenceClass com entRef -> + let expr = transformLeaveContext com ctx (Some Fable.Any) arg + + let boxMethod = + match shouldBeRefCountWrapped com ctx (Fable.DeclaredType(entRef, genArgs)) with + | Some Lrc -> "box_lrc" + | Some Rc -> "box_rc" + | Some Arc -> "box_arc" + | Some Box -> "box_box" + | None -> "box_" + + [ expr |> makeClone ] |> makeLibCall com ctx None "Native" boxMethod + | _, actualType when actualType <> Fable.Any -> transformCast com ctx Fable.Any arg + | _ -> transformLeaveContext com ctx (Some Fable.Any) arg + | Some(Replacements.Util.IsEntity ("System.IObservable`1") (_, [ observableType ])), + Replacements.Util.IsEntity ("Microsoft.FSharp.Control.IEvent`2") (_, [ _; eventType ]) when + observableType = eventType + -> + transformCast com ctx observableType arg + | _ -> transformLeaveContext com ctx argType arg ) let makeRefForPatternMatch (com: IRustCompiler) ctx typ (nameOpt: string option) fableExpr = @@ -1912,11 +2030,6 @@ module Util = makeLibCall com ctx None "Native" "is_null" [ value |> mkAddrOfExpr ] let makeNull com ctx (typ: Fable.Type) = - let typ = - match typ with - | Fable.Any -> Fable.Unit - | t -> t - let genArgsOpt = transformGenArgs com ctx [ typ ] makeLibCall com ctx genArgsOpt "Native" "null" [] @@ -1935,19 +2048,27 @@ module Util = // unsized `dyn Any`, which does not compile). makeLibCall com ctx None "Native" "getZeroObj" [] | _ -> - // Only concrete (sized) `Lrc`-wrapped reference types can use the null-ref - // placeholder. Unsized refs (interfaces, enumerators) and Arc/Box-wrapped - // types keep `getZero` (they don't reach this path in practice). - let isConcreteLrcRef = - match shouldBeRefCountWrapped com ctx typ with - | Some Lrc -> - match typ with - | Fable.DeclaredType(entRef, _) -> not (com.GetEntity(entRef)).IsInterface - | Replacements.Util.IsEnumerator _ -> false - | _ -> true - | _ -> false + // Everything with a `NullableRef` implementation takes the null-ref + // placeholder: concrete (sized) `Lrc`-wrapped reference types, plus + // string, option, array and list, whose library types implement it + // directly. What is left is the primitives, for which `mem::zeroed` + // is a valid bit pattern. + let isNullableRef = + match typ with + | Fable.String + | Fable.Option _ + | Fable.Array _ + | Fable.List _ -> true + | _ -> + match shouldBeRefCountWrapped com ctx typ with + | Some Lrc -> + match typ with + | Fable.DeclaredType(entRef, _) -> not (com.GetEntity(entRef)).IsInterface + | Replacements.Util.IsEnumerator _ -> false + | _ -> true + | _ -> false - if isConcreteLrcRef then + if isNullableRef then makeNull com ctx typ else let genArgsOpt = transformGenArgs com ctx [ typ ] @@ -2039,6 +2160,23 @@ module Util = let ent = com.GetEntity(entRef) let idents = getEntityFieldsAsIdents com ent + // An F# `exception` declaration gets a synthetic __base__ field, so that + // the generated Deref to System.Exception (and hence .Message) works. + // A class constructor puts a matching value at the head of its NewRecord, + // but an exception constructor carries only the declared fields, leaving + // one more ident than values. Supply the base here instead of zipping + // lists of different lengths, which failed with no source location. + let baseFields, idents = + match idents with + | baseIdent :: restIdents when + ent.IsFSharpExceptionDeclaration && List.length restIdents = List.length values + -> + let msg = mkStrLitExpr "" |> makeStaticString com ctx + let baseExpr = makeLibCall com ctx None "Util" "new_Exception" [ msg ] + let fieldName = baseIdent.Name |> sanitizeMember + [ mkExprField [] fieldName baseExpr false false ], restIdents + | _ -> [], idents + let fields = List.zip idents values |> List.map (fun (ident, value) -> @@ -2064,13 +2202,14 @@ module Util = mkExprField [] fieldName expr false false ) - let fields = List.append fields phantomFields + let fields = List.append baseFields (List.append fields phantomFields) let genArgsOpt = transformGenArgs com ctx genArgs let entName = getEntityFullName com ctx entRef let path = makeFullNamePath entName genArgsOpt let expr = mkStructExpr path fields // TODO: range - expr |> maybeWrapSmartPtr com ctx ent + let expr = expr |> maybeWrapSmartPtr com ctx ent + expr let tryUseKnownUnionCaseNames fullName = match fullName with @@ -2141,12 +2280,13 @@ module Util = let fmt = makeFormatString parts makeFormatExpr com ctx fmt values - // Builds a rich reflection value for `typeof`. The emitted expression + // Builds a rich reflection value for a record declaration. The emitted expression // registers the record's field metadata + constructor/getter closures keyed by // its concrete TypeId and returns a boxed RecordTypeInfo (the runtime value that // a `System.Type` holds on the Rust target). This backs FSharpValue.MakeRecord, // FSharpValue.GetRecordFields/GetRecordField and FSharpType.GetRecordFields. let makeRecordTypeInfo (com: IRustCompiler) ctx r (entRef: Fable.EntityRef) genArgs (typ: Fable.Type) : Rust.Expr = + let ctx = { ctx with SkipRecordTypeRegistration = true } let ent = com.GetEntity(entRef) let idents = getEntityFieldsAsIdents com ent let objType = Fable.Any @@ -2195,7 +2335,7 @@ module Util = |> List.map (fun ident -> let oIdent = makeTypedIdent objType "o" let recExpr = Fable.TypeCast(Fable.IdentExpr oIdent, typ) // unbox obj -> Foo - let fieldInfo = Fable.FieldInfo.Create(ident.Name, ident.Type) + let fieldInfo = Fable.FieldInfo.Create(ident.Name, ident.Type, ident.IsMutable) let fieldGet = Fable.Get(recExpr, fieldInfo, ident.Type, None) let body = Fable.TypeCast(fieldGet, objType) // box the field value Fable.Delegate([ oIdent ], body, None, Fable.Tags.empty) @@ -2206,10 +2346,26 @@ module Util = makeLibCall com ctx None "Reflection" "recordType" [ tidExpr; nameExpr; fieldNamesExpr; makeExpr; gettersExpr ] + let makeRecordTypeInfoItem (com: IRustCompiler) ctx (ent: Fable.Entity) genArgs = + let typ = Fable.DeclaredType(ent.Ref, genArgs) + + let body = + makeRecordTypeInfo com ctx None ent.Ref genArgs typ |> mkExprBlock |> Some + + let returnType = transformType com ctx Fable.MetaType |> mkFnRetTy + let fnDecl = mkFnDecl [] returnType + let fnKind = mkFnKind DEFAULT_FN_HEADER fnDecl NO_GENERICS body + + let attr = [ mkAttr "cfg" [ "feature = \"reflection\"" ] ] + mkFnAssocItem attr "__type_info__" fnKind |> mkPublicAssocItem + let makeTypeInfo (com: IRustCompiler) ctx r (typ: Fable.Type) : Rust.Expr = match typ with | Fable.DeclaredType(entRef, genArgs) when (com.GetEntity(entRef)).IsFSharpRecord -> - makeRecordTypeInfo com ctx r entRef genArgs typ + let entName = getEntityFullName com ctx entRef + let genArgsOpt = transformGenArgs com ctx genArgs + let callee = makeStaticCallPathExpr (entName + "::__type_info__") genArgsOpt None + mkCallExpr callee [] | _ -> // Non-record `System.Type` value: carry the concrete `TypeId`, boxed as // `LrcPtr` so it flows through the same `obj` slot as record type @@ -2332,6 +2488,19 @@ module Util = let ctx = { ctx with IsParamByRefPreferred = false } com.TransformExpr(ctx, e) + let expr = + if isThisArgumentIdentExpr ctx e && not targetIsRef && not ctx.IsFluentMember then + let expr = expr |> makeClone + + match shouldBeRefCountWrapped com ctx e.Type with + | Some Lrc -> makeLrcPtrValue com ctx expr + | Some Rc -> makeRcValue com ctx expr + | Some Arc -> makeArcValue com ctx expr + | Some Box -> makeBoxValue com ctx expr + | None -> expr + else + expr + match sourceIsRef, targetIsRef, implCopy, implClone, mustClone, isUnreachable with | true, true, _, _, _, false -> expr | false, true, _, _, _, false -> expr |> mkAddrOfExpr @@ -2376,7 +2545,6 @@ module Util = assert (ent.IsInterface) FSharp2Fable.Util.getInterfaceMembers com ent - |> Seq.filter (fun (ifc, memb) -> not memb.IsProperty) |> Seq.distinctBy (fun (ifc, memb) -> memb.CompiledName) // skip inherited overwrites let getInterfaceMemberNames (com: IRustCompiler) (entRef: Fable.EntityRef) = @@ -2387,7 +2555,11 @@ module Util = |> Set.ofSeq let makeObjectExprMemberDecl (com: IRustCompiler) ctx (memb: Fable.ObjectExprMember) = - let capturedIdents = getCapturedIdents com ctx (Some memb.Name) memb.Args memb.Body + let capturedIdents = + getCapturedIdents com ctx (Some memb.Name) memb.Args memb.Body + |> Map.toList + |> List.map (fun (_, (ident: Fable.Ident)) -> ident.Name, ident) + |> Map.ofList let thisArg = { makeIdent selfName with IsThisArgument = true } |> Fable.IdentExpr @@ -2458,17 +2630,6 @@ module Util = memberDecl ) - let decl: Fable.ClassDecl = - { - Name = entName - Entity = entRef - Constructor = None - BaseCall = baseCall - AttachedMembers = members - XmlDoc = None - Tags = [] - } - let fieldIdents = fieldsMap.Values // |> Seq.map (fun ident -> { ident with Type = FableTransforms.uncurryType ident.Type }) @@ -2478,6 +2639,18 @@ module Util = fieldIdents |> List.map (fun ident -> let expr = transformIdent com ctx None ident |> makeClone + + let expr = + if ident.IsThisArgument then + match shouldBeRefCountWrapped com ctx ident.Type with + | Some Lrc -> makeLrcPtrValue com ctx expr + | Some Rc -> makeRcValue com ctx expr + | Some Arc -> makeArcValue com ctx expr + | Some Box -> makeBoxValue com ctx expr + | None -> expr + else + expr + let fieldName = ident.Name |> sanitizeMember mkExprField [] fieldName expr false false ) @@ -2490,6 +2663,122 @@ module Util = else fieldIdents + let interfaceGenMap = + let genericNames = + ent.GenericParameters |> List.map (fun parameter -> parameter.Name) + + List.zip genericNames genArgs |> Map + + let existingMemberNames = + members + |> List.map (getDeclMember com >> fun memberValue -> memberValue.CompiledName) + |> Set.ofList + + let eventPropertyNames = + existingMemberNames + |> Seq.choose (fun memberName -> + if memberName.StartsWith("add_", StringComparison.Ordinal) then + Some(memberName.Substring(4)) + elif memberName.StartsWith("remove_", StringComparison.Ordinal) then + Some(memberName.Substring(7)) + else + None + ) + |> Set.ofSeq + + let eventGetters = + FSharp2Fable.Util.getInterfaceMembers com ent + |> Seq.filter (fun (_, interfaceMember) -> + interfaceMember.CompiledName.StartsWith("get_", StringComparison.Ordinal) + ) + |> Seq.distinctBy (fun (_, interfaceMember) -> interfaceMember.CompiledName) + |> Seq.choose (fun (_, interfaceMember) -> + let propertyName = interfaceMember.CompiledName.Substring(4) + + if + Set.contains interfaceMember.CompiledName existingMemberNames + || not (Set.contains propertyName eventPropertyNames) + then + None + else + fieldIdents + |> List.tryFind (fun ident -> + ident.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase) + ) + |> Option.map (fun backingField -> + let propertyType = + interfaceMember.ReturnParameter.Type + |> resolveInlineType interfaceGenMap + |> FableTransforms.uncurryType + + let interfaceType = Fable.DeclaredType(entRef, genArgs) + + let thisArg = { makeTypedIdent interfaceType "this" with IsThisArgument = true } + + let objectSelf = + { makeIdent selfName with IsThisArgument = true } |> Fable.IdentExpr + + let eventExpr = + Fable.Get( + objectSelf, + Fable.FieldInfo.Create(backingField.Name, backingField.Type), + backingField.Type, + None + ) + + let body = + Fable.Get( + eventExpr, + Fable.FieldInfo.Create("Publish", propertyType), + propertyType, + None + ) + + let memberRef = + Fable.MemberRef( + interfaceMember.DeclaringEntity |> Option.defaultValue entRef, + { + IsInstance = true + CompiledName = interfaceMember.CompiledName + NonCurriedArgTypes = Some [] + AttributeFullNames = [] + } + ) + + let memberDecl: Fable.MemberDecl = + { + Name = interfaceMember.CompiledName + Args = [ thisArg ] + Body = body + MemberRef = memberRef + IsMangled = false + ImplementedSignatureRef = None + UsedNames = Set.empty + XmlDoc = None + Tags = [] + } + + memberDecl + ) + ) + |> Seq.toList + + let members = members @ eventGetters + + let decl: Fable.ClassDecl = + { + Name = entName + Entity = entRef + Constructor = None + BaseCall = baseCall + AttachedMembers = members + XmlDoc = None + Tags = [] + } + + let objectGenArgs = + FSharp2Fable.Util.getGenParamTypes (genArgs @ (fieldIdents |> List.map (fun ident -> ident.Type))) + let fields = fieldIdents |> List.map (fun ident -> @@ -2507,13 +2796,13 @@ module Util = exprField :: exprFields | _ -> exprFields - let attrs = [ mkAttr "derive" (makeDerivedFrom com ctx ent) ] - let generics = makeGenerics com ctx genArgs - let genParams = FSharp2Fable.Util.getGenParamTypes genArgs + let attrs = [ mkAttr "derive" [ rawIdent "Clone" ] ] + let generics = makeGenerics com ctx objectGenArgs + let genParams = FSharp2Fable.Util.getGenParamTypes objectGenArgs let structItems = [ mkStructItem attrs entName fields generics ] - let memberItems = transformClassMembers com ctx genArgs decl + let memberItems = transformClassMembers com ctx objectGenArgs decl (Some genArgs) let objExpr = // match baseCall with @@ -2739,6 +3028,12 @@ module Util = if needGenArgs && (mi.IsModuleMember || not mi.IsInstanceMember) then match typ with | Fable.Tuple _ -> transformGenArgs com ctx [ typ ] + // FSharpEvent`2 has two F# generic args but the Rust FSharpEvent_2 + // struct only takes the args type, so drop the delegate type + | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.FSharpEvent`2") (_, + [ _delegateType + argsType ]) -> + transformGenArgs com ctx [ argsType ] | _ -> transformGenArgs com ctx typ.Generics else None @@ -2750,9 +3045,10 @@ module Util = mkCallExpr callee args | _ -> + let fableExpr = Fable.Call(calleeExpr, callInfo, typ, range) + match ctx.TailCallOpportunity with - | Some tc when tc.IsRecursiveRef(calleeExpr) && List.length tc.Args = List.length callInfo.Args -> - optimizeTailCall com ctx range tc callInfo.Args + | Some tc when tc.IsRecursiveRef(fableExpr) -> optimizeTailCall com ctx range tc callInfo.Args | _ -> match callInfo.ThisArg, calleeExpr, membOpt with | Some thisArg, Fable.IdentExpr ident, Some memb when memb.IsExtension -> @@ -2948,19 +3244,8 @@ module Util = let expr = transformLeaveContext com ctx None e mkExprStmt expr - // Only the block's true tail can omit the trailing `;`. A block-like expression (`if`, - // `match`, nested `{ }`) in a non-tail statement needs it forced via `Semi`, or a - // non-`()`-typed value there is a Rust type error, not just a style choice. let transformExprsAsStmts (com: IRustCompiler) ctx (exprs: Fable.Expr list) : Rust.Stmt list = - match List.rev exprs with - | [] -> [] - | last :: revRest -> - let nonTailStmts = - revRest - |> List.rev - |> List.map (fun e -> transformLeaveContext com ctx None e |> mkSemiStmt) - - nonTailStmts @ [ transformAsStmt com ctx last ] + List.map (transformAsStmt com ctx) exprs // flatten nested Let binding expressions let rec flattenLet acc (expr: Fable.Expr) = @@ -2984,7 +3269,7 @@ module Util = let prepareLambdaArgsAndCtx ctx args body = let ctx = { ctx with IsLambda = true } let genArgs, ctx = getNewGenArgsAndCtx ctx args body - let args = args |> discardUnitArg genArgs + let args = args |> FableTransforms.discardUnitArg genArgs genArgs, ctx, args let getLetCapturedNames bindings letBody = @@ -2993,8 +3278,39 @@ module Util = let addCapturedNames expr = capturedNames.UnionWith(FableTransforms.getCapturedNames expr) + // A try/with/finally lowers to closures on this target, but the Fable AST + // has no Lambda there, so the shared capture walk does not see those + // bodies as closure contexts. Everything they reference is captured all + // the same: without this a `let mutable` assigned in a finally block was + // emitted as a bare MutCell, and the closure mutated a clone of it. + let addTryCatchCaptures expr = + expr + |> deepExists (fun e -> + match e with + | Fable.TryCatch(body, catch, finalizer, _) -> + [ Some body; catch |> Option.map snd; finalizer ] + |> List.choose id + |> List.iter (fun part -> + part + |> deepExists (fun inner -> + match inner with + | Fable.IdentExpr ident -> + capturedNames.Add ident.Name |> ignore + false + | _ -> false + ) + |> ignore + ) + + false + | _ -> false + ) + |> ignore + bindings |> List.iter (snd >> addCapturedNames) addCapturedNames letBody + bindings |> List.iter (snd >> addTryCatchCaptures) + addTryCatchCaptures letBody capturedNames let getScopedIdentCtx com ctx (ident: Fable.Ident) isArm isRef isBox isFunc usages = @@ -3197,7 +3513,7 @@ module Util = mkForLoopExpr None varPat rangeExpr bodyExpr // ?loc=range) let makeLocalLambda com ctx (args: Fable.Ident list) (body: Fable.Expr) = - let args = args |> discardUnitArg [] + let args = args |> FableTransforms.discardUnitArg [] let fnDecl = transformFunctionDecl com ctx args [] Fable.Unit let fnBody = transformExpr com ctx body mkClosureExpr false fnDecl fnBody @@ -3258,9 +3574,10 @@ module Util = | _ -> false let transformCurriedApply (com: IRustCompiler) ctx r typ calleeExpr args = + let fableExpr = Fable.CurriedApply(calleeExpr, args, typ, r) + match ctx.TailCallOpportunity with - | Some tc when tc.IsRecursiveRef(calleeExpr) && List.length tc.Args = List.length args -> - optimizeTailCall com ctx r tc args + | Some tc when tc.IsRecursiveRef(fableExpr) -> optimizeTailCall com ctx r tc args | _ -> let callee = transformCallee com ctx calleeExpr @@ -3307,14 +3624,16 @@ module Util = (mkInferTy ()) :: (transformGenTypes com ctx [ typ ]) |> mkTypesGenericArgs match fableExpr with - | Fable.IdentExpr ident when isDowncast -> + | MaybeCasted(Fable.IdentExpr ident) when isDowncast -> let downcastExpr = makeLibCall com ctx genArgsOpt "Native" "try_downcast" [ expr ] let pat = makeUnionCasePat (rawIdent "Some") [ makeFullNameIdentPat ident.Name ] mkLetExpr pat downcastExpr | _ -> makeLibCall com ctx genArgsOpt "Native" "type_test" [ expr ] - let makeUnionCasePatOpt (com: IRustCompiler) ctx typ nameOpt caseIndex = + let rec makeUnionCasePatOpt (com: IRustCompiler) ctx typ nameOpt caseIndex = match typ with + | Replacements.Util.IsInRefType com typ -> makeUnionCasePatOpt com ctx typ nameOpt caseIndex + | Fable.Option(genArg, _) -> // let genArgsOpt = transformGenArgs com ctx [genArg] let unionCaseFullName = [ "Some"; "None" ] |> List.item caseIndex |> rawIdent @@ -3335,6 +3654,7 @@ module Util = let pat = makeUnionCasePat unionCaseName fields Some(pat) + | Fable.DeclaredType(entRef, genArgs) -> let ent = com.GetEntity(entRef) @@ -3397,8 +3717,9 @@ module Util = mkIfThenElseExpr guardExpr thenExpr elseExpr let transformSwitch (com: IRustCompiler) ctx (evalExpr: Fable.Expr) cases defaultCase : Rust.Expr = - let namesForIndex evalType evalName caseIndex = //todo refactor with below + let rec namesForIndex evalType evalName caseIndex = //todo refactor with below match evalType with + | Replacements.Util.IsInRefType com typ -> namesForIndex typ evalName caseIndex | Fable.Option(genArg, _) -> match evalName with | Some idName -> @@ -3570,15 +3891,19 @@ module Util = | _, Fable.Value((Fable.CharConstant _ | Fable.NumberConstant _), _) -> Some(left, right) | Fable.Value((Fable.CharConstant _ | Fable.NumberConstant _), _), _ -> Some(right, left) | _ -> None - // Only a plain-ident scrutinee is convertible: transformSwitch can't recover the - // `Option` type from anything else, and would emit bogus `0_i32` patterns. + // Only a plain-ident scrutinee is convertible: transformSwitch recovers the + // union (or `Option`) type from the ident, and without one it falls back to + // `evalExpr.Type`, which is the tag's `int32`. `makeUnionCasePatOpt` then + // declines and the arm becomes a bare `1_i32` literal pattern matched + // against the union value itself, which does not compile. Anything else is + // left to the if/else decision-tree path, which tests with `if let`. | Fable.Test(Fable.IdentExpr _ as expr, Fable.OptionTest isSome, r) -> let evalExpr = Fable.Get(expr, Fable.UnionTag, Fable.Number(Int32, Fable.NumberInfo.Empty), r) let right = makeIntConst (makeTest isSome 0 1) Some(evalExpr, right) - | Fable.Test(expr, Fable.UnionCaseTest tag, r) -> + | Fable.Test(Fable.IdentExpr _ as expr, Fable.UnionCaseTest tag, r) -> let evalExpr = Fable.Get(expr, Fable.UnionTag, Fable.Number(Int32, Fable.NumberInfo.Empty), r) @@ -3632,13 +3957,22 @@ module Util = | _ -> None let simplifyDecisionTree (treeExpr: Fable.Expr) = + let rec containsTypeTest expr = + match expr with + | Fable.Test(_, Fable.TypeTest _, _) -> true + | expr -> getSubExpressions expr |> List.exists containsTypeTest + treeExpr |> visitFromInsideOut ( function | Fable.IfThenElse(guardExpr1, Fable.IfThenElse(guardExpr2, thenExpr, Fable.DecisionTreeSuccess(index2, [], _), _), Fable.DecisionTreeSuccess(index1, [], t), - r) when index1 = index2 -> + r) when + index1 = index2 + && not (containsTypeTest guardExpr1) + && not (containsTypeTest guardExpr2) + -> Fable.IfThenElse( makeLogOp None guardExpr1 guardExpr2 LogicalAnd, thenExpr, @@ -3946,26 +4280,72 @@ module Util = match entryPoint with | Some path -> // add some imports for main function - let asArr = getLibraryImportName com ctx "NativeArray" "array_from" - let asStr = getLibraryImportName com ctx "String" "fromString" + let ofArr = getLibraryImportName com ctx "NativeArray" "array_from" + let ofStr = getLibraryImportName com ctx "String" "fromString" + let asStr = getLibraryImportName com ctx "String" "string" // main entrypoint let mainName = String.concat "::" path - let strBody = - [ - $"let args = std::env::args().skip(1).map(%s{asStr}).collect()" - $"%s{mainName}(%s{asArr}(args))" - ] + // main function + let mainFnItem = + let strBody = + [ + $"let iter = std::env::args()" + $"let args = iter.skip(1).map(%s{ofStr}).collect()" + $"%s{mainName}(%s{ofArr}(args))" + ] + + let fnBody = strBody |> Seq.map mkEmitSemiStmt |> mkBlock |> Some + + let attrs = [ mkAttr "cfg" [ "not(feature = \"no_std\")" ] ] + let fnDecl = mkFnDecl [] VOID_RETURN_TY + let fnKind = mkFnKind DEFAULT_FN_HEADER fnDecl NO_GENERICS fnBody + let fnItem = mkFnItem attrs "main" fnKind + fnItem |> mkPublicItem + + // main function (no_std) + let mainFnItemNoStd = + let strBody = + [ + $"let iter = fable_library_rust::Native_::get_args(argc, argv)" + $"let args = iter.skip(1).map(%s{asStr}).collect()" + $"%s{mainName}(%s{ofArr}(args)) as u32" + ] + + let lastStatementIndex = List.length strBody - 1 - let fnBody = strBody |> Seq.map mkEmitSemiStmt |> mkBlock |> Some + let fnBody = + strBody + |> List.mapi (fun index statement -> + if index = lastStatementIndex then + mkEmitExprStmt statement + else + mkEmitSemiStmt statement + ) + |> mkBlock + |> Some - let attrs = [ mkAttr "cfg" [ "not(feature = \"no_std\")" ] ] - let fnDecl = mkFnDecl [] VOID_RETURN_TY - let fnKind = mkFnKind DEFAULT_FN_HEADER fnDecl NO_GENERICS fnBody - let fnItem = mkFnItem attrs "main" fnKind - [ fnItem |> mkPublicItem ] + let attrs = + [ mkAttr "cfg" [ "feature = \"no_std\"" ]; mkAttr "r#unsafe" [ "no_mangle" ] ] + + let argcTy = primitiveType "isize" + let argvTy = primitiveType "u8" |> mkPtrTy |> mkPtrTy + let retTy = primitiveType "u32" |> mkFnRetTy + + let args = + [ + mkTypedParam "argc" argcTy false false + mkTypedParam "argv" argvTy false false + ] + let fnHeader = mkFnHeader false false false (Some "C") + let fnDecl = mkFnDecl args retTy + let fnKind = mkFnKind fnHeader fnDecl NO_GENERICS fnBody + let fnItem = mkFnItem attrs "main" fnKind + fnItem |> mkPublicItem + + [ mainFnItem; mainFnItemNoStd ] | None -> [] let getEntityPhantomGenParams _com (ent: Fable.Entity) : (string * Fable.Type) list = @@ -3984,6 +4364,7 @@ module Util = let getEntityFieldsAsIdents com (ent: Fable.Entity) : Fable.Ident list = let fieldIdents = ent.FSharpFields + |> List.filter (fun field -> not field.IsStatic) |> List.map (fun field -> let name = field.Name let typ = FableTransforms.uncurryType field.FieldType @@ -4152,9 +4533,9 @@ module Util = TailCallOpportunity = tco } - let isTailRecursive (nameOpt: string option) (body: Fable.Expr) = + let isTailRecursive com (nameOpt: string option) (args: Fable.Ident list) (body: Fable.Expr) = match nameOpt with - | Some name -> FableTransforms.isTailRecursive name body + | Some name -> FableTransforms.isTailRecursive com name args body | None -> false, false let transformFunctionBody com ctx (args: Fable.Ident list) (body: Fable.Expr) = @@ -4184,9 +4565,9 @@ module Util = | _ -> transformLeaveContext com ctx None body let transformFunc com ctx parameters returnType (name: string option) (args: Fable.Ident list) (body: Fable.Expr) = - let isRecursive, isTailRec = isTailRecursive name body + let isRecursive, isTailRec = isTailRecursive com name args body let genArgs, ctx = getNewGenArgsAndCtx ctx args body - let args = args |> discardUnitArg genArgs + let args = args |> FableTransforms.discardUnitArg genArgs // let returnType = // match body.Type with @@ -4209,7 +4590,7 @@ module Util = (capturedIdentsOpt: Map option) = let genArgs, ctx, args = prepareLambdaArgsAndCtx ctx args body - let isRecursive, isTailRec = isTailRecursive name body + let isRecursive, isTailRec = isTailRecursive com name args body let fnDecl = transformFunctionDecl com ctx args [] Fable.Unit let ctx = getFunctionBodyCtx com ctx name args body isTailRec @@ -4516,9 +4897,7 @@ module Util = let transformModuleAction (com: IRustCompiler) ctx (body: Fable.Expr) = // optional, uses startup::on_startup! for static execution (before main). // See also: https://doc.rust-lang.org/1.6.0/complement-design-faq.html#there-is-no-life-before-or-after-main-no-static-ctorsdtors - "For Rust, support for F# static and module do bindings is disabled by default. " - + "It can be enabled with the 'static_do_bindings' feature. Use at your own risk!" - |> addWarning com [] body.Range + WarningCodes.staticDoBindingsDisabled |> addWarningWithCode com [] body.Range let expr = transformExpr com ctx body let attrs = [] //[mkAttr "cfg" ["feature = \"static_do_bindings\""]] @@ -4543,19 +4922,37 @@ module Util = let isByRefPreferred = memb.Attributes |> Seq.exists (fun a -> a.Entity.FullName = Atts.rustByRef) + let hasFactAttribute = + memb.Attributes + |> Seq.exists (fun a -> a.Entity.FullName.EndsWith(".FactAttribute", StringComparison.Ordinal)) + + let asyncTestReturnType = + match hasFactAttribute, decl.Body.Type with + | true, Replacements.Util.IsEntity (Types.fsharpAsyncGeneric) (_, [ returnType ]) -> Some returnType + | _ -> None + + let returnType = asyncTestReturnType |> Option.defaultValue decl.Body.Type // memb.ReturnParameter.Type + let fnDecl, fnBody, genArgs = let ctx = { ctx with IsParamByRefPreferred = isByRefPreferred } let parameters = memb.CurriedParameterGroups |> List.concat - let returnType = decl.Body.Type // memb.ReturnParameter.Type transformFunc com ctx parameters returnType (Some memb.FullName) decl.Args decl.Body + let fnBody = + match asyncTestReturnType with + | Some _returnType -> + // wrap the async test body with runSynchronously + let none = mkGenericPathExpr [ rawIdent "None" ] None + makeLibCall com ctx None "Async" "runSynchronously" [ fnBody; none; none ] + | None -> fnBody + let isUnsafe = memb.Attributes |> Seq.exists (fun a -> a.Entity.FullName = Atts.rustUnsafe) let fnBodyBlock = if isUnsafe then fnBody |> mkUnsafeBlockExpr |> mkBodyBlock - else if decl.Body.Type = Fable.Unit then + else if returnType = Fable.Unit then mkSemiBlock fnBody else mkExprBlock fnBody @@ -4654,7 +5051,7 @@ module Util = = let ctx = { ctx with IsAssocMember = true } let name = memb.CompiledName - let args = args |> discardUnitArg [] + let args = args |> FableTransforms.discardUnitArg [] let parameters = memb.CurriedParameterGroups |> List.concat let returnType = memb.ReturnParameter.Type |> FableTransforms.uncurryType let fnDecl = transformFunctionDecl com ctx args parameters returnType @@ -4701,6 +5098,15 @@ module Util = let args = decl.Args let body = decl.Body + let returnType = memb.ReturnParameter.Type |> FableTransforms.uncurryType + + let isFluentMember = + match memb.DeclaringEntity, returnType with + | Some declaringEntity, Fable.DeclaredType(returnEntity, _) -> + declaringEntity.FullName = returnEntity.FullName + | _ -> false + + let ctx = { ctx with IsFluentMember = isFluentMember } let body = if memb.IsInstance && not (memb.IsConstructor) then @@ -4716,7 +5122,7 @@ module Util = let parameters = memb.CurriedParameterGroups |> List.concat // let returnType = memb.ReturnParameter.Type |> FableTransforms.uncurryType let returnType = body.Type // |> FableTransforms.uncurryType - transformFunc com ctx parameters returnType (Some name) args body + transformFunc com ctx parameters returnType (Some memb.FullName) args body let fnBody = if isIdentAtTailPos (fun ident -> ident.IsThisArgument) body then @@ -4823,7 +5229,11 @@ module Util = let entName = Fable.Naming.splitLast ent.FullName let genArgs = FSharp2Fable.Util.getEntityGenArgs ent let generics = makeGenerics com ctx genArgs - let isPublic = ent.IsFSharpRecord + // F# exception fields (Data0, Data1, ...) are public on .NET, and an + // exception declared in one crate has to be constructible from another — + // MatchFailureException lives in the runtime library but is constructed + // by user code for any match F# cannot prove exhaustive. + let isPublic = ent.IsFSharpRecord || ent.IsFSharpExceptionDeclaration let idents = getEntityFieldsAsIdents com ent let fields = @@ -5052,6 +5462,10 @@ module Util = let fieldValues = getEntityFieldsAsIdents com ent + // Skip the synthetic __base__ field. The message describes the + // exception's own data; formatting the base System.Exception + // with {:?} also walks its null innerException, which faults. + |> List.filter (fun ident -> ident.Name <> baseName) |> List.map (fun ident -> let info = Fable.FieldInfo.Create(ident.Name, ident.Type, ident.IsMutable) Fable.Get(thisArg, info, ident.Type, None) @@ -5170,11 +5584,13 @@ module Util = // } // impl Eq for {self_ty} {} // impl core::hash::Hash for {self_ty} { + // #[inline] // fn hash(&self, state: &mut H) { // core::hash::Hash::hash(&(self as *const Self as usize), state) // } // } // impl Native_::Hashable for {self_ty} { + // #[inline] // fn getHashCode(&self) -> i32 { Native_::referenceHash(self) } // } @@ -5216,7 +5632,7 @@ module Util = let generics = [ hParam ] |> mkGenerics let fnKind = mkFnKind DEFAULT_FN_HEADER fnDecl generics fnBody - mkFnAssocItem [] "hash" fnKind + mkFnAssocItem [ mkAttr "inline" [] ] "hash" fnKind let getHashCodeFnItem = let referenceHashName = getLibraryImportName com ctx "Native" "referenceHash" @@ -5420,14 +5836,81 @@ module Util = let ifcEnt = com.GetEntity(ifcEntRef) FSharp2Fable.Util.getEntityGenArgs ifcEnt + let specializeParameter (genMap: Map) (parameter: Fable.Parameter) = + { new Fable.Parameter with + member _.Attributes = parameter.Attributes + member _.Name = parameter.Name + member _.Type = parameter.Type |> resolveInlineType genMap + member _.IsIn = parameter.IsIn + member _.IsOut = parameter.IsOut + member _.IsNamed = parameter.IsNamed + member _.IsOptional = parameter.IsOptional + member _.DefaultValue = parameter.DefaultValue + } + + let specializeInterfaceMember (genMap: Map) (memberValue: Fable.MemberFunctionOrValue) = + { new Fable.MemberFunctionOrValue with + member _.DisplayName = memberValue.DisplayName + member _.CompiledName = memberValue.CompiledName + member _.FullName = memberValue.FullName + member _.Attributes = memberValue.Attributes + member _.HasSpread = memberValue.HasSpread + member _.IsInline = memberValue.IsInline + member _.IsPublic = memberValue.IsPublic + member _.IsPrivate = memberValue.IsPrivate + member _.IsInternal = memberValue.IsInternal + member _.IsConstructor = memberValue.IsConstructor + member _.IsInstance = memberValue.IsInstance + member _.IsExtension = memberValue.IsExtension + member _.IsValue = memberValue.IsValue + member _.IsMutable = memberValue.IsMutable + member _.IsGetter = memberValue.IsGetter + member _.IsSetter = memberValue.IsSetter + member _.IsProperty = memberValue.IsProperty + + member _.IsOverrideOrExplicitInterfaceImplementation = + memberValue.IsOverrideOrExplicitInterfaceImplementation + + member _.IsDispatchSlot = memberValue.IsDispatchSlot + + member _.GenericParameters = + memberValue.GenericParameters + |> List.filter (fun parameter -> not (Map.containsKey parameter.Name genMap)) + + member _.CurriedParameterGroups = + memberValue.CurriedParameterGroups + |> List.map (List.map (specializeParameter genMap)) + + member _.ReturnParameter = specializeParameter genMap memberValue.ReturnParameter + member _.DeclaringEntity = memberValue.DeclaringEntity + member _.ApparentEnclosingEntity = memberValue.ApparentEnclosingEntity + member _.ImplementedAbstractSignatures = memberValue.ImplementedAbstractSignatures + member _.XmlDoc = memberValue.XmlDoc + } + let ignoredInterfaceNames = set [ Types.ienumerable; Types.ienumerator ] - let transformClassMembers (com: IRustCompiler) ctx genArgs (classDecl: Fable.ClassDecl) = + let transformClassMembers + (com: IRustCompiler) + ctx + genArgs + (classDecl: Fable.ClassDecl) + (objectExprInterfaceGenArgs: Fable.Type list option) + = let entRef = classDecl.Entity let ent = com.GetEntity(entRef) let isObjectExpr = classDecl.Name = "ObjectExpr" + let objectExprInterfaceGenMap = + match objectExprInterfaceGenArgs with + | Some interfaceGenArgs -> + let genericNames = + ent.GenericParameters |> List.map (fun parameter -> parameter.Name) + + List.zip genericNames interfaceGenArgs |> Map + | None -> Map.empty + let entName = if isObjectExpr then classDecl.Name @@ -5454,7 +5937,14 @@ module Util = let interfaceMembers, nonInterfaceMembers = classDecl.AttachedMembers - |> List.map (fun decl -> decl, getDeclMember com decl) + |> List.map (fun decl -> + let memberValue = getDeclMember com decl + + if isObjectExpr then + decl, specializeInterfaceMember objectExprInterfaceGenMap memberValue + else + decl, memberValue + ) |> List.partition (snd >> isInterfaceMember com) let nonInterfaceImpls = @@ -5475,6 +5965,12 @@ module Util = |> List.append (makeFSharpExceptionItems com ctx ent) |> List.append (makePrimaryConstructorItems com ctx ent classDecl) + let memberItems = + if ent.IsFSharpRecord then + makeRecordTypeInfoItem com ctx ent genArgs :: memberItems + else + memberItems + if List.isEmpty memberItems then [] else @@ -5483,7 +5979,7 @@ module Util = let baseTypeOpt = if isObjectExpr then - Some(Fable.DeclaredType(entRef, genArgs)) + Some(Fable.DeclaredType(entRef, objectExprInterfaceGenArgs |> Option.defaultValue genArgs)) else ent.BaseType |> Option.map (fun t -> Fable.DeclaredType(t.Entity, t.GenericArgs)) @@ -5499,7 +5995,7 @@ module Util = let hasToString = nonInterfaceMembers |> List.exists (fun (d, m) -> m.CompiledName = "ToString") - let hasDebug = not (List.isEmpty ent.GenericParameters) + let hasDebug = isObjectExpr || not (List.isEmpty ent.GenericParameters) makeDisplayTraitImpls com ctx entName genParams hasToString hasDebug let operatorTraitImpls = @@ -5541,7 +6037,7 @@ module Util = // different arity - so blindly reusing genArgs would emit e.g. // `IDisposable`. Look those up in the declared type's hierarchy. if ifcEntRef.FullName = entRef.FullName then - genArgs + objectExprInterfaceGenArgs |> Option.defaultValue genArgs else ifcEntRef |> findInterfaceGenArgs com ent else @@ -5586,7 +6082,7 @@ module Util = |> entityItemWithVis com ctx ent let genArgs = FSharp2Fable.Util.getEntityGenArgs ent - let memberItems = transformClassMembers com ctx genArgs decl + let memberItems = transformClassMembers com ctx genArgs decl None entityItem :: memberItems let getVis (com: IRustCompiler) ctx declaringEntity isInternal isPrivate = @@ -5729,13 +6225,15 @@ module Util = | Fable.ClassDeclaration decl -> transformClassDecl com ctx decl let transformDeclarations (com: IRustCompiler) ctx decls = + let useItem = mkGlobUseItem [] [ "super" ] + let items = decls |> mergeNamespaceDecls com ctx |> List.collect (transformDecl com ctx) // wrap the last file in a module to consistently handle namespaces if isLastFileInProject com then let modPath = fixFileExtension com com.CurrentFile let modName = getImportModuleName com modPath - let modItem = mkModItem [] modName items + let modItem = mkModItem [] modName (useItem :: items) let useItem = mkGlobUseItem [] [ modName ] [ modItem; useItem |> mkPublicItem ] @@ -5960,9 +6458,10 @@ module Compiler = member _.TryGetEntity(entRef) = com.TryGetEntity(entRef) member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = RustCompiler(com) @@ -5996,18 +6495,25 @@ module Compiler = ScopedSymbols = Map.empty // HasMultipleUses = false InferAnyType = false + SkipRecordTypeRegistration = false IsAssocMember = false + IsFluentMember = false IsLambda = false IsParamByRefPreferred = false RequiresSendSync = false ModuleDepth = 0 } + let entryPointItems = file.Declarations |> getEntryPointItems com ctx + let topAttrs = [ if isLastFileInProject com then // adds "no_std" to crate if feature is enabled mkInnerAttr "cfg_attr" [ "feature = \"no_std\""; "no_std" ] + // Libraries retain Cargo's test-harness entry point. + if not (List.isEmpty entryPointItems) then + mkInnerAttr "cfg_attr" [ "feature = \"no_std\""; "no_main" ] // TODO: make some of those conditional on compiler options mkInnerAttr "allow" [ "dead_code" ] @@ -6029,7 +6535,6 @@ module Compiler = // mkInnerAttr "feature" ["destructuring_assignment"] ] - let entryPointItems = file.Declarations |> getEntryPointItems com ctx let importItems = com.GetAllImports(ctx) |> transformImports com ctx let declItems = file.Declarations |> transformDeclarations com ctx let modItems = getModuleItems com ctx // global module imports diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 604d1db1d3..0936064146 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -53,6 +53,7 @@ let coreModFor = | BclDateTimeOffset -> "DateTimeOffset" | BclDateOnly -> "DateOnly" | BclTimeOnly -> "TimeOnly" + | BclRune -> "Rune" | BclTimer -> "Timer" | BclTimeSpan -> "TimeSpan" | FSharpSet _ -> "Set" @@ -94,12 +95,8 @@ let makeStaticMemberCall com r t (i: CallInfo) moduleName memberName args = let makeStaticFieldCall com r t moduleName entityName memberName = let memberName = entityName + "::" + memberName - Helper.LibCall(com, moduleName, memberName, t, [], ?isModuleMember = Some(false), ?loc = r) -let makeLibCall com r t (i: CallInfo) moduleName memberName args = - Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, i.GenericArgs, ?loc = r) - let makeLibModuleCall com r t (i: CallInfo) moduleName memberName (thisArg: Expr option) (args: Expr list) = let args, argTypes = match thisArg with @@ -108,6 +105,24 @@ let makeLibModuleCall com r t (i: CallInfo) moduleName memberName (thisArg: Expr Helper.LibCall(com, moduleName, memberName, t, args, argTypes, i.GenericArgs, ?loc = r) +let makeLibCall com r t (i: CallInfo) moduleName memberName args = + Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, ?loc = r) + +let libCallThis com r t (i: CallInfo) moduleName memberName thisArg args = + Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + +let libCallCons com r t (i: CallInfo) moduleName memberName args = + Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, isConstructor = true, ?loc = r) + +let libCallTyped com r t moduleName memberName args argTypes = + Helper.LibCall(com, moduleName, memberName, t, args, argTypes, ?loc = r) + +let libCall com r t moduleName memberName args = + Helper.LibCall(com, moduleName, memberName, t, args, ?loc = r) + +let libValue com r t moduleName memberName args = + Helper.LibValue(com, moduleName, memberName, t) // TODO: range + let makeGlobalIdent (ident: string, memb: string, typ: Type) = makeTypedIdentExpr typ (ident + "::" + memb) @@ -116,7 +131,6 @@ let makeUniqueIdent com ctx t name = let makeDecimal com r t (x: decimal) = let str = x.ToString(System.Globalization.CultureInfo.InvariantCulture) - Helper.LibCall(com, "Decimal", "fromString", t, [ makeStrConst str ], isConstructor = true, ?loc = r) let makeRef (value: Expr) = @@ -133,7 +147,6 @@ let setRefCell com r (expr: Expr) (value: Expr) = let makeRefCell com r genArg args = let typ = makeFSharpCoreType [ genArg ] Types.refCell - Helper.LibCall(com, "Native", "refCell", typ, args, isConstructor = true, ?loc = r) let makeRefCellFromValue com r (value: Expr) = makeRefCell com r value.Type [ value ] @@ -189,11 +202,11 @@ let convertTo com (ctx: Context) r t (args: Expr list) = | Boolean -> match sourceType with | Boolean -> args.Head - | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toBoolean", t, args, ?loc = r) - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toBoolean", t, args, ?loc = r) - | Number(_kind, _) -> Helper.LibCall(com, "Convert", "toBoolean", t, args, ?loc = r) - | Char -> Helper.LibCall(com, "Convert", "toBoolean", t, args, ?loc = r) - | String -> Helper.LibCall(com, "Convert", "parseBoolean", t, args, ?loc = r) + | Number(Decimal, _) -> libCall com r t "Decimal" "toBoolean" args + | Number(BigInt, _) -> libCall com r t "BigInt" "toBoolean" args + | Number(_kind, _) -> libCall com r t "Convert" "toBoolean" args + | Char -> libCall com r t "Convert" "toBoolean" args + | String -> libCall com r t "Convert" "parseBoolean" args | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) @@ -201,39 +214,39 @@ let convertTo com (ctx: Context) r t (args: Expr list) = | Char -> match sourceType with | Char -> args.Head - | String -> Helper.LibCall(com, "Convert", "parseChar", t, args, ?loc = r) - | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toChar", t, args, ?loc = r) - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toChar", t, args, ?loc = r) + | String -> libCall com r t "Convert" "parseChar" args + | Number(Decimal, _) -> libCall com r t "Decimal" "toChar" args + | Number(BigInt, _) -> libCall com r t "BigInt" "toChar" args | Number(_kind, _) -> let code = TypeCast(args.Head, UInt32.Number) - Helper.LibCall(com, "Char", "fromCharCode", t, [ code ]) + libCall com r t "Char" "fromCharCode" [ code ] | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) | Number(Decimal, _) -> match sourceType with - | Array(Number(Int32, _), _) -> Helper.LibCall(com, "Decimal", "fromIntArray", t, args, ?loc = r) - | Boolean -> Helper.LibCall(com, "Decimal", "fromBoolean", t, args, ?loc = r) - | Char -> Helper.LibCall(com, "Decimal", "fromChar", t, args, ?loc = r) - | String -> Helper.LibCall(com, "Decimal", "fromString", t, args, ?loc = r) - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toDecimal", t, args, ?loc = r) + | Array(Number(Int32, _), _) -> libCall com r t "Decimal" "fromIntArray" args + | Boolean -> libCall com r t "Decimal" "fromBoolean" args + | Char -> libCall com r t "Decimal" "fromChar" args + | String -> libCall com r t "Decimal" "fromString" args + | Number(BigInt, _) -> libCall com r t "BigInt" "toDecimal" args | Number(kind, _) -> let meth = "from" + kind.ToString() - Helper.LibCall(com, "Decimal", meth, t, args, ?loc = r) + libCall com r t "Decimal" meth args | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) | Number(BigInt, _) -> match sourceType with - | Array(Number(UInt8, _), _) -> Helper.LibCall(com, "BigInt", "fromByteArray", t, args, ?loc = r) - | Boolean -> Helper.LibCall(com, "BigInt", "fromBoolean", t, args, ?loc = r) - | Char -> Helper.LibCall(com, "BigInt", "fromChar", t, args, ?loc = r) - | String -> Helper.LibCall(com, "BigInt", "fromString", t, args, ?loc = r) + | Array(Number(UInt8, _), _) -> libCall com r t "BigInt" "fromByteArray" args + | Boolean -> libCall com r t "BigInt" "fromBoolean" args + | Char -> libCall com r t "BigInt" "fromChar" args + | String -> libCall com r t "BigInt" "fromString" args | Number(kind, _) -> let meth = "from" + kind.ToString() - Helper.LibCall(com, "BigInt", meth, t, args, ?loc = r) + libCall com r t "BigInt" meth args | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) @@ -249,13 +262,13 @@ let convertTo com (ctx: Context) r t (args: Expr list) = TypeCast(code, t) | String -> let meth = "to" + kind.ToString() - Helper.LibCall(com, "Convert", meth, t, args, ?loc = r) + libCall com r t "Convert" meth args | Number(Decimal, _) -> let meth = "to" + kind.ToString() - Helper.LibCall(com, "Decimal", meth, t, args, ?loc = r) + libCall com r t "Decimal" meth args | Number(BigInt, _) -> let meth = "to" + kind.ToString() - Helper.LibCall(com, "BigInt", meth, t, args, ?loc = r) + libCall com r t "BigInt" meth args | Number _ -> TypeCast(args.Head, t) | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" @@ -265,7 +278,7 @@ let convertTo com (ctx: Context) r t (args: Expr list) = addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) -let toString com (ctx: Context) r (args: Expr list) = +let rec toString com (ctx: Context) r (args: Expr list) = match args with | [] -> "toString is called with empty args" @@ -273,16 +286,30 @@ let toString com (ctx: Context) r (args: Expr list) = | head :: tail -> match head.Type with | String -> head - | Char -> Helper.LibCall(com, "String", "ofChar", String, [ head ]) - | Boolean -> Helper.LibCall(com, "String", "ofBoolean", String, [ head ]) - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toString", String, args) - | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toString", String, args) + | Char -> libCall com r String "String" "ofChar" [ head ] + | Boolean -> libCall com r String "String" "ofBoolean" [ head ] + // Rust has no Display for tuples, and the orphan rule rules out adding + // one. Building the text here also matches .NET more closely than a + // derived Debug would: each element goes through its own ToString, so a + // string element is unquoted and a bool prints as True/False. + | Tuple(genArgs, _) when not (List.isEmpty genArgs) -> + let element i genArg = + toString com ctx r [ Get(head, TupleIndex i, genArg, r) ] + + let separated = + genArgs + |> List.mapi element + |> List.reduce (fun acc part -> add (add acc (makeStrConst ", ")) part) + + add (add (makeStrConst "(") separated) (makeStrConst ")") + | Number(BigInt, _) -> libCall com r String "BigInt" "toString" args + | Number(Decimal, _) -> libCall com r String "Decimal" "toString" args // | Array _ | List _ -> - // Helper.LibCall(com, "Types", "seqToString", String, [head], ?loc=r) + // libCall com r String "Types" "seqToString" [ head ] // | DeclaredType(ent, _) when ent.IsFSharpUnion || ent.IsFSharpRecord || ent.IsValueType -> // Helper.InstanceCall(head, "toString", String, [], ?loc=r) // | DeclaredType(ent, _) -> - | _ -> Helper.LibCall(com, "String", "toString", String, [ head ]) + | _ -> libCall com r String "String" "toString" [ head ] let toRoundInt com (ctx: Context) r t i (args: Expr list) = let sourceType = args.Head.Type @@ -300,35 +327,35 @@ let toRadixInt com (ctx: Context) r t i (args: Expr list) = match t with | Number(kind, _) -> let meth = "to" + kind.ToString() + "_radix" - Helper.LibCall(com, "Convert", meth, t, args, ?loc = r) + libCall com r t "Convert" meth args | _ -> FableError $"Unexpected conversion %s{i.CompiledName}" |> raise let toArray com t (expr: Expr) = match expr.Type with | Array _ -> expr - | List _ -> Helper.LibCall(com, "List", "toArray", t, [ expr ]) - | String -> Helper.LibCall(com, "String", "toCharArray", t, [ expr ]) - | IEnumerable -> Helper.LibCall(com, "Seq", "toArray", t, [ expr ]) + | List _ -> libCall com None t "List" "toArray" [ expr ] + | String -> libCall com None t "String" "toCharArray" [ expr ] + | IEnumerable -> libCall com None t "Seq" "toArray" [ expr ] | _ -> TypeCast(expr, t) let toList com t (expr: Expr) = match expr.Type with | List _ -> expr - | Array _ -> Helper.LibCall(com, "List", "ofArray", t, [ expr ]) + | Array _ -> libCall com None t "List" "ofArray" [ expr ] | String -> - let chars = Helper.LibCall(com, "String", "toCharArray", t, [ expr ]) - Helper.LibCall(com, "List", "ofArray", t, [ chars ]) - | IEnumerable -> Helper.LibCall(com, "List", "ofSeq", t, [ expr ]) + let chars = libCall com None t "String" "toSeq" [ expr ] + libCall com None t "List" "ofSeq" [ chars ] + | IEnumerable -> libCall com None t "List" "ofSeq" [ expr ] | _ -> TypeCast(expr, t) let toSeq com t (expr: Expr) = match expr.Type with | IEnumerable -> expr - | List _ -> Helper.LibCall(com, "Seq", "ofList", t, [ expr ]) - | Array _ -> Helper.LibCall(com, "Seq", "ofArray", t, [ expr ]) - | String -> - let chars = Helper.LibCall(com, "String", "toCharArray", t, [ expr ]) - Helper.LibCall(com, "Seq", "ofArray", t, [ chars ]) + | List _ -> libCall com None t "Seq" "ofList" [ expr ] + | Array _ -> libCall com None t "Seq" "ofArray" [ expr ] + | Builtin(FSharpMap _) -> libCall com None t "Map" "toEnumerable" [ expr ] + | Builtin(FSharpSet _) -> libCall com None t "Set" "toEnumerable" [ expr ] + | String -> libCall com None t "String" "toSeq" [ expr ] | _ -> TypeCast(expr, t) let emitRawString (s: string) = $"\"%s{s}\"" |> emitExpr None String [] @@ -347,7 +374,7 @@ let emitFormat (com: ICompiler) r t (args: Expr list) macro = | _ -> (emitRawString "{0}") :: args let unboxedArgs = args |> FSharp2Fable.Util.unboxBoxedArgs - Helper.LibCall(com, "String", macro, t, unboxedArgs) + libCall com r t "String" macro unboxedArgs let getMut expr = Helper.InstanceCall(expr, "get_mut", expr.Type, []) @@ -388,8 +415,7 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = | _ -> binOp BinaryMinus left right | Operators.multiply, [ left; right ] -> binOp BinaryMultiply left right | Operators.division, [ left; right ] -> binOp BinaryDivide left right - | Operators.divideByInt, [ left; right ] -> - Helper.LibCall(com, "Native", "divideByInt", t, [ left; right ], argTypes, ?loc = r) + | Operators.divideByInt, [ left; right ] -> libCallTyped com r t "Native" "divideByInt" [ left; right ] argTypes | Operators.modulus, [ left; right ] -> binOp BinaryModulus left right | Operators.leftShift, [ left; right ] -> binOp BinaryShiftLeft left right |> truncateUnsigned // See #1530 | Operators.rightShift, [ left; right ] -> @@ -412,8 +438,23 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = match argTypes with // | Number(BigInt as kind,_)::_ -> - // Helper.LibCall(com, "BigInt", opName, t, args, argTypes, ?loc=r) - | Builtin(BclDateTime | BclDateTimeOffset | BclTimeOnly | BclTimeSpan) :: _ -> nativeOp opName argTypes args + // libCallTyped com r t "BigInt" opName [ left; right ] argTypes + | Number(Decimal, _) :: _ -> + let decimalMethod = + match opName with + | Operators.addition -> Some "add" + | Operators.subtraction -> Some "subtract" + | Operators.multiply -> Some "multiply" + | Operators.division -> Some "divide" + | Operators.modulus -> Some "remainder" + | Operators.unaryNegation -> Some "negate" + | _ -> None + + match decimalMethod with + | Some meth -> libCallTyped com r t "Decimal" meth args argTypes + | None -> nativeOp opName argTypes args + | Builtin(BclDateTime | BclDateTimeOffset | BclTimeOnly | BclTimeSpan | BclRune) :: _ -> + nativeOp opName argTypes args | Builtin(FSharpSet _) :: _ -> let methName = match opName with @@ -421,10 +462,10 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = | Operators.subtraction -> "difference" | _ -> opName - Helper.LibCall(com, "Set", methName, t, args, argTypes, ?loc = r) + libCallTyped com r t "Set" methName args argTypes // | Builtin (FSharpMap _)::_ -> // let mangledName = Naming.buildNameWithoutSanitationFrom "FSharpMap" true opName overloadSuffix.Value - // Helper.LibCall(com, "Map", mangledName, t, args, argTypes, ?loc=r) + // libCallTyped com r t "Map" mangledName args argTypes | CustomOp com ctx r t opName args e -> e | _ -> nativeOp opName argTypes args @@ -439,6 +480,7 @@ let isCompatibleWithNativeComparison = // | List _ | Builtin(BclGuid) -> true | Builtin(BclTimeSpan) -> true + | Builtin(BclRune) -> true | _ -> false // Overview of hash rules: @@ -446,35 +488,35 @@ let isCompatibleWithNativeComparison = // * `.GetHashCode` called directly defaults to identity hash (for reference types except string) if not implemented. // * `LanguagePrimitive.PhysicalHash` creates an identity hash no matter whether GetHashCode is implemented or not. -let referenceHash (com: ICompiler) ctx r (arg: Expr) = +let referenceHash (com: ICompiler) ctx r t (arg: Expr) = match arg.Type with | Boolean | Char | String - | Number _ -> Helper.InstanceCall(arg, "getHashCode", Int32.Number, [], [], [], ?loc = r) - | _ -> Helper.LibCall(com, "Native", "referenceHash", Int32.Number, [ makeRef arg ], ?loc = r) + | Number _ -> libCall com r t "Native" "getHashCode" [ makeRef arg ] + | _ -> libCall com r t "Native" "referenceHash" [ makeRef arg ] -let getHashCode (com: ICompiler) ctx r (arg: Expr) = +let getHashCode (com: ICompiler) ctx r t (arg: Expr) = match arg.Type with - | HasReferenceEquality com _ -> referenceHash com ctx r arg - | _ -> Helper.InstanceCall(arg, "getHashCode", Int32.Number, [], [], [], ?loc = r) + | HasReferenceEquality com _ -> referenceHash com ctx r t arg + | _ -> libCall com r t "Native" "getHashCode" [ makeRef arg ] -let objectHash (com: ICompiler) ctx r (arg: Expr) = +let objectHash (com: ICompiler) ctx r t (arg: Expr) = match arg.Type with - | Array _ -> referenceHash com ctx r arg - | _ -> getHashCode com ctx r arg + | Array _ -> referenceHash com ctx r t arg + | _ -> getHashCode com ctx r t arg let referenceEquals (com: ICompiler) ctx r (left: Expr) (right: Expr) = match left, right with | Value(Null _, _), o - | o, Value(Null _, _) -> Helper.LibCall(com, "Native", "is_null", Boolean, [ makeRef o ], ?loc = r) + | o, Value(Null _, _) -> libCall com r Boolean "Native" "is_null" [ makeRef o ] | _ -> match left.Type with | Boolean | Char | String | Number _ -> makeEqOp r left right BinaryEqual - | _ -> Helper.LibCall(com, "Native", "referenceEquals", Boolean, [ makeRef left; makeRef right ], ?loc = r) + | _ -> libCall com r Boolean "Native" "referenceEquals" [ makeRef left; makeRef right ] let equals (com: ICompiler) ctx r (left: Expr) (right: Expr) = let t = Boolean @@ -485,14 +527,14 @@ let equals (com: ICompiler) ctx r (left: Expr) (right: Expr) = | String | Number _ | Builtin(FSharpChoice _ | FSharpResult _) -> makeEqOp r left right BinaryEqual - | Builtin kind -> Helper.LibCall(com, coreModFor kind, "equals", t, [ left; right ], ?loc = r) + | Builtin kind -> libCall com r t (coreModFor kind) "equals" [ left; right ] | Array(_, ResizeArray) -> referenceEquals com ctx r left right - | Array _ -> Helper.LibCall(com, "Array", "equals", t, [ left; right ], ?loc = r) - | List _ -> Helper.LibCall(com, "List", "equals", t, [ left; right ], ?loc = r) - | IEnumerable -> Helper.LibCall(com, "Seq", "equals", t, [ left; right ], ?loc = r) + | Array _ -> libCall com r t "Array" "equals" [ left; right ] + | List _ -> libCall com r t "List" "equals" [ left; right ] + | IEnumerable -> libCall com r t "Seq" "equals" [ left; right ] // System.Type is erased to a boxed reflection object (dyn Any, no PartialEq), // so type-identity equality is compared on the carried TypeId at runtime. - | MetaType -> Helper.LibCall(com, "Reflection", "typeEquals", t, [ left; right ], ?loc = r) + | MetaType -> libCall com r t "Reflection" "typeEquals" [ left; right ] | HasReferenceEquality com _ -> referenceEquals com ctx r left right | Nullable _ -> // transforms null checks into option tests @@ -501,7 +543,7 @@ let equals (com: ICompiler) ctx r (left: Expr) (right: Expr) = | Value(NewOption(None, _, _), _), expr -> Test(expr, OptionTest false, r) | _ -> makeEqOp r left right BinaryEqual | _ -> - // Helper.LibCall(com, "Native", "equals", t, [left; right], ?loc=r) + // libCall com r t "Native" "equals" [ left; right ] makeEqOp r left right BinaryEqual /// Compare function that will call Util.compare or instance `CompareTo` as appropriate @@ -513,12 +555,12 @@ let compare (com: ICompiler) ctx r (left: Expr) (right: Expr) = | Char | String | Number _ - | Builtin(FSharpChoice _ | FSharpResult _) -> Helper.LibCall(com, "Native", "compare", t, [ left; right ], ?loc = r) - | Builtin kind -> Helper.LibCall(com, coreModFor kind, "compareTo", t, [ left; right ], ?loc = r) - | Array _ -> Helper.LibCall(com, "Array", "compareTo", t, [ left; right ], ?loc = r) - | List _ -> Helper.LibCall(com, "List", "compareTo", t, [ left; right ], ?loc = r) - | IEnumerable -> Helper.LibCall(com, "Seq", "compareTo", t, [ left; right ], ?loc = r) - | _ -> Helper.LibCall(com, "Native", "compare", t, [ left; right ], ?loc = r) + | Builtin(FSharpChoice _ | FSharpResult _) -> libCall com r t "Native" "compare" [ left; right ] + | Builtin kind -> libCall com r t (coreModFor kind) "compareTo" [ left; right ] + | Array _ -> libCall com r t "Array" "compareTo" [ left; right ] + | List _ -> libCall com r t "List" "compareTo" [ left; right ] + | IEnumerable -> libCall com r t "Seq" "compareTo" [ left; right ] + | _ -> libCall com r t "Native" "compare" [ left; right ] /// Boolean comparison operators like <, >, <=, >= let booleanCompare (com: ICompiler) ctx r (left: Expr) (right: Expr) op = @@ -597,7 +639,7 @@ let makeSet (com: ICompiler) ctx r t args genArg = | [ ExprType(Array _) ] -> "ofArray" | _ -> "ofSeq" - Helper.LibCall(com, "Set", meth, t, args, ?loc = r) + libCall com r t "Set" meth args /// Adds comparer as last argument for map creator methods let makeMap (com: ICompiler) ctx r t args genArg = @@ -609,7 +651,7 @@ let makeMap (com: ICompiler) ctx r t args genArg = | [ ExprType(Array _) ] -> "ofArray" | _ -> "ofSeq" - Helper.LibCall(com, "Map", Naming.lowerFirst meth, t, args, ?loc = r) + libCall com r t "Map" (Naming.lowerFirst meth) args // let makeDictionaryWithComparer com r t sourceSeq comparer = // Helper.LibCall(com, "MutableMap", "Dictionary", t, [sourceSeq; comparer], isConstructor=true, ?loc=r) @@ -633,30 +675,31 @@ let rec getZero (com: ICompiler) (ctx: Context) (t: Type) = | Nullable(genArg, true) -> NewOption(None, genArg, false) |> makeValue None | Nullable(genArg, false) -> Null t |> makeValue None | Boolean -> makeBoolConst false - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "zero", t, []) - | Number(Decimal, _) -> Helper.LibValue(com, "Decimal", "Zero", t) + | Number(BigInt, _) -> libCall com None t "BigInt" "zero" [] + | Number(Decimal, _) -> libValue com None t "Decimal" "Zero" [] | Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue None | Char -> CharConstant '\u0000' |> makeValue None | String -> Null t |> makeValue None | Array(typ, _) -> makeArray typ [] | List genArg -> NewList(None, genArg) |> makeValue None - | Builtin BclDateTime -> Helper.LibCall(com, "DateTime", "zero", t, []) - | Builtin BclDateTimeOffset -> Helper.LibCall(com, "DateTimeOffset", "zero", t, []) - | Builtin BclDateOnly -> Helper.LibCall(com, "DateOnly", "zero", t, []) - | Builtin BclTimeOnly -> Helper.LibCall(com, "TimeOnly", "zero", t, []) - | Builtin BclTimeSpan -> Helper.LibValue(com, "TimeSpan", "zero", t) + | Builtin BclDateTime -> libCall com None t "DateTime" "zero" [] + | Builtin BclDateTimeOffset -> libCall com None t "DateTimeOffset" "zero" [] + | Builtin BclDateOnly -> libCall com None t "DateOnly" "zero" [] + | Builtin BclTimeOnly -> libCall com None t "TimeOnly" "zero" [] + | Builtin BclRune -> libCall com None t "Rune" "zero" [] + | Builtin BclTimeSpan -> libValue com None t "TimeSpan" "zero" [] | Builtin(FSharpSet genArg) -> makeSet com ctx None t [] genArg - | Builtin BclGuid -> Helper.LibValue(com, "Guid", "empty", t) + | Builtin BclGuid -> libValue com None t "Guid" "empty" [] | Builtin(BclKeyValuePair(k, v)) -> makeTuple None true [ getZero com ctx k; getZero com ctx v ] // | ListSingleton(CustomOp com ctx None t "get_Zero" [] e) -> e | IsReferenceType com _ -> Null t |> makeValue None - | _ -> Helper.LibCall(com, "Native", "getZero", t, []) + | _ -> libCall com None t "Native" "getZero" [] let getOne (com: ICompiler) (ctx: Context) (t: Type) = match t with | Boolean -> makeBoolConst true - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "one", t, []) - | Number(Decimal, _) -> Helper.LibValue(com, "Decimal", "One", t) + | Number(BigInt, _) -> libCall com None t "BigInt" "one" [] + | Number(Decimal, _) -> libValue com None t "Decimal" "One" [] | Number(kind, uom) -> NumberConstant(NumberValue.GetOne kind, uom) |> makeValue None // | ListSingleton(CustomOp com ctx None t "get_One" [] e) -> e | _ -> makeIntConst 1 @@ -723,16 +766,16 @@ let makeAddFunction (com: ICompiler) ctx t = // | Some injectInfo -> injectArgInner args injectInfo let tryOp com r t op args = - Helper.LibCall(com, "Option", "tryOp", t, op :: args, ?loc = r) + libCall com r t "Option" "tryOp" (op :: args) let tryCoreOp com r t coreModule coreMember args = - let op = Helper.LibValue(com, coreModule, coreMember, Any) + let op = libValue com r Any coreModule coreMember [] tryOp com r t op args let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.DeclaringEntityFullName, i.CompiledName with | _, UniversalFableCoreHelpers com ctx r t i args error expr -> Some expr - | "Fable.Core.Reflection", meth -> Helper.LibCall(com, "Reflection", meth, t, args, ?loc = r) |> Some + | "Fable.Core.Reflection", meth -> libCall com r t "Reflection" meth args |> Some | "Fable.Core.Compiler", meth -> match meth with | "version" -> makeStrConst Literals.VERSION |> Some @@ -773,8 +816,8 @@ let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp let refCells (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | "get_Value", Some callee, _ -> getRefCell com r t callee |> Some - | "set_Value", Some callee, [ value ] -> setRefCell com r callee value |> Some + | "get_Value", Some c, _ -> getRefCell com r t c |> Some + | "set_Value", Some c, [ value ] -> setRefCell com r c value |> Some | _ -> None let getMemberName isStatic (i: CallInfo) = @@ -810,9 +853,9 @@ let getModuleAndMemberName (i: CallInfo) (thisArg: Expr option) = let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match thisArg with - | Some callee -> + | Some c -> let memberName = getMemberName false i - makeInstanceCall r t i callee memberName args |> Some + makeInstanceCall r t i c memberName args |> Some | None -> let moduleName, memberName = getModuleAndMemberName i thisArg makeStaticLibCall com r t i moduleName memberName args |> Some @@ -820,8 +863,7 @@ let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt let fsharpModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let moduleName, memberName = getModuleAndMemberName i thisArg - Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i moduleName memberName args |> Some // Maps a .NET standard numeric format specifier (e.g. "X4", "D5", "F2") to a // Rust format spec body { flags; width; precision; type } so it can be merged @@ -988,7 +1030,7 @@ let makeRustFormatString interpolated (fmt: string) = rustFmt, argCount let makeRustFormatExpr com r t (fmt: string) args macro = - let macroExpr = Helper.LibValue(com, "String", macro, Any) + let macroExpr = libValue com r Any "String" macro [] let rustFmt, argCount = makeRustFormatString false fmt let argCount = argCount + 1 + (List.length args) // +1 is for fmt let applied = Extended(Curry(macroExpr, argCount), r) @@ -997,8 +1039,8 @@ let makeRustFormatExpr com r t (fmt: string) args macro = let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // | "get_Value", Some callee, _ -> - // callee |> Some //TODO: + // | "get_Value", Some c, _ -> + // c |> Some // TODO: | ("PrintFormatToString" | "PrintFormatToStringThen"), None, [ StringConst fmt ] -> "sprintf!" |> makeRustFormatExpr com r t fmt [] |> Some | ("PrintFormatToString" | "PrintFormatToStringThen"), None, [ MaybeCasted(template) ] -> template |> Some @@ -1023,16 +1065,16 @@ let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op "failwithf!" |> makeRustFormatExpr com r t fmt [] |> Some | "PrintFormatToStringThenFail", None, _ -> "failwithf!" |> emitFormat com r t args |> Some | "PrintFormatToStringBuilder", None, [ sb; StringConst fmt ] -> - let cont = Helper.LibCall(com, "Util", "bprintf", t, [ sb ]) + let cont = libCall com r t "Util" "bprintf" [ sb ] "kprintf!" |> makeRustFormatExpr com r t fmt [ cont ] |> Some | "PrintFormatToStringBuilder", None, [ sb; MaybeCasted(template) ] -> - let cont = Helper.LibCall(com, "Util", "bprintf", t, [ sb ]) + let cont = libCall com r t "Util" "bprintf" [ sb ] Helper.Application(cont, t, [ template ], ?loc = r) |> Some | "PrintFormatToStringBuilderThen", None, [ cont; sb; StringConst fmt ] -> - let cont = Helper.LibCall(com, "Util", "kbprintf", t, [ cont; sb ]) + let cont = libCall com r t "Util" "kbprintf" [ cont; sb ] "kprintf!" |> makeRustFormatExpr com r t fmt [ cont ] |> Some | "PrintFormatToStringBuilderThen", None, [ cont; sb; MaybeCasted(template) ] -> - let cont = Helper.LibCall(com, "Util", "kbprintf", t, [ cont; sb ]) + let cont = libCall com r t "Util" "kbprintf" [ cont; sb ] Helper.Application(cont, t, [ template ], ?loc = r) |> Some | ".ctor", _, (StringConst fmt) :: (Value(NewArray(ArrayValues templateArgs, _, _), _)) :: _ -> let rustFmt, _argCount = makeRustFormatString true fmt @@ -1056,9 +1098,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o match opt with | Some value -> Some value | None -> Some defValue - | _ -> - Helper.LibCall(com, "Option", "defaultArg", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | _ -> makeLibCall com r t i "Option" "defaultArg" args |> Some | "DefaultAsyncBuilder", _ -> makeImportLib com t "singleton" "AsyncBuilder" |> Some // Erased operators. // Rust compiles KeyValuePair as a struct tuple, but the KeyValue active pattern expects a regular tuple. @@ -1079,8 +1119,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | "ToString", _ -> toString com ctx r args |> Some | "CreateSequence", [ xs ] -> toSeq com t xs |> Some | ("CreateDictionary" | "CreateReadOnlyDictionary"), [ arg ] -> - Helper.LibCall(com, "HashMap", "new_from_tuple_array", t, [ toArray com t arg ]) - |> Some + libCall com r t "HashMap" "new_from_tuple_array" [ toArray com t arg ] |> Some | "CreateSet", _ -> (genArg com ctx r 0 i.GenericArgs) |> makeSet com ctx r t args |> Some // Ranges | ("op_Range" | "op_RangeStep"), _ -> @@ -1096,8 +1135,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | Char -> "rangeChar", args | _ -> "rangeNumeric", addStep args - Helper.LibCall(com, "Range", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Range" meth args |> Some // Pipes and composition | "op_PipeRight", [ x; f ] | "op_PipeLeft", [ f; x ] -> curriedApply r t f [ x ] |> Some @@ -1111,9 +1149,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _ -> fsFormat com ctx r t i thisArg args | ("Failure" | "FailurePattern" | "LazyPattern" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args - | "Lock", _ -> - Helper.LibCall(com, "Monitor", "lock", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | "Lock", _ -> libCallThis com r t i "Monitor" "lock" thisArg args |> Some | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions @@ -1136,12 +1172,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o let argTypes = args |> List.map (fun a -> a.Type) match argTypes with - | Number(Decimal, _) :: _ -> - Helper.LibCall(com, "Decimal", "pown", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | Number(BigInt, _) :: _ -> - Helper.LibCall(com, "BigInt", "pow", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | Number(Decimal, _) :: _ -> libCallThis com r t i "Decimal" "pown" thisArg args |> Some + | Number(BigInt, _) :: _ -> libCallThis com r t i "BigInt" "pow" thisArg args |> Some | Number((Float32 | Float64), _) :: _ -> let meth = if i.CompiledName = "PowInteger" then @@ -1152,13 +1184,11 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o math r t args i.SignatureArgTypes meth |> Some | CustomOp com ctx r t "Pow" args e -> Some e | _ -> math r t args i.SignatureArgTypes "pow" |> Some - | ("Ceiling" | "Floor" as meth), _ -> + | ("Ceiling" | "Floor") as meth, _ -> let meth = Naming.lowerFirst meth match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" meth thisArg args |> Some | _ -> let meth = if meth = "ceiling" then @@ -1170,12 +1200,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | "Log", [ arg ] -> math r t args i.SignatureArgTypes "ln" |> Some | "Abs", _ -> match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", "abs", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | ExprType(Number(BigInt, _)) :: _ -> - Helper.LibCall(com, "BigInt", "abs", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" "abs" thisArg args |> Some + | ExprType(Number(BigInt, _)) :: _ -> libCallThis com r t i "BigInt" "abs" thisArg args |> Some | _ -> math r t args i.SignatureArgTypes i.CompiledName |> Some | ("Acos" | "Asin" | "Atan" | "Atan2" | "Cos" | "Cosh" | "Exp" | "Log" | "Log2" | "Log10" | "Sin" | "Sinh" | "Sqrt" | "Tan" | "Tanh"), _ -> @@ -1184,36 +1210,25 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | _ -> applyOp com ctx r t i.CompiledName args |> Some | "Round", _ -> match args with - | [ ExprType(Number(Decimal, _)) ] -> - Helper.LibCall(com, "Decimal", "round", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | [ ExprType(Number(Decimal, _)) ] -> libCallThis com r t i "Decimal" "round" thisArg args |> Some + | [ ExprType(Number(Decimal, _)); ExprTypeEnum "System.MidpointRounding" _ ] -> + makeLibCall com r t i "Decimal" "roundMode" args |> Some | [ ExprType(Number(Decimal, _)); ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "Decimal", "roundTo", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | [ ExprType(Number(Decimal, _)); mode ] -> - Helper.LibCall(com, "Decimal", "roundMode", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ ExprType(Number(Decimal, _)); dp; mode ] -> - Helper.LibCall(com, "Decimal", "roundToMode", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + libCallThis com r t i "Decimal" "roundTo" thisArg args |> Some + | [ ExprType(Number(Decimal, _)); mode ] -> makeLibCall com r t i "Decimal" "roundMode" args |> Some + | [ ExprType(Number(Decimal, _)); dp; mode ] -> makeLibCall com r t i "Decimal" "roundToMode" args |> Some | [ ExprTypeAs(Number(Float64, _), arg) ] -> - //TODO: other midpoint modes for Double + // TODO: other midpoint modes for Double makeInstanceCall r t i arg "round" [] |> Some | _ -> None | "Truncate", [ arg ] -> match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", "truncate", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" "truncate" thisArg args |> Some | _ -> makeInstanceCall r t i arg "trunc" [] |> Some | "Sign", [ arg ] -> match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", "sign", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | ExprType(Number(BigInt, _)) :: _ -> - Helper.LibCall(com, "BigInt", "sign", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" "sign" thisArg args |> Some + | ExprType(Number(BigInt, _)) :: _ -> libCallThis com r t i "BigInt" "sign" thisArg args |> Some | ExprType(Number((Float16 | Float32 | Float64), _)) :: _ -> compare com ctx r arg (getZero com ctx arg.Type) |> Some | _ -> @@ -1221,12 +1236,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o TypeCast(sign, Int32.Number) |> Some | "DivRem", _ -> match args with - | [ x; y ] -> - Helper.LibCall(com, "Util", "divRem", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ x; y; rem ] -> - Helper.LibCall(com, "Util", "divRemOut", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | [ x; y ] -> makeLibCall com r t i "Util" "divRem" args |> Some + | [ x; y; rem ] -> makeLibCall com r t i "Util" "divRemOut" args |> Some | _ -> None // Numbers | "Infinity", _ -> makeGlobalIdent ("f64", "INFINITY", t) |> Some @@ -1246,36 +1257,29 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o let v = sub (getRefCell com r t arg) (getOne com ctx t) setRefCell com r arg v |> Some // Concatenates two lists - | "op_Append", _ -> - Helper.LibCall(com, "List", "append", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | "op_Append", _ -> libCallThis com r t i "List" "append" thisArg args |> Some | "IsNull", [ arg ] -> nullCheck r true arg |> Some - | "Hash", [ arg ] -> getHashCode com ctx r arg |> Some + | "Hash", [ arg ] -> getHashCode com ctx r t arg |> Some // Comparison | Patterns.SetContains Operators.compareSet, [ left; right ] -> applyCompareOp com ctx r t i.CompiledName left right |> Some | "Compare", [ left; right ] -> compare com ctx r left right |> Some | "Clamp", _ -> math r t args i.SignatureArgTypes i.CompiledName |> Some - | ("Min" | "Max" as meth), _ -> + | ("Min" | "Max") as meth, _ -> match args.Head.Type with | Boolean | Char - | String + | String -> math r t args i.SignatureArgTypes i.CompiledName |> Some + | Number(Decimal, _) -> libCall com r t "Decimal" (Naming.lowerFirst meth) args |> Some | Number _ -> math r t args i.SignatureArgTypes i.CompiledName |> Some - | _ -> Helper.LibCall(com, "Native", Naming.lowerFirst meth, t, args, ?loc = r) |> Some - | ("MinMagnitude" | "MaxMagnitude" as meth), _ -> + | _ -> libCall com r t "Native" (Naming.lowerFirst meth) args |> Some + | ("MinMagnitude" | "MaxMagnitude") as meth, _ -> let meth = Naming.lowerFirst meth match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | ExprType(Number(BigInt, _)) :: _ -> - Helper.LibCall(com, "BigInt", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | ExprType(Number _) :: _ -> - Helper.LibCall(com, "Numeric", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" meth thisArg args |> Some + | ExprType(Number(BigInt, _)) :: _ -> libCallThis com r t i "BigInt" meth thisArg args |> Some + | ExprType(Number _) :: _ -> libCallThis com r t i "Numeric" meth thisArg args |> Some | _ -> None | "Not", [ operand ] -> // TODO: Check custom operator? makeUnOp r t operand UnaryNot |> Some @@ -1296,14 +1300,13 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | Array _, _ | _, Array _ -> referenceEquals com ctx r arg1 arg2 |> Some | _ -> TypeCast(arg2, arg1.Type) |> equals com ctx r arg1 |> Some - | "GetHashCode", Some arg, _ -> objectHash com ctx r arg |> Some + | "GetHashCode", Some arg, _ -> objectHash com ctx r t arg |> Some | "GetType", Some arg, _ -> if arg.Type = Any then // Dynamic type of a boxed value: resolve via the runtime reflection // registry (populated by typeof). Returns the concrete type's info // when registered, else the value as an opaque placeholder. - Helper.LibCall(com, "Reflection", "getTypeFromObj", t, [ arg ], ?loc = r) - |> Some + libCall com r t "Reflection" "getTypeFromObj" [ arg ] |> Some else makeTypeInfo r arg.Type |> Some | _ -> None @@ -1314,7 +1317,7 @@ let valueTypes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | "ToString", Some arg, _ -> toString com ctx r [ arg ] |> Some | "Equals", Some arg1, [ arg2 ] | "Equals", None, [ arg1; arg2 ] -> equals com ctx r arg1 arg2 |> Some - | "GetHashCode", Some arg, _ -> getHashCode com ctx r arg |> Some + | "GetHashCode", Some arg, _ -> getHashCode com ctx r t arg |> Some | "CompareTo", Some arg1, [ arg2 ] | "Compare", None, [ arg1; arg2 ] -> compare com ctx r arg1 arg2 |> Some | _ -> None @@ -1326,42 +1329,36 @@ let chars (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | _ -> meth match i.CompiledName, thisArg, args with - | ("IsBetween" as meth), None, _ -> - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("IsAscii" | "IsAsciiDigit" | "IsAsciiLetter" | "IsAsciiLetterLower" | "IsAsciiLetterUpper" | "IsAsciiLetterOrDigit" | "IsAsciiHexDigit" | "IsAsciiHexDigitLower" | "IsAsciiHexDigitUpper" as meth), + | ("IsBetween") as meth, None, _ -> makeLibCall com r t i "Char" meth args |> Some + | ("IsAscii" | "IsAsciiDigit" | "IsAsciiLetter" | "IsAsciiLetterLower" | "IsAsciiLetterUpper" | "IsAsciiLetterOrDigit" | "IsAsciiHexDigit" | "IsAsciiHexDigitLower" | "IsAsciiHexDigitUpper") as meth, None, - [ c ] -> - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("IsControl" | "IsDigit" | "IsLetter" | "IsLetterOrDigit" | "IsLower" | "IsUpper" | "IsNumber" | "IsPunctuation" | "IsSeparator" | "IsSymbol" | "IsWhiteSpace" as meth), + [ c ] -> makeLibCall com r t i "Char" meth args |> Some + | ("IsControl" | "IsDigit" | "IsLetter" | "IsLetterOrDigit" | "IsLower" | "IsUpper" | "IsNumber" | "IsPunctuation" | "IsSeparator" | "IsSymbol" | "IsWhiteSpace") as meth, None, _ -> let meth = getMethodName meth args - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("GetNumericValue" | "GetUnicodeCategory" | "ConvertToUtf32" as meth), None, _ -> + makeLibCall com r t i "Char" meth args |> Some + | ("GetNumericValue" | "GetUnicodeCategory" | "ConvertToUtf32") as meth, None, _ -> let meth = getMethodName meth args - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Char" meth args |> Some | "ToString", None, [ ExprType(Char) ] -> toString com ctx r args |> Some | "ToString", Some c, [] -> toString com ctx r [ c ] |> Some - | ("ConvertFromUtf32" | "ToLower" | "ToLowerInvariant" | "ToUpper" | "ToUpperInvariant" as meth), None, [ c ] -> - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("TryParse" | "Parse" as meth), None, _ -> - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("IsSurrogate" | "IsHighSurrogate" | "IsLowSurrogate" | "IsSurrogatePair" as meth), None, _ -> - $"Rust chars are Unicode scalar values, so surrogate tests will be false." - |> addWarning com ctx.InlinePath r + | "ToString", Some c, [ _ ] -> toString com ctx r [ c ] |> Some + | ("ToLower" | "ToUpper") as meth, None, [ c; ExprType(DeclaredType(ent, _)) ] when ent.FullName = Types.cultureInfo -> + makeLibCall com r t i "Char" meth [ c ] |> Some + | ("ConvertFromUtf32" | "ToLower" | "ToLowerInvariant" | "ToUpper" | "ToUpperInvariant") as meth, None, [ c ] -> + makeLibCall com r t i "Char" meth args |> Some + | "GetTypeCode", Some c, [] -> makeLibCall com r t i "Char" "GetTypeCode" [ c ] |> Some + | ("TryParse" | "Parse") as meth, None, _ -> makeLibCall com r t i "Char" meth args |> Some + | ("IsSurrogate" | "IsHighSurrogate" | "IsLowSurrogate" | "IsSurrogatePair") as meth, None, _ -> + WarningCodes.charSurrogateTestsAreFalse + |> addWarningWithCode com ctx.InlinePath r let meth = getMethodName meth args - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Char" meth args |> Some | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _, _ -> valueTypes com ctx r t i thisArg args | _ -> None @@ -1373,21 +1370,21 @@ let getEnumerator com r t i (expr: Expr) = // | IsEntity (Types.regexMatchCollection) _ // | IsEntity (Types.regexGroupCollection) _ // | IsEntity (Types.regexCaptureCollection) _ - | Array _ -> Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ expr ], ?loc = r) - | List _ -> Helper.LibCall(com, "Seq", "Enumerable::ofList", t, [ expr ], ?loc = r) + | Array _ -> libCall com r t "Seq" "Enumerable::ofArray" [ expr ] + | List _ -> libCall com r t "Seq" "Enumerable::ofList" [ expr ] + | String -> + let en = toSeq com Any expr + makeInstanceCall r t i en "GetEnumerator" [] | IsEntity (Types.hashset) _ | IsEntity (Types.iset) _ -> - let ar = Helper.LibCall(com, "HashSet", "entries", t, [ expr ]) - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) + let ar = libCall com r t "HashSet" "entries" [ expr ] + libCall com r t "Seq" "Enumerable::ofArray" [ ar ] | IsEntity (Types.dictionary) _ | IsEntity (Types.idictionary) _ | IsEntity (Types.ireadonlydictionary) _ -> - let ar = Helper.LibCall(com, "HashMap", "entries", t, [ expr ], [ expr.Type ]) - - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) - | _ -> - // Helper.LibCall(com, "Util", "getEnumerator", t, [toSeq com Any expr], ?loc=r) - makeInstanceCall r t i expr "GetEnumerator" [] + let ar = libCallTyped com r t "HashMap" "entries" [ expr ] [ expr.Type ] + libCall com r t "Seq" "Enumerable::ofArray" [ ar ] + | _ -> makeInstanceCall r t i expr "GetEnumerator" [] let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = // let isIgnoreCase args = @@ -1403,96 +1400,86 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt match i.CompiledName, thisArg, args with | ".ctor", _, _ -> match i.SignatureArgTypes with - | [ Char; Number(Int32, _) ] -> Helper.LibCall(com, "String", "fromChar", t, args, ?loc = r) |> Some - | [ MaybeNullable(Array(Char, _)) ] -> Helper.LibCall(com, "String", "fromChars", t, args, ?loc = r) |> Some + | [ Char; Number(Int32, _) ] -> libCall com r t "String" "fromChar" args |> Some + | [ MaybeNullable(Array(Char, _)) ] -> libCall com r t "String" "fromChars" args |> Some | [ MaybeNullable(Array(Char, _)); Number(Int32, _); Number(Int32, _) ] -> - Helper.LibCall(com, "String", "fromChars2", t, args, ?loc = r) |> Some + libCall com r t "String" "fromChars2" args |> Some | _ -> None - | "get_Length", Some c, _ -> Helper.LibCall(com, "String", "length", t, c :: args, ?loc = r) |> Some - | "get_Chars", Some c, _ -> Helper.LibCall(com, "String", "getCharAt", t, c :: args, ?loc = r) |> Some + | "get_Length", Some c, _ -> libCall com r t "String" "length" (c :: args) |> Some + | "get_Chars", Some c, _ -> libCall com r t "String" "getCharAt" (c :: args) |> Some + | "EnumerateRunes", Some c, _ -> libCall com r t "String" "enumerateRunes" [ c ] |> Some | "CompareOrdinal", None, _ -> match args with - | [ ExprType String; ExprType String ] -> - Helper.LibCall(com, "String", "compareOrdinal", t, args, ?loc = r) |> Some + | [ ExprType String; ExprType String ] -> libCall com r t "String" "compareOrdinal" args |> Some | [ ExprType String ExprType(Number(Int32, _)) ExprType String ExprType(Number(Int32, _)) - ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "compareOrdinal2", t, args, ?loc = r) |> Some + ExprType(Number(Int32, _)) ] -> libCall com r t "String" "compareOrdinal2" args |> Some | _ -> None | "CompareTo", Some c, [ ExprTypeAs(String, arg) ] -> - $"String.CompareTo will be compiled as String.CompareOrdinal" - |> addWarning com ctx.InlinePath r + WarningCodes.stringCompareIsOrdinal "CompareTo" + |> addWarningWithCode com ctx.InlinePath r - Helper.LibCall(com, "String", "compareOrdinal", t, [ c; arg ], ?loc = r) |> Some + libCall com r t "String" "compareOrdinal" [ c; arg ] |> Some | "Compare", None, _ -> - $"String.Compare will be compiled as String.CompareOrdinal" - |> addWarning com ctx.InlinePath r + WarningCodes.stringCompareIsOrdinal "Compare" + |> addWarningWithCode com ctx.InlinePath r match args with - | [ ExprType String; ExprType String ] -> - Helper.LibCall(com, "String", "compareOrdinal", t, args, ?loc = r) |> Some + | [ ExprType String; ExprType String ] -> libCall com r t "String" "compareOrdinal" args |> Some | ExprType String :: ExprType String :: ExprType Boolean :: restArgs -> - Helper.LibCall(com, "String", "compareCase", t, args, ?loc = r) |> Some - | [ ExprType String; ExprType String; comparison ] -> - Helper.LibCall(com, "String", "compareWith", t, args, ?loc = r) |> Some + libCall com r t "String" "compareCase" args |> Some + | [ ExprType String; ExprType String; comparison ] -> libCall com r t "String" "compareWith" args |> Some | [ ExprType String ExprType(Number(Int32, _)) ExprType String ExprType(Number(Int32, _)) - ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "compareOrdinal2", t, args, ?loc = r) |> Some + ExprType(Number(Int32, _)) ] -> libCall com r t "String" "compareOrdinal2" args |> Some | ExprType String :: ExprType(Number(Int32, _)) :: ExprType String :: ExprType(Number(Int32, _)) :: ExprType(Number(Int32, _)) :: ExprType Boolean :: restArgs -> - Helper.LibCall(com, "String", "compareCase2", t, args, ?loc = r) |> Some + libCall com r t "String" "compareCase2" args |> Some | ExprType String :: ExprType(Number(Int32, _)) :: ExprType String :: ExprType(Number(Int32, _)) :: ExprType(Number(Int32, _)) :: comparison :: restArgs -> - Helper.LibCall(com, "String", "compareWith2", t, args, ?loc = r) |> Some + libCall com r t "String" "compareWith2" args |> Some | _ -> None | "Concat", None, _ -> match args with - | [ ExprTypeAs(IEnumerable, arg) ] -> - Helper.LibCall(com, "String", "concat", t, [ toArray com t arg ], ?loc = r) - |> Some + | [ ExprTypeAs(IEnumerable, arg) ] -> libCall com r t "String" "concat" [ toArray com t arg ] |> Some | [ ExprType String; ExprType String ] | [ ExprType String; ExprType String; ExprType String ] | [ ExprType String; ExprType String; ExprType String; ExprType String ] -> - Helper.LibCall(com, "String", "concat", t, [ makeArray String args ], ?loc = r) - |> Some - | [ ExprType(Array(String, _)) ] -> Helper.LibCall(com, "String", "concat", t, args, ?loc = r) |> Some + libCall com r t "String" "concat" [ makeArray String args ] |> Some + | [ ExprType(Array(String, _)) ] -> libCall com r t "String" "concat" args |> Some | _ -> None | "Contains", Some c, _ -> match args with - | [ ExprType Char ] -> Helper.LibCall(com, "String", "containsChar", t, c :: args, ?loc = r) |> Some - | [ ExprType Char; _comparison ] -> - Helper.LibCall(com, "String", "containsChar2", t, c :: args, ?loc = r) |> Some - | [ ExprType String ] -> Helper.LibCall(com, "String", "contains", t, c :: args, ?loc = r) |> Some - | [ ExprType String; _comparison ] -> Helper.LibCall(com, "String", "contains2", t, c :: args, ?loc = r) |> Some + | [ ExprType Char ] -> libCall com r t "String" "containsChar" (c :: args) |> Some + | [ ExprType Char; _comparison ] -> libCall com r t "String" "containsChar2" (c :: args) |> Some + | [ ExprType String ] -> libCall com r t "String" "contains" (c :: args) |> Some + | [ ExprType String; _comparison ] -> libCall com r t "String" "contains2" (c :: args) |> Some | _ -> None | "EndsWith", Some c, _ -> match args with - | [ ExprType Char ] -> Helper.LibCall(com, "String", "endsWithChar", t, c :: args, ?loc = r) |> Some - | [ ExprType String ] -> Helper.LibCall(com, "String", "endsWith", t, c :: args, ?loc = r) |> Some - | [ ExprType String; _comparison ] -> Helper.LibCall(com, "String", "endsWith2", t, c :: args, ?loc = r) |> Some - | [ pattern; ignoreCase; _culture ] -> - Helper.LibCall(com, "String", "endsWith3", t, [ c; pattern; ignoreCase ], ?loc = r) - |> Some + | [ ExprType Char ] -> libCall com r t "String" "endsWithChar" (c :: args) |> Some + | [ ExprType String ] -> libCall com r t "String" "endsWith" (c :: args) |> Some + | [ ExprType String; _comparison ] -> libCall com r t "String" "endsWith2" (c :: args) |> Some + | [ pattern; ignoreCase; _culture ] -> libCall com r t "String" "endsWith3" [ c; pattern; ignoreCase ] |> Some | _ -> None | "Equals", _, _ -> match thisArg, args with | Some x, [ ExprTypeAs(String, y) ] | None, [ ExprTypeAs(String, x); ExprTypeAs(String, y) ] -> - Helper.LibCall(com, "String", "equalsOrdinal", t, [ x; y ], ?loc = r) |> Some + libCall com r t "String" "equalsOrdinal" [ x; y ] |> Some | Some x, [ ExprTypeAs(String, y); comparison ] | None, [ ExprTypeAs(String, x); ExprTypeAs(String, y); comparison ] -> - Helper.LibCall(com, "String", "equals2", t, [ x; y; comparison ], ?loc = r) - |> Some + libCall com r t "String" "equals2" [ x; y; comparison ] |> Some | _ -> None | "Format", None, _ -> match args with | (ExprType String :: _) -> "sprintf!" |> emitFormat com r t args |> Some | (cultureInfo :: restArgs) -> - $"String.Format(): Format provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r "sprintf!" |> emitFormat com r t restArgs |> Some | _ -> None @@ -1515,18 +1502,18 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | Some suffix -> let methName = (Naming.lowerFirst i.CompiledName) + suffix - Helper.LibCall(com, "String", methName, t, c :: args, ?loc = r) |> Some + libCall com r t "String" methName (c :: args) |> Some | _ -> None - | "Insert", Some c, _ -> Helper.LibCall(com, "String", "insert", t, c :: args, ?loc = r) |> Some - | "IsNullOrEmpty", None, _ -> Helper.LibCall(com, "String", "isNullOrEmpty", t, args, ?loc = r) |> Some - | "IsNullOrWhiteSpace", None, _ -> Helper.LibCall(com, "String", "isNullOrWhitespace", t, args, ?loc = r) |> Some + | "Insert", Some c, _ -> libCall com r t "String" "insert" (c :: args) |> Some + | "IsNullOrEmpty", None, _ -> libCall com r t "String" "isNullOrEmpty" args |> Some + | "IsNullOrWhiteSpace", None, _ -> libCall com r t "String" "isNullOrWhitespace" args |> Some | "Join", None, _ -> let args = match args with | [ ExprTypeAs(String, sep); ExprTypeAs(IEnumerable, arg) ] -> [ sep; toArray com t arg ] | [ ExprTypeAs(String, sep); ExprTypeAs(Array(String, _), arg) ] -> [ sep; arg ] | [ ExprTypeAs(Char, sep); ExprTypeAs(Array(String, _), arg) ] -> - let sep = Helper.LibCall(com, "String", "ofChar", String, [ sep ]) + let sep = libCall com r String "String" "ofChar" [ sep ] [ sep; arg ] | [ ExprTypeAs(String, sep) @@ -1534,23 +1521,23 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt ExprTypeAs(Number(Int32, _), idx) ExprTypeAs(Number(Int32, _), cnt) ] -> let arg = - Helper.LibCall(com, "Array", "getSubArray", Array(String, MutableArray), [ arg; idx; cnt ]) + libCall com r (Array(String, MutableArray)) "Array" "getSubArray" [ arg; idx; cnt ] [ sep; arg ] | [ ExprTypeAs(Char, sep) ExprTypeAs(Array(String, _), arg) ExprTypeAs(Number(Int32, _), idx) ExprTypeAs(Number(Int32, _), cnt) ] -> - let sep = Helper.LibCall(com, "String", "ofChar", String, [ sep ]) + let sep = libCall com r String "String" "ofChar" [ sep ] let arg = - Helper.LibCall(com, "Array", "getSubArray", Array(String, MutableArray), [ arg; idx; cnt ]) + libCall com r (Array(String, MutableArray)) "Array" "getSubArray" [ arg; idx; cnt ] [ sep; arg ] | _ -> [] if not (List.isEmpty args) then - Helper.LibCall(com, "String", "join", t, args, ?loc = r) |> Some + libCall com r t "String" "join" args |> Some else None | ("PadLeft" | "PadRight"), Some c, _ -> @@ -1560,94 +1547,67 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | [ ExprTypeAs(Number(Int32, _), arg) ] -> let ch = makeTypeConst None Char ' ' - Helper.LibCall(com, "String", methName, t, [ c; arg; ch ], ?loc = r) |> Some - | [ ExprType(Number(Int32, _)); ExprType Char ] -> - Helper.LibCall(com, "String", methName, t, c :: args, ?loc = r) |> Some + libCall com r t "String" methName [ c; arg; ch ] |> Some + | [ ExprType(Number(Int32, _)); ExprType Char ] -> libCall com r t "String" methName (c :: args) |> Some | _ -> None | "Remove", Some c, _ -> match args with - | [ ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "remove", t, c :: args, ?loc = r) |> Some + | [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "String" "remove" (c :: args) |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "String", "remove2", t, c :: args, ?loc = r) |> Some + makeLibCall com r t i "String" "remove2" (c :: args) |> Some | _ -> None | "Replace", Some c, _ -> match args with - | [ ExprType String; ExprType String ] -> - Helper.LibCall(com, "String", "replace", t, c :: args, ?loc = r) |> Some - | [ ExprType Char; ExprType Char ] -> - Helper.LibCall(com, "String", "replaceChar", t, c :: args, ?loc = r) |> Some + | [ ExprType String; ExprType String ] -> libCall com r t "String" "replace" (c :: args) |> Some + | [ ExprType Char; ExprType Char ] -> libCall com r t "String" "replaceChar" (c :: args) |> Some | _ -> None | "Split", Some c, _ -> match args with | [] -> - Helper.LibCall(com, "String", "split", t, [ c; makeStrConst ""; makeIntConst -1; makeIntConst 0 ], ?loc = r) + libCall com r t "String" "split" [ c; makeStrConst ""; makeIntConst -1; makeIntConst 0 ] |> Some | [ ExprTypeAs(String, arg1) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; makeIntConst -1; makeIntConst 0 ], ?loc = r) - |> Some - | [ ExprTypeAs(String, arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; makeIntConst -1; arg2 ], ?loc = r) - |> Some - | [ ExprTypeAs(String, arg1) - ExprTypeAs(Number(Int32, _), arg2) - ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; arg2; arg3 ], ?loc = r) + libCall com r t "String" "split" [ c; arg1; makeIntConst -1; makeIntConst 0 ] |> Some + | [ ExprTypeAs(String, arg1); ExprTypeEnum Types.stringSplitOptions arg2 ] -> + libCall com r t "String" "split" [ c; arg1; makeIntConst -1; arg2 ] |> Some + | [ ExprTypeAs(String, arg1); ExprTypeAs(Number(Int32, _), arg2); ExprTypeEnum Types.stringSplitOptions arg3 ] -> + libCall com r t "String" "split" [ c; arg1; arg2; arg3 ] |> Some | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; makeIntConst -1; makeIntConst 0 ], ?loc = r) - |> Some - | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; makeIntConst -1; arg2 ], ?loc = r) - |> Some - | [ ExprTypeAs(Array(String, _), arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall(com, "String", "splitStrings", t, [ c; arg1; arg2 ], ?loc = r) + libCall com r t "String" "split" [ c; arg1; makeIntConst -1; makeIntConst 0 ] |> Some + | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _); ExprTypeEnum Types.stringSplitOptions arg2 ] -> + libCall com r t "String" "split" [ c; arg1; makeIntConst -1; arg2 ] |> Some + | [ ExprTypeAs(Array(String, _), arg1); ExprTypeEnum Types.stringSplitOptions arg2 ] -> + libCall com r t "String" "splitStrings" [ c; arg1; arg2 ] |> Some | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _) ExprTypeAs(Number(Int32, _), arg2) - ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; arg2; arg3 ], ?loc = r) - |> Some + ExprTypeEnum Types.stringSplitOptions arg3 ] -> + libCall com r t "String" "split" [ c; arg1; arg2; arg3 ] |> Some | [ ExprTypeAs(Char, arg1) ] -> - Helper.LibCall( - com, - "String", - "splitChars", - t, - [ c; makeArray Char [ arg1 ]; makeIntConst -1; makeIntConst 0 ], - ?loc = r - ) + libCall com r t "String" "splitChars" [ c; makeArray Char [ arg1 ]; makeIntConst -1; makeIntConst 0 ] |> Some - | [ ExprTypeAs(Char, arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall( - com, - "String", - "splitChars", - t, - [ c; makeArray Char [ arg1 ]; makeIntConst -1; arg2 ], - ?loc = r - ) + | [ ExprTypeAs(Char, arg1); ExprTypeEnum Types.stringSplitOptions arg2 ] -> + libCall com r t "String" "splitChars" [ c; makeArray Char [ arg1 ]; makeIntConst -1; arg2 ] |> Some - | [ ExprTypeAs(Char, arg1); ExprTypeAs(Number(Int32, _), arg2); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; makeArray Char [ arg1 ]; arg2; arg3 ], ?loc = r) + | [ ExprTypeAs(Char, arg1); ExprTypeAs(Number(Int32, _), arg2); ExprTypeEnum Types.stringSplitOptions arg3 ] -> + libCall com r t "String" "splitChars" [ c; makeArray Char [ arg1 ]; arg2; arg3 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; arg1; makeIntConst -1; makeIntConst 0 ], ?loc = r) - |> Some - | [ ExprTypeAs(Array(Char, _), arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; arg1; makeIntConst -1; arg2 ], ?loc = r) + libCall com r t "String" "splitChars" [ c; arg1; makeIntConst -1; makeIntConst 0 ] |> Some + | [ ExprTypeAs(Array(Char, _), arg1); ExprTypeEnum Types.stringSplitOptions arg2 ] -> + libCall com r t "String" "splitChars" [ c; arg1; makeIntConst -1; arg2 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1); ExprTypeAs(Number(Int32, _), arg2) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; arg1; arg2; makeIntConst 0 ], ?loc = r) - |> Some + libCall com r t "String" "splitChars" [ c; arg1; arg2; makeIntConst 0 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1) ExprTypeAs(Number(Int32, _), arg2) - ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; arg1; arg2; arg3 ], ?loc = r) - |> Some + ExprTypeEnum Types.stringSplitOptions arg3 ] -> + libCall com r t "String" "splitChars" [ c; arg1; arg2; arg3 ] |> Some // Remaining gap: the count-bearing string[] overload Split(string[], int, options) // for multi-element / non-literal arrays. splitStrings has no count parameter, and a @@ -1656,50 +1616,39 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | _ -> None | "StartsWith", Some c, _ -> match args with - | [ ExprType Char ] -> Helper.LibCall(com, "String", "startsWithChar", t, c :: args, ?loc = r) |> Some - | [ ExprType String ] -> Helper.LibCall(com, "String", "startsWith", t, c :: args, ?loc = r) |> Some - | [ ExprType String; _comparison ] -> - Helper.LibCall(com, "String", "startsWith2", t, c :: args, ?loc = r) |> Some - | [ pattern; ignoreCase; _culture ] -> - Helper.LibCall(com, "String", "startsWith3", t, [ c; pattern; ignoreCase ], ?loc = r) - |> Some + | [ ExprType Char ] -> libCall com r t "String" "startsWithChar" (c :: args) |> Some + | [ ExprType String ] -> libCall com r t "String" "startsWith" (c :: args) |> Some + | [ ExprType String; _comparison ] -> libCall com r t "String" "startsWith2" (c :: args) |> Some + | [ pattern; ignoreCase; _culture ] -> libCall com r t "String" "startsWith3" [ c; pattern; ignoreCase ] |> Some | _ -> None | "Substring", Some c, _ -> match args with - | [ ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "substring", t, c :: args, ?loc = r) |> Some + | [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "String" "substring" (c :: args) |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "String", "substring2", t, c :: args, ?loc = r) |> Some + makeLibCall com r t i "String" "substring2" (c :: args) |> Some | _ -> None | "ToCharArray", Some c, _ -> match args with - | [] -> Helper.LibCall(com, "String", "toCharArray", t, c :: args, ?loc = r) |> Some + | [] -> makeLibCall com r t i "String" "toCharArray" (c :: args) |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "String", "toCharArray2", t, c :: args, ?loc = r) |> Some + makeLibCall com r t i "String" "toCharArray2" (c :: args) |> Some | _ -> None - | ("ToLower" | "ToLowerInvariant"), Some c, args -> - Helper.LibCall(com, "String", "toLower", t, c :: args, ?loc = r) |> Some - | ("ToUpper" | "ToUpperInvariant"), Some c, args -> - Helper.LibCall(com, "String", "toUpper", t, c :: args, ?loc = r) |> Some + | ("ToLower" | "ToLowerInvariant"), Some c, args -> libCall com r t "String" "toLower" (c :: args) |> Some + | ("ToUpper" | "ToUpperInvariant"), Some c, args -> libCall com r t "String" "toUpper" (c :: args) |> Some | ("Trim" | "TrimStart" | "TrimEnd"), Some c, _ -> let methName = Naming.lowerFirst i.CompiledName match args with - | [] -> Helper.LibCall(com, "String", methName, t, c :: args, ?loc = r) |> Some - | [ ExprType Char ] -> Helper.LibCall(com, "String", methName + "Char", t, c :: args, ?loc = r) |> Some - | [ ExprType(Array(Char, _)) ] -> - Helper.LibCall(com, "String", methName + "Chars", t, c :: args, ?loc = r) - |> Some + | [] -> libCall com r t "String" methName (c :: args) |> Some + | [ ExprType Char ] -> libCall com r t "String" (methName + "Char") (c :: args) |> Some + | [ ExprType(Array(Char, _)) ] -> libCall com r t "String" (methName + "Chars") (c :: args) |> Some | _ -> None | _ -> None let stringModule (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with - | "Concat", [ sep; arg ] -> - Helper.LibCall(com, "String", "join", t, [ sep; toArray com t arg ], ?loc = r) - |> Some - | meth, args -> - Helper.LibCall(com, "String", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "Concat", [ sep; arg ] -> libCall com r t "String" "join" [ sep; toArray com t arg ] |> Some + | meth, args -> makeLibCall com r t i "String" (Naming.lowerFirst meth) args |> Some let stringBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with @@ -1708,14 +1657,13 @@ let stringBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Ex | (ExprType String :: _) -> let s = "sprintf!" |> emitFormat com None String args - Helper.LibCall(com, "Util", "sb_Append", t, [ sb; s ], ?loc = r) |> Some + libCall com r t "Util" "sb_Append" [ sb; s ] |> Some | (cultureInfo :: restArgs) -> - $"StringBuilder.AppendFormat(): Format provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r let s = "sprintf!" |> emitFormat com None String restArgs - Helper.LibCall(com, "Util", "sb_Append", t, [ sb; s ], ?loc = r) |> Some + libCall com r t "Util" "sb_Append" [ sb; s ] |> Some | _ -> None | _ -> bclType com ctx r t i thisArg args @@ -1740,7 +1688,7 @@ let formattableString let tag = if not hasFormat then - Helper.LibValue(com, "String", "fmt", Any) |> Some + libValue com r Any "String" "fmt" [] |> Some else let fmtArg = matches @@ -1748,7 +1696,7 @@ let formattableString |> Array.toList |> makeArray String - Helper.LibCall(com, "String", "fmtWith", Any, [ fmtArg ]) |> Some + libCall com r Any "String" "fmtWith" [ fmtArg ] |> Some let holes = matches @@ -1762,7 +1710,7 @@ let formattableString let template = makeStringTemplate tag str holes args |> makeValue r // Use a type cast to keep the FormattableString type TypeCast(template, t) |> Some - | "get_Format", Some x, _ -> Helper.LibCall(com, "String", "getFormat", t, [ x ], ?loc = r) |> Some + | "get_Format", Some x, _ -> libCall com r t "String" "getFormat" [ x ] |> Some | "get_ArgumentCount", Some x, _ -> getFieldWith r t (getField x "args") "length" |> Some | "GetArgument", Some x, [ idx ] -> getExpr r t (getField x "args") idx |> Some | "GetArguments", Some x, [] -> getFieldWith r t x "args" |> Some @@ -1771,58 +1719,45 @@ let formattableString let seqModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, args with // | "ToArray", [arg] -> - // Helper.LibCall(com, "Array", "ofSeq", t, args, i.SignatureArgTypes, ?loc=r) |> Some - | "ToList", [ arg ] -> - Helper.LibCall(com, "List", "ofSeq", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + // makeLibCall com r t i "Array" "ofSeq" args |> Some + | "ToList", [ arg ] -> makeLibCall com r t i "List" "ofSeq" args |> Some | "CreateEvent", [ addHandler; removeHandler; createHandler ] -> - Helper.LibCall(com, "Event", "createEvent", t, [ addHandler; removeHandler ], i.SignatureArgTypes, ?loc = r) + makeLibCall com r t i "Event" "createEvent" [ addHandler; removeHandler ] |> Some - | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy" as meth), args -> + | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy") as meth, args -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "Seq", meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + makeLibCall com r t i "Seq" meth args |> Some | meth, _ -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "Seq", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + libCallThis com r t i "Seq" meth thisArg args |> Some let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | ".ctor", _, [] -> Helper.LibCall(com, "NativeArray", "new_empty", t, args, ?loc = r) |> Some - | ".ctor", _, [ ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "NativeArray", "new_with_capacity", t, args, ?loc = r) - |> Some - | ".ctor", _, [ ExprType(IEnumerable) ] -> - Helper.LibCall(com, "NativeArray", "new_from_enumerable", t, args, ?loc = r) - |> Some + | ".ctor", _, [] -> libCall com r t "NativeArray" "new_empty" args |> Some + | ".ctor", _, [ ExprType(Number(Int32, _)) ] -> libCall com r t "NativeArray" "new_with_capacity" args |> Some + | ".ctor", _, [ ExprType(IEnumerable) ] -> libCall com r t "NativeArray" "new_from_enumerable" args |> Some | ".ctor", _, [ arg ] -> toArray com t arg |> Some - | "get_Capacity", Some ar, _ -> Helper.LibCall(com, "NativeArray", "get_Capacity", t, [ ar ], ?loc = r) |> Some - | "get_Count", Some ar, _ -> Helper.LibCall(com, "NativeArray", "get_Count", t, [ ar ], ?loc = r) |> Some + | "get_Capacity", Some ar, _ -> libCall com r t "NativeArray" "get_Capacity" [ ar ] |> Some + | "get_Count", Some ar, _ -> libCall com r t "NativeArray" "get_Count" [ ar ] |> Some | "get_Item", Some ar, [ idx ] -> getExpr r t ar idx |> Some | "set_Item", Some ar, [ idx; value ] -> setExpr r ar idx value |> Some - | "GetEnumerator", Some(MaybeCasted(ar)), _ -> - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) |> Some + | "GetEnumerator", Some(MaybeCasted(ar)), _ -> libCall com r t "Seq" "Enumerable::ofArray" [ ar ] |> Some | ("Add" | "AddRange" | "Clear" | "Contains" | "ConvertAll" | "Exists" | "GetRange" | "Slice" | "ForEach" | "FindAll" | "Find" | "FindLast" | "FindIndex" | "FindLastIndex" | "Insert" | "InsertRange" | "Remove" | "RemoveAt" | "RemoveAll" | "RemoveRange" | "ToArray" | "TrimExcess" | "TrueForAll") as meth, Some ar, args -> // methods without overrides let meth = Naming.lowerFirst meth - Helper.LibCall(com, "NativeArray", meth, t, (ar :: args), ?loc = r) |> Some + libCall com r t "NativeArray" meth (ar :: args) |> Some | ("BinarySearch" | "CopyTo" | "IndexOf" | "LastIndexOf" | "Reverse") as meth, Some ar, args -> // methods with some overrides let meth = meth |> toLowerFirstWithArgsCountSuffix (ar :: args) - Helper.LibCall(com, "NativeArray", meth, t, (ar :: args), ?loc = r) |> Some - | "Sort", Some ar, [] -> Helper.LibCall(com, "NativeArray", "sort", t, (ar :: args), ?loc = r) |> Some - | "Sort", Some ar, [ ExprType(DelegateType _) ] -> - Helper.LibCall(com, "NativeArray", "sortBy", t, (ar :: args), ?loc = r) |> Some - | "Sort", Some ar, [ comparer ] -> - Helper.LibCall(com, "NativeArray", "sortWith", t, (ar :: args), ?loc = r) - |> Some - | "Sort", Some ar, [ index; count; comparer ] -> - Helper.LibCall(com, "NativeArray", "sortWith2", t, (ar :: args), ?loc = r) - |> Some + libCall com r t "NativeArray" meth (ar :: args) |> Some + | "Sort", Some ar, [] -> libCall com r t "NativeArray" "sort" (ar :: args) |> Some + | "Sort", Some ar, [ ExprType(DelegateType _) ] -> libCall com r t "NativeArray" "sortBy" (ar :: args) |> Some + | "Sort", Some ar, [ comparer ] -> libCall com r t "NativeArray" "sortWith" (ar :: args) |> Some + | "Sort", Some ar, [ index; count; comparer ] -> libCall com r t "NativeArray" "sortWith2" (ar :: args) |> Some | _ -> None let collectionExtensions @@ -1835,12 +1770,8 @@ let collectionExtensions (args: Expr list) = match i.CompiledName, thisArg, args with - | "AddRange", None, [ ar; arg ] -> - Helper.LibCall(com, "Array", "addRangeInPlace", t, [ arg; ar ], ?loc = r) - |> Some - | "InsertRange", None, [ ar; idx; arg ] -> - Helper.LibCall(com, "Array", "insertRangeInPlace", t, [ idx; arg; ar ], ?loc = r) - |> Some + | "AddRange", None, [ ar; arg ] -> libCall com r t "Array" "addRangeInPlace" [ arg; ar ] |> Some + | "InsertRange", None, [ ar; idx; arg ] -> libCall com r t "Array" "insertRangeInPlace" [ idx; arg; ar ] |> Some | _ -> None let readOnlySpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -1887,50 +1818,40 @@ let createArray (com: ICompiler) ctx r t i count value = |> addErrorAndReturnNull com ctx.InlinePath r let copyToArray (com: ICompiler) r t (i: CallInfo) args = - Helper.LibCall(com, "Array", "copyTo", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Array" "copyTo" args |> Some let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | "get_Length", Some ar, _ -> Helper.LibCall(com, "NativeArray", "get_Count", t, [ ar ], ?loc = r) |> Some + | "get_Length", Some ar, _ -> libCall com r t "NativeArray" "get_Count" [ ar ] |> Some | "get_Item", Some ar, [ idx ] -> getExpr r t ar idx |> Some | "set_Item", Some ar, [ idx; value ] -> setExpr r ar idx value |> Some - | "Clone", Some ar, _ -> Helper.LibCall(com, "NativeArray", "toArray", t, [ ar ], ?loc = r) |> Some + | "Clone", Some ar, _ -> libCall com r t "NativeArray" "toArray" [ ar ] |> Some | "Copy", None, [ _source; _sourceIndex; _target; _targetIndex; _count ] -> copyToArray com r t i args | "Copy", None, [ source; target; count ] -> copyToArray com r t i [ source; makeIntConst 0; target; makeIntConst 0; count ] - | "GetEnumerator", Some ar, _ -> Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) |> Some + | "GetEnumerator", Some ar, _ -> libCall com r t "Seq" "Enumerable::ofArray" [ ar ] |> Some | ("ConvertAll" | "Exists" | "GetRange" | "ForEach" | "FindAll" | "Find" | "FindLast" | "FindIndex" | "FindLastIndex" | "TrueForAll") as meth, None, args -> // methods without overrides let meth = Naming.lowerFirst meth - Helper.LibCall(com, "NativeArray", meth, t, args, ?loc = r) |> Some + libCall com r t "NativeArray" meth args |> Some | ("BinarySearch" | "CopyTo" | "IndexOf" | "LastIndexOf" | "Reverse") as meth, None, args -> // methods with some overrides let meth = meth |> toLowerFirstWithArgsCountSuffix args - Helper.LibCall(com, "NativeArray", meth, t, args, ?loc = r) |> Some - | "Sort", None, [ ar ] -> Helper.LibCall(com, "NativeArray", "sort", t, args, ?loc = r) |> Some - | "Sort", None, [ ar; ExprType(DelegateType _) as comparer ] -> - Helper.LibCall(com, "NativeArray", "sortBy", t, args, ?loc = r) |> Some - | "Sort", None, [ ar; comparer ] -> Helper.LibCall(com, "NativeArray", "sortWith", t, args, ?loc = r) |> Some + libCall com r t "NativeArray" meth args |> Some + | "Sort", None, [ ar ] -> libCall com r t "NativeArray" "sort" args |> Some + | "Sort", None, [ ar; ExprType(DelegateType _) as comparer ] -> libCall com r t "NativeArray" "sortBy" args |> Some + | "Sort", None, [ ar; comparer ] -> libCall com r t "NativeArray" "sortWith" args |> Some | _ -> None let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with - | "ToSeq", [ ar ] -> - Helper.LibCall(com, "Seq", "ofArray", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "OfSeq", [ ar ] -> - Helper.LibCall(com, "Seq", "toArray", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "OfList", [ ar ] -> - Helper.LibCall(com, "List", "toArray", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "ToList", args -> - Helper.LibCall(com, "List", "ofArray", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("Length" | "Count"), [ ar ] -> Helper.LibCall(com, "NativeArray", "get_Count", t, [ ar ], ?loc = r) |> Some + | "ToSeq", [ ar ] -> makeLibCall com r t i "Seq" "ofArray" args |> Some + | "OfSeq", [ ar ] -> makeLibCall com r t i "Seq" "toArray" args |> Some + | "OfList", [ ar ] -> makeLibCall com r t i "List" "toArray" args |> Some + | "ToList", args -> makeLibCall com r t i "List" "ofArray" args |> Some + | ("Length" | "Count"), [ ar ] -> libCall com r t "NativeArray" "get_Count" [ ar ] |> Some | "Item", [ idx; ar ] -> getExpr r t ar idx |> Some | "Get", [ ar; idx ] -> getExpr r t ar idx |> Some | "Set", [ ar; idx; value ] -> setExpr r ar idx value |> Some @@ -1939,21 +1860,19 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex | "Empty", [] -> createArray com ctx r t i (makeIntConst 0) None |> Some | "Singleton", [ value ] -> createArray com ctx r t i (makeIntConst 1) (Some value) |> Some | "IsEmpty", [ ar ] -> makeInstanceCall r t i ar "is_empty" [] |> Some - | "Copy", [ ar ] -> Helper.LibCall(com, "NativeArray", "toArray", t, args, ?loc = r) |> Some + | "Copy", [ ar ] -> libCall com r t "NativeArray" "toArray" args |> Some | "CopyTo", args -> copyToArray com r t i args - | ("Concat" | "Transpose" as meth), [ arg ] -> - Helper.LibCall(com, "Array", Naming.lowerFirst meth, t, [ toArray com t arg ], i.SignatureArgTypes, ?loc = r) + | ("Concat" | "Transpose") as meth, [ arg ] -> + makeLibCall com r t i "Array" (Naming.lowerFirst meth) [ toArray com t arg ] |> Some - | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy" as meth), args -> + | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy") as meth, args -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "Array", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Array" meth args |> Some | meth, _ -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "Array", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Array" meth args |> Some let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with @@ -1970,14 +1889,13 @@ let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Ex | [ ExprType Unit ] -> [ x ] | args -> args @ [ x ] - Helper.LibCall(com, "List", methName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "List" methName args |> Some | "get_IsEmpty", Some c, _ -> Test(c, ListTest false, r) |> Some | "get_Empty", None, _ -> NewList(None, (genArg com ctx r 0 i.GenericArgs)) |> makeValue r |> Some | "Cons", None, [ h; t ] -> NewList(Some(h, t), (genArg com ctx r 0 i.GenericArgs)) |> makeValue r |> Some // | ("GetHashCode" | "Equals" | "CompareTo"), Some c, _ -> // makeInstanceCall r t i c i.CompiledName args |> Some - | "GetEnumerator", Some c, _ -> Helper.LibCall(com, "Seq", "Enumerable::ofList", t, [ c ], ?loc = r) |> Some + | "GetEnumerator", Some c, _ -> libCall com r t "Seq" "Enumerable::ofList" [ c ] |> Some | _ -> None let listModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = @@ -1990,25 +1908,19 @@ let listModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Exp |> Some // Use a cast to give it better chances of optimization (e.g. converting list // literals to arrays) after the beta reduction pass - | "ToSeq", [ arg ] -> - Helper.LibCall(com, "Seq", "ofList", t, args, i.SignatureArgTypes, ?loc = r) + | "ToSeq", [ arg ] -> makeLibCall com r t i "Seq" "ofList" args |> Some + | "OfSeq", [ arg ] -> makeLibCall com r t i "List" "ofSeq" args |> Some + | ("Concat" | "Transpose") as meth, [ arg ] -> + makeLibCall com r t i "List" (Naming.lowerFirst meth) [ toList com t arg ] |> Some - | "OfSeq", [ arg ] -> - Helper.LibCall(com, "List", "ofSeq", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("Concat" | "Transpose" as meth), [ arg ] -> - Helper.LibCall(com, "List", Naming.lowerFirst meth, t, [ toList com t arg ], i.SignatureArgTypes, ?loc = r) - |> Some - | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy" as meth), args -> + | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy") as meth, args -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "List", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "List" meth args |> Some | meth, _ -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "List", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "List" meth args |> Some let discardUnitArgs args = match args with @@ -2027,43 +1939,36 @@ let sets (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Exp "IsProperSubsetOf", "isProperSubset" "IsProperSupersetOf", "isProperSuperset" "CopyTo", "copyToArray" ] meth, - Some callee -> Helper.LibCall(com, "Set", meth, t, callee :: args, ?loc = r) |> Some - | meth, _ -> + Some c -> libCall com r t "Set" meth (c :: args) |> Some + | meth, Some c -> let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst - - let args = - match thisArg with - | Some callee -> args @ [ callee ] - | _ -> args - - Helper.LibCall(com, "Set", meth, t, args, ?loc = r) |> Some + libCall com r t "Set" meth (args @ [ c ]) |> Some + | meth, None -> + let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst + libCall com r t "Set" meth args |> Some let setModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = let meth = Naming.lowerFirst i.CompiledName - Helper.LibCall(com, "Set", meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + makeLibCall com r t i "Set" meth args |> Some let maps (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let args = discardUnitArgs args match i.CompiledName, thisArg with | ".ctor", _ -> (genArg com ctx r 0 i.GenericArgs) |> makeMap com ctx r t args |> Some - | ReplaceName [ "CopyTo", "copyToArray" ] meth, Some callee -> - Helper.LibCall(com, "Map", meth, t, callee :: args, ?loc = r) |> Some - | meth, _ -> + | ReplaceName [ "CopyTo", "copyToArray" ] meth, Some c -> libCall com r t "Map" meth (c :: args) |> Some + | meth, Some c -> let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst - - let args = - match thisArg with - | Some callee -> args @ [ callee ] - | _ -> args - - Helper.LibCall(com, "Map", meth, t, args, ?loc = r) |> Some + libCall com r t "Map" meth (args @ [ c ]) |> Some + | meth, None -> + let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst + libCall com r t "Map" meth args |> Some let mapModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = let meth = Naming.lowerFirst i.CompiledName - Helper.LibCall(com, "Map", meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + makeLibCall com r t i "Map" meth args |> Some let results (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with @@ -2084,15 +1989,13 @@ let results (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr o | "ToArray" | "ToList" | "ToOption" - | "ToValueOption" as meth -> - Helper.LibCall(com, "Result", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "ToValueOption" as meth -> makeLibCall com r t i "Result" (Naming.lowerFirst meth) args |> Some | _ -> None let nullables (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with | ".ctor", None -> NewOption(List.tryHead args, t.Generics.Head, false) |> makeValue r |> Some - | "get_Value", Some c -> Helper.LibCall(com, "Option", "getValue", t, [ c ], ?loc = r) |> Some + | "get_Value", Some c -> libCall com r t "Option" "getValue" [ c ] |> Some | "get_HasValue", Some c -> Test(c, OptionTest true, r) |> Some | _ -> None @@ -2112,21 +2015,18 @@ let optionModule isStruct (com: ICompiler) (ctx: Context) r (t: Type) (i: CallIn | "GetValue", [ c ] -> Get(c, OptionValue, t, r) |> Some | "IsSome", [ c ] -> Test(c, OptionTest true, r) |> Some | "IsNone", [ c ] -> Test(c, OptionTest false, r) |> Some - | "OfObj", [ arg ] -> Helper.LibCall(com, "Native", "ofObj", t, args, ?loc = r) |> Some - | "ToObj", [ arg ] -> Helper.LibCall(com, "Native", "toObj", t, args, ?loc = r) |> Some - // | "ToArray", [ arg ] -> Helper.LibCall(com, "Array", "ofOption", t, args, ?loc = r) |> Some - // | "ToList", [ arg ] -> Helper.LibCall(com, "List", "ofOption", t, args, ?loc = r) |> Some - | meth, args -> - Helper.LibCall(com, "Option", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "OfObj", [ arg ] -> libCall com r t "Native" "ofObj" args |> Some + | "ToObj", [ arg ] -> libCall com r t "Native" "toObj" args |> Some + // | "ToArray", [ arg ] -> libCall com r t "Array" "ofOption" args |> Some + // | "ToList", [ arg ] -> libCall com r t "List" "ofOption" args |> Some + | meth, args -> makeLibCall com r t i "Option" (Naming.lowerFirst meth) args |> Some let parseBool (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, args with | ("Parse" | "TryParse" as method), args -> let meth = Naming.lowerFirst method + "Boolean" - Helper.LibCall(com, "Convert", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Convert" meth args |> Some | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args | _ -> None @@ -2151,16 +2051,16 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else [ str; makeIntConst style ] @ outValue - Helper.LibCall(com, moduleName, memberName, t, args, ?loc = r) + libCall com r t moduleName memberName args let isFloat = match i.SignatureArgTypes with | Number((Float16 | Float32 | Float64), _) :: _ -> true | _ -> false - match i.CompiledName, args with - | "IsNaN", [ arg ] when isFloat -> makeInstanceCall r t i arg "is_nan" [] |> Some - | "Log2", [ arg ] -> + match i.CompiledName, thisArg, args with + | "IsNaN", _, [ arg ] when isFloat -> makeInstanceCall r t i arg "is_nan" [] |> Some + | "Log2", _, [ arg ] -> let log = if isFloat then makeInstanceCall r t i arg "log2" [] @@ -2168,23 +2068,23 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op makeInstanceCall r UInt32.Number i arg "ilog2" [] TypeCast(log, t) |> Some - | "IsPositiveInfinity", [ arg ] when isFloat -> + | "IsPositiveInfinity", _, [ arg ] when isFloat -> let op1 = makeInstanceCall r t i arg "is_sign_positive" [] let op2 = makeInstanceCall r t i arg "is_infinite" [] Operation(Logical(LogicalAnd, op1, op2), Tags.empty, t, None) |> Some - | "IsNegativeInfinity", [ arg ] when isFloat -> + | "IsNegativeInfinity", _, [ arg ] when isFloat -> let op1 = makeInstanceCall r t i arg "is_sign_negative" [] let op2 = makeInstanceCall r t i arg "is_infinite" [] Operation(Logical(LogicalAnd, op1, op2), Tags.empty, t, None) |> Some - | "IsInfinity", [ arg ] when isFloat -> makeInstanceCall r t i arg "is_infinite" [] |> Some - | ("Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp"), _ -> operators com ctx r t i thisArg args - | ("Parse" | "TryParse") as meth, str :: NumberConst(NumberValue.Int32 style, _) :: _ -> + | "IsInfinity", _, [ arg ] when isFloat -> makeInstanceCall r t i arg "is_infinite" [] |> Some + | ("Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp"), _, _ -> operators com ctx r t i thisArg args + | ("Parse" | "TryParse") as meth, _, str :: NumberConst(NumberValue.Int32 style, _) :: _ -> let hexConst = int System.Globalization.NumberStyles.HexNumber let intConst = int System.Globalization.NumberStyles.Integer if style <> hexConst && style <> intConst then - $"%s{i.DeclaringEntityFullName}.%s{meth}(): NumberStyle %d{style} is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.numberStylesIgnored style + |> addWarningWithCode com ctx.InlinePath r let acceptedArgs = if meth = "Parse" then @@ -2192,98 +2092,100 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 3 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, style, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r parseCall meth str args style |> Some - | ("Parse" | "TryParse") as meth, str :: _ -> + | ("Parse" | "TryParse") as meth, _, str :: _ -> let acceptedArgs = if meth = "Parse" then 1 else 2 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r let style = int System.Globalization.NumberStyles.Any parseCall meth str args style |> Some - | "Pow", (thisArg :: restArgs) -> makeInstanceCall r t i thisArg "powf" restArgs |> Some + | "Pow", _, (arg :: restArgs) -> makeInstanceCall r t i arg "powf" restArgs |> Some // | "ToString", [StringConst fmt] -> // let rustFmt = fmt // TODO: replace format specifiers with proper Rust format // let format = makeStrConst ("{0:" + rustFmt + "}") - // "sprintf!" |> emitFormat com r t [format; thisArg.Value] |> Some - | "ToString", _ -> toString com ctx r [ thisArg.Value ] |> Some - | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args + // "sprintf!" |> emitFormat com r t [format; thisArg] |> Some + | "ToString", Some c, _ -> toString com ctx r [ c ] |> Some + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg, _ -> valueTypes com ctx r t i thisArg args | _ -> None let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName, args with - | (".ctor" | "MakeDecimal"), ([ low; mid; high; isNegative; scale ] as args) -> - Helper.LibCall(com, "Decimal", "fromParts", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ".ctor", [ Value(NewArray(ArrayValues([ low; mid; high; signExp ] as args), _, _), _) ] -> - Helper.LibCall(com, "Decimal", "fromInts", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ".ctor", [ arg ] -> convertTo com ctx r t args |> Some - | "GetBits", _ -> - Helper.LibCall(com, "Decimal", "getBits", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Parse", _ -> - Helper.LibCall(com, "Decimal", "parse", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "TryParse", _ -> - Helper.LibCall(com, "Decimal", "tryParse", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | Patterns.SetContains Operators.compareSet, [ left; right ] -> + match i.CompiledName, thisArg, args with + | (".ctor" | "MakeDecimal"), _, ([ low; mid; high; isNegative; scale ] as args) -> + makeLibCall com r t i "Decimal" "fromParts" args |> Some + | ".ctor", _, [ Value(NewArray(ArrayValues([ low; mid; high; signExp ] as args), _, _), _) ] -> + makeLibCall com r t i "Decimal" "fromInts" args |> Some + | ".ctor", _, [ arg ] -> convertTo com ctx r t args |> Some + | "GetBits", _, _ -> makeLibCall com r t i "Decimal" "getBits" args |> Some + | "Parse", _, [ input; ExprTypeEnum Types.numberStyles style; FormatProvider _ ] -> + makeLibCall com r t i "Decimal" "parseWithStyle" [ input; style ] |> Some + | "Parse", _, [ input; FormatProvider _ ] -> makeLibCall com r t i "Decimal" "parse" [ input ] |> Some + | "Parse", _, [ input; ExprTypeEnum Types.numberStyles style ] -> + makeLibCall com r t i "Decimal" "parseWithStyle" [ input; style ] |> Some + | "Parse", _, [ input ] -> makeLibCall com r t i "Decimal" "parse" args |> Some + | "TryParse", _, [ input; ExprTypeEnum Types.numberStyles style; FormatProvider _; result ] -> + makeLibCall com r t i "Decimal" "tryParseWithStyle" [ input; style; result ] + |> Some + | "TryParse", _, [ input; FormatProvider _; result ] -> + makeLibCall com r t i "Decimal" "tryParse" [ input; result ] |> Some + | "TryParse", _, [ input; ExprTypeEnum Types.numberStyles style; result ] -> + makeLibCall com r t i "Decimal" "tryParseWithStyle" [ input; style; result ] + |> Some + | "TryParse", _, [ input; result ] -> makeLibCall com r t i "Decimal" "tryParse" args |> Some + | "Parse", _, _ -> makeLibCall com r t i "Decimal" "parse" args |> Some + | "TryParse", _, _ -> makeLibCall com r t i "Decimal" "tryParse" args |> Some + | Patterns.SetContains Operators.compareSet, _, [ left; right ] -> applyCompareOp com ctx r t i.CompiledName left right |> Some - | Patterns.SetContains Operators.standardSet, _ -> applyOp com ctx r t i.CompiledName args |> Some - | "op_Explicit", _ -> convertTo com ctx r t args |> Some - | ("Ceiling" | "Floor" | "Truncate" | "Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Add" | "Subtract" | "Multiply" | "Divide" | "Remainder" | "Negate" as meth), + | Patterns.SetContains Operators.standardSet, _, _ -> applyOp com ctx r t i.CompiledName args |> Some + | "op_Explicit", _, _ -> convertTo com ctx r t args |> Some + | ("Abs" | "Sign" | "Ceiling" | "Floor" | "Truncate" | "Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Add" | "Subtract" | "Multiply" | "Divide" | "Remainder" | "Negate") as meth, + _, _ -> let meth = Naming.lowerFirst meth - - Helper.LibCall(com, "Decimal", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("get_Zero" | "get_One" | "get_MinusOne" | "get_MinValue" | "get_MaxValue"), _ -> - Helper.LibValue(com, "Decimal", Naming.removeGetSetPrefix i.CompiledName, t) - |> Some - | ("IsInteger" | "IsEvenInteger" | "IsOddInteger" | "IsCanonical" | "IsNegative" | "IsPositive"), _ -> - Helper.LibCall(com, "Decimal", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "get_Scale", [] -> - match thisArg with - | Some c -> - Helper.LibCall(com, "Decimal", "scale", t, [ c ], i.SignatureArgTypes, ?loc = r) - |> Some - | None -> None - | "Round", _ -> + makeLibCall com r t i "Decimal" meth args |> Some + | ("CopySign" | "FromOACurrency" | "GetTypeCode" | "ToOACurrency") as meth, _, _ -> + makeLibCall com r t i "Decimal" (Naming.lowerFirst meth) args |> Some + | ("ToByte" | "ToSByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToSingle" | "ToDouble"), + None, + [ _ ] -> convertTo com ctx r t args |> Some + | ("get_Zero" | "get_One" | "get_MinusOne" | "get_MinValue" | "get_MaxValue"), _, _ -> + libValue com r t "Decimal" (Naming.removeGetSetPrefix i.CompiledName) [] |> Some + | ("IsInteger" | "IsEvenInteger" | "IsOddInteger" | "IsCanonical" | "IsNegative" | "IsPositive"), _, _ -> + makeLibCall com r t i "Decimal" (Naming.lowerFirst i.CompiledName) args |> Some + | "get_Scale", Some c, [] -> makeLibCall com r t i "Decimal" "scale" [ c ] |> Some + | "get_Scale", None, [] -> None + | "Round", _, _ -> match args with - | [ x ] -> - Helper.LibCall(com, "Decimal", "round", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ x; ExprTypeAs(Number(Int32, _), dp) ] -> - Helper.LibCall(com, "Decimal", "roundTo", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ x; mode ] -> - Helper.LibCall(com, "Decimal", "roundMode", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ x; dp; mode ] -> - Helper.LibCall(com, "Decimal", "roundToMode", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | [ x ] -> makeLibCall com r t i "Decimal" "round" args |> Some + | [ x; ExprTypeEnum "System.MidpointRounding" mode ] -> makeLibCall com r t i "Decimal" "roundMode" args |> Some + | [ x; ExprTypeAs(Number(Int32, _), dp) ] -> makeLibCall com r t i "Decimal" "roundTo" args |> Some + | [ x; mode ] -> makeLibCall com r t i "Decimal" "roundMode" args |> Some + | [ x; dp; mode ] -> makeLibCall com r t i "Decimal" "roundToMode" args |> Some | _ -> None - // | "ToString", [StringConst fmt] -> - // let rustFmt = fmt // TODO: replace format specifiers with proper Rust format - // let format = makeStrConst ("{0:" + rustFmt + "}") - // "sprintf!" |> emitFormat com r t [format; thisArg.Value] |> Some - | "ToString", _ -> - Helper.LibCall(com, "Decimal", "toString", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args + | "ToString", Some c, [ format; FormatProvider _ ] -> + libCall com r t "Decimal" "toStringWithFormat" [ c; format ] |> Some + | "ToString", Some c, [ FormatProvider _ ] -> makeLibCall com r t i "Decimal" "toString" [ c ] |> Some + | "ToString", Some c, [ format ] -> libCall com r t "Decimal" "toStringWithFormat" [ c; format ] |> Some + | "ToString", Some c, [] -> makeLibCall com r t i "Decimal" "toString" [ c ] |> Some + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg, _ -> valueTypes com ctx r t i thisArg args | _ -> None let bigints (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -2292,31 +2194,17 @@ let bigints (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: | Patterns.SetContains Operators.compareSet, _, [ left; right ] -> applyCompareOp com ctx r t i.CompiledName left right |> Some | Patterns.SetContains Operators.standardSet, _, _ -> applyOp com ctx r t i.CompiledName args |> Some - | "DivRem", None, [ x; y ] -> - Helper.LibCall(com, "BigInt", "divRem", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "DivRem", None, [ x; y; rem ] -> - Helper.LibCall(com, "BigInt", "divRemOut", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "DivRem", None, [ x; y ] -> makeLibCall com r t i "BigInt" "divRem" args |> Some + | "DivRem", None, [ x; y; rem ] -> makeLibCall com r t i "BigInt" "divRemOut" args |> Some | "op_Explicit", None, _ -> convertTo com ctx r t args |> Some - | "Log", None, [ arg1; arg2 ] -> - Helper.LibCall(com, "BigInt", "log", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | "Log", None, [ arg ] -> - Helper.LibCall(com, "BigInt", "ln", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | "Log2", None, [ arg ] -> - Helper.LibCall(com, "BigInt", "ilog2", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | "Log", None, [ arg1; arg2 ] -> makeLibCall com r t i "BigInt" "log" args |> Some + | "Log", None, [ arg ] -> makeLibCall com r t i "BigInt" "ln" args |> Some + | "Log2", None, [ arg ] -> makeLibCall com r t i "BigInt" "ilog2" args |> Some | meth, None, _ when meth.StartsWith("get_", StringComparison.Ordinal) -> let meth = meth |> Naming.removeGetSetPrefix |> Naming.lowerFirst - Helper.LibCall(com, "BigInt", meth, t, []) |> Some - | meth, None, _ -> - Helper.LibCall(com, "BigInt", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | meth, Some c, _ -> - Helper.LibCall(com, "BigInt", Naming.lowerFirst meth, t, c :: args, i.SignatureArgTypes, ?loc = r) - |> Some + libCall com r t "BigInt" meth [] |> Some + | meth, None, _ -> makeLibCall com r t i "BigInt" (Naming.lowerFirst meth) args |> Some + | meth, Some c, _ -> makeLibCall com r t i "BigInt" (Naming.lowerFirst meth) (c :: args) |> Some // Compile static strings to their constant values // reference: https://msdn.microsoft.com/en-us/visualfsharpdocs/conceptual/languageprimitives.errorstrings-module-%5bfsharp%5d @@ -2341,16 +2229,16 @@ let languagePrimitives (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr else applyOp com ctx r t operation args |> Some | "DivideByInt", _ -> applyOp com ctx r t i.CompiledName args |> Some - | "GenericZero", _ -> Helper.LibCall(com, "Native", "defaultOf", t, []) |> Some + | "GenericZero", _ -> libCall com r t "Native" "defaultOf" [] |> Some // | "GenericZero", _ -> getZero com ctx t |> Some | "GenericOne", _ -> getOne com ctx t |> Some | ("SByteWithMeasure" | "Int16WithMeasure" | "Int32WithMeasure" | "Int64WithMeasure" | "Float32WithMeasure" | "FloatWithMeasure" | "DecimalWithMeasure"), [ arg ] -> arg |> Some | "EnumOfValue", [ arg ] -> TypeCast(arg, t) |> Some | "EnumToValue", [ arg ] -> TypeCast(arg, t) |> Some - | ("GenericHash" | "GenericHashIntrinsic"), [ arg ] -> getHashCode com ctx r arg |> Some - | ("FastHashTuple2" | "FastHashTuple3" | "FastHashTuple4" | "FastHashTuple5" | "GenericHashWithComparer" | "GenericHashWithComparerIntrinsic"), - [ comp; arg ] -> makeInstanceCall r t i comp "GetHashCode" [ arg ] |> Some + | ("GenericHash" | "GenericHashIntrinsic"), [ arg ] -> getHashCode com ctx r t arg |> Some + // | ("FastHashTuple2" | "FastHashTuple3" | "FastHashTuple4" | "FastHashTuple5" | "GenericHashWithComparer" | "GenericHashWithComparerIntrinsic"), + // [ comp; arg ] -> makeInstanceCall r t i comp "GetHashCode" [ arg ] |> Some | ("GenericComparison" | "GenericComparisonIntrinsic"), [ left; right ] -> compare com ctx r left right |> Some | ("FastCompareTuple2" | "FastCompareTuple3" | "FastCompareTuple4" | "FastCompareTuple5" | "GenericComparisonWithComparer" | "GenericComparisonWithComparerIntrinsic"), [ comp; left; right ] -> makeInstanceCall r t i comp "Compare" [ left; right ] |> Some @@ -2370,7 +2258,7 @@ let languagePrimitives (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr [ comp; left; right ] -> makeInstanceCall r t i comp "Equals" [ left; right ] |> Some | ("PhysicalEquality" | "PhysicalEqualityIntrinsic"), [ left; right ] -> referenceEquals com ctx r left right |> Some - | ("PhysicalHash" | "PhysicalHashIntrinsic"), [ arg ] -> referenceHash com ctx r arg |> Some + | ("PhysicalHash" | "PhysicalHashIntrinsic"), [ arg ] -> referenceHash com ctx r t arg |> Some | ("GenericEqualityComparer" | "GenericEqualityERComparer" | "FastGenericComparer" | "FastGenericComparerFromTable" | "FastGenericEqualityComparer" | "FastGenericEqualityComparerFromTable"), _ -> fsharpModule com ctx r t i thisArg args | ("ParseInt32" | "ParseUInt32" | "ParseInt64" | "ParseUInt64"), [ arg ] -> convertTo com ctx r t args |> Some @@ -2379,22 +2267,16 @@ let languagePrimitives (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with // Erased operators - | "CheckThis", _, [ arg ] - | "UnboxFast", _, [ arg ] - | "UnboxGeneric", _, [ arg ] -> Some arg + | "CheckThis", _, [ arg ] -> Some arg + | "UnboxFast", _, [ arg ] -> Some arg + | "UnboxGeneric", _, [ arg ] -> TypeCast(arg, t) |> Some | "MakeDecimal", _, _ -> decimals com ctx r t i thisArg args - | "GetString", _, [ ar; idx ] -> Helper.LibCall(com, "String", "getCharAt", t, args, ?loc = r) |> Some - | "GetStringSlice", None, [ ar; lower; upper ] -> - Helper.LibCall(com, "String", "getSlice", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "GetString", _, [ ar; idx ] -> libCall com r t "String" "getCharAt" args |> Some + | "GetStringSlice", None, [ ar; lower; upper ] -> makeLibCall com r t i "String" "getSlice" args |> Some | "GetArray", _, [ ar; idx ] -> getExpr r t ar idx |> Some | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some - | "GetArraySlice", None, [ ar; lower; upper ] -> - Helper.LibCall(com, "Array", "getSlice", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "SetArraySlice", None, args -> - Helper.LibCall(com, "Array", "setSlice", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "GetArraySlice", None, [ ar; lower; upper ] -> makeLibCall com r t i "Array" "getSlice" args |> Some + | "SetArraySlice", None, args -> makeLibCall com r t i "Array" "setSlice" args |> Some | ("TypeTestGeneric" | "TypeTestFast"), None, [ expr ] -> Test(expr, TypeTest((genArg com ctx r 0 i.GenericArgs)), r) |> Some // | "CreateInstance", None, _ -> @@ -2408,28 +2290,22 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr // Type: PowDouble : float -> int -> float // Usage: PowDouble x n | "PowDouble", None, (thisArg :: restArgs) -> makeInstanceCall r t i thisArg "powf" restArgs |> Some - | "PowDecimal", None, _ -> - Helper.LibCall(com, "Decimal", "pown", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "PowDecimal", None, _ -> makeLibCall com r t i "Decimal" "pown" args |> Some // reference: https://msdn.microsoft.com/visualfsharpdocs/conceptual/operatorintrinsics.rangechar-function-%5bfsharp%5d // Type: RangeChar : char -> char -> char seq // Usage: RangeChar start stop - | "RangeChar", None, _ -> - Helper.LibCall(com, "Range", "rangeChar", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "RangeChar", None, _ -> makeLibCall com r t i "Range" "rangeChar" args |> Some // reference: https://msdn.microsoft.com/visualfsharpdocs/conceptual/operatorintrinsics.rangedouble-function-%5bfsharp%5d // Type: RangeDouble: float -> float -> float -> float seq // Usage: RangeDouble start step stop | ("RangeSByte" | "RangeByte" | "RangeInt16" | "RangeUInt16" | "RangeInt32" | "RangeUInt32" | "RangeInt64" | "RangeUInt64" | "RangeSingle" | "RangeDouble"), None, - args -> - Helper.LibCall(com, "Range", "rangeNumeric", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + args -> makeLibCall com r t i "Range" "rangeNumeric" args |> Some | _ -> None let runtimeHelpers (com: ICompiler) (ctx: Context) r t (i: CallInfo) thisArg args = match i.CompiledName, args with - | "GetHashCode", [ arg ] -> getHashCode com ctx r arg |> Some + | "GetHashCode", [ arg ] -> getHashCode com ctx r t arg |> Some | _ -> None // ExceptionDispatchInfo is used to raise exceptions through different threads in async workflows @@ -2446,7 +2322,7 @@ let funcs (com: ICompiler) (ctx: Context) r t (i: CallInfo) thisArg args = match args, t with | [ arg ], DeclaredType(_, genArgs) -> uncurryExprAtRuntime com (List.length genArgs - 1) arg |> Some | _ -> emitExpr r t args "$0" |> Some - | "Invoke", Some callee -> Helper.Application(callee, t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "Invoke", Some c -> Helper.Application(c, t, args, i.SignatureArgTypes, ?loc = r) |> Some | _ -> None let keyValuePairs (com: ICompiler) (ctx: Context) r t (i: CallInfo) thisArg args = @@ -2460,23 +2336,20 @@ let dictionaries (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp match i.CompiledName, thisArg with | ".ctor", None -> match i.SignatureArgTypes with - | [] -> Helper.LibCall(com, "HashMap", "new_empty", t, args) |> Some - | [ Number _ ] -> Helper.LibCall(com, "HashMap", "new_with_capacity", t, args) |> Some - | [ IEqualityComparer ] -> Helper.LibCall(com, "HashMap", "new_with_comparer", t, args) |> Some - | [ Number _; IEqualityComparer ] -> - Helper.LibCall(com, "HashMap", "new_with_capacity_comparer", t, args) |> Some - | [ IEnumerable ] -> Helper.LibCall(com, "HashMap", "new_from_enumerable", t, args) |> Some - | [ IEnumerable; IEqualityComparer ] -> - Helper.LibCall(com, "HashMap", "new_from_enumerable_comparer", t, args) |> Some - | [ IDictionary ] -> Helper.LibCall(com, "HashMap", "new_from_dictionary", t, args) |> Some - | [ IDictionary; IEqualityComparer ] -> - Helper.LibCall(com, "HashMap", "new_from_dictionary_comparer", t, args) |> Some + | [] -> libCall com r t "HashMap" "new_empty" args |> Some + | [ Number _ ] -> libCall com r t "HashMap" "new_with_capacity" args |> Some + | [ IEqualityComparer ] -> libCall com r t "HashMap" "new_with_comparer" args |> Some + | [ Number _; IEqualityComparer ] -> libCall com r t "HashMap" "new_with_capacity_comparer" args |> Some + | [ IEnumerable ] -> libCall com r t "HashMap" "new_from_enumerable" args |> Some + | [ IEnumerable; IEqualityComparer ] -> libCall com r t "HashMap" "new_from_enumerable_comparer" args |> Some + | [ IDictionary ] -> libCall com r t "HashMap" "new_from_dictionary" args |> Some + | [ IDictionary; IEqualityComparer ] -> libCall com r t "HashMap" "new_from_dictionary_comparer" args |> Some | _ -> None | "GetEnumerator", Some c -> - let ar = Helper.LibCall(com, "HashMap", "entries", t, [ c ], [ c.Type ]) - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) |> Some - | "get_Item", Some c -> makeLibModuleCall com r t i "HashMap" "get" thisArg args |> Some - | "set_Item", Some c -> makeLibModuleCall com r t i "HashMap" "set" thisArg args |> Some + let ar = libCall com r t "HashMap" "entries" [ c ] + libCall com r t "Seq" "Enumerable::ofArray" [ ar ] |> Some + | "get_Item", Some c -> makeLibModuleCall com r t i "HashMap" "get" (Some c) args |> Some + | "set_Item", Some c -> makeLibModuleCall com r t i "HashMap" "set" (Some c) args |> Some | meth, _ -> let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst makeLibModuleCall com r t i "HashMap" meth thisArg args |> Some @@ -2485,18 +2358,16 @@ let hashSets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op match i.CompiledName, thisArg with | ".ctor", None -> match i.SignatureArgTypes with - | [] -> Helper.LibCall(com, "HashSet", "new_empty", t, args) |> Some - | [ Number _ ] -> Helper.LibCall(com, "HashSet", "new_with_capacity", t, args) |> Some - | [ IEqualityComparer ] -> Helper.LibCall(com, "HashSet", "new_with_comparer", t, args) |> Some - | [ Number _; IEqualityComparer ] -> - Helper.LibCall(com, "HashSet", "new_with_capacity_comparer", t, args) |> Some - | [ IEnumerable ] -> Helper.LibCall(com, "HashSet", "new_from_enumerable", t, args) |> Some - | [ IEnumerable; IEqualityComparer ] -> - Helper.LibCall(com, "HashSet", "new_from_enumerable_comparer", t, args) |> Some + | [] -> libCall com r t "HashSet" "new_empty" args |> Some + | [ Number _ ] -> libCall com r t "HashSet" "new_with_capacity" args |> Some + | [ IEqualityComparer ] -> libCall com r t "HashSet" "new_with_comparer" args |> Some + | [ Number _; IEqualityComparer ] -> libCall com r t "HashSet" "new_with_capacity_comparer" args |> Some + | [ IEnumerable ] -> libCall com r t "HashSet" "new_from_enumerable" args |> Some + | [ IEnumerable; IEqualityComparer ] -> libCall com r t "HashSet" "new_from_enumerable_comparer" args |> Some | _ -> None | "GetEnumerator", Some c -> - let ar = Helper.LibCall(com, "HashSet", "entries", t, [ c ]) - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) |> Some + let ar = libCall com r t "HashSet" "entries" [ c ] + libCall com r t "Seq" "Enumerable::ofArray" [ ar ] |> Some | "CopyTo", Some c -> let meth = i.CompiledName |> toLowerFirstWithArgsCountSuffix (c :: args) makeLibModuleCall com r t i "HashSet" meth thisArg args |> Some @@ -2510,8 +2381,8 @@ let hashSets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let collections (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match thisArg with - | Some callee -> - match callee.Type with + | Some c -> + match c.Type with | IsEntity (Types.keyCollection) _ | IsEntity (Types.valueCollection) _ | IsEntity (Types.icollectionGeneric) _ @@ -2536,7 +2407,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with | "DefaultOf", _ -> (genArg com ctx r 0 i.GenericArgs) |> getZero com ctx |> Some - | "Hash", [ arg ] -> getHashCode com ctx r arg |> Some + | "Hash", [ arg ] -> getHashCode com ctx r t arg |> Some | "Equals", [ arg1; arg2 ] -> equals com ctx r arg1 arg2 |> Some | "Compare", [ arg1; arg2 ] -> compare com ctx r arg1 arg2 |> Some | "NonNull", [ arg ] -> arg |> Some @@ -2544,9 +2415,9 @@ let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) let enums (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | "HasFlag", Some this, [ arg ] -> + | "HasFlag", Some c, [ arg ] -> // x.HasFlags(y) => (int x) &&& (int y) <> 0 - makeBinOp r (Int32.Number) this arg BinaryAndBitwise + makeBinOp r (Int32.Number) c arg BinaryAndBitwise |> fun bitwise -> makeEqOp r bitwise (makeIntConst 0) BinaryUnequal |> Some | Patterns.DicContains (dict [ "Parse", "parseEnum" @@ -2566,7 +2437,7 @@ let enums (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio [ genArg com ctx r 0 i.GenericArgs |> makeTypeInfo None; value; refValue ] | _ -> args - Helper.LibCall(com, "Reflection", meth, t, args, ?loc = r) |> Some + libCall com r t "Reflection" meth args |> Some | _ -> None let bitConvert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = @@ -2579,8 +2450,7 @@ let bitConvert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option | Number(kind, _) -> "getBytes" + kind.ToString() | x -> FableError $"Unsupported type in BitConverter.GetBytes(): %A{x}" |> raise - let expr = - Helper.LibCall(com, "BitConverter", memberName, t, args, i.SignatureArgTypes, ?loc = r) + let expr = makeLibCall com r t i "BitConverter" memberName args if com.Options.TypedArrays then expr |> Some @@ -2589,28 +2459,27 @@ let bitConvert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option | "ToString" -> let memberName = "toString" + args.Length.ToString() - Helper.LibCall(com, "BitConverter", memberName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "BitConverter" memberName args |> Some | _ -> let memberName = Naming.lowerFirst i.CompiledName - Helper.LibCall(com, "BitConverter", memberName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "BitConverter" memberName args |> Some let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with - | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" as meth), + | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64") as meth, [ ExprType(String); ExprType(Number(Int32, _)) ] -> toRadixInt com ctx r t i args |> Some | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64"), [ arg ] -> toRoundInt com ctx r t i args |> Some + | "ToDecimal", [ ExprTypeAs(String, input) ] -> libCall com r t "Decimal" "convert" [ input ] |> Some + | "ToDecimal", [ ExprTypeAs(String, input); FormatProvider _ ] -> + libCall com r t "Decimal" "convert" [ input ] |> Some | ("ToSingle" | "ToDouble" | "ToDecimal"), [ arg ] -> convertTo com ctx r t args |> Some | "ToChar", [ arg ] -> convertTo com ctx r t args |> Some | "ToString", [ arg ] -> toString com ctx r args |> Some - | "ToString", [ arg; ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "Convert", "toStringRadix", t, args, ?loc = r) |> Some + | "ToString", [ arg; ExprType(Number(Int32, _)) ] -> libCall com r t "Convert" "toStringRadix" args |> Some | ("ToHexString" | "ToHexStringLower" | "FromHexString" | "ToBase64String" | "FromBase64String"), [ arg ] -> - Helper.LibCall(com, "Convert", (Naming.lowerFirst i.CompiledName), t, args, ?loc = r) - |> Some + libCall com r t "Convert" (Naming.lowerFirst i.CompiledName) args |> Some | _ -> None let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -2631,27 +2500,52 @@ let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | _ -> None | _ -> None -let ignoreFormatProvider compiledName args = +/// Drops the `IFormatProvider` (and `DateTimeStyles`, where present) a date/time `Parse` was +/// given, warning once per discarded argument. `Parse arg` with no extra argument discards +/// nothing, so it must not warn. `ToString` stays silent here, as it does on every other target. +let ignoreFormatProvider com (ctx: Context) r compiledName args = + // Passing InvariantCulture asks for exactly what Fable does, so there is nothing to report. + let warnProvider culture = + match culture with + | InvariantCulture -> () + | _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + + let warnStyles styles = + match styles with + | NumberConst(NumberValue.Int32 0, _) -> () // DateTimeStyles.None: no special handling + | _ -> WarningCodes.dateTimeStylesIgnored |> addWarningWithCode com ctx.InlinePath r + match compiledName, args with - // Ignore IFormatProvider | "ToString", ExprTypeAs(String, arg) :: _ -> [ arg ] | "ToString", _ -> [ makeStrConst "" ] // default (no format string) + | "Parse", arg :: culture :: styles :: _ -> + warnProvider culture + warnStyles styles + [ arg ] + | "Parse", arg :: culture :: _ -> + warnProvider culture + [ arg ] | "Parse", arg :: _ -> [ arg ] - | "TryParse", input :: _culture :: _styles :: defVal :: _ -> [ input; defVal ] - | "TryParse", input :: _culture :: defVal :: _ -> [ input; defVal ] + | "TryParse", input :: culture :: styles :: defVal :: _ -> + warnProvider culture + warnStyles styles + [ input; defVal ] + | "TryParse", input :: culture :: defVal :: _ -> + warnProvider culture + [ input; defVal ] | _ -> args let makeMemberCall com ctx r t i moduleName memberName (thisArg: Expr option) (args: Expr list) = let memberName = Naming.removeGetSetPrefix memberName |> Naming.lowerFirst - let args = ignoreFormatProvider i.CompiledName args + let args = ignoreFormatProvider com ctx r i.CompiledName args match thisArg with - | Some callee -> makeInstanceCall r t i callee memberName args + | Some c -> makeInstanceCall r t i c memberName args | None -> makeStaticMemberCall com r t i moduleName memberName args let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [] -> "new_empty" |> Some | [ ExprType(Number(Int64, _)) ] -> "new_ticks" |> Some @@ -2666,8 +2560,7 @@ let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) - ExprType(Number(_, NumberInfo.IsEnum ent)) ] when ent.FullName = "System.DateTimeKind" -> - "new_ymdhms_kind" |> Some + ExprTypeEnum Types.datetimeKind _ ] -> "new_ymdhms_kind" |> Some | [ ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) @@ -2675,8 +2568,7 @@ let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) - ExprType(Number(_, NumberInfo.IsEnum ent)) ] when ent.FullName = "System.DateTimeKind" -> - "new_ymdhms_milli_kind" |> Some + ExprTypeEnum Types.datetimeKind _ ] -> "new_ymdhms_milli_kind" |> Some | [ ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) @@ -2685,8 +2577,7 @@ let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) - ExprType(Number(_, NumberInfo.IsEnum ent)) ] when ent.FullName = "System.DateTimeKind" -> - "new_ymdhms_micro_kind" |> Some + ExprTypeEnum Types.datetimeKind _ ] -> "new_ymdhms_micro_kind" |> Some | [ ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) @@ -2710,21 +2601,20 @@ let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ] -> "new_ymdhms_micro" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "DateTime" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args - | "Add" -> - Operation(Binary(BinaryOperator.BinaryPlus, thisArg.Value, args.Head), Tags.empty, t, r) + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args + | "Add", Some c -> + Operation(Binary(BinaryOperator.BinaryPlus, c, args.Head), Tags.empty, t, r) |> Some - | "Subtract" -> - Operation(Binary(BinaryOperator.BinaryMinus, thisArg.Value, args.Head), Tags.empty, t, r) + | "Add", None -> None + | "Subtract", Some c -> + Operation(Binary(BinaryOperator.BinaryMinus, c, args.Head), Tags.empty, t, r) |> Some - | meth -> makeMemberCall com ctx r t i "DateTime" meth thisArg args |> Some + | "Subtract", None -> None + | meth, thisArg -> makeMemberCall com ctx r t i "DateTime" meth thisArg args |> Some let dateTimeOffsets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [] -> "new_empty" |> Some | ExprType(Number(Int64, _)) :: _ -> "new_ticks" |> Some @@ -2757,35 +2647,33 @@ let dateTimeOffsets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: _offset ] -> "new_ymdhms_micro" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "DateTimeOffset" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args - | "Add" -> - Operation(Binary(BinaryOperator.BinaryPlus, thisArg.Value, args.Head), Tags.empty, t, r) + | "op_Implicit", _ -> makeStaticMemberCall com r t i "DateTimeOffset" "new_datetime" args |> Some + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args + | "Add", Some c -> + Operation(Binary(BinaryOperator.BinaryPlus, c, args.Head), Tags.empty, t, r) |> Some - | "Subtract" -> - Operation(Binary(BinaryOperator.BinaryMinus, thisArg.Value, args.Head), Tags.empty, t, r) + | "Add", None -> None + | "Subtract", Some c -> + Operation(Binary(BinaryOperator.BinaryMinus, c, args.Head), Tags.empty, t, r) |> Some - | meth -> makeMemberCall com ctx r t i "DateTimeOffset" meth thisArg args |> Some + | "Subtract", None -> None + | meth, thisArg -> makeMemberCall com ctx r t i "DateTimeOffset" meth thisArg args |> Some let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> "new_ymd" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "DateOnly" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args - | "ToDateTime" when args.Length = 2 -> makeInstanceCall r t i thisArg.Value "toDateTime2" args |> Some - | meth -> makeMemberCall com ctx r t i "DateOnly" meth thisArg args |> Some + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args + | "ToDateTime", Some c when args.Length = 2 -> makeInstanceCall r t i c "toDateTime2" args |> Some + | "ToDateTime", None when args.Length = 2 -> None + | meth, thisArg -> makeMemberCall com ctx r t i "DateOnly" meth thisArg args |> Some let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [ ExprType(Number(Int64, _)) ] -> "new_ticks" |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> "new_hm" |> Some @@ -2801,17 +2689,63 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op ExprType(Number(Int32, _)) ] -> "new_hms_micro" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "TimeOnly" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args - | "Add" when args.Length = 2 -> makeInstanceCall r t i thisArg.Value "add2" args |> Some - | meth -> makeMemberCall com ctx r t i "TimeOnly" meth thisArg args |> Some + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args + | "Add", Some c when args.Length = 2 -> makeInstanceCall r t i c "add2" args |> Some + | "Add", None when args.Length = 2 -> None + | meth, thisArg -> makeMemberCall com ctx r t i "TimeOnly" meth thisArg args |> Some + +let runes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + match i.CompiledName, thisArg, args with + | ".ctor", None, [ ExprType(Char) ] -> Some args.Head + | ".ctor", None, [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "Rune" "newInt" args |> Some + | ".ctor", None, [ ExprType(Number(UInt32, _)) ] -> makeLibCall com r t i "Rune" "newUInt" args |> Some + | ".ctor", None, [ ExprType(Char); ExprType(Char) ] -> makeLibCall com r t i "Rune" "newPair" args |> Some + | ("ReplacementChar" | "get_ReplacementChar"), None, [] -> makeLibCall com r t i "Rune" "replacementChar" [] |> Some + | "op_Explicit", None, [ ExprType(Char) ] -> Some args.Head + | "op_Explicit", None, [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "Rune" "newInt" args |> Some + | "op_Explicit", None, [ ExprType(Number(UInt32, _)) ] -> makeLibCall com r t i "Rune" "newUInt" args |> Some + | "TryCreate", None, [ ExprType(Char); _ ] -> makeLibCall com r t i "Rune" "tryCreateChar" args |> Some + | "TryCreate", None, [ ExprType(Char); ExprType(Char); _ ] -> + makeLibCall com r t i "Rune" "tryCreatePair" args |> Some + | "TryCreate", None, [ ExprType(Number(Int32, _)); _ ] -> makeLibCall com r t i "Rune" "tryCreateInt" args |> Some + | "TryCreate", None, [ ExprType(Number(UInt32, _)); _ ] -> makeLibCall com r t i "Rune" "tryCreateUInt" args |> Some + | "get_Value", Some c, [] -> makeLibCall com r t i "Rune" "value" [ c ] |> Some + | "get_Utf8SequenceLength", Some c, [] -> makeLibCall com r t i "Rune" "utf8SequenceLength" [ c ] |> Some + | "get_Utf16SequenceLength", Some c, [] -> makeLibCall com r t i "Rune" "utf16SequenceLength" [ c ] |> Some + | "get_IsAscii", Some c, [] -> makeLibCall com r t i "Rune" "isAscii" [ c ] |> Some + | "get_IsBmp", Some c, [] -> makeLibCall com r t i "Rune" "isBmp" [ c ] |> Some + | "get_Plane", Some c, [] -> makeLibCall com r t i "Rune" "plane" [ c ] |> Some + | "GetNumericValue", None, [ arg ] -> makeLibCall com r t i "Rune" "getNumericValue" [ arg ] |> Some + | "GetUnicodeCategory", None, [ arg ] -> makeLibCall com r t i "Rune" "getUnicodeCategory" [ arg ] |> Some + | ("IsControl" | "IsDigit" | "IsLetter" | "IsLetterOrDigit" | "IsLower" | "IsNumber" | "IsPunctuation" | "IsSeparator" | "IsSymbol" | "IsUpper" | "IsWhiteSpace"), + None, + [ arg ] -> + let meth = Naming.lowerFirst i.CompiledName + makeLibCall com r t i "Rune" meth args |> Some + | "IsValid", None, [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "Rune" "isValidInt" args |> Some + | "IsValid", None, [ ExprType(Number(UInt32, _)) ] -> makeLibCall com r t i "Rune" "isValidUInt" args |> Some + | "ToLower", None, [ runeArg; ExprType(DeclaredType(ent, _)) ] when ent.FullName = Types.cultureInfo -> + makeLibCall com r t i "Rune" "toLowerInvariant" [ runeArg ] |> Some + | "ToLowerInvariant", None, [ arg ] -> makeLibCall com r t i "Rune" "toLowerInvariant" [ arg ] |> Some + | "ToUpper", None, [ runeArg; ExprType(DeclaredType(ent, _)) ] when ent.FullName = Types.cultureInfo -> + makeLibCall com r t i "Rune" "toUpperInvariant" [ runeArg ] |> Some + | "ToUpperInvariant", None, [ arg ] -> makeLibCall com r t i "Rune" "toUpperInvariant" [ arg ] |> Some + | "GetRuneAt", None, _ -> makeLibCall com r t i "Rune" "getRuneAt" args |> Some + | "TryGetRuneAt", None, _ -> makeLibCall com r t i "Rune" "tryGetRuneAt" args |> Some + | "Parse", None, [ arg ] -> makeLibCall com r t i "Rune" "parse" args |> Some + | "TryParse", None, _ -> makeLibCall com r t i "Rune" "tryParse" args |> Some + | "ToString", Some c, _ -> makeLibCall com r t i "Rune" "toString" [ c ] |> Some + | "CompareTo", Some c, _ -> makeLibCall com r t i "Rune" "compareTo" (c :: args) |> Some + | "Compare", None, _ -> makeLibCall com r t i "Rune" "compareTo" args |> Some + | "Equals", Some c, _ -> makeLibCall com r t i "Rune" "equals" (c :: args) |> Some + | "Equals", None, _ -> makeLibCall com r t i "Rune" "equals" args |> Some + | "GetHashCode", Some c, _ -> makeLibCall com r t i "Rune" "getHashCode" [ c ] |> Some + | _ -> None let timeSpans (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = // let callee = match i.callee with Some c -> c | None -> i.args.Head - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [ ExprType(Number(Int64, _)) ] -> "new_ticks" |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> "new_hms" |> Some @@ -2832,29 +2766,25 @@ let timeSpans (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ] -> "new_dhms_micro" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "TimeSpan" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args // | "Zero" etc. -> // for static fields, see tryField - | "Add" -> - Operation(Binary(BinaryOperator.BinaryPlus, thisArg.Value, args.Head), Tags.empty, t, r) + | "Add", Some c -> + Operation(Binary(BinaryOperator.BinaryPlus, c, args.Head), Tags.empty, t, r) |> Some - | "Subtract" -> - Operation(Binary(BinaryOperator.BinaryMinus, thisArg.Value, args.Head), Tags.empty, t, r) + | "Add", None -> None + | "Subtract", Some c -> + Operation(Binary(BinaryOperator.BinaryMinus, c, args.Head), Tags.empty, t, r) |> Some - | "Multiply" -> - Operation(Binary(BinaryOperator.BinaryMultiply, thisArg.Value, args.Head), Tags.empty, t, r) + | "Subtract", None -> None + | "Multiply", Some c -> + Operation(Binary(BinaryOperator.BinaryMultiply, c, args.Head), Tags.empty, t, r) |> Some - | "Divide" -> - Operation(Binary(BinaryOperator.BinaryDivide, thisArg.Value, args.Head), Tags.empty, t, r) + | "Multiply", None -> None + | "Divide", Some c -> + Operation(Binary(BinaryOperator.BinaryDivide, c, args.Head), Tags.empty, t, r) |> Some - | "FromDays" - | "FromHours" - | "FromMinutes" - | "FromSeconds" - | "FromMilliseconds" - | "FromMicroseconds" as meth -> + | ("FromDays" | "FromHours" | "FromMinutes" | "FromSeconds" | "FromMilliseconds" | "FromMicroseconds") as meth, + thisArg -> match args with | [ ExprType(Number(Float64, _)) ] -> // overloads that take a float @@ -2864,16 +2794,14 @@ let timeSpans (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o let argCount = List.length args let meth = meth + (string argCount) makeMemberCall com ctx r t i "TimeSpan" meth thisArg args |> Some - | meth -> makeMemberCall com ctx r t i "TimeSpan" meth thisArg args |> Some + | meth, thisArg -> makeMemberCall com ctx r t i "TimeSpan" meth thisArg args |> Some let timers (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | ".ctor", _, _ -> - Helper.LibCall(com, "Timer", "default", t, args, i.SignatureArgTypes, isConstructor = true, ?loc = r) - |> Some + | ".ctor", _, _ -> libCallCons com r t i "Timer" "default" args |> Some | Naming.StartsWith "get_" meth, Some x, _ -> getFieldWith r t x meth |> Some | Naming.StartsWith "set_" meth, Some x, [ value ] -> setExpr r x (makeStrConst meth) value |> Some - | meth, Some callee, args -> makeInstanceCall r t i callee meth args |> Some + | meth, Some c, args -> makeInstanceCall r t i c meth args |> Some | _ -> None let systemEnv @@ -2887,141 +2815,97 @@ let systemEnv = match i.CompiledName with | "get_NewLine" -> Some(makeStrConst "\n") - | "GetEnvironmentVariable" -> - Helper.LibCall(com, "Environment", "getEnvironmentVariable", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "get_CurrentDirectory" -> - Helper.LibCall(com, "Environment", "getCurrentDirectory", t, [], ?loc = r) - |> Some + | "GetEnvironmentVariable" -> makeLibCall com r t i "Environment" "getEnvironmentVariable" args |> Some + | "get_CurrentDirectory" -> libCall com r t "Environment" "getCurrentDirectory" [] |> Some | _ -> None let paths (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with | "Combine" -> match args with - | [ _; _ ] -> - Helper.LibCall(com, "Path", "combine2", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ _; _; _ ] -> - Helper.LibCall(com, "Path", "combine3", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | [ _; _ ] -> makeLibCall com r t i "Path" "combine2" args |> Some + | [ _; _; _ ] -> makeLibCall com r t i "Path" "combine3" args |> Some | _ -> None - | "GetDirectoryName" -> - Helper.LibCall(com, "Path", "getDirectoryName", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetFileName" -> - Helper.LibCall(com, "Path", "getFileName", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetFileNameWithoutExtension" -> - Helper.LibCall(com, "Path", "getFileNameWithoutExtension", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetExtension" -> - Helper.LibCall(com, "Path", "getExtension", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "HasExtension" -> - Helper.LibCall(com, "Path", "hasExtension", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetTempPath" -> Helper.LibCall(com, "Path", "getTempPath", t, [], ?loc = r) |> Some - | "GetRandomFileName" -> Helper.LibCall(com, "Path", "getRandomFileName", t, [], ?loc = r) |> Some + | "GetDirectoryName" -> makeLibCall com r t i "Path" "getDirectoryName" args |> Some + | "GetFileName" -> makeLibCall com r t i "Path" "getFileName" args |> Some + | "GetFileNameWithoutExtension" -> makeLibCall com r t i "Path" "getFileNameWithoutExtension" args |> Some + | "GetExtension" -> makeLibCall com r t i "Path" "getExtension" args |> Some + | "HasExtension" -> makeLibCall com r t i "Path" "hasExtension" args |> Some + | "GetTempPath" -> libCall com r t "Path" "getTempPath" [] |> Some + | "GetRandomFileName" -> libCall com r t "Path" "getRandomFileName" [] |> Some | _ -> None let files (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with - | "Exists" -> - Helper.LibCall(com, "File", "exists", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "WriteAllText" -> - Helper.LibCall(com, "File", "writeAllText", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "ReadAllText" -> - Helper.LibCall(com, "File", "readAllText", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Delete" -> - Helper.LibCall(com, "File", "delete", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "Exists" -> makeLibCall com r t i "File" "exists" args |> Some + | "WriteAllText" -> makeLibCall com r t i "File" "writeAllText" args |> Some + | "ReadAllText" -> makeLibCall com r t i "File" "readAllText" args |> Some + | "Delete" -> makeLibCall com r t i "File" "delete" args |> Some | _ -> None // Initial support, making at least InvariantCulture compile-able // to be used System.Double.Parse and System.Single.Parse // see https://github.com/fable-compiler/Fable/pull/1197#issuecomment-348034660 -let globalization - (com: ICompiler) - (ctx: Context) - (_: SourceLocation option) - t - (i: CallInfo) - (_: Expr option) - (_: Expr list) - = +let globalization (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with - | "get_InvariantCulture" -> - // System.Globalization namespace is not supported by Fable. The value InvariantCulture will be compiled to an empty object literal - ObjectExpr([], t, None) |> Some + | "get_InvariantCulture" -> bclType com ctx r t i thisArg args | _ -> None let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with | ".ctor", _ -> match args with - | [] -> Helper.LibCall(com, "Random", "new", t, [], [], ?loc = r) |> Some - | args -> - Helper.LibCall(com, "Random", "new_seeded", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | meth, Some thisArg -> + | [] -> libCall com r t "Random" "new" [] |> Some + | args -> makeLibCall com r t i "Random" "new_seeded" args |> Some + | meth, Some c -> let meth = if meth = "Next" then $"next{List.length args}" else meth |> Naming.lowerFirst - Helper.InstanceCall(thisArg, meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeInstanceCall r t i c meth args |> Some | _ -> None let cancels (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | "get_None" // TODO: implement as non-cancellable token - | ".ctor" -> - Helper.LibCall(com, "Async", "createCancellationToken", t, args, i.SignatureArgTypes) - |> Some - | "get_Token" -> thisArg - | "Cancel" - | "CancelAfter" - | "get_IsCancellationRequested" - | "ThrowIfCancellationRequested" -> + match i.CompiledName, thisArg with + // TODO: implement get_None as a non-cancellable token + | ("get_None" | ".ctor"), _ -> makeLibCall com r t i "Async" "createCancellationToken" args |> Some + | "get_Token", thisArg -> thisArg + | ("Cancel" | "CancelAfter" | "get_IsCancellationRequested" | "ThrowIfCancellationRequested"), thisArg -> let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst makeLibModuleCall com r t i "Async" meth thisArg args |> Some // TODO: Add check so CancellationTokenSource cannot be cancelled after disposed? - | "Dispose" -> Null Type.Unit |> makeValue r |> Some - | "Register" -> makeInstanceCall r t i thisArg.Value "register" args |> Some + | "Dispose", _ -> Null Type.Unit |> makeValue r |> Some + | "Register", Some c -> makeInstanceCall r t i c "register" args |> Some | _ -> None let monitor (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with - | "Enter" -> Helper.LibCall(com, "Monitor", "enter", t, args, ?loc = r) |> Some - | "Exit" -> Helper.LibCall(com, "Monitor", "exit", t, args, ?loc = r) |> Some + | "Enter" -> libCall com r t "Monitor" "enter" args |> Some + | "Exit" -> libCall com r t "Monitor" "exit" args |> Some | _ -> None let tasks com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, i.GenericArgs with - | ".ctor", None, [ tType ] -> Helper.LibCall(com, "Task", "new", tType, args, ?loc = r) |> Some - | "FromResult", None, [ tType ] -> Helper.LibCall(com, "Task", "from_result", tType, args, ?loc = r) |> Some - | "get_Result", Some callee, _ -> makeInstanceCall r t i callee "get_result" args |> Some + | ".ctor", None, [ tType ] -> libCall com r tType "Task" "new" args |> Some + | "FromResult", None, [ tType ] -> libCall com r tType "Task" "from_result" args |> Some + | "get_Result", Some c, _ -> makeInstanceCall r t i c "get_result" args |> Some | _ -> None let threads com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, i.GenericArgs, args with - | ".ctor", None, [], _ -> Helper.LibCall(com, "Thread", "new", t, args, ?loc = r) |> Some - | "Sleep", None, _, [ ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "Thread", "sleep", t, args, ?loc = r) |> Some - | "Start", Some callee, [], [] -> makeInstanceCall r t i callee "start" args |> Some - | "Join", Some callee, [], [] -> makeInstanceCall r t i callee "join" args |> Some + | ".ctor", None, [], _ -> libCall com r t "Thread" "new" args |> Some + | "Sleep", None, _, [ ExprType(Number(Int32, _)) ] -> libCall com r t "Thread" "sleep" args |> Some + | "Start", Some c, [], [] -> makeInstanceCall r t i c "start" args |> Some + | "Join", Some c, [], [] -> makeInstanceCall r t i c "join" args |> Some | _ -> None let activator (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | "CreateInstance", None, ([ _type ] | [ _type; (ExprType(Array(Any, _))) ]) -> - Helper.LibCall(com, "Reflection", "createInstance", t, args, ?loc = r) |> Some + | "CreateInstance", None, [ _type ] + | "CreateInstance", None, [ _type; (ExprType(Array(Any, _))) ] -> + libCall com r t "Reflection" "createInstance" args |> Some | _ -> None // alternative member suffix for languages that don't support member overloads @@ -3065,10 +2949,23 @@ let bclNativeImpl com ctx r t i moduleName memberName (thisArg: Expr option) (ar let memberName = memberName + suffix match thisArg with - | Some callee -> makeInstanceCall r t i callee memberName args + | Some c -> makeInstanceCall r t i c memberName args | None -> makeStaticMemberCall com r t i moduleName memberName args let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.IgnorePatternWhitespace + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + match i.CompiledName with // | "GetEnumerator" -> getEnumerator com r t i thisArg.Value |> Some | meth -> @@ -3083,10 +2980,8 @@ let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Exp let encoding (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | ("get_Unicode" | "get_UTF8"), _, _ -> - Helper.LibCall(com, "Encoding", i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("GetBytes" | "GetByteCount"), Some callee, ExprType(Array(Char, _)) :: _ -> + | ("get_Unicode" | "get_UTF8"), _, _ -> makeLibCall com r t i "Encoding" i.CompiledName args |> Some + | ("GetBytes" | "GetByteCount"), Some c, ExprType(Array(Char, _)) :: _ -> let meth = Naming.lowerFirst i.CompiledName + "FromChars" let meth = @@ -3095,9 +2990,9 @@ let encoding (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else meth - makeInstanceCall r t i callee meth args |> Some + makeInstanceCall r t i c meth args |> Some | ("GetBytes" | "GetByteCount" | "GetChars" | "GetCharCount" | "GetMaxByteCount" | "GetMaxCharCount" | "GetString"), - Some callee, + Some c, _ -> let meth = Naming.lowerFirst i.CompiledName @@ -3107,171 +3002,141 @@ let encoding (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else meth - makeInstanceCall r t i callee meth args |> Some + makeInstanceCall r t i c meth args |> Some | _ -> None let enumerators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with - | meth, Some callee -> + | meth, Some c -> // // Enumerators are mangled, use the fully qualified name // let isGenericCurrent = i.CompiledName = "get_Current" && i.DeclaringEntityFullName <> Types.ienumerator // let entityName = if isGenericCurrent then Types.ienumeratorGeneric else Types.ienumerator // let methName = entityName + "." + i.CompiledName - makeInstanceCall r t i callee meth args |> Some + makeInstanceCall r t i c meth args |> Some | _ -> None let enumerables (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (_: Expr list) = match i.CompiledName, thisArg with // This property only belongs to Key and Value Collections - | "get_Count", Some callee -> Helper.LibCall(com, "Seq", "length", t, [ callee ], ?loc = r) |> Some - | "GetEnumerator", Some callee -> getEnumerator com r t i callee |> Some + | "get_Count", Some c -> libCall com r t "Seq" "length" [ c ] |> Some + | "GetEnumerator", Some c -> getEnumerator com r t i c |> Some | _ -> None let events (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with | ".ctor", _ -> - Helper.LibCall(com, "Event", "default", t, args, i.SignatureArgTypes, isConstructor = true, ?loc = r) - |> Some - | "get_Publish", Some callee -> getFieldWith r t callee "Publish" |> Some - | meth, Some callee -> makeInstanceCall r t i callee meth args |> Some - | meth, None -> - Helper.LibCall(com, "Event", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + let constructor = + if i.DeclaringEntityFullName.EndsWith("`2", StringComparison.Ordinal) then + "default2" + else + "default" + + libCallCons com r t i "Event" constructor args |> Some + | "get_Publish", Some c -> getFieldWith r t c "Publish" |> Some + | meth, Some c -> makeInstanceCall r t i c meth args |> Some + | meth, None -> makeLibCall com r t i "Event" (Naming.lowerFirst meth) args |> Some let observable (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = - Helper.LibCall(com, "Observable", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) + makeLibCall com r t i "Observable" (Naming.lowerFirst i.CompiledName) args |> Some let mailbox (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match thisArg with | None -> match i.CompiledName with - | ".ctor" -> - Helper.LibCall( - com, - "MailboxProcessor", - "default", - t, - args, - i.SignatureArgTypes, - isConstructor = true, - ?loc = r - ) - |> Some - | "Start" -> - Helper.LibCall(com, "MailboxProcessor", "start", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | ".ctor" -> libCallCons com r t i "MailboxProcessor" "default" args |> Some + | "Start" -> makeLibCall com r t i "MailboxProcessor" "start" args |> Some | _ -> None - | Some callee -> + | Some c -> match i.CompiledName with // `reply` belongs to AsyncReplyChannel - | "Start" - | "Receive" - | "PostAndAsyncReply" - | "Post" -> - let memb = - if i.CompiledName = "Start" then - "startInstance" - else - Naming.lowerFirst i.CompiledName + | "Start" -> libCallThis com r t i "MailboxProcessor" "startInstance" thisArg args |> Some + | ("Receive" | "PostAndAsyncReply" | "Post") as meth -> + let meth = Naming.lowerFirst i.CompiledName + libCallThis com r t i "MailboxProcessor" meth thisArg args |> Some + | "Reply" -> makeInstanceCall r t i c "reply" args |> Some + | _ -> None - Helper.LibCall(com, "MailboxProcessor", memb, t, args, i.SignatureArgTypes, thisArg = callee, ?loc = r) - |> Some - | "Reply" -> makeInstanceCall r t i callee "reply" args |> Some +let tryGetAsyncDelayBinder = + function + | MaybeCasted(Call(Import({ Selector = "AsyncBuilder_::delay" }, _, _), callInfo, _, _)) -> + match callInfo.Args with + | [ binder ] -> Some binder | _ -> None + | _ -> None let asyncBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | "Singleton", _, _ -> Value(UnitConstant, r) |> Some - //makeImportLib com t "singleton" "AsyncBuilder" |> Some - // For Using we need to cast the argument to IDisposable - | "Using", Some callee, [ arg; f ] -> makeInstanceCall r t i callee "Using" [ arg; f ] |> Some - | "Delay", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "delay", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Bind", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "bind", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Return", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "r_return", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "ReturnFrom", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "return_from", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Zero", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "zero", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | meth, Some callee, _ -> makeInstanceCall r t i callee meth args |> Some - | meth, None, _ -> - Helper.LibCall(com, "AsyncBuilder", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "Bind", _, _ -> makeLibCall com r t i "AsyncBuilder" "bind" args |> Some + | "Combine", _, _ -> makeLibCall com r t i "AsyncBuilder" "combine" args |> Some + | "Delay", _, _ -> makeLibCall com r t i "AsyncBuilder" "delay" args |> Some + | "For", _, _ -> makeLibCall com r t i "AsyncBuilder" "for_loop" args |> Some + | "Return", _, _ -> makeLibCall com r t i "AsyncBuilder" "r_return" args |> Some + | "ReturnFrom", _, _ -> makeLibCall com r t i "AsyncBuilder" "return_from" args |> Some + | "TryFinally", _, _ -> makeLibCall com r t i "AsyncBuilder" "try_finally" args |> Some + | "TryWith", _, _ -> makeLibCall com r t i "AsyncBuilder" "try_with" args |> Some + | "Using", _, _ -> makeLibCall com r t i "AsyncBuilder" "using" args |> Some + | "While", _, [ guard; body ] -> + match tryGetAsyncDelayBinder body with + | Some body -> makeLibCall com r t i "AsyncBuilder" "while_loop" [ guard; body ] |> Some + | None -> + "Async builder While expects a delayed body." + |> addErrorAndReturnNull com ctx.InlinePath r + |> Some + | "Zero", _, _ -> makeLibCall com r t i "AsyncBuilder" "zero" args |> Some + | meth, Some c, _ -> makeInstanceCall r t i c meth args |> Some + | meth, None, _ -> makeLibCall com r t i "AsyncBuilder" (Naming.lowerFirst meth) args |> Some let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with // TODO: Throw error for RunSynchronously | "Start" -> - "Async.Start will behave as StartImmediate" |> addWarning com ctx.InlinePath r + WarningCodes.asyncStartBehavesAsStartImmediate + |> addWarningWithCode com ctx.InlinePath r - Helper.LibCall(com, "Async", "start", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Async" "start" args |> Some // Make sure cancellationToken is called as a function and not a getter - | "get_CancellationToken" -> Helper.LibCall(com, "Async", "cancellationToken", t, [], ?loc = r) |> Some + | "get_CancellationToken" -> libCall com r t "Async" "cancellationToken" [] |> Some // `catch` cannot be used as a function name in JS - | "Catch" -> - Helper.LibCall(com, "Async", "catchAsync", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "Catch" -> makeLibCall com r t i "Async" "catchAsync" args |> Some // Fable.Core extensions - | meth -> - Helper.LibCall(com, "Async", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | meth -> makeLibCall com r t i "Async" (Naming.lowerFirst meth) args |> Some -let taskBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName, thisArg, args with - // | "Run", _, _ -> Helper.LibCall(com, "Task", "run", t, args, ?loc=r) |> Some - // | "Singleton", _, _ -> makeImportLib com t "singleton" "TaskBuilder" |> Some - | ".ctor", None, _ -> makeImportLib com t "new" "TaskBuilder" |> Some - | "Run", Some callee, _ -> makeInstanceCall r t i callee "run" args |> Some - | meth, Some callee, _ -> makeInstanceCall r t i callee meth args |> Some - | meth, None, _ -> - Helper.LibCall(com, "TaskBuilder", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - -let taskBuilderB (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName, thisArg, args with - | "Bind", _, _ -> - Helper.LibCall(com, "Task", "bind", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Return", _, _ -> - Helper.LibCall(com, "Task", "r_return", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Delay", _, _ -> - Helper.LibCall(com, "Task", "delay", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Zero", _, _ -> - Helper.LibCall(com, "Task", "zero", t, args, i.SignatureArgTypes, ?loc = r) - |> Some +let tryGetTaskDelayBinder = + function + | MaybeCasted(Call(Import({ Selector = "TaskBuilder_::delay" }, _, _), callInfo, _, _)) -> + match callInfo.Args with + | [ binder ] -> Some binder + | _ -> None | _ -> None -let taskBuilderHP (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName, thisArg, args with - | "TaskBuilderBase.Bind", _, _ -> - Helper.LibCall(com, "Task", "bind", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "TaskBuilderBase.Zero", _, _ -> - Helper.LibCall(com, "Task", "zero", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | meth, Some callee, _ -> makeInstanceCall r t i callee meth args |> Some - | meth, None, _ -> - Helper.LibCall(com, "TaskBuilder", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - -let taskBuilderM (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = +let taskBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | "task", _, _ -> Helper.LibCall(com, "TaskBuilder", "new", t, [], ?loc = r) |> Some - | meth, Some callee, _ -> makeInstanceCall r t i callee meth args |> Some - | meth, None, _ -> - Helper.LibCall(com, "TaskBuilder", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | ".ctor", None, _ -> makeImportLib com t "new" "TaskBuilder" |> Some + | "task", _, _ -> libCall com r t "TaskBuilder" "new" [] |> Some + | "Run", Some c, _ -> makeInstanceCall r t i c "run" args |> Some + | ("Bind" | "TaskBuilderBase.Bind"), _, _ -> makeLibCall com r t i "TaskBuilder" "bind" args |> Some + | ("Combine" | "TaskBuilderBase.Combine"), _, _ -> makeLibCall com r t i "TaskBuilder" "combine" args |> Some + | ("Delay" | "TaskBuilderBase.Delay"), _, _ -> makeLibCall com r t i "TaskBuilder" "delay" args |> Some + | ("For" | "TaskBuilderBase.For"), _, _ -> makeLibCall com r t i "TaskBuilder" "for_loop" args |> Some + | ("Return" | "TaskBuilderBase.Return"), _, _ -> makeLibCall com r t i "TaskBuilder" "r_return" args |> Some + | ("ReturnFrom" | "TaskBuilderBase.ReturnFrom"), _, _ -> + makeLibCall com r t i "TaskBuilder" "return_from" args |> Some + | ("TryFinally" | "TaskBuilderBase.TryFinally"), _, _ -> + makeLibCall com r t i "TaskBuilder" "try_finally" args |> Some + | ("TryWith" | "TaskBuilderBase.TryWith"), _, _ -> makeLibCall com r t i "TaskBuilder" "try_with" args |> Some + | ("Using" | "TaskBuilderBase.Using"), _, _ -> makeLibCall com r t i "TaskBuilder" "using" args |> Some + | ("While" | "TaskBuilderBase.While"), _, [ guard; body ] -> + match tryGetTaskDelayBinder body with + | Some body -> makeLibCall com r t i "TaskBuilder" "while_loop" [ guard; body ] |> Some + | None -> + "Task builder While expects a delayed body." + |> addErrorAndReturnNull com ctx.InlinePath r + |> Some + | ("Zero" | "TaskBuilderBase.Zero"), _, _ -> makeLibCall com r t i "TaskBuilder" "zero" args |> Some + | meth, Some c, _ -> makeInstanceCall r t i c meth args |> Some + | meth, None, _ -> makeLibCall com r t i "TaskBuilder" (Naming.lowerFirst meth) args |> Some let guids (com: ICompiler) @@ -3285,7 +3150,7 @@ let guids match i.CompiledName, thisArg, args with | ".ctor", None, _ -> match args with - | [] -> Helper.LibCall(com, "Guid", "empty", t, [], ?loc = r) |> Some + | [] -> libCall com r t "Guid" "empty" [] |> Some | [ ExprType String ] -> makeMemberCall com ctx r t i "Guid" "parse" None args |> Some | [ ExprType(Array(Number(UInt8, _), _)) ] -> makeMemberCall com ctx r t i "Guid" "new_from_array" thisArg args |> Some @@ -3312,35 +3177,25 @@ let uris (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> - Helper.LibCall(com, "Uri", "Uri.create", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "TryCreate" -> - Helper.LibCall(com, "Uri", "Uri.tryCreate", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "UnescapeDataString" -> - Helper.LibCall(com, "Util", "unescapeDataString", t, args, i.SignatureArgTypes) - |> Some - | "EscapeDataString" -> - Helper.LibCall(com, "Util", "escapeDataString", t, args, i.SignatureArgTypes) - |> Some - | "EscapeUriString" -> - Helper.LibCall(com, "Util", "escapeUriString", t, args, i.SignatureArgTypes) - |> Some - | "get_IsAbsoluteUri" - | "get_Scheme" - | "get_Host" - | "get_AbsolutePath" - | "get_AbsoluteUri" - | "get_PathAndQuery" - | "get_Query" - | "get_Fragment" - | "get_OriginalString" -> - Naming.removeGetSetPrefix i.CompiledName - |> Naming.lowerFirst - |> getFieldWith r t thisArg.Value - |> Some + match i.CompiledName, thisArg with + | ".ctor", _ -> + match args with + | [ ExprType String ] -> makeLibCall com r t i "Uri" "create" args |> Some + | [ ExprType String; _ ] -> makeLibCall com r t i "Uri" "createWithKind" args |> Some + | [ _; ExprType String ] -> makeLibCall com r t i "Uri" "createFromString" args |> Some + | [ _; _ ] -> makeLibCall com r t i "Uri" "createFromUri" args |> Some + | _ -> None + | "TryCreate", _ -> + match args with + | (ExprType String) :: _ -> makeLibCall com r t i "Uri" "tryCreateWithKind" args |> Some + | _ :: (ExprType String) :: _ -> makeLibCall com r t i "Uri" "tryCreateFromString" args |> Some + | _ -> makeLibCall com r t i "Uri" "tryCreateFromUri" args |> Some + | "ToString", Some c -> toString com ctx r [ c ] |> Some + | "UnescapeDataString", _ -> makeLibCall com r t i "Uri" "unescapeDataString" args |> Some + | "EscapeDataString", _ -> makeLibCall com r t i "Uri" "escapeDataString" args |> Some + | "EscapeUriString", _ -> makeLibCall com r t i "Uri" "escapeUriString" args |> Some + | ("get_IsAbsoluteUri" | "get_Scheme" | "get_Host" | "get_Port" | "get_IsDefaultPort" | "get_AbsolutePath" | "get_AbsoluteUri" | "get_PathAndQuery" | "get_Query" | "get_Fragment" | "get_OriginalString"), + thisArg -> makeMemberCall com ctx r t i "Uri" i.CompiledName thisArg args |> Some | _ -> None let laziness (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3352,9 +3207,9 @@ let laziness (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op // // | "CreateFromValue", _, _ -> // // Helper.LibCall(com, "Native", "lazyFromValue", t, args, i.SignatureArgTypes, ?loc = r) // // |> Some -// | ("Force" | "get_Value"), Some callee, _ -> makeInstanceCall r t i callee "force" [] |> Some -// // | "get_IsValueCreated", Some callee, _ -> -// // Naming.removeGetSetPrefix i.CompiledName |> getFieldWith r t callee |> Some +// | ("Force" | "get_Value"), Some c, _ -> makeInstanceCall r t i c "force" [] |> Some +// // | "get_IsValueCreated", Some c, _ -> +// // Naming.removeGetSetPrefix i.CompiledName |> getFieldWith r t c |> Some // | _ -> None let controlExtensions @@ -3377,7 +3232,7 @@ let controlExtensions |> Option.defaultValue (args, i.SignatureArgTypes) |> fun (args, argTypes) -> List.rev args, List.rev argTypes - Helper.LibCall(com, "Observable", meth, t, args, argTypes) + libCallTyped com None t "Observable" meth args argTypes ) let types (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3482,16 +3337,12 @@ let types (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio match resolved, thisArg with | Some _, _ -> resolved - | None, Some thisArg -> + | None, Some c -> match i.CompiledName with - | "GetTypeInfo" -> Some thisArg + | "GetTypeInfo" -> Some c | "get_GenericTypeArguments" - | "GetGenericArguments" -> - Helper.LibCall(com, "Reflection", "getGenerics", t, [ thisArg ], ?loc = r) - |> Some - | "MakeGenericType" -> - Helper.LibCall(com, "Reflection", "makeGenericType", t, thisArg :: args, ?loc = r) - |> Some + | "GetGenericArguments" -> libCall com r t "Reflection" "getGenerics" [ c ] |> Some + | "MakeGenericType" -> libCall com r t "Reflection" "makeGenericType" (c :: args) |> Some | "get_FullName" | "get_Namespace" | "get_IsArray" @@ -3506,7 +3357,7 @@ let types (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | "IsInstanceOfType" -> let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst - Helper.LibCall(com, "Reflection", meth, t, thisArg :: args, ?loc = r) |> Some + libCall com r t "Reflection" meth (c :: args) |> Some | _ -> None | None, None -> None @@ -3516,46 +3367,61 @@ let fsharpType com methName (r: SourceLocation option) t (i: CallInfo) (args: Ex Helper.LibCall(com, "Reflection", "tuple_type", t, args, i.SignatureArgTypes, hasSpread = true, ?loc = r) |> Some // Prevent name clash with FSharpValue.GetRecordFields - | "GetRecordFields" -> - Helper.LibCall(com, "Reflection", "getRecordElements", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "GetRecordFields" -> makeLibCall com r t i "Reflection" "getRecordElements" args |> Some | "GetUnionCases" | "GetTupleElements" | "GetFunctionElements" | "IsUnion" | "IsRecord" | "IsTuple" - | "IsFunction" -> - Helper.LibCall(com, "Reflection", Naming.lowerFirst methName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "IsFunction" -> makeLibCall com r t i "Reflection" (Naming.lowerFirst methName) args |> Some | "IsExceptionRepresentation" | "GetExceptionFields" -> None // TODO!!! | _ -> None let fsharpValue com methName (r: SourceLocation option) t (i: CallInfo) (args: Expr list) = match methName with + | "GetRecordFields" -> + let args, argTypes = + match args with + | record :: rest -> + let rec getConcreteType (expr: Expr) = + match expr with + | TypeCast(inner, _) -> getConcreteType inner + | _ -> expr.Type + + let typeInfo = Fable.Value(Fable.TypeInfo(getConcreteType record, []), None) + + let argTypes = + match i.SignatureArgTypes with + | first :: restTypes -> first :: Fable.MetaType :: restTypes + | [] -> [] + + record :: typeInfo :: rest, argTypes + | _ -> args, i.SignatureArgTypes + + libCallTyped com r t "Reflection" "getRecordFields" args argTypes |> Some | "GetUnionFields" - | "GetRecordFields" | "GetRecordField" | "GetTupleFields" | "GetTupleField" | "MakeUnion" | "MakeRecord" - | "MakeTuple" -> - Helper.LibCall(com, "Reflection", Naming.lowerFirst methName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "MakeTuple" -> makeLibCall com r t i "Reflection" (Naming.lowerFirst methName) args |> Some | "GetExceptionFields" -> None // TODO!!! | _ -> None let tryField com t ownerTyp fieldName = match ownerTyp, fieldName with - | Number(Decimal, _), _ -> Helper.LibValue(com, "Decimal", fieldName, t) |> Some + | Number(Decimal, _), _ -> libValue com None t "Decimal" fieldName [] |> Some | String, "Empty" -> makeStrConst "" |> Some - | Builtin BclGuid, "Empty" -> Helper.LibValue(com, "Guid", "empty", t) |> Some + | Builtin BclGuid, "Empty" -> libValue com None t "Guid" "empty" [] |> Some | Builtin BclTimeSpan, _ -> let meth = fieldName |> Naming.applyCaseRule Fable.Core.CaseRules.SnakeCase - Helper.LibValue(com, "TimeSpan", meth, t) |> Some + libValue com None t "TimeSpan" meth [] |> Some + | Builtin BclRune, "ReplacementChar" + | Builtin BclRune, "get_ReplacementChar" -> libValue com None t "Rune" "replacementChar" [] |> Some | Builtin BclDateTime, _ -> let meth = fieldName |> Naming.lowerFirst makeStaticFieldCall com None t "DateTime" "DateTime" meth |> Some @@ -3567,7 +3433,7 @@ let tryField com t ownerTyp fieldName = let meth = fieldName |> Naming.lowerFirst match ent.FullName with - | "System.BitConverter" -> Helper.LibCall(com, "BitConverter", meth, t, []) |> Some + | "System.BitConverter" -> libCall com None t "BitConverter" meth [] |> Some | _ -> None | _ -> None @@ -3670,13 +3536,14 @@ let private replacedModules = Types.datetimeOffset, dateTimeOffsets Types.dateOnly, dateOnly Types.timeOnly, timeOnly + Types.rune, runes Types.timespan, timeSpans - "System.Timers.Timer", timers + Types.timer, timers "System.Environment", systemEnv "System.IO.File", files "System.IO.Path", paths - "System.Globalization.CultureInfo", globalization - "System.Random", random + Types.cultureInfo, globalization + Types.random, random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels "System.Threading.Monitor", monitor @@ -3706,10 +3573,11 @@ let private replacedModules = "Microsoft.FSharp.Control.AsyncActivation`1", asyncBuilder "Microsoft.FSharp.Control.FSharpAsync", asyncs "Microsoft.FSharp.Control.AsyncPrimitives", asyncs - "Microsoft.FSharp.Control.TaskBuilderModule", taskBuilderM + "Microsoft.FSharp.Control.TaskBuilderModule", taskBuilder "Microsoft.FSharp.Control.TaskBuilder", taskBuilder - "Microsoft.FSharp.Control.TaskBuilderBase", taskBuilderB - "Microsoft.FSharp.Control.TaskBuilderExtensions.HighPriority", taskBuilderHP + "Microsoft.FSharp.Control.TaskBuilderBase", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderExtensions.HighPriority", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderExtensions.LowPriority", taskBuilder Types.guid, guids "System.Uri", uris "System.Lazy`1", laziness @@ -3757,8 +3625,8 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr // UnionCaseInfo from a NewUnionCase quotation deconstruction is a Rust-native // FSharpUnionCaseInfo carrier; route member access to the quotation runtime. match thisArg, info.CompiledName with - | Some c, "get_Name" -> Helper.LibCall(com, "quotation", "unionCaseName", t, [ c ], ?loc = r) |> Some - | Some c, "get_Tag" -> Helper.LibCall(com, "quotation", "unionCaseTag", t, [ c ], ?loc = r) |> Some + | Some c, "get_Name" -> libCall com r t "quotation" "unionCaseName" [ c ] |> Some + | Some c, "get_Tag" -> libCall com r t "quotation" "unionCaseTag" [ c ] |> Some | _ -> None | "System.Reflection.PropertyInfo" | "System.Reflection.ParameterInfo" @@ -3776,26 +3644,22 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr | _ -> false match thisArg, info.CompiledName with - | Some c, "get_Name" when isMethodInfoCarrier c -> - Helper.LibCall(com, "quotation", "methodName", t, [ c ], ?loc = r) |> Some + | Some c, "get_Name" when isMethodInfoCarrier c -> libCall com r t "quotation" "methodName" [ c ] |> Some | Some c, "get_DeclaringType" when isMethodInfoCarrier c -> - Helper.LibCall(com, "quotation", "methodDeclaringType", t, [ c ], ?loc = r) - |> Some + libCall com r t "quotation" "methodDeclaringType" [ c ] |> Some | Some c, "get_Tag" -> makeStrConst "tag" |> getExpr r t c |> Some | Some c, "get_ReturnType" -> makeStrConst "returnType" |> getExpr r t c |> Some | Some c, "GetParameters" -> makeStrConst "parameters" |> getExpr r t c |> Some | Some c, ("get_PropertyType" | "get_ParameterType") -> makeIntConst 1 |> getExpr r t c |> Some - | Some c, "GetFields" -> - Helper.LibCall(com, "Reflection", "getUnionCaseFields", t, [ c ], ?loc = r) - |> Some - | Some c, "GetValue" -> Helper.LibCall(com, "Reflection", "getValue", t, c :: args, ?loc = r) |> Some + | Some c, "GetFields" -> libCall com r t "Reflection" "getUnionCaseFields" [ c ] |> Some + | Some c, "GetValue" -> libCall com r t "Reflection" "getValue" (c :: args) |> Some | Some c, "get_Name" -> match c with | Value(TypeInfo(exprType, _), loc) -> getTypeName com ctx loc exprType |> StringConstant |> makeValue r |> Some // Runtime PropertyInfo (e.g. a record field info): read its carried name. // Distinct from the generic type-name helper `name()` to avoid a clash. - | c -> Helper.LibCall(com, "Reflection", "propertyName", t, [ c ], ?loc = r) |> Some + | c -> libCall com r t "Reflection" "propertyName" [ c ] |> Some | _ -> None // F# Quotations | typeName -> Quotations.tryQuotationCall "quotation" com ctx r t info thisArg args typeName @@ -3838,7 +3702,8 @@ let tryType typ = | BclDateTimeOffset -> Some(Types.datetimeOffset, dateTimeOffsets, []) | BclDateOnly -> Some(Types.dateOnly, dateOnly, []) | BclTimeOnly -> Some(Types.timeOnly, timeOnly, []) - | BclTimer -> Some("System.Timers.Timer", timers, []) + | BclRune -> Some(Types.rune, runes, []) + | BclTimer -> Some(Types.timer, timers, []) | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclHashSet genArg -> Some(Types.hashset, hashSets, [ genArg ]) | BclDictionary(key, value) -> Some(Types.dictionary, dictionaries, [ key; value ]) diff --git a/src/Fable.Transforms/State.fs b/src/Fable.Transforms/State.fs index 9106c58ae8..60bb8832a1 100644 --- a/src/Fable.Transforms/State.fs +++ b/src/Fable.Transforms/State.fs @@ -14,7 +14,13 @@ type PluginRef = TypeFullName: string } -type Assemblies(getPlugin, fsharpAssemblies: FSharpAssembly list, addLog: Severity -> string -> unit) = +type Assemblies + ( + getPlugin: (PluginRef -> System.Type) option, + fsharpAssemblies: FSharpAssembly list, + addLog: Severity -> string -> unit + ) + = let assemblies = Dictionary() let coreAssemblies = Dictionary() let entities = ConcurrentDictionary() @@ -55,8 +61,23 @@ type Assemblies(getPlugin, fsharpAssemblies: FSharpAssembly list, addLog: Severi false if scanForPlugins then - for e in asm.Contents.Entities do - if e.IsAttributeType && FSharp2Fable.Util.inherits e "Fable.PluginAttribute" then + let pluginEntities = + asm.Contents.Entities + |> Seq.filter (fun e -> + e.IsAttributeType && FSharp2Fable.Util.inherits e "Fable.PluginAttribute" + ) + |> Seq.toList + + match getPlugin with + | None -> + if not pluginEntities.IsEmpty then + let names = pluginEntities |> List.map (fun e -> e.FullName) |> String.concat ", " + + addLog + Severity.Warning + $"Skipping Fable plugins from %s{asmName} because plugins cannot be loaded in this environment: %s{names}" + | Some getPlugin -> + for e in pluginEntities do try let plugin = getPlugin @@ -135,6 +156,53 @@ type Assemblies(getPlugin, fsharpAssemblies: FSharpAssembly list, addLog: Severi member _.Plugins = plugins +type ParameterSignature = + { + Name: string option + IsOptional: bool + IsParamArray: bool + } + +type MemberSignature = + { + Type: Fable.Type + Parameters: ParameterSignature list list + IsInline: FSharpInlineAnnotation + IsMutable: bool + Attributes: (string * obj list) list + } + +type EntitySignature = + { + Attributes: (string * obj list) list + Kind: bool * bool * bool * bool * bool + Fields: (string * Fable.Type * obj) list + UnionCases: (string * (string * Fable.Type) list) list + } + +/// The parts of a file other files can depend on without naming them in their own typed tree +type FileSignature = + { + IsUnknown: bool + Literals: Map + Members: Map + Entities: Map + } + + static member Unknown = + { + IsUnknown = true + Literals = Map.empty + Members = Map.empty + Entities = Map.empty + } + + static member Changed(before: FileSignature, after: FileSignature) = + before.IsUnknown || after.IsUnknown || before <> after + + static member LiteralsChanged(before: FileSignature, after: FileSignature) = + before.IsUnknown || after.IsUnknown || before.Literals <> after.Literals + type ImplFile = { Declarations: FSharpImplementationFileDeclaration list @@ -142,6 +210,7 @@ type ImplFile = RootComment: FSharpXmlDoc option Entities: IReadOnlyDictionary InlineExprs: (string * InlineExprLazy) list + Signature: FileSignature } static member From(file: FSharpImplementationFileContents) = @@ -154,6 +223,96 @@ type ImplFile = loop entities e.NestedEntities + let signature (declarations: FSharpImplementationFileDeclaration list) = + try + let makeType = FSharp2Fable.TypeHelpers.makeType Map.empty + + let attributes (atts: FSharpAttribute seq) = + atts + |> Seq.map (fun att -> + FSharp2Fable.FsEnt.FullName att.AttributeType, + att.ConstructorArguments |> Seq.map snd |> List.ofSeq + ) + |> List.ofSeq + + let rec members (decls: FSharpImplementationFileDeclaration list) = + decls + |> List.collect ( + function + | FSharpImplementationFileDeclaration.Entity(_, decls) -> members decls + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(memb, _, _) when + not memb.IsCompilerGenerated && not memb.Accessibility.IsPrivate + -> + [ memb ] + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue _ + | FSharpImplementationFileDeclaration.InitAction _ -> [] + ) + + let rec entities (ents: FSharpEntity seq) = + ents |> Seq.collect (fun e -> Seq.append [ e ] (entities e.NestedEntities)) + + let members = members declarations + + { + IsUnknown = false + Literals = + members + |> List.choose (fun memb -> + memb.LiteralValue + |> Option.map (fun value -> FSharp2Fable.Helpers.getMemberUniqueName memb, value) + ) + |> Map + Members = + members + |> List.map (fun memb -> + FSharp2Fable.Helpers.getMemberUniqueName memb, + { + Type = makeType memb.FullType + Parameters = + memb.CurriedParameterGroups + |> Seq.map ( + Seq.map (fun p -> + { + Name = p.Name + IsOptional = p.IsOptionalArg + IsParamArray = p.IsParamArrayArg + } + ) + >> List.ofSeq + ) + |> List.ofSeq + IsInline = memb.InlineAnnotation + IsMutable = memb.IsMutable + Attributes = attributes memb.Attributes + } + ) + |> Map + Entities = + FSharp2Fable.Compiler.getRootFSharpEntities declarations + |> entities + |> Seq.map (fun e -> + FSharp2Fable.FsEnt.FullName e, + { + Attributes = attributes e.Attributes + Kind = e.IsFSharpRecord, e.IsFSharpUnion, e.IsEnum, e.IsInterface, e.IsValueType + Fields = + e.FSharpFields + |> Seq.map (fun f -> f.Name, makeType f.FieldType, Option.toObj f.LiteralValue) + |> List.ofSeq + UnionCases = + e.UnionCases + |> Seq.map (fun c -> + c.Name, + c.Fields |> Seq.map (fun f -> f.Name, makeType f.FieldType) |> List.ofSeq + ) + |> List.ofSeq + } + ) + |> Map + } + with _ -> + FileSignature.Unknown + // add all entities to the entity cache let entities = Dictionary() @@ -166,12 +325,15 @@ type ImplFile = FSharp2Fable.Compiler.getRootFSharpEntities declarations |> loop entities let rootModule, rootComment = FSharp2Fable.Compiler.getRootModule declarations + let fileName = Path.normalizePathAndEnsureFsExtension file.FileName + { Declarations = declarations Entities = entities RootModule = rootModule RootComment = rootComment - InlineExprs = FSharp2Fable.Compiler.getInlineExprs file.FileName declarations + InlineExprs = FSharp2Fable.Compiler.getInlineExprs fileName declarations + Signature = signature declarations } type PrecompiledInfo = @@ -179,6 +341,12 @@ type PrecompiledInfo = abstract TryGetRootModule: normalizedFullPath: string -> string option abstract TryGetInlineExpr: memberUniqueName: string -> InlineExpr option +let private removeAll keys map = + (map, keys) ||> Seq.fold (fun map key -> Map.remove key map) + +let private addAll entries map = + (map, entries) ||> Seq.fold (fun map (key, value) -> Map.add key value map) + type Project private ( @@ -186,12 +354,14 @@ type Project projectOptions: FSharpProjectOptions, implFiles: Map, assemblies: Assemblies, - ?precompiledInfo: PrecompiledInfo + ?precompiledInfo: PrecompiledInfo, + ?inlineExprs: Map ) = - let inlineExprsDic = - implFiles |> Map.values |> Seq.collect (fun f -> f.InlineExprs) |> dict + let inlineExprs = + inlineExprs + |> Option.defaultWith (fun () -> implFiles |> Map.values |> Seq.collect (fun f -> f.InlineExprs) |> Map) let precompiledInfo = precompiledInfo @@ -218,8 +388,6 @@ type Project let checknulls = projectOptions.OtherOptions |> Array.exists ((=) "--checknulls+") Compiler.SetCheckNullsUnsafe checknulls // set it one time only as early as possible - let getPlugin = defaultArg getPlugin (fun _ -> failwith "Plugins are not supported") - let assemblies = Assemblies(getPlugin, fsharpAssemblies, addLog) let implFilesMap = @@ -234,19 +402,36 @@ type Project Project(projectFile, projectOptions, implFilesMap, assemblies, ?precompiledInfo = precompiledInfo) member this.Update(files: FSharpImplementationFileContents list) = - let implFiles = - (this.ImplementationFiles, files) - ||> List.fold (fun implFiles file -> + let implFiles, inlineExprs = + ((this.ImplementationFiles, inlineExprs), files) + ||> List.fold (fun (implFiles, inlineExprs) file -> let key = Path.normalizePathAndEnsureFsExtension file.FileName let file = ImplFile.From(file) - Map.add key file implFiles + + let previousInlineExprs = + match Map.tryFind key implFiles with + | Some previous -> previous.InlineExprs + | None -> [] + + let inlineExprs = + inlineExprs + |> removeAll (List.map fst previousInlineExprs) + |> addAll file.InlineExprs + + Map.add key file implFiles, inlineExprs ) - Project(this.ProjectFile, this.ProjectOptions, implFiles, this.Assemblies, this.PrecompiledInfo) + Project( + this.ProjectFile, + this.ProjectOptions, + implFiles, + this.Assemblies, + this.PrecompiledInfo, + inlineExprs = inlineExprs + ) member _.TryGetInlineExpr(com: Compiler, memberUniqueName: string) = - inlineExprsDic - |> Dictionary.tryFind memberUniqueName + Map.tryFind memberUniqueName inlineExprs |> Option.map (fun e -> e.Calculate(com)) member _.GetFileInlineExprs(com: Compiler) : (string * InlineExpr) array = @@ -269,15 +454,17 @@ type LogEntry = Severity: Severity Range: SourceLocation option FileName: string option + Code: string option } - static member Make(severity, msg, ?fileName, ?range, ?tag) = + static member Make(severity, msg, ?fileName, ?range, ?tag, ?code) = { Message = msg Tag = defaultArg tag "FABLE" Severity = severity Range = range FileName = fileName + Code = code } static member MakeError(msg, ?fileName, ?range, ?tag) = @@ -294,8 +481,10 @@ type CompilerImpl ?outType: OutputType, ?outDir: string, ?watchDependencies: HashSet, + ?signatureDependencies: HashSet, ?logs: ResizeArray, - ?isPrecompilingInlineFunction: bool + ?isPrecompilingInlineFunction: bool, + ?warningSuppression: WarningSuppression.Resolver ) = @@ -304,6 +493,11 @@ type CompilerImpl let logs = Option.defaultWith ResizeArray logs let fableLibraryDir = fableLibDir.TrimEnd('/') + let getSuppressions fileName = + match warningSuppression with + | Some resolver -> resolver.For(fileName) + | None -> WarningSuppression.FileSuppressions.Empty + member _.Logs = logs.ToArray() member _.WatchDependencies = @@ -311,6 +505,11 @@ type CompilerImpl | Some w -> Array.ofSeq w | None -> [||] + member _.SignatureDependencies = + match signatureDependencies with + | Some w -> Array.ofSeq w + | None -> [||] + interface Compiler with member _.Options = options member _.Plugins = project.Assemblies.Plugins @@ -344,8 +543,10 @@ type CompilerImpl outType, ?outDir = outDir, ?watchDependencies = watchDependencies, + ?signatureDependencies = signatureDependencies, logs = logs, - isPrecompilingInlineFunction = true + isPrecompilingInlineFunction = true, + ?warningSuppression = warningSuppression ) member _.GetImplementationFile(fileName) = @@ -404,6 +605,34 @@ type CompilerImpl | Some watchDependencies when file <> currentFile -> watchDependencies.Add(file) |> ignore | _ -> () - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - LogEntry.Make(severity, msg, ?range = range, ?fileName = fileName, ?tag = tag) - |> logs.Add + member _.AddSignatureDependency(file) = + match signatureDependencies with + | Some signatureDependencies when file <> currentFile -> signatureDependencies.Add(file) |> ignore + | _ -> () + + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + // Only warnings can be suppressed, errors always surface (matches F#'s own #nowarn), + // and neither can the warnings about the `fable-disable` directives themselves. + let isSuppressed = + severity = Severity.Warning + && WarningCodes.isSuppressible code + && ( + match range with + | None -> false + | Some(r: SourceLocation) -> + let file = + match fileName with + | Some f -> f + | None -> + match r.File with + | Some f -> f + | None -> currentFile + + // The whole range, not just its first line, so a trailing + // `// fable-disable-line` still works on a multi-line expression. + (getSuppressions file).IsSuppressed(r.start.line, r.``end``.line, code) + ) + + if not isSuppressed then + LogEntry.Make(severity, msg, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) + |> logs.Add diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 22e7f9bbe8..58ed2763b4 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -34,6 +34,9 @@ module Atts = [] let compiledValue = "Fable.Core.CompiledValueAttribute" // typeof.FullName + [] + let caseTest = "Fable.Core.CaseTestAttribute" // typeof.FullName + [] let entryPoint = "Microsoft.FSharp.Core.EntryPointAttribute" // typeof.FullName @@ -222,12 +225,27 @@ module Types = [] let datetimeOffset = "System.DateTimeOffset" + [] + let datetimeKind = "System.DateTimeKind" + [] let dateOnly = "System.DateOnly" [] let timeOnly = "System.TimeOnly" + [] + let rune = "System.Text.Rune" + + [] + let cultureInfo = "System.Globalization.CultureInfo" + + [] + let numberStyles = "System.Globalization.NumberStyles" + + [] + let stringSplitOptions = "System.StringSplitOptions" + [] let timer = "System.Timers.Timer" @@ -303,6 +321,8 @@ module Types = [] let regexCaptureCollection = "System.Text.RegularExpressions.CaptureCollection" + let regexOptions = "System.Text.RegularExpressions.RegexOptions" + [] let random = "System.Random" @@ -492,6 +512,9 @@ module Types = [] let patternsModule = "Microsoft.FSharp.Quotations.PatternsModule" + [] + let derivedPatternsModule = "Microsoft.FSharp.Quotations.DerivedPatternsModule" + [] let printfModule = "Microsoft.FSharp.Core.PrintfModule" @@ -668,7 +691,14 @@ module Log = FromRange: SourceLocation option } - let private addLog (com: Compiler) (inlinePath: InlinePath list) (range: SourceLocation option) msg severity = + let private addLogWithCode + (com: Compiler) + (inlinePath: InlinePath list) + (range: SourceLocation option) + msg + severity + (code: string option) + = let printInlineSource fromPath (p: InlinePath) = let path = Path.getRelativeFileOrDirPath false fromPath false p.FromFile @@ -685,11 +715,22 @@ module Log = file, msg + " - Inline call from " + inlinePath | [] -> range |> Option.bind (fun r -> r.File) |> Option.defaultValue com.CurrentFile, msg - com.AddLog(msg, severity, ?range = range, fileName = actualFile) + com.AddLog(msg, severity, ?range = range, fileName = actualFile, ?code = code) + + let private addLog (com: Compiler) (inlinePath: InlinePath list) (range: SourceLocation option) msg severity = + addLogWithCode com inlinePath range msg severity None let addWarning (com: Compiler) inlinePath range warning = addLog com inlinePath range warning Severity.Warning + /// Same as `addWarning`, but tags the warning with a stable code (e.g. "FABLE0001") so it + /// can be suppressed via `// fable-disable-line/-next-line/-enable CODE` comments. + /// Meant to be used as `WarningCodes.someWarning arg1 arg2 |> addWarningWithCode com inlinePath range`. + /// + /// Note that a warning coming out of an inlined function is attributed to the file that *defines* it + let addWarningWithCode (com: Compiler) inlinePath range ((code, warning): string * string) = + addLogWithCode com inlinePath range warning Severity.Warning (Some code) + let addError (com: Compiler) inlinePath range error = addLog com inlinePath range error Severity.Error @@ -724,6 +765,25 @@ module AST = let inline (|EntFullName|) (e: Entity) = e.FullName let inline (|EntRefFullName|) (e: EntityRef) = e.FullName + [] + let (|ExprTypeEnum|_|) (fullName: string) (expr: Expr) = + match expr.Type with + | Number(_, NumberInfo.IsEnum entRef) when entRef.FullName = fullName -> ValueSome expr + | _ -> ValueNone + + [] + let (|ExprTypeObj|_|) (fullName: string) (expr: Expr) = + match expr.Type with + | DeclaredType(entRef, _) when entRef.FullName = fullName -> ValueSome expr + | _ -> ValueNone + + [] + let (|FormatProvider|_|) (expr: Expr) = + match expr with + | ExprTypeObj Types.cultureInfo _ + | ExprTypeObj "System.IFormatProvider" _ -> ValueSome expr + | _ -> ValueNone + [] let (|DeclaredTypeFullName|_|) = function @@ -917,6 +977,16 @@ module AST = | _ -> ValueNone | _ -> ValueNone + /// The field type of a TypeScriptTaggedUnion case that compiles to its value: a single unnamed field of an interface type + let tryInlinedTaggedUnionCaseType (com: Compiler) (uci: UnionCase) = + match uci.UnionCaseFields with + | [ field ] when field.Name = "Item" -> + match field.FieldType with + | DeclaredType(entRef, _) when com.TryGetEntity(entRef) |> Option.exists _.IsInterface -> + Some field.FieldType + | _ -> None + | _ -> None + // TODO: Improve this, see https://github.com/fable-compiler/Fable/issues/1659#issuecomment-445071965 // This is mainly used for inlining so a computation or a reference to a mutable value are understood // as a side effects too (because we don't want to duplicate or change the order of execution) @@ -990,6 +1060,12 @@ module AST = | Option _ -> true | _ -> false + let rec containsFunctionType (t: Type) = + match t with + | LambdaType _ + | DelegateType _ -> true + | t -> t.Generics |> List.exists containsFunctionType + let isUnitOfMeasure t = match t with | Measure _ @@ -1263,7 +1339,7 @@ module AST = LibraryImportInfo.Create(isInstanceMember = false, isModuleMember = true) |> makeImportLibWithInfo com t memberName moduleName - let private makeInternalImport (com: Compiler) t (selector: string) (path: string) kind = + let private makeInternalImport (com: Compiler) t (selector: string) (path: string) range kind = let path = if com.CurrentFile = path then "./" + Path.GetFileName(path) @@ -1277,14 +1353,14 @@ module AST = Kind = kind }, t, - None + range ) - let makeInternalMemberImport com t membRef (selector: string) (path: string) = - MemberImport(membRef) |> makeInternalImport com t selector path + let makeInternalMemberImport com t membRef (selector: string) (path: string) range = + MemberImport(membRef) |> makeInternalImport com t selector path range let makeInternalClassImport com entRef (selector: string) (path: string) = - ClassImport(entRef) |> makeInternalImport com Any selector path + ClassImport(entRef) |> makeInternalImport com Any selector path None let makeCallInfo thisArg args sigArgTypes = CallInfo.Create(?thisArg = thisArg, args = args, sigArgTypes = sigArgTypes) @@ -1801,7 +1877,12 @@ module AST = | None -> body :: (Option.toList finalizer) | DecisionTree(expr, targets) -> expr :: (List.map snd targets) | DecisionTreeSuccess(_, boundValues, _) -> boundValues - | Quote _ -> [] // Quoted expressions are opaque data — don't traverse + // Quoted expressions must not be *rewritten* (see `visit` above, which + // leaves them alone), but analysis has to see inside them: a local that + // is only referenced from within a quotation is still referenced, and + // returning [] here let reference counting treat it as dead and drop the + // binding out from under the captured value. + | Quote(quotedExpr, _, _) -> [ quotedExpr ] let deepExists (f: Expr -> bool) expr = let rec deepExistsInner (exprs: ResizeArray) = diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index aba5fef45d..b84a463df1 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: b517d52df145ee30d1d3bd7e20fded1a9003ac54 +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,292 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.5.0 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(all)* Keep reads of imported mutable values in place (#5242) ([1190dfa5](https://github.com/fable-compiler/Fable/commit/1190dfa5e5f12793fc2b3b2d15a24afb8253bce3)) +* *(all)* Rename idents of a witness inlined twice (#5249) ([1e41dcc5](https://github.com/fable-compiler/Fable/commit/1e41dcc5ac5a990a81aa15d43921d824b5466740)) +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Match Dictionary/HashSet array keys by reference (#5239) ([1d77d628](https://github.com/fable-compiler/Fable/commit/1d77d6282b8dea284bca3cff9a6455e561575e3a)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Escape */ in JSDoc comments (#5255) ([1072231c](https://github.com/fable-compiler/Fable/commit/1072231c4c64467547d7f69ab741824f2e1a522c)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Write negated integer zero without a sign (#5256) ([ce7fc717](https://github.com/fable-compiler/Fable/commit/ce7fc717c7597b133dc6f999f88a15b2fb636d65)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python)* Call base ctor from explicit constructors (#5226) ([508026e7](https://github.com/fable-compiler/Fable/commit/508026e73511f2636245515b8ec9476176e34b5c)) +* *(js/ts/python)* Clamp negative slice bounds like .NET (#5232) ([7319a994](https://github.com/fable-compiler/Fable/commit/7319a994a9c3be7d31348d29d2c464482b5f21f9)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) +* *(rust)* Inline hashing, more Decimal tests (#5236) ([705b6b70](https://github.com/fable-compiler/Fable/commit/705b6b70bf0b732811bd6e69cfc8fc0614b831c7)) +* *(ts)* Declare Mangle(false) interface members unmangled (#5234) ([86c00004](https://github.com/fable-compiler/Fable/commit/86c000041f286668353615003692b07e9082ff01)) +* *(ts)* Extend library classes for BCL collection bases (#5240) ([d82ef893](https://github.com/fable-compiler/Fable/commit/d82ef8932c58ddc73ff5aa6ca669be86aedbe9db)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + +## 4.4.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + +## 4.3.0 - 2026-10-29 + +### 🚀 Features + +* *(all)* Error on unsupported RegexOptions at compile time (#5180) ([a8cbc5dc](https://github.com/fable-compiler/Fable/commit/a8cbc5dc0c3a96639028057e132b39e559f40ced)) +* *(js/ts/python/dart)* Support Nullable conversions (#5183) ([6efa86d3](https://github.com/fable-compiler/Fable/commit/6efa86d35c5943dc8bbb6da0f27cc37095276064)) +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Warn on duplicate names in erased unions (#5169) ([459166d2](https://github.com/fable-compiler/Fable/commit/459166d2500a963a3616d8b1a63e1d8585d61ec6)) +* *(all)* Recompile inline users of a file with a signature (#5179) ([b383673d](https://github.com/fable-compiler/Fable/commit/b383673da61cfe3ca06416a624aaafbd732f876c)) +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(beam)* Raise ArgumentNullException from nullArg ([0b8f77f9](https://github.com/fable-compiler/Fable/commit/0b8f77f9880bcbd9317272d9b0d89f8fe8ac0257)) +* *(beam)* Keep parameter name in ArgumentException ([9e5d34f7](https://github.com/fable-compiler/Fable/commit/9e5d34f7b32cfdfb8b1adc93378895b26a5d69b9)) +* *(js/ts)* Zero-fill Array.zeroCreate for numeric types without a typed array (#5211) ([d96b6ab8](https://github.com/fable-compiler/Fable/commit/d96b6ab82f0181be4a379c806abcedde23f1359a)) +* *(js/ts)* Throw on out-of-range string indexer (#5214) ([d5f1f8fd](https://github.com/fable-compiler/Fable/commit/d5f1f8fdfab535eaff5c492661d0178be1c4c76a)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/dart)* Float to small integer and char conversions (#5182) ([bb0faa79](https://github.com/fable-compiler/Fable/commit/bb0faa794080a04e7791d419b2fa4d2ad107c186)) +* *(js/ts/python)* Compare ReferenceEquality types by ref (#5174) ([9c351626](https://github.com/fable-compiler/Fable/commit/9c3516268674b54aec2efae381cad920d4f15922)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) +* *(python/beam/rust)* Implicit DateTime to DateTimeOffset (#5173) ([401c881f](https://github.com/fable-compiler/Fable/commit/401c881f729494d06d20da9687e78f7315f5efb2)) +* *(ts)* Annotate Fable.Core typed arrays with native TS types ([e35d06c3](https://github.com/fable-compiler/Fable/commit/e35d06c36ba8ce4ab6cce75d6b332b9dcd50d711)) +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +### ⚡ Performance Improvements + +* *(js/ts)* Cast erased option unwraps in TypeScript output (#5181) ([65cd419d](https://github.com/fable-compiler/Fable/commit/65cd419d78e4dbc2008f8a707d5298e390defbee)) +* *(js/ts)* Compile isNull to a plain null check (#5186) ([71a0ad92](https://github.com/fable-compiler/Fable/commit/71a0ad92237586e9569fd1483f35c88f8477f478)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Throw invalidArg and invalidOp at the call site (#5194) ([e6a742b1](https://github.com/fable-compiler/Fable/commit/e6a742b1ebda71342d4445705ea9198c2059ae90)) +* *(js/ts)* Pass array literals to typed array constructors as plain arrays (#5209) ([cb7d6d6e](https://github.com/fable-compiler/Fable/commit/cb7d6d6e49bb43b83afe80477ea2d6d667be3f5b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + +## 4.2.0 - 2026-10-22 + +### 🚀 Features + +* *(all)* Support suppressing warnings via `// fable-disable` comments (#4808) ([f4a1a8cd](https://github.com/fable-compiler/Fable/commit/f4a1a8cdae2e8ac81edbdc6eba36cc140234170b)) +* *(all)* Warn on duplicate CompiledName on union cases ([2098f64a](https://github.com/fable-compiler/Fable/commit/2098f64a25e96385306ef19ed8c289f3b6c84db9)) +* *(js/ts)* Map static Decimal conversion methods (#5129) ([cd63e57e](https://github.com/fable-compiler/Fable/commit/cd63e57e909393ab493a1d8bfedbea9a50a377c9)) +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +### 🐞 Bug Fixes + +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Warn on type test against Pojo class (#5128) ([798df101](https://github.com/fable-compiler/Fable/commit/798df10179576a28f602fad58e204881792a0aef)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Inline interface cases of TypeScriptTaggedUnion (#5146) ([2be66611](https://github.com/fable-compiler/Fable/commit/2be66611d884cd564fa3f7b06fd4a015add1cefd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + +## 4.1.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +### 🐞 Bug Fixes + +* *(standalone)* Warn and skip plugins instead of throwing (#5114) ([021ab65c](https://github.com/fable-compiler/Fable/commit/021ab65c6d3adfe57cffb9f5ba6f595b034ac8bd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..5ee8bbb832d9677ad702104ded8e00c09619393f) + +## 4.0.0 - 2026-10-16 + +### 🏗️ Breaking changes + +* *(standalone)* Return tagged tooltips instead of text (#5088) ([6b995913](https://github.com/fable-compiler/Fable/commit/6b995913acbe98434f252b19b07182cc831a8a0f)) +* *(standalone)* Keep file name in declaration locations (#5089) ([b210461c](https://github.com/fable-compiler/Fable/commit/b210461c1651eaaa47e1027597c7545b6f2097fa)) +* *(standalone)* Return full completions with lazy docs (#5102) ([53849cc5](https://github.com/fable-compiler/Fable/commit/53849cc5a65f5de57dc65c76158b840e7012bc9c)) +* *(standalone)* Report error codes and severities (#5103) ([4ec83ff4](https://github.com/fable-compiler/Fable/commit/4ec83ff4e280db3075230e72b02b699a16f2bc76)) +* *(standalone)* Drop GetErrors, use Errors on results (#5111) ([ec72286d](https://github.com/fable-compiler/Fable/commit/ec72286d3528d1bbc1bede72df11deb3432040b4)) +* *(standalone)* Take 0-based columns in positional APIs (#5112) ([e33c5787](https://github.com/fable-compiler/Fable/commit/e33c57870454fea189a3eb33519f583a5003f8c3)) + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) +* *(standalone)* Expose symbol lookup and uses in file (#5101) ([9eff071c](https://github.com/fable-compiler/Fable/commit/9eff071c8e5cf418e4bf9af63c051eae121d4512)) +* *(standalone)* Expose lexer tokens and classification (#5110) ([e65d42c0](https://github.com/fable-compiler/Fable/commit/e65d42c03b8262b94e058354904f1126560443b4)) + +### 🐞 Bug Fixes + +* *(all)* Recompile dependents on signature change in watch (#5084) ([87fc89f7](https://github.com/fable-compiler/Fable/commit/87fc89f75b6ad22fafec72b4a57415682b4059df)) +* *(beam)* Make lazy sequence wrappers process-portable (#5063) ([9e78a3b3](https://github.com/fable-compiler/Fable/commit/9e78a3b327ca78d01863bf19b18c543673633af7)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) +* *(python)* Settle cancelled tasks awaited by async workflows (#5064) ([52186937](https://github.com/fable-compiler/Fable/commit/5218693738defc8a4502d28c206e391b241f9fd8)) +* *(python)* Preserve reusable replies and unit continuations (#5082) ([f19e2c56](https://github.com/fable-compiler/Fable/commit/f19e2c56566488dfeb48e71508bdb218aaa58582)) +* *(python)* Emit widening integer casts (#5087) ([07989b16](https://github.com/fable-compiler/Fable/commit/07989b16214c4fcf8e33318fba1dd3f77df77cbb)) +* *(rust)* Support inref struct unions (#5060) ([a3f27e0e](https://github.com/fable-compiler/Fable/commit/a3f27e0e8c149d6bfeb347ca4f4146ccfd8fb063)) + +### ⚡ Performance Improvements + +* Update inline expressions incrementally in watch mode (#5080) ([594d17b4](https://github.com/fable-compiler/Fable/commit/594d17b4b551ae33b5f2df5e4049611b9dd24bc2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..e33c57870454fea189a3eb33519f583a5003f8c3) + +## 3.8.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) +* *(beam)* Preserve async cancellation and finalize resources once (#5057) ([617f7111](https://github.com/fable-compiler/Fable/commit/617f7111bf02f8536b321b4373bdffa7a0f7d633)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..a8661874d60694f0faad235adb7f760fed6f72a5) + +## 3.8.0 - 2026-10-12 + +### 🚀 Features + +* *(all)* Add implicit conversion to erased unions U2-U9 ([4e486f20](https://github.com/fable-compiler/Fable/commit/4e486f201fb229964af213d6ce3030ac3c7e26de)) + +### 🐞 Bug Fixes + +* *(beam)* Disambiguate property accessors from ordinary methods (#5049) ([b30dc89d](https://github.com/fable-compiler/Fable/commit/b30dc89d0091c2bd1d9fd7a20042b7fbb4e5a3fa)) +* *(beam)* Normalize exception messages and string split separators (#5053) ([f13a9e14](https://github.com/fable-compiler/Fable/commit/f13a9e1494d8bbde8e74bfbf9dfbc8bd051a157e)) +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Preserve JS/TS generic unit arguments and Python unit continuations (#5037) ([e05bc752](https://github.com/fable-compiler/Fable/commit/e05bc752f1d8f16f08e2409a191d9c33463a1f04)) +* *(rust)* Fix tailcall elimination for members (#5051) ([4eb21772](https://github.com/fable-compiler/Fable/commit/4eb2177263f544ec354076da07d8a097d56c273e)) +* *(rust)* Fix temporary struct locals type (#5052) ([17b36588](https://github.com/fable-compiler/Fable/commit/17b3658876a3bd776c4a0129ec3a4cddc9d7e05d)) +* *(rust)* Add main function for no_std (#5054) ([10fdccc0](https://github.com/fable-compiler/Fable/commit/10fdccc04bb0e421643426d21dfcbd940e3e5a16)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/23bc5c54db51469431e6fdab713dfe1b6a3df019..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + +## 3.7.0 - 2026-10-07 + +### 🚀 Features + +* *(python)* Give System exceptions native Python bases (#5040) ([4ee31586](https://github.com/fable-compiler/Fable/commit/4ee31586fd88ed0708e218e7f170432baa33551e)) + +### 🐞 Bug Fixes + +* *(beam)* Recognize boxed discriminated unions in type tests (#5033) ([dcfaa3d6](https://github.com/fable-compiler/Fable/commit/dcfaa3d6181f6789d0868e0bf9132ecdcdc76287)) +* *(beam)* Prevent identifier collisions and atom overflow (#5017) ([b9ce2fdb](https://github.com/fable-compiler/Fable/commit/b9ce2fdb53fc86a615970f4828005ee3a490385f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/aaf35f200be5bbc2698abe242f77b8de745e4337..23bc5c54db51469431e6fdab713dfe1b6a3df019) + +## 3.6.0 - 2026-10-03 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(all)* Minor type const strings cleanup (#5029) ([9b69e5be](https://github.com/fable-compiler/Fable/commit/9b69e5be8a2cffc3cbcb3920bf9924b72ed385d6)) +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) +* *(rust)* Remove trailing comma from macro args (#5031) ([e6e9f54c](https://github.com/fable-compiler/Fable/commit/e6e9f54c905750a1bf78ec454bca7d83cdffb61e)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..aaf35f200be5bbc2698abe242f77b8de745e4337) + +## 3.5.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +### 🐞 Bug Fixes + +* *(beam)* Preserve local spawn calls (#4992) ([70278ec6](https://github.com/fable-compiler/Fable/commit/70278ec636b7f948a6ba3d215b36a3974de89edc)) +* *(dart)* Prefix file scope names shadowed by class members (#4982) ([ece7b071](https://github.com/fable-compiler/Fable/commit/ece7b0714eae18650124cad017b0bda4da74ebc8)) +* *(python)* Keep the payload type when reading an erased union field (#4981) ([e0536ecd](https://github.com/fable-compiler/Fable/commit/e0536ecd9b9aeb12cc96c7a2ef2edde7089e2dd0)) +* *(rust)* Less allocations when enumerating strings (#4970) ([5d62380c](https://github.com/fable-compiler/Fable/commit/5d62380cb30dc1080c6943445a256e27db6246f8)) +* *(rust)* Remove reflection registration on boxing (#4983) ([50a37f78](https://github.com/fable-compiler/Fable/commit/50a37f78f31394d61349b09a025c1713529d8ec0)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + +## 3.4.0 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(python)* Preserve record field names in structured formatting (#4952) ([fbc162d6](https://github.com/fable-compiler/Fable/commit/fbc162d6912d62b9a75e73f8264814f6519b6194)) +* *(python)* Reflect an erased union as the type it erases to ([0f564c74](https://github.com/fable-compiler/Fable/commit/0f564c744e86096c779d0acea6d677bce6d56ee1)) +* *(ts)* Declare the type parameter of a flexible-type overload ([6f1f1b8a](https://github.com/fable-compiler/Fable/commit/6f1f1b8afd9158743978720507ced5c4a8b2144f)) + +### ⚡ Performance Improvements + +* *(standalone)* Accumulate compile errors in a ResizeArray ([f282ab48](https://github.com/fable-compiler/Fable/commit/f282ab48203fc5544c74b809f4396bb924724a2a)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + +## 3.3.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) +* *(python)* Build typed arrays from lists and sequences (#4944) ([5af37779](https://github.com/fable-compiler/Fable/commit/5af37779ce33c1e9036df7a9548830c4f914bdd5)) +* *(rust)* Fix IEnumerable casting, interop and type testing (#4946) ([93c2df5b](https://github.com/fable-compiler/Fable/commit/93c2df5ba2b3316893dc85dc4fd746a09a07bf39)) +* *(rust)* Restore missing bindings (#4947) ([2fb46a26](https://github.com/fable-compiler/Fable/commit/2fb46a269bc4827494cab878f6e3f0e1b4052035)) +* *(ts)* Declare local function type params only reachable via constraints (#4943) ([8aa6d3b9](https://github.com/fable-compiler/Fable/commit/8aa6d3b9e9f3c1dc013d67406878ea93637ad74c)) +* *(ts/python)* Erase byref from struct copy-and-update locals (#4945) ([19a6522e](https://github.com/fable-compiler/Fable/commit/19a6522ea517db53dabd5e221206849d382e1025)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + +## 3.2.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) +* *(rust)* Observables, Events and IEnumerable (PR 6 of 7 Rust improvements) (#4789) ([20175eea](https://github.com/fable-compiler/Fable/commit/20175eea841adf14ed57c0068458fad591c8d5f4)) +* *(rust)* System.Uri (PR 7 of 7 Rust improvements) (#4790) ([8a551bc9](https://github.com/fable-compiler/Fable/commit/8a551bc9b2433035fc704dffc639e5f031ca7593)) + +### 🐞 Bug Fixes + +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) +* *(all)* Allow private inline functions to use private values (#4915) ([b0740ba9](https://github.com/fable-compiler/Fable/commit/b0740ba95859a80e7add07f21e2fc81a31c00853)) +* *(python)* Emit Python names for float32 specials (#4938) ([b4c45a93](https://github.com/fable-compiler/Fable/commit/b4c45a93385f249a8586789037b0af18005786e9)) +* *(rust)* Observables and Events cleanup (#4908) ([ca165045](https://github.com/fable-compiler/Fable/commit/ca165045703797f0558da4bc6e06cbea7af231de)) +* *(rust)* Support `string` on a tuple (#4916) ([510c8a43](https://github.com/fable-compiler/Fable/commit/510c8a43c159ffb9d7d7bdc4e2ee16b84b80b28e)) +* *(rust)* Decimal culture overloads, and mutables assigned in try/finally (#4918) ([58402471](https://github.com/fable-compiler/Fable/commit/584024712450d333193baddc239db977af05bd54)) +* *(rust)* Let a string be enumerated (#4921) ([395c152b](https://github.com/fable-compiler/Fable/commit/395c152bd4ef0c26af7a4ff48ed44c728aa73d25)) +* *(rust)* F# exceptions, non-exhaustive matches and obj type tests (#4917) ([28da1d76](https://github.com/fable-compiler/Fable/commit/28da1d764e63c39b96ca20e94d9a08786b3c9052)) +* *(rust)* Cleanup AsyncBuilder and TaskBuilder (#4936) ([3e6bc9bd](https://github.com/fable-compiler/Fable/commit/3e6bc9bd4d4337e901cc748f35df2a5933f0ba55)) +* *(rust)* Fix interface casting, move reflection into declarations (#4939) ([04299157](https://github.com/fable-compiler/Fable/commit/042991572969e1864a160b0a0145b1d59bc77236)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b517d52df145ee30d1d3bd7e20fded1a9003ac54..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 3.1.0 - 2026-08-25 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 6501ad628b..139c6911d9 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.1.0", + "version": "4.5.0", "main": "index.js", "bin": { "fable": "index.js" @@ -24,8 +24,8 @@ }, "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { - "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.1.0" + "@fable-org/fable-metadata": "^2.2.0", + "@fable-org/fable-standalone": "^4.0.0-beta.6" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-compiler-js/src/app.fs b/src/fable-compiler-js/src/app.fs index e9d64032e2..400e532611 100644 --- a/src/fable-compiler-js/src/app.fs +++ b/src/fable-compiler-js/src/app.fs @@ -179,20 +179,25 @@ type SourceWriter(sourcePath, targetPath, projDir, options: CmdLineOptions, file member _.Dispose() = () - member _.SourceMap = mapGenerator.Force().toJSON () + member _.SourceMap = mapGenerator.Force().toJSON() member _.Result = sb.ToString() let printErrors showWarnings (errors: Fable.Standalone.Error[]) = let printError (e: Fable.Standalone.Error) = let errorType = - if e.IsWarning then - "Warning" - else - "Error" + match e.Severity with + | Fable.Standalone.DiagnosticSeverity.Hidden + | Fable.Standalone.DiagnosticSeverity.Info -> "Info" + | Fable.Standalone.DiagnosticSeverity.Warning -> "Warning" + | Fable.Standalone.DiagnosticSeverity.Error -> "Error" + + printfn $"%s{e.FileName} (%d{e.StartLine},%d{e.StartColumn}): %s{errorType} %s{e.Code}: %s{e.Message}" - printfn $"%s{e.FileName} (%d{e.StartLine},%d{e.StartColumn}): %s{errorType}: %s{e.Message}" + let warnings, errors = + errors + |> Array.filter (fun e -> e.Severity <> Fable.Standalone.DiagnosticSeverity.Hidden) + |> Array.partition (fun e -> e.Severity <> Fable.Standalone.DiagnosticSeverity.Error) - let warnings, errors = errors |> Array.partition (fun e -> e.IsWarning) let hasErrors = not (Array.isEmpty errors) if showWarnings then diff --git a/src/fable-library-beam/Fable.Library.Beam.fsproj b/src/fable-library-beam/Fable.Library.Beam.fsproj index beec4f5437..b87dc1a110 100644 --- a/src/fable-library-beam/Fable.Library.Beam.fsproj +++ b/src/fable-library-beam/Fable.Library.Beam.fsproj @@ -10,7 +10,7 @@ - + diff --git a/src/fable-library-beam/Range.fs b/src/fable-library-beam/Range.fs index e97846a5af..6d729e8cf7 100644 --- a/src/fable-library-beam/Range.fs +++ b/src/fable-library-beam/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T when 'T: comparison> (step: 'T) (stop: 'T) (zero: ' let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-beam/Seq.fs b/src/fable-library-beam/Seq.fs new file mode 100644 index 0000000000..cfc77eec02 --- /dev/null +++ b/src/fable-library-beam/Seq.fs @@ -0,0 +1,1459 @@ +// Adapted from: +// https://github.com/fsprojects/FSharpx.Extras/blob/master/src/FSharpx.Extras/ComputationExpressions/Enumerator.fs +// https://github.com/dotnet/fsharp/blob/main/src/fsharp/FSharp.Core/seq.fs +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module SeqModule + +open Fable.Core + +type IEnumerator<'T> = System.Collections.Generic.IEnumerator<'T> +type IEnumerable<'T> = System.Collections.Generic.IEnumerable<'T> + +module SR = + let enumerationAlreadyFinished = "Enumeration already finished." + let enumerationNotStarted = "Enumeration has not started. Call MoveNext." + let inputMustBePositive = "The input must be positive." + let inputSequenceEmpty = "The input sequence was empty." + + let inputSequenceTooLong = "The input sequence contains more than one element." + + let keyNotFoundAlt = + "An index satisfying the predicate was not found in the collection." + + let notEnoughElements = "The input sequence has an insufficient number of elements." + + let resetNotSupported = "Reset is not supported on this enumerator." + +module Enumerator = + + let noReset () = + raise (System.NotSupportedException(SR.resetNotSupported)) + + let notStarted () = + raise (System.InvalidOperationException(SR.enumerationNotStarted)) + + let alreadyFinished () = + raise (System.InvalidOperationException(SR.enumerationAlreadyFinished)) + + [] + [] + type Enumerable<'T>(f) = + interface IEnumerable<'T> with + member x.GetEnumerator() = f () + + interface System.Collections.IEnumerable with + member x.GetEnumerator() = f () :> System.Collections.IEnumerator + + override _.ToString() = + let maxCount = 4 + let mutable i = 0 + let mutable str = "seq [" + // decision: calls the factory directly so the wrapper stays an immutable map portable across BEAM processes + use e: IEnumerator<'T> = f () + + while (i < maxCount && e.MoveNext()) do + if i > 0 then + str <- str + "; " + + str <- str + (string<'T> e.Current) + i <- i + 1 + + if i = maxCount then + str <- str + "; ..." + + str + "]" + + type FromFunctions<'T>(current, next, dispose) = + interface IEnumerator<'T> with + member _.Current = current () + + interface System.Collections.IEnumerator with + member _.Current = box (current ()) + member _.MoveNext() = next () + member _.Reset() = noReset () + + interface System.IDisposable with + member _.Dispose() = dispose () + + let inline fromFunctions current next dispose : IEnumerator<'T> = + new FromFunctions<_>(current, next, dispose) :> IEnumerator<'T> + + // // implementation for languages where arrays are not IEnumerable + // + // let empty<'T>(): IEnumerator<'T> = + // let mutable started = false + // let current() = if not started then notStarted() else alreadyFinished() + // let next() = started <- true; false + // let dispose() = () + // fromFunctions current next dispose + // + // let singleton (x: 'T): IEnumerator<'T> = + // let mutable index = -1 + // let current() = + // if index < 0 then notStarted() + // if index > 0 then alreadyFinished() + // x + // let next() = index <- index + 1; index = 0 + // let dispose() = () + // fromFunctions current next dispose + // + // let ofArray (arr: 'T[]): IEnumerator<'T> = + // let len = arr.Length + // let mutable i = -1 + // let current() = + // if i < 0 then notStarted() + // elif i >= len then alreadyFinished() + // else arr.[i] + // let next() = + // if i < len then + // i <- i + 1 + // i < len + // else false + // let dispose() = () + // fromFunctions current next dispose + + let cast (e: IEnumerator<'T>) : IEnumerator<'T> = + let current () = unbox<'T> e.Current + let next () = e.MoveNext() + let dispose () = e.Dispose() + + fromFunctions current next dispose + + let concat<'T, 'U when 'U :> seq<'T>> (sources: seq<'U>) = + let mutable outerOpt: IEnumerator<'U> option = None + let mutable innerOpt: IEnumerator<'T> option = None + let mutable started = false + let mutable finished = false + let mutable curr = None + + let current () = + if not started then + notStarted () + elif finished then + alreadyFinished () + + match curr with + | None -> alreadyFinished () + | Some x -> x + + let finish () = + finished <- true + + match innerOpt with + | None -> () + | Some inner -> + try + inner.Dispose() + finally + innerOpt <- None + + match outerOpt with + | None -> () + | Some outer -> + try + outer.Dispose() + finally + outerOpt <- None + + let loop () = + let mutable res = None + + while Option.isNone res do + match outerOpt, innerOpt with + | None, _ -> outerOpt <- Some(sources.GetEnumerator()) + | Some outer, None -> + if outer.MoveNext() then + let ie = outer.Current + innerOpt <- Some(ie.GetEnumerator()) + else + finish () + res <- Some false + | Some _, Some inner -> + if inner.MoveNext() then + curr <- Some(inner.Current) + res <- Some true + else + try + inner.Dispose() + finally + innerOpt <- None + + res.Value + + let next () = + if not started then + started <- true + + if finished then + false + else + loop () + + let dispose () = + if not finished then + finish () + + fromFunctions current next dispose + + let enumerateThenFinally f (e: IEnumerator<'T>) : IEnumerator<'T> = + let current () = e.Current + let next () = e.MoveNext() + + let dispose () = + try + e.Dispose() + finally + f () + + fromFunctions current next dispose + + let generateWhileSome (openf: unit -> 'T) (compute: 'T -> 'U option) (closef: 'T -> unit) : IEnumerator<'U> = + let mutable started = false + let mutable curr = None + let mutable state = Some(openf ()) + + let current () = + if not started then + notStarted () + + match curr with + | None -> alreadyFinished () + | Some x -> x + + let dispose () = + match state with + | None -> () + | Some x -> + try + closef x + finally + state <- None + + let finish () = + try + dispose () + finally + curr <- None + + let next () = + if not started then + started <- true + + match state with + | None -> false + | Some s -> + match + (try + compute s + with _ -> + finish () + reraise ()) + with + | None -> + finish () + false + | Some _ as x -> + curr <- x + true + + fromFunctions current next dispose + + let unfold (f: 'State -> ('T * 'State) option) (state: 'State) : IEnumerator<'T> = + let mutable curr: ('T * 'State) option = None + let mutable acc: 'State = state + + let current () = + match curr with + | None -> notStarted () + | Some(x, st) -> x + + let next () = + curr <- f acc + + match curr with + | None -> false + | Some(x, st) -> + acc <- st + true + + let dispose () = () + fromFunctions current next dispose + +// [] +// [] +// module Seq = + +let indexNotFound () = + raise (System.Collections.Generic.KeyNotFoundException(SR.keyNotFoundAlt)) + +let mkSeq (f: unit -> IEnumerator<'T>) : seq<'T> = + Enumerator.Enumerable(f) :> IEnumerable<'T> + +let ofSeq (xs: seq<'T>) : IEnumerator<'T> = + let xs = nullArgCheck "source" xs + xs.GetEnumerator() + +let delay (generator: unit -> seq<'T>) = + mkSeq (fun () -> generator().GetEnumerator()) + +let concat<'Collection, 'T when 'Collection :> seq<'T>> (sources: seq<'Collection>) : seq<'T> = + mkSeq (fun () -> Enumerator.concat sources) + +let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) = + mkSeq (fun () -> Enumerator.unfold generator state) + +let empty () = + delay (fun () -> Array.empty :> seq<'T>) + +let singleton x = + delay (fun () -> (Array.singleton x) :> seq<'T>) + +let ofArray (arr: 'T[]) = arr :> seq<'T> + +let toArray (xs: seq<'T>) : 'T[] = + match xs with + // | :? array<'T> as a -> Array.ofSeq a + | :? list<'T> as a -> Array.ofList a + | _ -> Array.ofSeq xs + +let ofList (xs: list<'T>) = (xs :> seq<'T>) + +let toList (xs: seq<'T>) : list<'T> = + match xs with + | :? array<'T> as a -> List.ofArray a + | :? list<'T> as a -> a + | _ -> List.ofSeq xs + +let generate create compute dispose = + mkSeq (fun () -> Enumerator.generateWhileSome create compute dispose) + +let generateIndexed create compute dispose = + mkSeq (fun () -> + let mutable i = -1 + + Enumerator.generateWhileSome + create + (fun x -> + i <- i + 1 + compute i x + ) + dispose + ) + +// let inline generateUsing (openf: unit -> ('U :> System.IDisposable)) compute = +// generate openf compute (fun (s: 'U) -> s.Dispose()) + +let append (xs: seq<'T>) (ys: seq<'T>) = concat [| xs; ys |] + +let cast (xs: IEnumerable<'T>) = + mkSeq (fun () -> + let xs = nullArgCheck "source" xs + xs.GetEnumerator() |> Enumerator.cast + ) + +let choose (chooser: 'T -> 'U option) (xs: seq<'T>) = + generate + (fun () -> ofSeq xs) + (fun e -> + let mutable curr = None + + while (Option.isNone curr && e.MoveNext()) do + curr <- chooser e.Current + + curr + ) + (fun e -> e.Dispose()) + +let compareWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) (ys: seq<'T>) : int = + use e1 = ofSeq xs + use e2 = ofSeq ys + let mutable c = 0 + let mutable b1 = e1.MoveNext() + let mutable b2 = e2.MoveNext() + + while c = 0 && b1 && b2 do + c <- comparer e1.Current e2.Current + + if c = 0 then + b1 <- e1.MoveNext() + b2 <- e2.MoveNext() + + if c <> 0 then + c + elif b1 then + 1 + elif b2 then + -1 + else + 0 + +let contains (value: 'T) (xs: seq<'T>) ([] comparer: System.Collections.Generic.IEqualityComparer<'T>) = + use e = ofSeq xs + let mutable found = false + + while (not found && e.MoveNext()) do + found <- comparer.Equals(value, e.Current) + + found + +let enumerateFromFunctions create moveNext current = + generate + create + (fun x -> + if moveNext x then + Some(current x) + else + None + ) + (fun x -> + match box (x) with + | :? System.IDisposable as id -> id.Dispose() + | _ -> () + ) + +let inline finallyEnumerable<'T> (compensation: unit -> unit, restf: unit -> seq<'T>) = + mkSeq (fun () -> + try + let e = restf () |> ofSeq + Enumerator.enumerateThenFinally compensation e + with _ -> + compensation () + reraise () + ) + +let enumerateThenFinally (source: seq<'T>) (compensation: unit -> unit) = + finallyEnumerable (compensation, (fun () -> source)) + +let enumerateUsing (resource: 'T :> System.IDisposable) (source: 'T -> #seq<'U>) : seq<'U> = + finallyEnumerable ( + (fun () -> + match box resource with + | null -> () + | _ -> resource.Dispose() + ), + (fun () -> source resource :> seq<_>) + ) + +let enumerateWhile (guard: unit -> bool) (xs: seq<'T>) = + concat ( + unfold + (fun i -> + if guard () then + Some(xs, i + 1) + else + None + ) + 0 + ) + +let enumerateTryWith (source: seq<'T>) (catchFilter: exn -> int) (catchHandler: exn -> seq<'T>) : seq<'T> = + mkSeq (fun () -> + let mutable e: IEnumerator<'T> option = None + let mutable caught = false // switched to the handler sequence + let mutable started = false + let mutable finished = false + let mutable curr = None + + let current () = + if not started then + Enumerator.notStarted () + elif finished then + Enumerator.alreadyFinished () + + match curr with + | None -> Enumerator.alreadyFinished () + | Some x -> x + + let rec tryNext () = + try + let en = + match e with + | Some en -> en + | None -> + let en = source.GetEnumerator() + e <- Some en + en + + if en.MoveNext() then + curr <- Some en.Current + true + else + finished <- true + false + with ex when not caught -> + if catchFilter ex <> 0 then + caught <- true + + match e with + | Some en -> + (try + en.Dispose() + with _ -> + ()) + + e <- None + | None -> () + + e <- Some((catchHandler ex).GetEnumerator()) + tryNext () + else + reraise () + + let next () = + if not started then + started <- true + + if finished then + false + else + tryNext () + + let dispose () = + match e with + | Some en -> en.Dispose() + | None -> () + + Enumerator.fromFunctions current next dispose + ) + +let filter f (xs: seq<'T>) = + xs + |> choose (fun x -> + if f x then + Some x + else + None + ) + +let exists predicate (xs: seq<'T>) = + use e = ofSeq xs + let mutable found = false + + while (not found && e.MoveNext()) do + found <- predicate e.Current + + found + +let exists2 (predicate: 'T1 -> 'T2 -> bool) (xs: seq<'T1>) (ys: seq<'T2>) = + use e1 = ofSeq xs + use e2 = ofSeq ys + let mutable found = false + + while (not found && e1.MoveNext() && e2.MoveNext()) do + found <- predicate e1.Current e2.Current + + found + +let exactlyOne (xs: seq<'T>) = + use e = ofSeq xs + + if e.MoveNext() then + let v = e.Current + + if e.MoveNext() then + invalidArg "source" SR.inputSequenceTooLong + else + v + else + invalidArg "source" SR.inputSequenceEmpty + +let tryExactlyOne (xs: seq<'T>) = + use e = ofSeq xs + + if e.MoveNext() then + let v = e.Current + + if e.MoveNext() then + None + else + Some v + else + None + +let tryFind predicate (xs: seq<'T>) = + use e = ofSeq xs + let mutable res = None + + while (Option.isNone res && e.MoveNext()) do + let c = e.Current + + if predicate c then + res <- Some c + + res + +let find predicate (xs: seq<'T>) = + match tryFind predicate xs with + | Some x -> x + | None -> indexNotFound () + +let tryFindBack predicate (xs: seq<'T>) = + xs |> toArray |> Array.tryFindBack predicate + +let findBack predicate (xs: seq<'T>) = + match tryFindBack predicate xs with + | Some x -> x + | None -> indexNotFound () + +let tryFindIndex predicate (xs: seq<'T>) = + use e = ofSeq xs + + let rec loop i = + if e.MoveNext() then + if predicate e.Current then + Some i + else + loop (i + 1) + else + None + + loop 0 + +let findIndex predicate (xs: seq<'T>) = + match tryFindIndex predicate xs with + | Some x -> x + | None -> + indexNotFound () + -1 + +let tryFindIndexBack predicate (xs: seq<'T>) = + xs |> toArray |> Array.tryFindIndexBack predicate + +let findIndexBack predicate (xs: seq<'T>) = + match tryFindIndexBack predicate xs with + | Some x -> x + | None -> + indexNotFound () + -1 + +let fold<'T, 'State> (folder: 'State -> 'T -> 'State) (state: 'State) (xs: seq<'T>) = + use e = ofSeq xs + let mutable acc = state + + while e.MoveNext() do + acc <- folder acc e.Current + + acc + +let foldBack<'T, 'State> folder (xs: seq<'T>) state = + Array.foldBack folder (toArray xs) state + +let fold2<'T1, 'T2, 'State> (folder: 'State -> 'T1 -> 'T2 -> 'State) (state: 'State) (xs: seq<'T1>) (ys: seq<'T2>) = + use e1 = ofSeq xs + use e2 = ofSeq ys + let mutable acc = state + + while e1.MoveNext() && e2.MoveNext() do + acc <- folder acc e1.Current e2.Current + + acc + +let foldBack2 (folder: 'T1 -> 'T2 -> 'State -> 'State) (xs: seq<'T1>) (ys: seq<'T2>) (state: 'State) = + let xs = toArray xs + let ys = toArray ys + let len = System.Math.Min(xs.Length, ys.Length) + let mutable acc = state + + for i = len - 1 downto 0 do + acc <- folder xs[i] ys[i] acc + + acc + +let forAll predicate xs = + not (exists (fun x -> not (predicate x)) xs) + +let forAll2 predicate xs ys = + not (exists2 (fun x y -> not (predicate x y)) xs ys) + +let tryHead (xs: seq<'T>) = + match xs with + | :? array<'T> as a -> Array.tryHead a + | :? list<'T> as a -> List.tryHead a + | _ -> + use e = ofSeq xs + + if e.MoveNext() then + Some(e.Current) + else + None + +let head (xs: seq<'T>) = + match tryHead xs with + | Some x -> x + | None -> invalidArg "source" SR.inputSequenceEmpty + +let initialize count f = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + unfold + (fun i -> + if (i < count) then + Some(f i, i + 1) + else + None + ) + 0 + +let initializeInfinite f = initialize (System.Int32.MaxValue) f + +let isEmpty (xs: seq<'T>) = + match xs with + | :? array<'T> as a -> Array.isEmpty a + | :? list<'T> as a -> List.isEmpty a + | _ -> + use e = ofSeq xs + not (e.MoveNext()) + +let tryItem index (xs: seq<'T>) = + match xs with + | :? array<'T> as a -> Array.tryItem index a + | :? list<'T> as a -> List.tryItem index a + | _ -> + use e = ofSeq xs + + let rec loop index = + if not (e.MoveNext()) then + None + elif index = 0 then + Some e.Current + else + loop (index - 1) + + loop index + +let item index (xs: seq<'T>) = + match tryItem index xs with + | Some x -> x + | None -> invalidArg "index" SR.notEnoughElements + +let iterate action xs = fold (fun () x -> action x) () xs + +let iterate2 action xs ys = + fold2 (fun () x y -> action x y) () xs ys + +let iterateIndexed action xs = + fold + (fun i x -> + action i x + i + 1 + ) + 0 + xs + |> ignore + +let iterateIndexed2 action xs ys = + fold2 + (fun i x y -> + action i x y + i + 1 + ) + 0 + xs + ys + |> ignore + +let tryLast (xs: seq<'T>) = + // if isEmpty xs then None + // else Some (reduce (fun _ x -> x) xs) + use e = ofSeq xs + + let rec loop acc = + if not (e.MoveNext()) then + acc + else + loop e.Current + + if e.MoveNext() then + Some(loop e.Current) + else + None + +let last (xs: seq<'T>) = + match tryLast xs with + | Some x -> x + | None -> invalidArg "source" SR.notEnoughElements + +let length (xs: seq<'T>) = + match xs with + | :? array<'T> as a -> Array.length a + | :? list<'T> as a -> List.length a + | _ -> + use e = ofSeq xs + let mutable count = 0 + + while e.MoveNext() do + count <- count + 1 + + count + +let map (mapping: 'T -> 'U) (xs: seq<'T>) = + generate + (fun () -> ofSeq xs) + (fun e -> + if e.MoveNext() then + Some(mapping e.Current) + else + None + ) + (fun e -> e.Dispose()) + +let mapIndexed (mapping: int -> 'T -> 'U) (xs: seq<'T>) = + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if e.MoveNext() then + Some(mapping i e.Current) + else + None + ) + (fun e -> e.Dispose()) + +let indexed (xs: seq<'T>) = xs |> mapIndexed (fun i x -> (i, x)) + +let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) = + generate + (fun () -> (ofSeq xs, ofSeq ys)) + (fun (e1, e2) -> + if e1.MoveNext() && e2.MoveNext() then + Some(mapping e1.Current e2.Current) + else + None + ) + (fun (e1, e2) -> + try + e1.Dispose() + finally + e2.Dispose() + ) + +let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) = + generateIndexed + (fun () -> (ofSeq xs, ofSeq ys)) + (fun i (e1, e2) -> + if e1.MoveNext() && e2.MoveNext() then + Some(mapping i e1.Current e2.Current) + else + None + ) + (fun (e1, e2) -> + try + e1.Dispose() + finally + e2.Dispose() + ) + +let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) (zs: seq<'T3>) = + generate + (fun () -> (ofSeq xs, ofSeq ys, ofSeq zs)) + (fun (e1, e2, e3) -> + if e1.MoveNext() && e2.MoveNext() && e3.MoveNext() then + Some(mapping e1.Current e2.Current e3.Current) + else + None + ) + (fun (e1, e2, e3) -> + try + e1.Dispose() + finally + try + e2.Dispose() + finally + e3.Dispose() + ) + +let readOnly (xs: seq<'T>) = + let xs = nullArgCheck "source" xs + map id xs + +type CachedSeq<'T>(cleanup, res: seq<'T>) = + interface System.IDisposable with + member _.Dispose() = cleanup () + + interface System.Collections.Generic.IEnumerable<'T> with + member _.GetEnumerator() = res.GetEnumerator() + + interface System.Collections.IEnumerable with + member _.GetEnumerator() = + (res :> System.Collections.IEnumerable).GetEnumerator() + + member _.Clear() = cleanup () + +// Adapted from https://github.com/dotnet/fsharp/blob/eb1337f218275da5294b5fbab2cf77f35ca5f717/src/fsharp/FSharp.Core/seq.fs#L971 +let cache (source: seq<'T>) = + let source = nullArgCheck "source" source + // Wrap a seq to ensure that it is enumerated just once and only as far as is necessary. + // + // This code is required to be thread safe. + // The necessary calls should be called at most once (include .MoveNext() = false). + // The enumerator should be disposed (and dropped) when no longer required. + //------ + // The state is (prefix,enumerator) with invariants: + // * the prefix followed by elts from the enumerator are the initial sequence. + // * the prefix contains only as many elements as the longest enumeration so far. + let prefix = ResizeArray<_>() + + // None = Unstarted. + // Some(Some e) = Started. + // Some None = Finished. + let mutable enumeratorR = None + + let oneStepTo i = + // If possible, step the enumeration to prefix length i (at most one step). + // Be speculative, since this could have already happened via another thread. + if i >= prefix.Count then // is a step still required? + // If not yet started, start it (create enumerator). + let optEnumerator = + match enumeratorR with + | None -> + let optEnumerator = Some(source.GetEnumerator()) + enumeratorR <- Some optEnumerator + optEnumerator + | Some optEnumerator -> optEnumerator + + match optEnumerator with + | Some enumerator -> + if enumerator.MoveNext() then + prefix.Add(enumerator.Current) + else + enumerator.Dispose() // Move failed, dispose enumerator, + enumeratorR <- Some None // drop it and record finished. + | None -> () + + let result = + unfold + (fun i -> + // i being the next position to be returned + // A lock is needed over the reads to prefix.Count since the list may be being resized + // NOTE: we could change to a reader/writer lock here + lock prefix + <| fun () -> + if i < prefix.Count then + Some(prefix.[i], i + 1) + else + oneStepTo i + + if i < prefix.Count then + Some(prefix.[i], i + 1) + else + None + ) + 0 + + let cleanup () = + lock prefix + <| fun () -> + prefix.Clear() + + match enumeratorR with + | Some(Some e) -> e.Dispose() + | _ -> () + + enumeratorR <- None + + (new CachedSeq<_>(cleanup, result) :> seq<_>) + +let allPairs (xs: seq<'T1>) (ys: seq<'T2>) : seq<'T1 * 'T2> = + let ysCache = cache ys + + delay (fun () -> + let mapping (x: 'T1) = ysCache |> map (fun y -> (x, y)) + concat (map mapping xs) + ) + +let mapFold (mapping: 'State -> 'T -> 'Result * 'State) state (xs: seq<'T>) = + let arr, state = Array.mapFold mapping state (toArray xs) + readOnly arr, state + +let mapFoldBack (mapping: 'T -> 'State -> 'Result * 'State) (xs: seq<'T>) state = + let arr, state = Array.mapFoldBack mapping (toArray xs) state + readOnly arr, state + +let tryPick chooser (xs: seq<'T>) = + use e = ofSeq xs + let mutable res = None + + while (Option.isNone res && e.MoveNext()) do + res <- chooser e.Current + + res + +let pick chooser (xs: seq<'T>) = + match tryPick chooser xs with + | Some x -> x + | None -> indexNotFound () + +let reduce folder (xs: seq<'T>) = + use e = ofSeq xs + + let rec loop acc = + if e.MoveNext() then + loop (folder acc e.Current) + else + acc + + if e.MoveNext() then + loop e.Current + else + invalidArg "source" SR.inputSequenceEmpty + +let reduceBack folder (xs: seq<'T>) = + let arr = toArray xs + + if arr.Length > 0 then + Array.reduceBack folder arr + else + invalidArg "source" SR.inputSequenceEmpty + +let replicate n x = + // FSharp.Core uses Enumerable.Repeat, which raises ArgumentOutOfRangeException + if n < 0 then + raise (System.ArgumentOutOfRangeException("count")) + + initialize n (fun _ -> x) + +let reverse (xs: seq<'T>) = + delay (fun () -> xs |> toArray |> Array.rev |> ofArray) + +let scan folder (state: 'State) (xs: seq<'T>) = + delay (fun () -> + let first = singleton state + let mutable acc = state + + let rest = + xs + |> map (fun x -> + acc <- folder acc x + acc + ) + + [| first; rest |] |> concat + ) + +let scanBack folder (xs: seq<'T>) (state: 'State) = + delay (fun () -> + let arr = toArray xs + Array.scanBack folder arr state |> ofArray + ) + +let skip count (source: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq source + + try + for _ = 1 to count do + if not (e.MoveNext()) then + invalidOp SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) + +let skipWhile predicate (xs: seq<'T>) = + delay (fun () -> + let mutable skipped = true + + xs + |> filter (fun x -> + if skipped then + skipped <- predicate x + + not skipped + ) + ) + +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException +let tail (xs: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) + +let take count (xs: seq<'T>) = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if i < count then + if e.MoveNext() then + Some(e.Current) + else + invalidOp SR.notEnoughElements + else + None + ) + (fun e -> e.Dispose()) + +let takeWhile predicate (xs: seq<'T>) = + generate + (fun () -> ofSeq xs) + (fun e -> + if e.MoveNext() && predicate e.Current then + Some(e.Current) + else + None + ) + (fun e -> e.Dispose()) + +let truncate count (xs: seq<'T>) = + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if i < count && e.MoveNext() then + Some(e.Current) + else + None + ) + (fun e -> e.Dispose()) + +let zip (xs: seq<'T1>) (ys: seq<'T2>) = map2 (fun x y -> (x, y)) xs ys + +let zip3 (xs: seq<'T1>) (ys: seq<'T2>) (zs: seq<'T3>) = map3 (fun x y z -> (x, y, z)) xs ys zs + +let collect<'T, 'Collection, 'U when 'Collection :> 'U seq> (mapping: 'T -> 'Collection) (xs: seq<'T>) : seq<'U> = + delay (fun () -> xs |> map mapping |> concat) + +let where predicate (xs: seq<'T>) = filter predicate xs + +let pairwise (xs: seq<'T>) = + delay (fun () -> xs |> toArray |> Array.pairwise |> ofArray) + +let splitInto (chunks: int) (xs: seq<'T>) : 'T[] seq = + if chunks < 1 then + invalidArg "count" SR.inputMustBePositive + + delay (fun () -> xs |> toArray |> Array.splitInto chunks |> ofArray) + +let windowed (windowSize: int) (xs: seq<'T>) : 'T[] seq = + if windowSize < 1 then + invalidArg "windowSize" SR.inputMustBePositive + + delay (fun () -> xs |> toArray |> Array.windowed windowSize |> ofArray) + +let transpose (xss: seq<#seq<'T>>) : seq> = + delay (fun () -> + let rec columns (rows: 'T list list) = + match rows |> List.filter (List.isEmpty >> not) with + | [] -> [] + | rows -> ofList (List.map List.head rows) :: columns (List.map List.tail rows) + + xss |> toList |> List.map toList |> columns |> ofList + ) + +let sortWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) = + delay (fun () -> + let arr = toArray xs + Array.sortInPlaceWith comparer arr // Note: In JS this sort is stable + arr |> ofArray + ) + +let sort (xs: seq<'T>) ([] comparer: System.Collections.Generic.IComparer<'T>) = + sortWith (fun (x: 'T) (y: 'T) -> comparer.Compare(x, y)) xs + +let sortBy (projection: 'T -> 'U) (xs: seq<'T>) ([] comparer: System.Collections.Generic.IComparer<'U>) = + sortWith (fun x y -> comparer.Compare(projection x, projection y)) xs + +let sortDescending (xs: seq<'T>) ([] comparer: System.Collections.Generic.IComparer<'T>) = + sortWith (fun (x: 'T) (y: 'T) -> comparer.Compare(x, y) * -1) xs + +let sortByDescending + (projection: 'T -> 'U) + (xs: seq<'T>) + ([] comparer: System.Collections.Generic.IComparer<'U>) + = + sortWith (fun x y -> comparer.Compare(projection x, projection y) * -1) xs + +let sum (xs: seq<'T>) ([] adder: IGenericAdder<'T>) : 'T = + fold (fun acc x -> adder.Add(acc, x)) (adder.GetZero()) xs + +let sumBy (f: 'T -> 'U) (xs: seq<'T>) ([] adder: IGenericAdder<'U>) : 'U = + fold (fun acc x -> adder.Add(acc, f x)) (adder.GetZero()) xs + +let maxBy (projection: 'T -> 'U) xs ([] comparer: System.Collections.Generic.IComparer<'U>) : 'T = + reduce + (fun x y -> + if comparer.Compare(projection y, projection x) > 0 then + y + else + x + ) + xs + +let max xs ([] comparer: System.Collections.Generic.IComparer<'T>) : 'T = + reduce + (fun x y -> + if comparer.Compare(y, x) > 0 then + y + else + x + ) + xs + +let minBy (projection: 'T -> 'U) xs ([] comparer: System.Collections.Generic.IComparer<'U>) : 'T = + reduce + (fun x y -> + if comparer.Compare(projection y, projection x) > 0 then + x + else + y + ) + xs + +let min (xs: seq<'T>) ([] comparer: System.Collections.Generic.IComparer<'T>) : 'T = + reduce + (fun x y -> + if comparer.Compare(y, x) > 0 then + x + else + y + ) + xs + +let average (xs: seq<'T>) ([] averager: IGenericAverager<'T>) : 'T = + let mutable count = 0 + + let folder acc x = + count <- count + 1 + averager.Add(acc, x) + + let total = fold folder (averager.GetZero()) xs + + if count = 0 then + invalidArg "source" SR.inputSequenceEmpty + else + averager.DivideByInt(total, count) + +let averageBy (f: 'T -> 'U) (xs: seq<'T>) ([] averager: IGenericAverager<'U>) : 'U = + let mutable count = 0 + + let inline folder acc x = + count <- count + 1 + averager.Add(acc, f x) + + let total = fold folder (averager.GetZero()) xs + + if count = 0 then + invalidArg "source" SR.inputSequenceEmpty + else + averager.DivideByInt(total, count) + +let permute f (xs: seq<'T>) = + delay (fun () -> xs |> toArray |> Array.permute f |> ofArray) + +let chunkBySize (chunkSize: int) (xs: seq<'T>) : seq<'T[]> = + if chunkSize < 1 then + invalidArg "chunkSize" SR.inputMustBePositive + + delay (fun () -> xs |> toArray |> Array.chunkBySize chunkSize |> ofArray) + +// let init = initialize +// let initInfinite = initializeInfinite +// let iter = iterate +// let iter2 = iterate2 +// let iteri = iterateIndexed +// let iteri2 = iterateIndexed2 +// let forall = forAll +// let forall2 = forAll2 +// let mapi = mapIndexed +// let mapi2 = mapIndexed2 +// let readonly = readOnly +// let rev = reverse + +let insertAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = + let mutable isDone = false + + if index < 0 then + invalidArg "index" SR.indexOutOfBounds + + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if (isDone || i < index) && e.MoveNext() then + Some e.Current + elif i = index then + isDone <- true + Some y + else + if not isDone then + invalidArg "index" SR.indexOutOfBounds + + None + ) + (fun e -> e.Dispose()) + +let insertManyAt (index: int) (ys: seq<'T>) (xs: seq<'T>) : seq<'T> = + // incomplete -1, in-progress 0, complete 1 + let mutable status = -1 + + if index < 0 then + invalidArg "index" SR.indexOutOfBounds + + generateIndexed + (fun () -> ofSeq xs, ofSeq ys) + (fun i (e1, e2) -> + if i = index then + status <- 0 + + let inserted = + if status = 0 then + if e2.MoveNext() then + Some e2.Current + else + status <- 1 + None + else + None + + match inserted with + | Some inserted -> Some inserted + | None -> + if e1.MoveNext() then + Some e1.Current + else + if status < 1 then + invalidArg "index" SR.indexOutOfBounds + + None + ) + (fun (e1, e2) -> + e1.Dispose() + e2.Dispose() + ) + +let removeAt (index: int) (xs: seq<'T>) : seq<'T> = + let mutable isDone = false + + if index < 0 then + invalidArg "index" SR.indexOutOfBounds + + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if (isDone || i < index) && e.MoveNext() then + Some e.Current + elif i = index && e.MoveNext() then + isDone <- true + + if e.MoveNext() then + Some e.Current + else + None + else + if not isDone then + invalidArg "index" SR.indexOutOfBounds + + None + ) + (fun e -> e.Dispose()) + +let removeManyAt (index: int) (count: int) (xs: seq<'T>) : seq<'T> = + if index < 0 then + invalidArg "index" "index must be within bounds of the array" + + let endIndex = index + count + + delay (fun () -> + let mutable i = -1 + + let rest () = + if i < index then + invalidArg "index" "index must be within bounds of the array" + + empty () + + let ys = + xs + |> filter (fun _ -> + i <- i + 1 + i < index || i >= endIndex + ) + + append ys (delay rest) + ) + +let updateAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = + let mutable isDone = false + + if index < 0 then + invalidArg "index" SR.indexOutOfBounds + + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if (isDone || i < index) && e.MoveNext() then + Some e.Current + elif i = index && e.MoveNext() then + isDone <- true + Some y + else + if not isDone then + invalidArg "index" SR.indexOutOfBounds + + None + ) + (fun e -> e.Dispose()) + +let randomShuffleBy (randomizer: unit -> float) (xs: 'T seq) : 'T seq = + let arr = toArray xs + Array.randomShuffleInPlaceBy randomizer arr + ofArray arr + +let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = + randomShuffleBy (fun () -> random.NextDouble()) xs + +let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs + +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + +let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer + +let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoiceWith random + +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice + +// Draws on enumeration, like FSharp.Core +let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) + +let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = + randomChoicesBy (fun () -> random.NextDouble()) count xs + +let randomChoices (count: int) (xs: 'T seq) : 'T seq = + randomChoicesWith (System.Random()) count xs + +let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = + xs |> toArray |> Array.randomSampleBy randomizer count |> ofArray + +let randomSampleWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = + randomSampleBy (fun () -> random.NextDouble()) count xs + +let randomSample (count: int) (xs: 'T seq) : 'T seq = + randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-beam/System.Text.fs b/src/fable-library-beam/System.Text.fs index ad653f440a..fe5b5d52b3 100644 --- a/src/fable-library-beam/System.Text.fs +++ b/src/fable-library-beam/System.Text.fs @@ -71,6 +71,10 @@ type StringBuilder(value: string, capacity: int) = buf.Clear() x + member x.get_Chars(index: int) : char = + let str = x.ToString() + str.[index] + member x.Length = let mutable len = 0 @@ -89,12 +93,8 @@ type StringBuilder(value: string, capacity: int) = let str = x.ToString().Replace(oldValue, newValue) x.Clear().Append(str) - member x.get_Chars(index: int) : char = - let str = x.ToString() - str.[index] - override _.ToString() = System.String.Concat(buf) - member x.ToString(firstIndex: int, length: int) = + member x.ToString(startIndex: int, length: int) = let str = x.ToString() - str.Substring(firstIndex, length) + str.Substring(startIndex, length) diff --git a/src/fable-library-beam/fable_async.erl b/src/fable-library-beam/fable_async.erl index 85b1e466b7..848b9f6594 100644 --- a/src/fable-library-beam/fable_async.erl +++ b/src/fable-library-beam/fable_async.erl @@ -2,13 +2,14 @@ -export([ start_immediate/1, start_immediate/2, start_child/1, start_child/2, - run_synchronously/1, run_synchronously/2, + run_synchronously/1, run_synchronously/2, run_synchronously/3, start_with_continuations/4, start_with_continuations/5, sleep/1, await_event/1, await_event/2, parallel/1, sequential/1, catch_async/1, + try_cancelled/2, ignore/1, from_continuations/1, start_as_task/1, @@ -24,7 +25,8 @@ on_success := fun(), on_error := fun(), on_cancel := fun(), - cancel_token := reference() | undefined + cancel_token := reference() | undefined, + wait_for_async := boolean() }. -type async(T) :: fun((async_ctx()) -> T). @@ -33,7 +35,8 @@ -spec start_child(async(term())) -> async(async(term())). -spec start_child(async(term()), non_neg_integer() | undefined) -> async(async(term())). -spec run_synchronously(async(term())) -> term(). --spec run_synchronously(async(term()), reference() | undefined) -> term(). +-spec run_synchronously(async(term()), integer() | reference() | undefined) -> term(). +-spec run_synchronously(async(term()), integer() | undefined, reference() | undefined) -> term(). -spec start_with_continuations(async(term()), fun(), fun(), fun()) -> term(). -spec start_with_continuations(async(term()), fun(), fun(), fun(), reference() | undefined) -> term(). @@ -41,6 +44,7 @@ -spec parallel(list() | reference()) -> async(reference()). -spec sequential(list()) -> async(list()). -spec catch_async(async(term())) -> async(term()). +-spec try_cancelled(async(term()), fun((term()) -> term())) -> async(term()). -spec ignore(async(term())) -> async(ok). -spec from_continuations(fun()) -> async(term()). -spec start_as_task(async(term())) -> term(). @@ -97,7 +101,8 @@ default_ctx(CancelToken) -> on_success => fun(_) -> ok end, on_error => fun(E) -> erlang:error(E) end, on_cancel => fun(_) -> ok end, - cancel_token => CancelToken + cancel_token => CancelToken, + wait_for_async => false }. %% StartImmediate: run with default context (fire-and-forget) @@ -120,10 +125,9 @@ start_immediate(Computation, CancelToken) -> %% it is killed (e.g. on timeout) or the VM stops, so a child whose result is %% never awaited leaves an idle process behind. %% -%% Cancellation tokens are intentionally not propagated to the child: tokens are -%% held in the process dictionary (see fable_cancellation), so they are only -%% meaningful within a single process and cannot be shared with the child's -%% separate process. This matches parallel/1. +%% StartChild does not currently propagate its parent context's token. Tokens are +%% process-portable, but changing combinator propagation is a separate semantic +%% change from making an explicitly supplied or captured token safe to share. start_child(Computation) -> start_child(Computation, undefined). start_child(Computation, Timeout) -> fun(Ctx) -> @@ -171,9 +175,53 @@ child_result_holder(ResultRef, Result) -> child_result_holder(ResultRef, Result) end. -%% RunSynchronously: run CPS chain in current process, capture result -run_synchronously(Computation) -> run_synchronously(Computation, undefined). +%% RunSynchronously: unlimited waits run inline; finite deadlines use a worker. +run_synchronously(Computation) -> run_synchronously(Computation, undefined, undefined). +%% decision: retain native /2 token callers while compiled F# uses the unambiguous /3 ABI. +run_synchronously(Computation, Timeout) when is_integer(Timeout) -> + run_synchronously(Computation, Timeout, undefined); run_synchronously(Computation, CancelToken) -> + run_synchronously(Computation, undefined, CancelToken). + +run_synchronously(Computation, Timeout, CancelToken) when Timeout =:= undefined; Timeout =:= -1 -> + run_inline(Computation, CancelToken); +run_synchronously(Computation, Timeout, CancelToken) when is_integer(Timeout), Timeout >= 0 -> + run_with_timeout(Computation, Timeout, CancelToken); +run_synchronously(_Computation, _Timeout, _CancelToken) -> + erlang:error(#{exn_type => argument_exception, message => <<"The timeout must be non-negative or -1.">>}). + +%% decision: finite timeouts use a monitored worker so blocking BEAM operations cannot block the deadline. +%% invariant: timeout requests cooperative cancellation rather than killing work and skipping finalizers. +%% tradeoff: timed work runs in another process and cannot share process-dictionary-backed mutable values. +run_with_timeout(Computation, Timeout, CancelToken) -> + Token = fable_cancellation:create(), + Registration = fable_cancellation:register(CancelToken, fun(_) -> fable_cancellation:cancel(Token) end), + {Pid, Monitor} = spawn_monitor(fun() -> + Result = try run_inline(Computation, CancelToken, Token) of + Value -> {ok, Value} + catch Class:Reason:Stacktrace -> {error, Class, Reason, Stacktrace} + end, + exit({fable_async_result, Result}) + end), + try + receive + {'DOWN', Monitor, process, Pid, {fable_async_result, {ok, Value}}} -> Value; + {'DOWN', Monitor, process, Pid, {fable_async_result, {error, Class, Reason, Stacktrace}}} -> + erlang:raise(Class, Reason, Stacktrace); + {'DOWN', Monitor, process, Pid, Reason} -> erlang:error(Reason) + after Timeout -> + fable_cancellation:cancel(Token), + erlang:error(#{exn_type => timeout_exception, message => <<"The operation has timed out.">>}) + end + after + erlang:demonitor(Monitor, [flush]), + fable_utils:safe_dispose(Registration) + end. + +run_inline(Computation, CancelToken) -> + run_inline(Computation, CancelToken, CancelToken). + +run_inline(Computation, UserToken, CancelToken) -> %% Use a unique ref as key to store result in process dict Ref = make_ref(), %% Take over stacktrace parking for the duration of this run, handing it back afterwards so a @@ -184,8 +232,14 @@ run_synchronously(Computation, CancelToken) -> Ctx = #{ on_success => fun(V) -> put(Ref, {ok, V}) end, on_error => fun(E) -> put(Ref, {error, E}) end, - on_cancel => fun(_) -> put(Ref, {cancelled}) end, - cancel_token => CancelToken + on_cancel => fun(E) -> put(Ref, {cancelled, E}) end, + cancel_token => CancelToken, + %% invariant: Async.CancellationToken returns the supplied token even when a timeout + %% needs a linked token internally to cancel waits without cancelling the user's source. + user_cancel_token => UserToken, + %% invariant: runtime-owned callbacks are handled by the process that created their + %% process-dictionary-backed continuations. + wait_for_async => true }, try Computation(Ctx) @@ -204,7 +258,14 @@ run_synchronously(Computation, CancelToken) -> case Result of {ok, Value} -> Value; {error, Error} -> reraise(Ref, Parked, Error); - {cancelled} -> erlang:error(operation_cancelled); + {cancelled, Error} -> + %% invariant: RunSynchronously rethrows the cancellation exception supplied by the + %% continuation; legacy native callbacks that pass unit still receive a typed exception. + Exception = wrap_error(Error), + case fable_utils:is_exception_type(Exception, operation_canceled_exception) of + true -> erlang:error(Exception); + false -> erlang:error(fable_cancellation:exception()) + end; undefined -> erlang:error(async_no_result) end. @@ -216,7 +277,8 @@ start_with_continuations(Comp, OnSuccess, OnError, OnCancel, Token) -> on_success => OnSuccess, on_error => OnError, on_cancel => OnCancel, - cancel_token => Token + cancel_token => Token, + wait_for_async => false }, try Comp(Ctx) @@ -238,12 +300,12 @@ sleep(Milliseconds) -> %% Check if already cancelled before sleeping case fable_cancellation:is_cancellation_requested(Token) of true -> - (maps:get(on_cancel, Ctx))(ok); + (maps:get(on_cancel, Ctx))(fable_cancellation:exception()); false -> %% Set up a timer to send us a wake-up message Self = self(), TimerRef = make_ref(), - timer:apply_after(Milliseconds, erlang, send, [Self, {sleep_done, TimerRef}]), + SleepTimer = erlang:send_after(Milliseconds, Self, {sleep_done, TimerRef}), %% Register a cancellation listener to wake us up early RegId = fable_cancellation:register(Token, fun(_) -> Self ! {sleep_cancelled, TimerRef} @@ -253,17 +315,10 @@ sleep(Milliseconds) -> {sleep_done, TimerRef} -> (maps:get(on_success, Ctx))(ok); {sleep_cancelled, TimerRef} -> - (maps:get(on_cancel, Ctx))(ok); - {cancel_token, Token} -> - %% Timer-based cancel via cancel_after - fable_cancellation:cancel(Token), - (maps:get(on_cancel, Ctx))(ok) + (maps:get(on_cancel, Ctx))(fable_cancellation:exception()) end, - %% Clean up registration if we have one - case RegId of - undefined -> ok; - _ -> ok - end + erlang:cancel_timer(SleepTimer), + fable_utils:safe_dispose(RegId) end end end. @@ -275,28 +330,92 @@ await_event(Event, CancelAction) -> OnSuccess = maps:get(on_success, Ctx), OnCancel = maps:get(on_cancel, Ctx), Token = maps:get(cancel_token, Ctx), + Owner = self(), + AwaitRef = make_ref(), + RegistrationRef = make_ref(), + put(AwaitRef, pending), + put(RegistrationRef, undefined), Handler = fun F(_Sender, Value) -> - (maps:get(remove_handler, Event))(F), - OnSuccess(Value) + case self() =:= Owner of + true -> + finish_await_event( + {success, Value}, Event, F, Token, CancelAction, + OnSuccess, OnCancel, AwaitRef, RegistrationRef + ); + false -> + Owner ! {fable_async_await_event, AwaitRef, {success, Value}} + end end, - case Token of - undefined -> ok; + %% Subscribe before registering cancellation so an already-cancelled token can remove the + %% handler synchronously instead of leaving a handler that can never complete successfully. + (maps:get(add_handler, Event))(Handler), + Registration = case Token of + undefined -> undefined; _ -> fable_cancellation:register(Token, fun(_) -> - (maps:get(remove_handler, Event))(Handler), - case CancelAction of - undefined -> ok; - _ -> CancelAction(ok) - end, - OnCancel(ok) + case self() =:= Owner of + true -> + finish_await_event( + cancelled, Event, Handler, Token, CancelAction, + OnSuccess, OnCancel, AwaitRef, RegistrationRef + ); + false -> + Owner ! {fable_async_await_event, AwaitRef, cancelled} + end end) end, + case get(AwaitRef) of + pending -> put(RegistrationRef, Registration); + _ -> fable_utils:safe_dispose(Registration) + end, + + case maps:get(wait_for_async, Ctx, false) andalso get(AwaitRef) =:= pending of + true -> + receive + {fable_async_await_event, AwaitRef, Result} -> + finish_await_event( + Result, Event, Handler, Token, CancelAction, + OnSuccess, OnCancel, AwaitRef, RegistrationRef + ) + end; + false -> ok + end + end. - (maps:get(add_handler, Event))(Handler) +finish_await_event( + Result, Event, Handler, Token, CancelAction, + OnSuccess, OnCancel, AwaitRef, RegistrationRef +) -> + case get(AwaitRef) of + pending -> + put(AwaitRef, completed), + (maps:get(remove_handler, Event))(Handler), + fable_utils:safe_dispose(get(RegistrationRef)), + erase(AwaitRef), + erase(RegistrationRef), + case Result of + {success, Value} -> + %% Cancellation that has already won the broker race must also win an event + %% delivery racing with its owner-process message. + case fable_cancellation:is_cancellation_requested(Token) of + true -> invoke_await_event_cancel(CancelAction, OnCancel); + false -> OnSuccess(Value) + end; + cancelled -> + invoke_await_event_cancel(CancelAction, OnCancel) + end; + _ -> ok end. +invoke_await_event_cancel(CancelAction, OnCancel) -> + case CancelAction of + undefined -> ok; + _ -> CancelAction(ok) + end, + OnCancel(fable_cancellation:exception()). + %% Parallel: spawn one process per computation, collect results in order parallel(Computations) when is_reference(Computations) -> parallel(get(Computations)); @@ -349,7 +468,7 @@ sequential(Computations) -> %% Uses atom tags matching Beam union representation: choice1_of2 = Choice1Of2, choice2_of2 = Choice2Of2 catch_async(Computation) -> fun(Ctx) -> - Ctx2 = #{ + Ctx2 = Ctx#{ on_success => fun(V) -> (maps:get(on_success, Ctx))({choice1_of2, V}) end, on_error => fun(E) -> (maps:get(on_success, Ctx))({choice2_of2, wrap_error(E)}) end, on_cancel => maps:get(on_cancel, Ctx), @@ -362,6 +481,21 @@ catch_async(Computation) -> end end. +%% TryCancelled: observe cancellation without changing its terminal outcome. +try_cancelled(Computation, Compensation) -> + fun(Ctx) -> + OnCancel = maps:get(on_cancel, Ctx), + Ctx2 = Ctx#{ + on_cancel => fun(Error) -> + %% decision: preserve the original cancellation even when compensation throws, matching .NET. + try Compensation(Error) catch _:_ -> ok end, + OnCancel(Error) + end + }, + %% invariant: install the handler without adding a cancellation checkpoint, including for nested handlers. + Computation(Ctx2) + end. + %% Ignore: discard result ignore(Computation) -> fable_async_builder:bind(Computation, fun(_) -> fable_async_builder:return(ok) end). @@ -381,10 +515,10 @@ start_as_task(Computation) -> %% convert — the task *is* the async computation. await_task(Task) -> Task. -%% CancellationToken: returns async that extracts cancel_token from context +%% CancellationToken: expose the supplied token independently of timeout wake-ups. cancellation_token() -> fun(Ctx) -> - Token = maps:get(cancel_token, Ctx), + Token = maps:get(user_cancel_token, Ctx, maps:get(cancel_token, Ctx)), (maps:get(on_success, Ctx))(Token) end. @@ -394,6 +528,8 @@ create_cancellation_token(Arg) -> fable_cancellation:create(Arg). %% Wrap raw error values as exception maps so .Message accessor works. %% Errors from failwith are raw binaries; errors from raise(exn ...) are already maps. +%% decision: recognize the legacy runtime atom by identity, never by a formatted message or token state. +wrap_error(operation_cancelled) -> fable_cancellation:exception(); wrap_error(Err) when is_binary(Err) -> #{message => Err}; wrap_error(Err) when is_map(Err) -> Err; wrap_error(Err) when is_reference(Err) -> Err; diff --git a/src/fable-library-beam/fable_async_builder.erl b/src/fable-library-beam/fable_async_builder.erl index 88a2e697d1..3ba2a5a27b 100644 --- a/src/fable-library-beam/fable_async_builder.erl +++ b/src/fable-library-beam/fable_async_builder.erl @@ -58,15 +58,22 @@ zero(_Unit) -> return(ok). %% Delay: defer generator execution (cold semantics) delay(Generator) -> - fun(Ctx) -> (Generator(ok))(Ctx) end. + fun(Ctx) -> + case fable_cancellation:is_cancellation_requested(maps:get(cancel_token, Ctx)) of + true -> (maps:get(on_cancel, Ctx))(fable_cancellation:exception()); + false -> (Generator(ok))(Ctx) + end + end. %% Bind: monadic bind — run computation, pass result to binder bind(Computation, Binder) -> fun(Ctx) -> Ctx2 = Ctx#{ on_success => fun(X) -> - try - (Binder(X))(Ctx) + %% Only binder evaluation is guarded. Exceptions from the returned computation + %% may belong to downstream continuations whose handlers have already settled. + try Binder(X) of + Bound -> Bound(Ctx) catch _:Err:St -> fable_async:record_stacktrace(Err, St), @@ -83,6 +90,8 @@ combine(Comp1, Comp2) -> %% TryWith: catch errors in computation, pass to handler %% Wrap raw errors (binaries from failwith) as exception maps so .Message works. +%% invariant: only on_error enters the handler; on_cancel bypasses it, while a thrown +%% OperationCanceledException remains catchable just like any explicitly thrown .NET exception. try_with(Computation, Handler) -> fun(Ctx) -> Ctx2 = Ctx#{ @@ -122,33 +131,53 @@ try_with(Computation, Handler) -> %% Compensation is generated as fun(UnitVar) -> ... end (arity 1), so call with ok. try_finally(Computation, Compensation) -> fun(Ctx) -> + %% invariant: settlement claims compensation before invoking it, so its exception cannot re-enter it. + %% decision: cancellation remains terminal when compensation throws, matching .NET Async.TryFinally. + Settled = atomics:new(1, []), + Finish = fun(Channel, Value) -> + case atomics:compare_exchange(Settled, 1, 0, 1) of + ok -> + Result = try Compensation(ok), ok + catch + _:_ when Channel =:= on_cancel -> ok; + _:Err:St -> + fable_async:record_stacktrace(Err, St), + {error, Err} + end, + case Result of + ok -> (maps:get(Channel, Ctx))(Value); + {error, Error} -> (maps:get(on_error, Ctx))(Error) + end; + 1 -> ok + end + end, Ctx2 = Ctx#{ on_success => fun(X) -> - Compensation(ok), - (maps:get(on_success, Ctx))(X) + Finish(on_success, X) end, on_error => fun(E) -> - Compensation(ok), - (maps:get(on_error, Ctx))(E) + Finish(on_error, E) end, on_cancel => fun(E) -> - Compensation(ok), - (maps:get(on_cancel, Ctx))(E) + Finish(on_cancel, E) end }, try Computation(Ctx2) catch - _:Err:St -> - fable_async:record_stacktrace(Err, St), - Compensation(ok), - (maps:get(on_error, Ctx))(Err) + Class:Err:St -> + case atomics:get(Settled, 1) of + 0 -> + fable_async:record_stacktrace(Err, St), + Finish(on_error, Err); + 1 -> erlang:raise(Class, Err, St) + end end end. -%% Using: TryFinally with IDisposable (Dispose = no-op in Erlang for now) +%% Using: install disposal before evaluating the binder, including a binder that throws. using(Resource, Binder) -> - try_finally(Binder(Resource), fun() -> ok end). + try_finally(delay(fun(_) -> Binder(Resource) end), fun(_) -> fable_utils:safe_dispose(Resource) end). %% While: recursive bind while guard is true while(Guard, Computation) -> diff --git a/src/fable-library-beam/fable_cancellation.erl b/src/fable-library-beam/fable_cancellation.erl index e5f339592d..3f81037e7e 100644 --- a/src/fable-library-beam/fable_cancellation.erl +++ b/src/fable-library-beam/fable_cancellation.erl @@ -5,7 +5,8 @@ cancel_after/2, is_cancellation_requested/1, throw_if_cancellation_requested/1, - register/2, register/3 + register/2, register/3, + exception/0 ]). -spec create() -> reference(). @@ -14,25 +15,34 @@ -spec cancel_after(reference() | undefined, non_neg_integer()) -> ok. -spec is_cancellation_requested(reference() | undefined) -> boolean(). -spec throw_if_cancellation_requested(reference() | undefined) -> ok. --spec register(reference() | undefined, fun()) -> {reference(), integer()} | undefined. --spec register(reference() | undefined, fun(), term()) -> {reference(), integer()} | undefined. +-spec exception() -> #{exn_type := operation_canceled_exception, message := binary()}. +-spec register(reference() | undefined, fun()) -> map() | undefined. +-spec register(reference() | undefined, fun(), term()) -> map() | undefined. + +-define(SERVER, fable_cancellation_server). +-define(CALLBACK_KEY, '$fable_cancellation_callback'). %% CancellationToken/CancellationTokenSource runtime. -%% Token = make_ref(), stored in process dict as: -%% #{cancelled => false, listeners => #{}, next_id => 1} %% -%% Uses process dict pattern (same as Ref/Dictionary/ResizeArray). -%% Since Beam async runs same-process CPS, no cross-process communication needed. +%% decision: stores the cancellation bit in atomics so token state is shared and garbage-collected +%% with the token instead of being tied to any BEAM process or ETS owner +%% decision: serializes registration, cancellation, and timers through one runtime-wide broker — +%% this closes the register-versus-cancel race without leaking one process per token +%% invariant: the broker never invokes a callback for direct cancellation — Cancel runs callbacks +%% synchronously in its caller, matching CancellationTokenSource.Cancel semantics +%% invariant: a running registration remains broker-owned until its callback completes — Dispose +%% can therefore wait for callbacks executing in another process +%% tradeoff: user callbacks cannot capture process-dictionary-backed Fable mutable values across +%% processes because Cancel runs them in its caller and CancelAfter uses a short-lived worker; +%% runtime-owned callbacks must marshal their work back to the registering process create() -> create(undefined). create(undefined) -> - Token = make_ref(), - put(Token, #{cancelled => false, listeners => #{}, next_id => 1}), - Token; + atomics:new(1, []); create(true) -> - Token = make_ref(), - put(Token, #{cancelled => true, listeners => #{}, next_id => 1}), + Token = create(undefined), + atomics:put(Token, 1, 1), Token; create(false) -> create(undefined); @@ -44,78 +54,256 @@ create(Ms) when is_integer(Ms) -> cancel(undefined) -> ok; cancel(Token) -> - State = get(Token), - put(Token, State#{cancelled := true}), - invoke_listeners(maps:get(listeners, State)), - ok. + case call({cancel, Token}) of + {invoke, Listeners} -> invoke_listeners(Token, Listeners); + already_cancelled -> ok + end. cancel_after(undefined, _Ms) -> ok; cancel_after(Token, Ms) -> - Self = self(), - timer:apply_after(Ms, erlang, send, [Self, {cancel_token, Token}]), - %% Spawn a receiver that watches for the cancel message - %% Since we're same-process CPS, we set up a process dict flag - %% and check it. But for timer-based cancel we need a receive loop. - %% Use a helper process that sends us a message we handle in sleep. + call({cancel_after, Token, Ms}), ok. is_cancellation_requested(undefined) -> false; is_cancellation_requested(Token) -> - drain_cancel_messages(), - maps:get(cancelled, get(Token)). + atomics:get(Token, 1) =:= 1. throw_if_cancellation_requested(undefined) -> ok; throw_if_cancellation_requested(Token) -> - drain_cancel_messages(), - case maps:get(cancelled, get(Token)) of - true -> erlang:error(operation_cancelled); + case is_cancellation_requested(Token) of + true -> erlang:error(exception()); false -> ok end. -register(Token, F) -> register(Token, F, undefined). +%% invariant: cancellation has a nominal exception tag, never just a matching message. +exception() -> + #{exn_type => operation_canceled_exception, message => <<"The operation was canceled.">>}. + +register(Token, F) -> register(Token, F, ok). register(undefined, _F, _State) -> undefined; -register(Token, F, _State) -> - State = get(Token), - Id = maps:get(next_id, State), - Listeners = maps:get(listeners, State), - put(Token, State#{listeners := Listeners#{Id => F}, next_id := Id + 1}), - %% Return a disposable-like value (the registration id + token) - {Token, Id}. +register(Token, F, State) -> + Id = make_ref(), + case call({register, Token, Id, F, State}) of + registered -> + #{dispose => fun(_) -> unregister(Token, Id) end}; + invoke -> + invoke_listener({F, State}), + #{dispose => fun(_) -> ok end} + end. + +unregister(Token, Id) -> + IsCurrentCallback = get(?CALLBACK_KEY) =:= {Token, Id}, + call({unregister, Token, Id, IsCurrentCallback}), + ok. %% Internal helpers -invoke_listeners(Listeners) -> - maps:foreach( - fun(_Id, F) -> - try - F(ok) - catch - _:_ -> ok - end - end, - Listeners - ). - -%% Drain any pending {cancel_token, Token} messages from the mailbox -%% and apply cancellation. This is needed for timer-based cancellation -%% since timer:apply_after sends a message to self(). -drain_cancel_messages() -> +invoke_listeners(Token, Listeners) -> + lists:foreach(fun(Listener) -> invoke_listener(Token, Listener) end, Listeners). + +invoke_listener(Token, {Id, F, State}) -> + PreviousCallback = put(?CALLBACK_KEY, {Token, Id}), + invoke_listener({F, State}), + case PreviousCallback of + undefined -> erase(?CALLBACK_KEY); + _ -> put(?CALLBACK_KEY, PreviousCallback) + end, + call({callback_complete, Token, Id}), + ok. + +invoke_listener({F, State}) -> + try + F(State) + catch + _:_ -> ok + end. + +call(Request) -> + Server = server(), + Ref = make_ref(), + Monitor = erlang:monitor(process, Server), + Server ! {call, self(), Ref, Request}, receive - {cancel_token, Token} -> - State = get(Token), - case maps:get(cancelled, State) of - true -> - ok; - false -> - put(Token, State#{cancelled := true}), - invoke_listeners(maps:get(listeners, State)) + {Ref, Reply} -> + erlang:demonitor(Monitor, [flush]), + Reply; + {'DOWN', Monitor, process, Server, Reason} -> + erlang:error({cancellation_server_stopped, Reason}) + end. + +server() -> + case whereis(?SERVER) of + undefined -> start_server(); + Pid -> Pid + end. + +start_server() -> + Pid = spawn(fun() -> server_loop(#{}) end), + try erlang:register(?SERVER, Pid) of + true -> Pid + catch + error:badarg -> + exit(Pid, kill), + server() + end. + +server_loop(Entries) -> + receive + {call, From, Ref, {register, Token, Id, F, State}} -> + case atomics:get(Token, 1) of + 1 -> + From ! {Ref, invoke}, + server_loop(Entries); + 0 -> + Entry = maps:get(Token, Entries, new_entry()), + Listeners = maps:get(listeners, Entry), + Entry1 = Entry#{listeners := Listeners#{Id => {F, State}}}, + From ! {Ref, registered}, + server_loop(Entries#{Token => Entry1}) + end; + {call, From, Ref, {unregister, Token, Id, IsCurrentCallback}} -> + {Entries1, Reply} = unregister_listener( + Token, Id, From, Ref, IsCurrentCallback, Entries + ), + case Reply of + now -> From ! {Ref, ok}; + after_callback -> ok end, - drain_cancel_messages() - after 0 -> - ok + server_loop(Entries1); + {call, From, Ref, {cancel, Token}} -> + case atomics:compare_exchange(Token, 1, 0, 1) of + ok -> + {Listeners, Entries1} = begin_callbacks(Token, From, Entries), + From ! {Ref, {invoke, Listeners}}, + server_loop(Entries1); + 1 -> + From ! {Ref, already_cancelled}, + server_loop(Entries) + end; + {call, From, Ref, {cancel_after, Token, Ms}} -> + Entries1 = schedule_cancel_after(Token, Ms, Entries), + From ! {Ref, ok}, + server_loop(Entries1); + {cancel_after, Token, Generation} -> + case maps:find(Token, Entries) of + {ok, #{timer := {_TimerRef, Generation}}} -> + case atomics:compare_exchange(Token, 1, 0, 1) of + ok -> + %% CancelAfter has no calling process in which callbacks can run. + %% A short-lived worker keeps user code out of the state broker. + Worker = spawn(fun() -> + receive + {invoke, Listeners} -> invoke_listeners(Token, Listeners) + end + end), + {Listeners, Entries1} = begin_callbacks(Token, Worker, Entries), + Worker ! {invoke, Listeners}, + server_loop(Entries1); + 1 -> + Entries1 = remove_timer(Token, Entries), + server_loop(Entries1) + end; + _ -> + server_loop(Entries) + end; + {call, From, Ref, {callback_complete, Token, Id}} -> + Entries1 = complete_callback(Token, Id, Entries), + From ! {Ref, ok}, + server_loop(Entries1) + end. + +new_entry() -> + #{listeners => #{}, running => #{}, waiters => #{}, timer => undefined}. + +schedule_cancel_after(Token, Ms, Entries) -> + case atomics:get(Token, 1) of + 1 -> Entries; + 0 -> + Entry = maps:get(Token, Entries, new_entry()), + cancel_timer(maps:get(timer, Entry)), + Generation = make_ref(), + TimerRef = erlang:send_after(Ms, self(), {cancel_after, Token, Generation}), + Entries#{Token => Entry#{timer := {TimerRef, Generation}}} + end. + +update_entry(Token, F, Entries) -> + case maps:find(Token, Entries) of + error -> Entries; + {ok, Entry} -> + Entry1 = F(Entry), + case entry_empty(Entry1) of + true -> maps:remove(Token, Entries); + false -> Entries#{Token := Entry1} + end + end. + +begin_callbacks(Token, Owner, Entries) -> + case maps:find(Token, Entries) of + error -> {[], Entries}; + {ok, Entry} -> + cancel_timer(maps:get(timer, Entry)), + Listeners = maps:get(listeners, Entry), + Running = maps:from_list([{Id, Owner} || Id <- maps:keys(Listeners)]), + Callbacks = [{Id, F, State} || {Id, {F, State}} <- maps:to_list(Listeners)], + Entry1 = Entry#{listeners := #{}, running := Running, timer := undefined}, + {Callbacks, put_entry(Token, Entry1, Entries)} + end. + +unregister_listener(Token, Id, From, Ref, IsCurrentCallback, Entries) -> + case maps:find(Token, Entries) of + error -> {Entries, now}; + {ok, Entry} -> + Listeners = maps:get(listeners, Entry), + Running = maps:get(running, Entry), + case maps:find(Id, Running) of + {ok, From} when IsCurrentCallback -> + %% A callback may dispose its own registration; waiting here would deadlock. + {Entries, now}; + {ok, _Owner} -> + Waiters = maps:get(waiters, Entry), + RegistrationWaiters = maps:get(Id, Waiters, []), + Entry1 = Entry#{waiters := Waiters#{Id => [{From, Ref} | RegistrationWaiters]}}, + {Entries#{Token := Entry1}, after_callback}; + error -> + Entry1 = Entry#{listeners := maps:remove(Id, Listeners)}, + {put_entry(Token, Entry1, Entries), now} + end + end. + +complete_callback(Token, Id, Entries) -> + case maps:find(Token, Entries) of + error -> Entries; + {ok, Entry} -> + Waiters = maps:get(waiters, Entry), + lists:foreach(fun({Pid, Ref}) -> Pid ! {Ref, ok} end, maps:get(Id, Waiters, [])), + Entry1 = Entry#{ + running := maps:remove(Id, maps:get(running, Entry)), + waiters := maps:remove(Id, Waiters) + }, + put_entry(Token, Entry1, Entries) + end. + +remove_timer(Token, Entries) -> + update_entry(Token, fun(Entry) -> Entry#{timer := undefined} end, Entries). + +put_entry(Token, Entry, Entries) -> + case entry_empty(Entry) of + true -> maps:remove(Token, Entries); + false -> Entries#{Token => Entry} end. + +entry_empty(#{listeners := Listeners, running := Running, waiters := Waiters, timer := Timer}) -> + map_size(Listeners) =:= 0 andalso + map_size(Running) =:= 0 andalso + map_size(Waiters) =:= 0 andalso + Timer =:= undefined. + +cancel_timer(undefined) -> ok; +cancel_timer({TimerRef, _Generation}) -> + erlang:cancel_timer(TimerRef), + ok. diff --git a/src/fable-library-beam/fable_comparison.erl b/src/fable-library-beam/fable_comparison.erl index ad71b43944..5897f152f4 100644 --- a/src/fable-library-beam/fable_comparison.erl +++ b/src/fable-library-beam/fable_comparison.erl @@ -1,9 +1,10 @@ -module(fable_comparison). --export([compare/2, compare_union/3, equals/2, hash/1]). +-export([compare/2, compare_union/3, equals/2, get_hash_code/1, hash/1]). -spec equals(term(), term()) -> boolean(). -spec compare(term(), term()) -> -1 | 0 | 1. -spec compare_union([binary()], term(), term()) -> -1 | 0 | 1. +-spec get_hash_code(term()) -> non_neg_integer(). -spec hash(term()) -> non_neg_integer(). %% Deep equality that handles ref-wrapped arrays (process dict refs) @@ -152,6 +153,11 @@ union_tag_index([Name | _], Name, I) -> I; union_tag_index([_ | T], Name, I) -> union_tag_index(T, Name, I + 1); union_tag_index([], _, _) -> -1. +%% invariant: Object.GetHashCode hashes reference-backed storage by identity, while hash/1 remains structural. +get_hash_code({byte_array, _, _} = V) -> erlang:phash2(V); +get_hash_code(V) when is_reference(V) -> erlang:phash2(V); +get_hash_code(V) -> hash(V). + %% Hash that derefs refs and byte arrays before hashing. hash({byte_array, _, _} = V) -> erlang:phash2(fable_utils:byte_array_to_list(V)); diff --git a/src/fable-library-beam/fable_convert.erl b/src/fable-library-beam/fable_convert.erl index f29abda335..e2aeebef66 100644 --- a/src/fable-library-beam/fable_convert.erl +++ b/src/fable-library-beam/fable_convert.erl @@ -2,29 +2,47 @@ -export([ to_float/1, to_int/1, + to_int/3, + convert_to_int/3, to_int_with_base/2, + to_int_with_base/4, to_string/1, to_string_with_base/3, to_base64/1, from_base64/1, + to_hex_string/1, + to_hex_string/3, + to_hex_string_lower/1, + to_hex_string_lower/3, + from_hex_string/1, boolean_parse/1, boolean_try_parse/2, int_to_string_with_format/2, try_parse_int/2, + try_parse_int/4, try_parse_float/2 ]). -spec to_float(binary() | integer() | float()) -> float(). -spec to_int(binary() | integer() | float()) -> integer(). +-spec to_int(binary() | integer() | float(), pos_integer(), boolean()) -> integer(). +-spec convert_to_int(boolean() | integer() | float(), pos_integer(), boolean()) -> integer(). -spec to_int_with_base(binary(), integer()) -> integer(). +-spec to_int_with_base(binary(), integer(), pos_integer(), boolean()) -> integer(). -spec to_string(term()) -> binary(). -spec to_string_with_base(integer(), integer(), integer()) -> binary(). -spec to_base64(tuple() | list() | binary()) -> binary(). -spec from_base64(binary()) -> tuple(). +-spec to_hex_string(tuple() | list() | binary() | reference() | undefined) -> binary(). +-spec to_hex_string(tuple() | list() | binary() | reference() | undefined, integer(), integer()) -> binary(). +-spec to_hex_string_lower(tuple() | list() | binary() | reference() | undefined) -> binary(). +-spec to_hex_string_lower(tuple() | list() | binary() | reference() | undefined, integer(), integer()) -> binary(). +-spec from_hex_string(binary() | undefined) -> tuple(). -spec boolean_parse(binary()) -> boolean(). -spec boolean_try_parse(binary(), reference()) -> boolean(). -spec int_to_string_with_format(integer(), binary()) -> binary(). -spec try_parse_int(binary(), reference()) -> boolean(). +-spec try_parse_int(binary(), reference(), pos_integer(), boolean()) -> boolean(). -spec try_parse_float(binary(), reference()) -> boolean(). %% Robust string-to-float conversion that handles edge cases @@ -68,10 +86,80 @@ to_int(Bin) when is_binary(Bin) -> to_int(N) when is_integer(N) -> N; to_int(F) when is_float(F) -> trunc(F). +to_int(Bin, Bits, Signed) when is_binary(Bin) -> + case Bin of + <<"0x", Rest/binary>> -> to_int_with_base(Rest, 16, Bits, Signed); + <<"0X", Rest/binary>> -> to_int_with_base(Rest, 16, Bits, Signed); + <<"0o", Rest/binary>> -> to_int_with_base(Rest, 8, Bits, Signed); + <<"0O", Rest/binary>> -> to_int_with_base(Rest, 8, Bits, Signed); + <<"0b", Rest/binary>> -> to_int_with_base(Rest, 2, Bits, Signed); + <<"0B", Rest/binary>> -> to_int_with_base(Rest, 2, Bits, Signed); + _ -> ensure_integer_range(binary_to_integer(Bin), Bits, Signed) + end; +to_int(N, Bits, Signed) when is_integer(N) -> ensure_integer_range(N, Bits, Signed); +to_int(F, Bits, Signed) when is_float(F) -> ensure_integer_range(trunc(F), Bits, Signed). + +%% System.Convert rounds floating-point values before checking the target range. +%% invariant: checked conversions round midpoint values to even and stay within the target width. +convert_to_int(true, Bits, Signed) -> ensure_integer_range(1, Bits, Signed); +convert_to_int(false, Bits, Signed) -> ensure_integer_range(0, Bits, Signed); +convert_to_int(N, Bits, Signed) when is_integer(N) -> ensure_integer_range(N, Bits, Signed); +convert_to_int(F, Bits, Signed) when is_float(F) -> + ensure_integer_range(round_half_to_even(F), Bits, Signed). + +round_half_to_even(F) -> + Truncated = trunc(F), + Fraction = F - Truncated, + AbsFraction = abs(Fraction), + if + AbsFraction < 0.5 -> + Truncated; + AbsFraction > 0.5 -> + Truncated + round_direction(Fraction); + Truncated rem 2 =:= 0 -> + Truncated; + true -> + Truncated + round_direction(Fraction) + end. + +round_direction(Fraction) when Fraction < 0 -> -1; +round_direction(_Fraction) -> 1. + %% Parse string to integer with given base (2, 8, 10, 16) to_int_with_base(Bin, Base) when is_binary(Bin), is_integer(Base) -> binary_to_integer(Bin, Base). +to_int_with_base(Bin, Base, Bits, Signed) when is_binary(Bin), is_integer(Base) -> + checked_base_integer(binary_to_integer(Bin, Base), Base, Bits, Signed). + +%% Non-decimal signed parsing accepts the target width's two's-complement form. +%% invariant: checked integer parsing never returns a value outside the selected target width. +checked_base_integer(N, Base, Bits, true) when Base =/= 10 -> + SignBit = 1 bsl (Bits - 1), + Modulus = 1 bsl Bits, + case N >= SignBit andalso N < Modulus of + true -> N - Modulus; + false -> ensure_integer_range(N, Bits, true) + end; +checked_base_integer(N, _Base, Bits, Signed) -> + ensure_integer_range(N, Bits, Signed). + +ensure_integer_range(N, Bits, Signed) -> + case integer_in_range(N, Bits, Signed) of + true -> N; + false -> + erlang:error(#{ + exn_type => overflow_exception, + message => <<"Value was either too large or too small for an integer type.">> + }) + end. + +integer_in_range(N, Bits, true) -> + Limit = 1 bsl (Bits - 1), + N >= -Limit andalso N < Limit; +integer_in_range(N, Bits, false) -> + N >= 0 andalso N < (1 bsl Bits). + %% Convert integer to string with given base and bit width %% BitWidth: 8 (SByte), 16 (Int16), 32 (Int32), 64 (Int64) %% For negative numbers with non-decimal bases, .NET uses two's complement @@ -118,6 +206,67 @@ to_base64(Bin) when is_binary(Bin) -> from_base64(Str) when is_binary(Str) -> fable_utils:new_byte_array(binary_to_list(base64:decode(Str))). +to_hex_string(Bytes) -> + binary:encode_hex(hex_input(Bytes)). + +to_hex_string(Bytes, Offset, Length) -> + binary:encode_hex(hex_input(Bytes, Offset, Length)). + +%% binary:encode_hex/2 with lowercase needs OTP 26 +to_hex_string_lower(Bytes) -> + string:lowercase(to_hex_string(Bytes)). + +to_hex_string_lower(Bytes, Offset, Length) -> + string:lowercase(to_hex_string(Bytes, Offset, Length)). + +hex_input(undefined) -> + argument_null(<<"inArray">>); +hex_input({byte_array, _, _} = BA) -> + list_to_binary(fable_utils:byte_array_to_list(BA)); +hex_input(Bytes) when is_list(Bytes) -> + list_to_binary(Bytes); +hex_input(Bin) when is_binary(Bin) -> + Bin; +hex_input(Ref) when is_reference(Ref) -> + hex_input(get(Ref)). + +hex_input(Bytes, Offset, Length) -> + Bin = hex_input(Bytes), + if + Length < 0 -> argument_out_of_range(<<"length">>, <<"Non-negative number required.">>); + Offset < 0 -> argument_out_of_range(<<"offset">>, <<"Non-negative number required.">>); + Offset > byte_size(Bin) - Length -> + argument_out_of_range(<<"offset">>, <<"Specified argument was out of the range of valid values.">>); + true -> binary:part(Bin, Offset, Length) + end. + +from_hex_string(undefined) -> + argument_null(<<"s">>); +from_hex_string(Str) when byte_size(Str) rem 2 =/= 0 -> + bad_hex_string(<<"The input is not a valid hex string as its length is not a multiple of 2.">>); +from_hex_string(Str) -> + try binary:decode_hex(Str) of + Bin -> fable_utils:new_byte_array(binary_to_list(Bin)) + catch + error:badarg -> + bad_hex_string(<<"The input is not a valid hex string as it contains a non-hex character.">>) + end. + +argument_null(ParamName) -> + erlang:error(#{ + exn_type => argument_null_exception, + message => fable_utils:argument_exception_message(<<"Value cannot be null.">>, ParamName) + }). + +argument_out_of_range(ParamName, Message) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => fable_utils:argument_exception_message(Message, ParamName) + }). + +bad_hex_string(Message) -> + erlang:error(#{exn_type => format_exception, message => Message}). + %% TryParse: returns bool and sets out-param via put(OutRef, Value). %% F# uses out-parameter pattern: result = TryParse(str, &outRef). try_parse_int(Bin, OutRef) when is_binary(Bin) -> @@ -132,6 +281,23 @@ try_parse_int(Bin, OutRef) when is_binary(Bin) -> try_parse_int(_, _) -> false. +try_parse_int(Bin, OutRef, Bits, Signed) when is_binary(Bin) -> + Trimmed = string:trim(binary_to_list(Bin)), + case string:to_integer(Trimmed) of + {Int, []} -> + case integer_in_range(Int, Bits, Signed) of + true -> + put(OutRef, Int), + true; + false -> + false + end; + _ -> + false + end; +try_parse_int(_, _, _, _) -> + false. + try_parse_float(Bin, OutRef) when is_binary(Bin) -> Trimmed = string:trim(binary_to_list(Bin)), case string:to_float(Trimmed) of diff --git a/src/fable-library-beam/fable_decimal.erl b/src/fable-library-beam/fable_decimal.erl index 9149707946..b1be6daa02 100644 --- a/src/fable-library-beam/fable_decimal.erl +++ b/src/fable-library-beam/fable_decimal.erl @@ -10,6 +10,8 @@ from_int/1, from_float/1, from_parts/5, + from_bits/1, + get_bits/1, parse/1, try_parse/2, get_one/0, @@ -33,6 +35,8 @@ -spec from_int(integer()) -> integer(). -spec from_float(float() | integer()) -> integer(). -spec from_parts(integer(), integer(), integer(), boolean(), non_neg_integer()) -> integer(). +-spec from_bits([integer()]) -> integer(). +-spec get_bits(integer()) -> [integer()]. -spec parse(binary()) -> integer(). -spec try_parse(binary(), reference()) -> boolean(). -spec get_one() -> integer(). @@ -150,7 +154,7 @@ shift(Value, Exp) -> %% MakeDecimal(low, mid, high, isNegative, scale) %% low/mid/high are signed int32 from .NET — mask to unsigned -from_parts(Low, Mid, High, IsNegative, Scale) -> +from_parts(Low, Mid, High, IsNegative, Scale) when Scale >= 0, Scale =< 28 -> L = Low band 16#FFFFFFFF, M = Mid band 16#FFFFFFFF, H = High band 16#FFFFFFFF, @@ -161,6 +165,66 @@ from_parts(Low, Mid, High, IsNegative, Scale) -> case IsNegative of true -> -Adjusted; false -> Adjusted + end; +from_parts(_Low, _Mid, _High, _IsNegative, Scale) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => <<"Decimal scale must be between 0 and 28.">>, + param_name => <<"scale">>, + actual_value => Scale + }). + +from_bits([Low, Mid, High, Flags]) -> + UnsignedFlags = Flags band 16#FFFFFFFF, + Scale = (UnsignedFlags bsr 16) band 16#FF, + ReservedBits = UnsignedFlags band 16#7F00FFFF, + case ReservedBits =:= 0 andalso Scale =< 28 of + true -> + IsNegative = (UnsignedFlags band 16#80000000) =/= 0, + from_parts(Low, Mid, High, IsNegative, Scale); + false -> + invalid_decimal_bits() + end; +from_bits(_Bits) -> + invalid_decimal_bits(). + +invalid_decimal_bits() -> + erlang:error(#{ + exn_type => argument_exception, + message => <<"Decimal constructor requires four integers with valid sign and scale bits.">> + }). + +%% decision: GetBits emits the smallest scale because fixed-scale values do not retain input scale or negative zero. +%% invariant: every representable .NET decimal round-trips numerically through get_bits/1 and from_bits/1. +get_bits(Value) -> + IsNegative = Value < 0, + {Coefficient, Scale} = canonical_coefficient(abs(Value), 28), + case Coefficient < (1 bsl 96) of + true -> + Low = to_signed_int32(Coefficient), + Mid = to_signed_int32(Coefficient bsr 32), + High = to_signed_int32(Coefficient bsr 64), + Flags = (Scale bsl 16) bor case IsNegative of true -> 16#80000000; false -> 0 end, + [Low, Mid, High, to_signed_int32(Flags)]; + false -> + erlang:error(#{ + exn_type => overflow_exception, + message => <<"Value was either too large or too small for a Decimal.">> + }) + end. + +canonical_coefficient(0, _Scale) -> + {0, 0}; +canonical_coefficient(Coefficient, Scale) when Scale > 0, Coefficient rem 10 =:= 0 -> + canonical_coefficient(Coefficient div 10, Scale - 1); +canonical_coefficient(Coefficient, Scale) -> + {Coefficient, Scale}. + +to_signed_int32(Value) -> + Masked = Value band 16#FFFFFFFF, + case Masked >= 16#80000000 of + true -> Masked - 16#100000000; + false -> Masked end. pow10(0) -> diff --git a/src/fable-library-beam/fable_int.erl b/src/fable-library-beam/fable_int.erl index 0cbbd5d04c..d75ed94194 100644 --- a/src/fable-library-beam/fable_int.erl +++ b/src/fable-library-beam/fable_int.erl @@ -7,7 +7,12 @@ wrap_u8/1, wrap_u16/1, wrap_u32/1, - wrap_u64/1 + wrap_u64/1, + log2/1, + bigint_from_byte_array/1, + bigint_to_byte_array/1, + bigint_pow/2, + bigint_gcd/2 ]). -spec wrap_i8(integer()) -> integer(). @@ -18,6 +23,11 @@ -spec wrap_u16(integer()) -> non_neg_integer(). -spec wrap_u32(integer()) -> non_neg_integer(). -spec wrap_u64(integer()) -> non_neg_integer(). +-spec log2(integer()) -> non_neg_integer(). +-spec bigint_from_byte_array(tuple()) -> integer(). +-spec bigint_to_byte_array(integer()) -> tuple(). +-spec bigint_pow(integer(), integer()) -> integer(). +-spec bigint_gcd(integer(), integer()) -> non_neg_integer(). %% Fixed-width (two's complement) integer semantics for .NET sized integers. %% @@ -71,3 +81,85 @@ wrap_u64(N) when N >= 0, N =< 16#FFFFFFFFFFFFFFFF -> N; wrap_u64(N) -> <> = <>, V. + +%% Compute integer log2 from bit length rather than floating point so UInt64 and +%% arbitrary-precision values stay exact above the IEEE-754 integer precision limit. +log2(N) when N < 0 -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => <<"Non-negative number required. (Parameter 'value')">>, + param_name => <<"value">> + }); +log2(0) -> + 0; +log2(N) -> + Bin = binary:encode_unsigned(N), + <> = Bin, + (byte_size(Bin) - 1) * 8 + log2_byte(MostSignificantByte). + +log2_byte(N) when N >= 16#80 -> 7; +log2_byte(N) when N >= 16#40 -> 6; +log2_byte(N) when N >= 16#20 -> 5; +log2_byte(N) when N >= 16#10 -> 4; +log2_byte(N) when N >= 16#08 -> 3; +log2_byte(N) when N >= 16#04 -> 2; +log2_byte(N) when N >= 16#02 -> 1; +log2_byte(_) -> 0. + +%% decision: encode BigInteger bytes with integer shifts -- width calculation must stay exact. +%% invariant: byte arrays use .NET's minimal little-endian two's-complement representation. +bigint_to_byte_array(0) -> + fable_utils:new_byte_array([0]); +bigint_to_byte_array(N) -> + fable_utils:new_byte_array(bigint_to_byte_list(N, [])). + +bigint_to_byte_list(N, Acc) -> + Byte = N band 16#FF, + Next = N bsr 8, + SignBitSet = Byte band 16#80 =/= 0, + case (Next =:= 0 andalso not SignBitSet) orelse (Next =:= -1 andalso SignBitSet) of + true -> lists:reverse([Byte | Acc]); + false -> bigint_to_byte_list(Next, [Byte | Acc]) + end. + +bigint_from_byte_array(BytesValue) -> + Bytes = fable_utils:byte_array_to_list(BytesValue), + case Bytes of + [] -> + 0; + _ -> + Unsigned = binary:decode_unsigned(list_to_binary(Bytes), little), + Last = lists:last(Bytes), + case Last band 16#80 of + 0 -> Unsigned; + _ -> Unsigned - (1 bsl (length(Bytes) * 8)) + end + end. + +%% decision: exponentiation stays in the integer domain -- math:pow/2 silently rounds large values. +%% invariant: bigint_pow/2 returns the exact integer result for every non-negative exponent. +bigint_pow(_Base, Exponent) when Exponent < 0 -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => <<"The number must be greater than or equal to zero. (Parameter 'exponent')">>, + param_name => <<"exponent">> + }); +bigint_pow(Base, Exponent) -> + bigint_pow(Base, Exponent, 1). + +bigint_pow(_Base, 0, Acc) -> + Acc; +bigint_pow(Base, 1, Acc) -> + Acc * Base; +bigint_pow(Base, Exponent, Acc) when Exponent band 1 =:= 1 -> + bigint_pow(Base * Base, Exponent bsr 1, Acc * Base); +bigint_pow(Base, Exponent, Acc) -> + bigint_pow(Base * Base, Exponent bsr 1, Acc). + +bigint_gcd(A, B) -> + bigint_gcd_positive(erlang:abs(A), erlang:abs(B)). + +bigint_gcd_positive(A, 0) -> + A; +bigint_gcd_positive(A, B) -> + bigint_gcd_positive(B, A rem B). diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index 948865dd99..fefea00a62 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -9,7 +9,9 @@ reduce_back/2, map_indexed/2, sort_by/2, + sort_by/3, sort_by_descending/2, + sort_by_descending/3, sort_with/2, find/2, try_find/2, @@ -23,7 +25,11 @@ collect/2, sum_by/2, min_by/2, + min_by/3, max_by/2, + max_by/3, + min_with/2, + max_with/2, indexed/1, zip/2, zip3/3, @@ -41,6 +47,8 @@ pairwise/1, exists2/3, forall2/3, + array_exists2/3, + array_forall2/3, map2/3, map3/4, mapi2/3, @@ -73,6 +81,8 @@ remove_many_at/3, index_of_value/2, get_slice/3, + sub/3, + blit/5, take/2, random_shuffle_by/2, random_shuffle_with/2, @@ -128,6 +138,8 @@ -spec pairwise(list()) -> list({term(), term()}). -spec exists2(fun(), list(), list()) -> boolean(). -spec forall2(fun(), list(), list()) -> boolean(). +-spec array_exists2(fun(), list(), list()) -> boolean(). +-spec array_forall2(fun(), list(), list()) -> boolean(). -spec map2(fun(), list(), list()) -> list(). -spec map3(fun(), list(), list(), list()) -> list(). -spec mapi2(fun(), list(), list()) -> list(). @@ -141,9 +153,9 @@ -spec group_by(fun(), list()) -> list({term(), list()}). -spec unfold(fun(), term()) -> list(). -spec split_at(non_neg_integer(), list()) -> {list(), list()}. --spec chunk_by_size(pos_integer(), list()) -> list(list()). --spec windowed(pos_integer(), list()) -> list(list()). --spec split_into(pos_integer(), list()) -> list(list()). +-spec chunk_by_size(integer(), list()) -> list(list()). +-spec windowed(integer(), list()) -> list(list()). +-spec split_into(integer(), list()) -> list(list()). -spec except(list(), list()) -> list(). -spec all_pairs(list(), list()) -> list({term(), term()}). -spec permute(fun(), list()) -> list(). @@ -207,16 +219,28 @@ map_indexed(Fn, List) -> sort_by(Fn, List) -> lists:sort(fun(A, B) -> Fn(A) =< Fn(B) end, List). +sort_by(Fn, Comparer, List) -> + lists:sort(fun(A, B) -> Comparer(Fn(A), Fn(B)) =< 0 end, List). + sort_by_descending(Fn, List) -> lists:reverse(lists:sort(fun(A, B) -> Fn(A) =< Fn(B) end, List)). +sort_by_descending(Fn, Comparer, List) -> + lists:reverse(lists:sort(fun(A, B) -> Comparer(Fn(A), Fn(B)) =< 0 end, List)). + sort_with(Fn, List) -> lists:sort(fun(A, B) -> (Fn(A))(B) =< 0 end, List). +index_not_found() -> + erlang:error(#{ + exn_type => key_not_found_exception, + message => <<"An index satisfying the predicate was not found in the collection.">> + }). + find(Fn, List) -> case lists:dropwhile(fun(E) -> not Fn(E) end, List) of [H | _] -> H; - [] -> erlang:error(<<"key_not_found">>) + [] -> index_not_found() end. try_find(Fn, List) -> @@ -233,7 +257,7 @@ find_index(Fn, [H | T], I) -> false -> find_index(Fn, T, I + 1) end; find_index(_Fn, [], _I) -> - erlang:error(<<"key_not_found">>). + index_not_found(). try_find_index(Fn, List) -> try_find_index(Fn, List, 0). @@ -271,9 +295,61 @@ sum_by(Fn, List) -> min_by(Fn, List) -> element(2, lists:min(lists:map(fun(E) -> {Fn(E), E} end, List))). +min_by(Fn, Comparer, [Head | Tail]) -> + {_, Result} = lists:foldl( + fun(Item, {BestKey, Best}) -> + ItemKey = Fn(Item), + case Comparer(ItemKey, BestKey) < 0 of + true -> {ItemKey, Item}; + false -> {BestKey, Best} + end + end, + {Fn(Head), Head}, + Tail + ), + Result. + max_by(Fn, List) -> element(2, lists:max(lists:map(fun(E) -> {Fn(E), E} end, List))). +max_by(Fn, Comparer, [Head | Tail]) -> + {_, Result} = lists:foldl( + fun(Item, {BestKey, Best}) -> + ItemKey = Fn(Item), + case Comparer(ItemKey, BestKey) > 0 of + true -> {ItemKey, Item}; + false -> {BestKey, Best} + end + end, + {Fn(Head), Head}, + Tail + ), + Result. + +min_with(Comparer, [Head | Tail]) -> + lists:foldl( + fun(Item, Best) -> + case Comparer(Item, Best) < 0 of + true -> Item; + false -> Best + end + end, + Head, + Tail + ). + +max_with(Comparer, [Head | Tail]) -> + lists:foldl( + fun(Item, Best) -> + case Comparer(Item, Best) > 0 of + true -> Item; + false -> Best + end + end, + Head, + Tail + ). + indexed(List) -> lists:zip(lists:seq(0, length(List) - 1), List). @@ -323,7 +399,8 @@ try_item(_, _) -> undefined. exactly_one([X]) -> X; -exactly_one(_) -> erlang:error(<<"Sequence contains more than one element">>). +exactly_one([]) -> invalid_arg(<<"source">>, <<"The input sequence was empty.">>); +exactly_one(_) -> invalid_arg(<<"source">>, <<"The input sequence contains more than one element.">>). try_exactly_one([X]) -> fable_option:some(X); try_exactly_one(_) -> undefined. @@ -353,11 +430,37 @@ pairwise([]) -> []; pairwise([_]) -> []; pairwise([A, B | T]) -> [{A, B} | pairwise([B | T])]. -exists2(Fn, L1, L2) -> - lists:any(fun({A, B}) -> (Fn(A))(B) end, lists:zip(L1, L2)). - -forall2(Fn, L1, L2) -> - lists:all(fun({A, B}) -> (Fn(A))(B) end, lists:zip(L1, L2)). +exists2(Fn, [A | T1], [B | T2]) -> + case (Fn(A))(B) of + true -> true; + false -> exists2(Fn, T1, T2) + end; +exists2(_Fn, [], []) -> + false; +exists2(_Fn, _L1, _L2) -> + lists_had_different_lengths(<<"list2">>). + +forall2(Fn, [A | T1], [B | T2]) -> + case (Fn(A))(B) of + true -> forall2(Fn, T1, T2); + false -> false + end; +forall2(_Fn, [], []) -> + true; +forall2(_Fn, [], _L2) -> + lists_had_different_lengths(<<"list1">>); +forall2(_Fn, _L1, []) -> + lists_had_different_lengths(<<"list2">>). + +array_exists2(Fn, A1, A2) when length(A1) =:= length(A2) -> + exists2(Fn, A1, A2); +array_exists2(_Fn, _A1, _A2) -> + invalid_arg(<<"array1">>, <<"The arrays have different lengths.">>). + +array_forall2(Fn, A1, A2) when length(A1) =:= length(A2) -> + forall2(Fn, A1, A2); +array_forall2(_Fn, _A1, _A2) -> + invalid_arg(<<"array1">>, <<"The arrays have different lengths.">>). map2(Fn, L1, L2) -> lists:zipwith(fun(A, B) -> (Fn(A))(B) end, L1, L2). @@ -436,6 +539,8 @@ unfold(Fn, State) -> split_at(Index, List) -> {lists:sublist(List, Index), lists:nthtail(Index, List)}. +chunk_by_size(Size, _List) when Size < 1 -> + invalid_arg(<<"chunkSize">>, <<"The input must be positive.">>); chunk_by_size(Size, List) -> chunk_by_size(Size, List, []). chunk_by_size(_Size, [], Acc) -> @@ -448,6 +553,8 @@ chunk_by_size(Size, List, Acc) -> end, chunk_by_size(Size, Rest, [Chunk | Acc]). +windowed(Size, _List) when Size < 1 -> + invalid_arg(<<"windowSize">>, <<"The input must be positive.">>); windowed(Size, List) -> windowed(Size, List, []). windowed(Size, List, Acc) when length(List) < Size -> lists:reverse(Acc); @@ -455,6 +562,10 @@ windowed(Size, [_ | T] = List, Acc) -> Window = lists:sublist(List, Size), windowed(Size, T, [Window | Acc]). +split_into(Count, _List) when Count < 1 -> + invalid_arg(<<"count">>, <<"The input must be positive.">>); +split_into(_Count, []) -> + []; split_into(Count, List) -> Len = length(List), ActualCount = min(Count, Len), @@ -516,7 +627,7 @@ map_fold_back(Fn, List, State) -> pick(Fn, List) -> case try_pick(Fn, List) of - undefined -> erlang:error(<<"key_not_found">>); + undefined -> index_not_found(); V -> V end. @@ -530,7 +641,7 @@ try_pick(Fn, [H | T]) -> find_index_back(Fn, List) -> case try_find_index_back(Fn, List) of - undefined -> erlang:error(<<"key_not_found">>); + undefined -> index_not_found(); Idx -> Idx end. @@ -546,12 +657,19 @@ try_find_index_back(Fn, [H | T], Idx) -> transpose([]) -> []; -transpose([[] | _]) -> +transpose([First | Rest] = Lists) -> + Len = length(First), + case lists:all(fun(L) -> length(L) =:= Len end, Rest) of + true -> transpose_rows(Lists); + false -> invalid_arg(<<"array.[0]">>, <<"The arrays have different lengths.">>) + end. + +transpose_rows([[] | _]) -> []; -transpose(Lists) -> +transpose_rows(Lists) -> Heads = [hd(L) || L <- Lists], Tails = [tl(L) || L <- Lists], - [Heads | transpose(Tails)]. + [Heads | transpose_rows(Tails)]. compare_with(_Fn, [], []) -> 0; @@ -605,11 +723,51 @@ get_slice(Lower, Upper, List) -> end, lists:sublist(List, Start + 1, End - Start + 1). +-spec sub(list(), integer(), integer()) -> list(). +sub(_List, StartIndex, _Count) when StartIndex < 0 -> + invalid_arg(<<"startIndex">>, <<"The input must be non-negative.">>); +sub(_List, _StartIndex, Count) when Count < 0 -> + invalid_arg(<<"count">>, <<"The input must be non-negative.">>); +sub(List, 0, Count) -> + case lists:sublist(List, Count) of + Sub when length(Sub) =:= Count -> Sub; + _ -> argument_out_of_range(<<"count">>, <<"The index is outside the legal range.">>) + end; +sub([_ | T], StartIndex, Count) -> + sub(T, StartIndex - 1, Count); +sub([], _StartIndex, _Count) -> + argument_out_of_range(<<"count">>, <<"The index is outside the legal range.">>). + +%% HashSet.CopyTo calls fable_resize_array:blit/5 directly, its count may exceed the set +-spec blit(list(), integer(), list(), integer(), integer()) -> list(). +blit(_Source, _SourceIdx, _Target, _TargetIdx, Count) when Count < 0 -> + argument_out_of_range(<<"length">>, <<"Non-negative number required.">>); +blit(_Source, SourceIdx, _Target, _TargetIdx, _Count) when SourceIdx < 0 -> + argument_out_of_range(<<"sourceIndex">>, <<"Non-negative number required.">>); +blit(Source, SourceIdx, _Target, _TargetIdx, Count) when SourceIdx + Count > length(Source) -> + invalid_arg( + <<"sourceArray">>, + <<"Source array was not long enough. Check the source index, length, and the array's lower bounds.">> + ); +blit(_Source, _SourceIdx, _Target, TargetIdx, _Count) when TargetIdx < 0 -> + argument_out_of_range(<<"destinationIndex">>, <<"Non-negative number required.">>); +blit(_Source, _SourceIdx, Target, TargetIdx, Count) when TargetIdx + Count > length(Target) -> + invalid_arg( + <<"destinationArray">>, + <<"Destination array was not long enough. Check the destination index, length, and the array's lower bounds.">> + ); +blit(Source, SourceIdx, Target, TargetIdx, Count) -> + fable_resize_array:blit(Source, SourceIdx, Target, TargetIdx, Count). + -spec take(non_neg_integer(), list()) -> list(). take(Count, List) -> case erlang:length(List) >= Count of true -> lists:sublist(List, Count); - false -> erlang:error(<<"The input sequence has an insufficient number of elements.">>) + false -> + erlang:error(#{ + exn_type => invalid_operation_exception, + message => <<"The input sequence has an insufficient number of elements.">> + }) end. %% ---- Random functions -------------------------------------------------------- @@ -621,7 +779,7 @@ take(Count, List) -> -spec random_shuffle_by(fun((ok) -> float()), list()) -> list(). random_shuffle_by(Randomizer, Xs) -> - shuffle_loop(Randomizer, erlang:list_to_tuple(Xs), erlang:length(Xs) - 1). + shuffle_loop(Randomizer, erlang:list_to_tuple(Xs), erlang:length(Xs), 0). -spec random_shuffle_with(reference(), list()) -> list(). random_shuffle_with(Random, Xs) -> @@ -633,16 +791,9 @@ random_shuffle(Xs) -> -spec random_choice_by(fun((ok) -> float()), list()) -> term(). random_choice_by(_Randomizer, []) -> - erlang:error(<<"The input sequence was empty.">>); + invalid_arg(<<"source">>, <<"The input sequence was empty.">>); random_choice_by(Randomizer, Xs) -> - Len = erlang:length(Xs), - R = Randomizer(ok), - if - R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); - true -> - lists:nth(erlang:trunc(R * Len) + 1, Xs) - end. + lists:nth(random_next(Randomizer, 0, erlang:length(Xs)) + 1, Xs). -spec random_choice_with(reference(), list()) -> term(). random_choice_with(Random, Xs) -> @@ -656,9 +807,9 @@ random_choice(Xs) -> random_choices_by(Randomizer, Count, Xs) -> if Count < 0 -> - erlang:error(<<"The input must be non-negative.">>); + negative_count(Count); Count > 0 andalso Xs =:= [] -> - erlang:error(<<"The input sequence was empty.">>); + invalid_arg(<<"source">>, <<"The input sequence was empty.">>); true -> Arr = erlang:list_to_tuple(Xs), Len = erlang:tuple_size(Arr), @@ -669,14 +820,8 @@ random_choices_by(Randomizer, Count, Xs) -> choices_loop(_Randomizer, _Arr, _Len, 0, Acc) -> lists:reverse(Acc); choices_loop(Randomizer, Arr, Len, N, Acc) -> - R = Randomizer(ok), - if - R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); - true -> - E = erlang:element(erlang:trunc(R * Len) + 1, Arr), - choices_loop(Randomizer, Arr, Len, N - 1, [E | Acc]) - end. + E = erlang:element(random_next(Randomizer, 0, Len) + 1, Arr), + choices_loop(Randomizer, Arr, Len, N - 1, [E | Acc]). -spec random_choices_with(reference(), non_neg_integer(), list()) -> list(). random_choices_with(Random, Count, Xs) -> @@ -690,16 +835,20 @@ random_choices(Count, Xs) -> random_sample_by(Randomizer, Count, Xs) -> if Count < 0 -> - erlang:error(<<"The input must be non-negative.">>); + negative_count(Count); true -> Len = erlang:length(Xs), if Len =:= 0 andalso Count > 0 -> - erlang:error(<<"The input sequence was empty.">>); + invalid_arg(<<"source">>, <<"The input sequence was empty.">>); Count > Len -> - erlang:error(<<"The input sequence has an insufficient number of elements.">>); + invalid_arg(<<"count">>, <<"The input sequence has an insufficient number of elements.">>); true -> - sample_loop(Randomizer, erlang:list_to_tuple(Xs), Len, Count, 0) + Arr = erlang:list_to_tuple(Xs), + case Len =< max_set_size(Count) of + true -> sample_pool(Randomizer, Arr, Len, Count, 0, []); + false -> sample_set(Randomizer, Arr, Len, Count, #{}, []) + end end end. @@ -711,34 +860,80 @@ random_sample_with(Random, Count, Xs) -> random_sample(Count, Xs) -> random_sample_with(fable_random:new(), Count, Xs). -%% Partial Fisher-Yates: after Count swaps the first Count positions hold the sample. -sample_loop(_Randomizer, Arr, _Len, Count, I) when I >= Count -> - lists:sublist(erlang:tuple_to_list(Arr), Count); -sample_loop(Randomizer, Arr, Len, Count, I) -> - R = Randomizer(ok), - if - R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); +%% FSharp.Core's sampling: a shrinking pool for small inputs, otherwise redraw seen indices. +max_set_size(Count) when Count > 5 -> + 21 + erlang:trunc(math:pow(4, math:ceil(math:log(Count * 3) / math:log(4)))); +max_set_size(_Count) -> + 21. + +sample_pool(_Randomizer, _Pool, _Len, Count, I, Acc) when I >= Count -> + lists:reverse(Acc); +sample_pool(Randomizer, Pool, Len, Count, I, Acc) -> + J = random_next(Randomizer, 0, Len - I), + Item = erlang:element(J + 1, Pool), + Pool2 = erlang:setelement(J + 1, Pool, erlang:element(Len - I, Pool)), + sample_pool(Randomizer, Pool2, Len, Count, I + 1, [Item | Acc]). + +sample_set(_Randomizer, _Arr, _Len, 0, _Selected, Acc) -> + lists:reverse(Acc); +sample_set(Randomizer, Arr, Len, N, Selected, Acc) -> + J = random_next(Randomizer, 0, Len), + case maps:is_key(J, Selected) of true -> - J = I + erlang:trunc(R * (Len - I)), - EI = erlang:element(I + 1, Arr), - EJ = erlang:element(J + 1, Arr), - Arr2 = erlang:setelement(I + 1, erlang:setelement(J + 1, Arr, EI), EJ), - sample_loop(Randomizer, Arr2, Len, Count, I + 1) + sample_set(Randomizer, Arr, Len, N, Selected, Acc); + false -> + Item = erlang:element(J + 1, Arr), + sample_set(Randomizer, Arr, Len, N - 1, Selected#{J => true}, [Item | Acc]) end. -%% Full Fisher-Yates shuffle: builds result from index Len-1 downto 1. -shuffle_loop(_Randomizer, Arr, I) when I =< 0 -> +%% FSharp.Core's forward Fisher-Yates shuffle. +shuffle_loop(_Randomizer, Arr, Len, I) when I >= Len - 1 -> erlang:tuple_to_list(Arr); -shuffle_loop(Randomizer, Arr, I) -> +shuffle_loop(Randomizer, Arr, Len, I) -> + J = random_next(Randomizer, I, Len), + EI = erlang:element(I + 1, Arr), + EJ = erlang:element(J + 1, Arr), + Arr2 = erlang:setelement(I + 1, erlang:setelement(J + 1, Arr, EI), EJ), + shuffle_loop(Randomizer, Arr2, Len, I + 1). + +%% Like FSharp.Core's Random.next: an index in [Min, Max) from a randomizer result in [0.0, 1.0). +random_next(Randomizer, Min, Max) -> R = Randomizer(ok), if R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); + randomizer_out_of_range(R); true -> - J = erlang:trunc(R * (I + 1)), - EI = erlang:element(I + 1, Arr), - EJ = erlang:element(J + 1, Arr), - Arr2 = erlang:setelement(I + 1, erlang:setelement(J + 1, Arr, EI), EJ), - shuffle_loop(Randomizer, Arr2, I - 1) + erlang:trunc(R * (Max - Min)) + Min end. + +negative_count(Count) -> + invalid_arg(<<"count">>, <<"The input must be non-negative.\ncount = ", (erlang:integer_to_binary(Count))/binary>>). + +randomizer_out_of_range(R) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => iolist_to_binary( + io_lib:format( + "The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0). (Parameter 'randomizer')", + [R] + ) + ), + param_name => <<"randomizer">> + }). + +invalid_arg(ParamName, Message) -> + erlang:error(#{ + exn_type => argument_exception, + message => fable_utils:argument_exception_message(Message, ParamName), + param_name => ParamName + }). + +argument_out_of_range(ParamName, Message) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => fable_utils:argument_exception_message(Message, ParamName), + param_name => ParamName + }). + +lists_had_different_lengths(ParamName) -> + invalid_arg(ParamName, <<"The lists had different lengths.">>). diff --git a/src/fable-library-beam/fable_map.erl b/src/fable-library-beam/fable_map.erl index d5f257ea4c..7ab10c6338 100644 --- a/src/fable-library-beam/fable_map.erl +++ b/src/fable-library-beam/fable_map.erl @@ -74,7 +74,7 @@ iterate(Fn, Map) -> find_key(Fn, Map) -> case lists:dropwhile(fun({K, V}) -> not (Fn(K))(V) end, maps:to_list(Map)) of [{K, _} | _] -> K; - [] -> erlang:error(<<"key_not_found">>) + [] -> key_not_found(<<"An index satisfying the predicate was not found in the collection.">>) end. try_find_key(Fn, Map) -> @@ -109,7 +109,7 @@ try_get_value(Key, Map, OutRef) -> pick(Fn, Map) -> case try_pick(Fn, Map) of - undefined -> erlang:error(<<"key_not_found">>); + undefined -> key_not_found(<<"The given key was not present in the dictionary.">>); V -> V end. @@ -142,3 +142,9 @@ change(Key, Fn, Map) -> NewV -> maps:put(Key, NewV, Map) end end. + +key_not_found(Message) -> + erlang:error(#{ + exn_type => key_not_found_exception, + message => Message + }). diff --git a/src/fable-library-beam/fable_quotation.erl b/src/fable-library-beam/fable_quotation.erl index 0abef8bb2a..4aa4d5c955 100644 --- a/src/fable-library-beam/fable_quotation.erl +++ b/src/fable-library-beam/fable_quotation.erl @@ -12,6 +12,7 @@ is_let/1, is_if_then_else/1, is_call/1, is_sequential/1, is_new_tuple/1, is_new_union_case/1, is_new_record/1, is_tuple_get/1, is_field_get/1, + is_and_also/1, is_or_else/1, is_specific_call/2, evaluate/1, expr_to_string/1, get_free_vars/1, substitute/2 ]). @@ -311,3 +312,41 @@ sub({expr, if_then_else, Guard, Then, Else}, Fn) -> sub({expr, sequential, First, Second}, Fn) -> {expr, sequential, sub(First, Fn), sub(Second, Fn)}; sub(E, _Fn) -> E. + +%% =================================================================== +%% DerivedPatterns +%% F# defines these on top of Patterns. AndAlso and OrElse recover the shape +%% `&&` and `||` desugar into; SpecificCall matches a call by the identity of a +%% template quotation rather than by compiled name. +%% =================================================================== + +%% a andalso b ==> if a then b else false +is_and_also({expr, if_then_else, G, T, {expr, value, false, _}}) -> {G, T}; +is_and_also(_) -> undefined. + +%% a orelse b ==> if a then true else b +is_or_else({expr, if_then_else, G, {expr, value, true, _}, E}) -> {G, E}; +is_or_else(_) -> undefined. + +%% A template such as <@ (=) @> quotes as nested lambdas around the call, so the +%% identifying Call node has to be dug out before comparing. +template_call({expr, lambda, _, Body}) -> template_call(Body); +template_call({expr, call, _, M, _, DT}) -> {M, DT}; +template_call(_) -> undefined. + +%% SpecificCall: returns {Instance, GenericArgs, Args}. The middle slot is F#'s +%% generic-argument list; this runtime models types as names rather than +%% System.Type, so it is always empty and callers match it with a wildcard. +%% Takes both arguments; the partial application FSharp.Core expresses by +%% currying is built by the replacement instead. +is_specific_call(Template, {expr, call, I, M, A, DT}) -> + case template_call(Template) of + {M, DT} -> + Instance = case I of + {expr, value, _, <<"novalue">>} -> undefined; + _ -> I + end, + {Instance, [], deref(A)}; + _ -> undefined + end; +is_specific_call(_, _) -> undefined. diff --git a/src/fable-library-beam/fable_random.erl b/src/fable-library-beam/fable_random.erl index dd6e1be1e0..92390ed102 100644 --- a/src/fable-library-beam/fable_random.erl +++ b/src/fable-library-beam/fable_random.erl @@ -3,6 +3,7 @@ -export([ new/0, new_seeded/1, + shared/0, next/1, next/2, next/3, @@ -27,6 +28,18 @@ new_seeded(Seed) -> put(Ref, rand:seed_s(exsss, Seed)), Ref. +%% Random.Shared: one unseeded instance per process. +-spec shared() -> reference(). +shared() -> + case get(fable_random_shared) of + undefined -> + Ref = new(), + put(fable_random_shared, Ref), + Ref; + Ref -> + Ref + end. + %% Random.Next() — returns a non-negative integer less than Int32.MaxValue. -spec next(reference()) -> non_neg_integer(). next(Ref) -> @@ -36,24 +49,29 @@ next(Ref) -> %% Random.Next(maxValue) — returns a non-negative integer less than maxValue. -spec next(reference(), integer()) -> non_neg_integer(). +next(_Ref, MaxValue) when MaxValue < 0 -> + out_of_range( + <<"maxValue">>, + <<"maxValue ('", (erlang:integer_to_binary(MaxValue))/binary, "') must be a non-negative value.">> + ); next(_Ref, 0) -> 0; -next(Ref, MaxValue) when MaxValue > 0 -> +next(Ref, MaxValue) -> {Val, NewState} = rand:uniform_s(MaxValue, get(Ref)), put(Ref, NewState), - Val - 1; -next(_, _) -> - erlang:error(badarg). + Val - 1. %% Random.Next(minValue, maxValue) — returns an integer in [minValue, maxValue). -spec next(reference(), integer(), integer()) -> integer(). -next(Ref, MinValue, MaxValue) when MaxValue > MinValue -> +next(_Ref, MinValue, MaxValue) when MinValue > MaxValue -> + out_of_range(<<"minValue">>, <<"'minValue' cannot be greater than maxValue.">>); +next(_Ref, MinValue, MinValue) -> + MinValue; +next(Ref, MinValue, MaxValue) -> Range = MaxValue - MinValue, {Val, NewState} = rand:uniform_s(Range, get(Ref)), put(Ref, NewState), - MinValue + Val - 1; -next(_, _, _) -> - erlang:error(badarg). + MinValue + Val - 1. %% Random.NextDouble() — returns a float in [0.0, 1.0). -spec next_double(reference()) -> float(). @@ -76,3 +94,9 @@ fill_random_bytes(Ref, AtomicsRef, Remaining, Idx) -> put(Ref, NewState), atomics:put(AtomicsRef, Idx, Val - 1), fill_random_bytes(Ref, AtomicsRef, Remaining - 1, Idx + 1). + +out_of_range(ParamName, Message) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => fable_utils:argument_exception_message(Message, ParamName) + }). diff --git a/src/fable-library-beam/fable_regex.erl b/src/fable-library-beam/fable_regex.erl index f51e10b7ca..e01420008a 100644 --- a/src/fable-library-beam/fable_regex.erl +++ b/src/fable-library-beam/fable_regex.erl @@ -450,6 +450,10 @@ get_options(#{options := O}) -> O. %% --- Internal helpers --- +options_to_erl(Options) when is_integer(Options), Options band bnot (1 bor 2 bor 8 bor 16 bor 256 bor 1024) =/= 0 -> + erlang:error( + <<"RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline, ECMAScript and NonBacktracking">> + ); options_to_erl(Options) when is_integer(Options) -> Flags = [], %% IgnoreCase diff --git a/src/fable-library-beam/fable_resize_array.erl b/src/fable-library-beam/fable_resize_array.erl index ec594f894c..ec6335786d 100644 --- a/src/fable-library-beam/fable_resize_array.erl +++ b/src/fable-library-beam/fable_resize_array.erl @@ -9,6 +9,9 @@ insert_range/3, get_range/3, index_of/2, index_of/3, + binary_search/3, + binary_search_range/5, + binary_search_range_count/5, find/3, find_last/2, find_all/2, @@ -32,6 +35,9 @@ -spec get_range(list(), non_neg_integer(), non_neg_integer()) -> list(). -spec index_of(list(), term()) -> integer(). -spec index_of(list(), term(), non_neg_integer()) -> integer(). +-spec binary_search(list(), term(), map() | reference()) -> integer(). +-spec binary_search_range(list(), integer(), integer(), term(), map() | reference()) -> integer(). +-spec binary_search_range_count(list(), integer(), integer(), term(), map() | reference()) -> integer(). -spec find(fun(), list(), term()) -> term(). -spec find_last(list(), fun()) -> term(). -spec find_all(list(), fun()) -> list(). @@ -117,6 +123,55 @@ index_of_impl([], _Item, _Idx) -> -1; index_of_impl([Item | _], Item, Idx) -> Idx; index_of_impl([_ | Rest], Item, Idx) -> index_of_impl(Rest, Item, Idx + 1). +%% BinarySearch with an IComparer, probing in .NET's order so duplicates and comparers get .NET's +%% answers; a miss returns the complement of the index of the first larger element. +binary_search(List, Value, Comparer) -> + Compare = fable_utils:iface_get(compare, Comparer), + binary_search_impl(list_to_tuple(List), 0, length(List) - 1, Value, Compare). + +%% System.Array.BinarySearch(array, index, length, value, comparer) +binary_search_range(List, Index, Length, Value, Comparer) -> + binary_search_range(List, Index, Length, Value, Comparer, <<"length">>). + +%% List.BinarySearch(index, count, item, comparer), whose count .NET calls "count" +binary_search_range_count(List, Index, Count, Item, Comparer) -> + binary_search_range(List, Index, Count, Item, Comparer, <<"count">>). + +%% .NET checks the range in this order before searching it +binary_search_range(_List, Index, _Count, _Value, _Comparer, _CountName) when Index < 0 -> + non_negative_number_required(<<"index">>); +binary_search_range(_List, _Index, Count, _Value, _Comparer, CountName) when Count < 0 -> + non_negative_number_required(CountName); +binary_search_range(List, Index, Count, _Value, _Comparer, _CountName) + when length(List) - Index < Count -> + erlang:error(#{ + exn_type => argument_exception, + message => + <<"Offset and length were out of bounds for the array or count is greater than the " + "number of elements from index to the end of the source collection.">> + }); +binary_search_range(List, Index, Count, Value, Comparer, _CountName) -> + Compare = fable_utils:iface_get(compare, Comparer), + binary_search_impl(list_to_tuple(List), Index, Index + Count - 1, Value, Compare). + +binary_search_impl(Tuple, Lo, Hi, Value, Compare) when Lo =< Hi -> + I = Lo + ((Hi - Lo) bsr 1), + case Compare(element(I + 1, Tuple), Value) of + 0 -> I; + C when C < 0 -> binary_search_impl(Tuple, I + 1, Hi, Value, Compare); + _ -> binary_search_impl(Tuple, Lo, I - 1, Value, Compare) + end; +binary_search_impl(_Tuple, Lo, _Hi, _Value, _Compare) -> + bnot Lo. + +non_negative_number_required(ParamName) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => + fable_utils:argument_exception_message(<<"Non-negative number required.">>, ParamName), + param_name => ParamName + }). + %% Find first matching predicate, return Default when not found find(_Pred, [], Default) -> Default; diff --git a/src/fable-library-beam/fable_string.erl b/src/fable-library-beam/fable_string.erl index b639de5706..933b72fb51 100644 --- a/src/fable-library-beam/fable_string.erl +++ b/src/fable-library-beam/fable_string.erl @@ -39,7 +39,7 @@ split_remove_empty/2, split_with_count/3, to_string/1, - printf/1, + printf/1, printf/2, to_text/1, to_text/2, to_text/3, to_text/4, to_text/5, to_console/1, to_console/2, to_console/3, to_console/4, to_console/5, to_console_error/1, to_console_error/2, to_console_error/3, @@ -103,6 +103,7 @@ -spec split_with_count(binary(), binary() | integer() | list(), non_neg_integer()) -> list(). -spec to_string(term()) -> binary(). -spec printf(binary()) -> map(). +-spec printf(binary(), list()) -> map(). -spec to_text(map() | binary()) -> binary() | fun(). -spec to_text(map() | binary(), term()) -> binary() | fun(). -spec to_text(map() | binary(), term(), term()) -> binary() | fun(). @@ -384,14 +385,14 @@ string_ctor_chars_range(Chars, Start, Len) -> normalize_sep(Sep) when is_integer(Sep) -> <>; normalize_sep(Sep) -> Sep. -%% Normalize separator that may be a list of chars to a list of binaries +%% decision: shares separator normalization across split overloads so char codes never reach binary:split normalize_sep_list(Sep) when is_integer(Sep) -> <>; normalize_sep_list(Sep) when is_binary(Sep) -> Sep; normalize_sep_list(Sep) when is_list(Sep) -> - [<> || C <- Sep]. + [normalize_sep(C) || C <- Sep]. split(Str, Sep) -> - binary:split(Str, normalize_sep(Sep), [global]). + binary:split(Str, normalize_sep_list(Sep), [global]). split(Str, Sep, 0) -> split(Str, Sep); @@ -401,7 +402,7 @@ split(Str, Sep, _Options) -> split(Str, Sep). split_remove_empty(Str, Seps) -> - Parts = binary:split(Str, normalize_sep(Seps), [global]), + Parts = binary:split(Str, normalize_sep_list(Seps), [global]), [P || P <- Parts, P =/= <<>>]. split_with_count(Str, Seps, Count) -> @@ -438,10 +439,14 @@ to_string(V) -> %% Printf / sprintf / String.Format support %% ===================================================================== -%% printf/1 — Parse an F# format string and return a format object (map). +%% printf/1,2 — Parse an F# format string and return a format object (map). +%% The formatter list preserves statically known custom ToString converters for %O arguments. %% The format object has: input (original string), cont (function accepting a continuation). printf(Str) -> - Printer = fs_format(Str), + printf(Str, []). + +printf(Str, Formatters) -> + Printer = fs_format(Str, Formatters), #{input => Str, cont => Printer}. %% apply_curried/2 — Apply a list of args one at a time to a curried function. @@ -801,11 +806,11 @@ pad_zeros(Bin, Width) -> %% Internal: F# format string parser %% ===================================================================== -%% fs_format/1 — Parse F# format string like "%d %s %.2f" and return +%% fs_format/2 — Parse F# format string like "%d %s %.2f" and return %% fun(Cont) -> fun(Arg1) -> fun(Arg2) -> ... -> Cont(Result) end end end -fs_format(Str) -> +fs_format(Str, Formatters) -> {Parts, Specs} = parse_format(Str, <<>>, [], []), - create_printer(Parts, Specs). + create_printer(Parts, Specs, Formatters). %% parse_format/4 — Split format string into literal parts and format specifiers. %% Returns {[binary()], [string()]} where Parts has one more element than Specs. @@ -845,30 +850,43 @@ parse_spec_precision(<>, Acc) when C >= $0, C =< $9 -> parse_spec_precision(<>, Acc) -> {[C | Acc], Rest}. -%% create_printer/2 — Build nested curried functions from parts and specifiers. +%% create_printer/3 — Build nested curried functions from parts and specifiers. %% Parts = [P0, P1, P2, ...], Specs = [S1, S2, ...] %% Result: fun(Cont) -> if no specs, Cont(P0); else fun(Arg1) -> ... end end -create_printer([SinglePart], []) -> +create_printer([SinglePart], [], _Formatters) -> %% No format specifiers — just a plain string fun(Cont) -> Cont(SinglePart) end; -create_printer(Parts, Specs) -> +create_printer(Parts, Specs, Formatters) -> fun(Cont) -> - build_curried(Parts, Specs, Cont, <<>>) + build_curried(Parts, Specs, Formatters, Cont, <<>>) end. -build_curried([Part], [], Cont, Acc) -> +build_curried([Part], [], _Formatters, Cont, Acc) -> Cont(<>); -build_curried([Part | RestParts], [Spec | RestSpecs], Cont, Acc) -> +build_curried([Part | RestParts], [Spec | RestSpecs], [Formatter | RestFormatters], Cont, Acc) -> + NewAcc = <>, + fun(Arg) -> + Formatted = format_value(Spec, Arg, Formatter), + build_curried(RestParts, RestSpecs, RestFormatters, Cont, <>) + end; +build_curried([Part | RestParts], [Spec | RestSpecs], [], Cont, Acc) -> NewAcc = <>, fun(Arg) -> - Formatted = format_value(Spec, Arg), - build_curried(RestParts, RestSpecs, Cont, <>) + Formatted = format_value(Spec, Arg, undefined), + build_curried(RestParts, RestSpecs, [], Cont, <>) end. -%% format_value/2 — Format a single value according to a format specifier string. +%% format_value/2,3 — Format a single value according to a format specifier string. format_value(Spec, Value) -> + format_value(Spec, Value, undefined). + +format_value(Spec, Value, Formatter) -> {Flags, Width, Prec, Type} = parse_spec_parts(Spec), - Raw = format_raw(Type, Prec, Value), + Raw = + case Type =:= $O andalso is_function(Formatter, 1) of + true -> Formatter(Value); + false -> format_raw(Type, Prec, Value) + end, WithSign = apply_sign_flag(Flags, Raw, Value), apply_width(Flags, Width, WithSign). diff --git a/src/fable-library-beam/fable_utils.erl b/src/fable-library-beam/fable_utils.erl index 0f2ac63b7e..6d5c72ac1b 100644 --- a/src/fable-library-beam/fable_utils.erl +++ b/src/fable-library-beam/fable_utils.erl @@ -24,6 +24,10 @@ using/2, to_list/1, enumerate_to_list/1, + aggregate_exception/2, + argument_exception_message/2, + exception_message/1, + is_exception_type/2, new_byte_array/1, new_byte_array_zeroed/1, new_byte_array_filled/2, @@ -63,6 +67,10 @@ -spec using(term(), fun()) -> term(). -spec to_list(term()) -> list(). -spec enumerate_to_list(term()) -> list(). +-spec aggregate_exception(binary(), term()) -> map(). +-spec argument_exception_message(binary(), binary() | undefined) -> binary(). +-spec exception_message(term()) -> binary(). +-spec is_exception_type(term(), atom()) -> boolean(). -spec new_byte_array(list() | binary() | tuple()) -> tuple(). -spec new_byte_array_zeroed(non_neg_integer()) -> tuple(). -spec new_byte_array_filled(non_neg_integer(), non_neg_integer()) -> tuple(). @@ -451,6 +459,83 @@ to_list(V) when is_binary(V) -> to_list(V) -> V. +%% AggregateException keeps the normal BEAM exception-map representation. Its exception +%% sequence may be a params array ref, a list, or any other Fable enumerable. +aggregate_exception(BaseMessage, ExceptionsValue) -> + Exceptions = to_list(ExceptionsValue), + InnerException = + case Exceptions of + [First | _] -> First; + [] -> undefined + end, + Message = iolist_to_binary([ + BaseMessage + | [[<<" (">>, exception_message(Exception), <<")">>] || Exception <- Exceptions] + ]), + #{ + exn_type => aggregate_exception, + message => Message, + inner_exception => InnerException, + inner_exceptions => new_ref(Exceptions) + }. + +argument_exception_message(Message, undefined) -> + Message; +argument_exception_message(Message, <<>>) -> + Message; +argument_exception_message(Message, ParamName) -> + <>. + +%% Built-in .NET exceptions carry a compact nominal tag. Follow the .NET exception +%% hierarchy here so a catch for a base exception also accepts its derived exceptions. +is_exception_type(Exception, Expected) when is_reference(Exception) -> + is_exception_type(get(Exception), Expected); +is_exception_type(#{exn_type := Actual}, Expected) -> + exception_type_is(Actual, Expected); +is_exception_type(#{message := _}, exception) -> + true; +is_exception_type(_, _) -> + false. + +exception_type_is(Type, Type) -> true; +exception_type_is(_, exception) -> true; +exception_type_is(application_exception, Expected) -> exception_type_is(exception, Expected); +exception_type_is(argument_exception, Expected) -> exception_type_is(system_exception, Expected); +exception_type_is(argument_null_exception, Expected) -> + exception_type_is(argument_exception, Expected); +exception_type_is(argument_out_of_range_exception, Expected) -> + exception_type_is(argument_exception, Expected); +exception_type_is(arithmetic_exception, Expected) -> exception_type_is(system_exception, Expected); +exception_type_is(divide_by_zero_exception, Expected) -> + exception_type_is(arithmetic_exception, Expected); +exception_type_is(not_finite_number_exception, Expected) -> + exception_type_is(arithmetic_exception, Expected); +exception_type_is(overflow_exception, Expected) -> exception_type_is(arithmetic_exception, Expected); +exception_type_is(aggregate_exception, Expected) -> exception_type_is(exception, Expected); +exception_type_is(key_not_found_exception, Expected) -> exception_type_is(system_exception, Expected); +exception_type_is(operation_canceled_exception, Expected) -> + exception_type_is(system_exception, Expected); +exception_type_is(Type, Expected) + when Type =:= format_exception; + Type =:= index_out_of_range_exception; + Type =:= invalid_operation_exception; + Type =:= not_implemented_exception; + Type =:= not_supported_exception; + Type =:= null_reference_exception; + Type =:= out_of_memory_exception; + Type =:= rank_exception; + Type =:= stack_overflow_exception; + Type =:= timeout_exception -> + exception_type_is(system_exception, Expected); +exception_type_is(system_exception, Expected) -> exception_type_is(exception, Expected); +exception_type_is(_, _) -> false. + +%% decision: formats message metadata at the read boundary so exception payload fields retain their original types +%% invariant: Message access and aggregate formatting return binaries for arbitrary F# exception payloads +exception_message(#{message := Message}) -> format_term(Message); +exception_message(Exception) when is_reference(Exception) -> exception_message(get(Exception)); +exception_message(Exception) -> format_term(Exception). + %% Enumerate a lazy seq object to a plain list using get_enumerator/move_next/get_current. enumerate_to_list(Seq) -> E = get_enumerator(Seq), diff --git a/src/fable-library-dart/Array.fs b/src/fable-library-dart/Array.fs index 447fd3cdb6..50cf4e305f 100644 --- a/src/fable-library-dart/Array.fs +++ b/src/fable-library-dart/Array.fs @@ -101,9 +101,10 @@ type Native = static member firstWhere(xs: 'T[], predicate: 'T -> bool) : 'T = jsNative let private indexNotFound () = - failwith "An index satisfying the predicate was not found in the collection." + raise (KeyNotFoundException(SR.keyNotFoundAlt)) -let private differentLengths () = failwith "Arrays had different lengths" +let private differentLengths (paramName: string) = + invalidArg paramName "The arrays have different lengths." // https://stackoverflow.com/a/9113136 let reverseInPlace (xs: 'T[]) : unit = @@ -142,12 +143,36 @@ let filter (predicate: 'T -> bool) (array: 'T[]) = Native.where predicate array // intentionally returns target instead of unit let fill (target: 'T[]) (targetIndex: int) (count: int) (value: 'T) : 'T[] = - Native.fillRange target targetIndex (targetIndex + count) value + if targetIndex < 0 then + invalidArg "targetIndex" SR.inputMustBeNonNegative + + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + // .NET fills up to the end of the array before raising, this checks first + if count > 0 then + if targetIndex + count > target.Length then + raise (System.IndexOutOfRangeException(SR.Arg_IndexOutOfRangeException)) + + Native.fillRange target targetIndex (targetIndex + count) value + target let getSubArray (array: 'T[]) (start: int) (count: int) : 'T[] = Native.sublist (array, start, start + count) +let sub (array: 'T[]) (startIndex: int) (count: int) : 'T[] = + if startIndex < 0 then + invalidArg "startIndex" SR.inputMustBeNonNegative + + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + if startIndex + count > array.Length then + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) + + getSubArray array startIndex count + let last (array: 'T[]) = if Array.isEmpty array then invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString @@ -168,25 +193,25 @@ let map (f: 'T -> 'U) (source: 'T[]) : 'U[] = let mapIndexed2 (f: int -> 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" Native.generate source1.Length (fun i -> f i source1[i] source2[i]) let map2 (f: 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" Native.generate source1.Length (fun i -> f source1[i] source2[i]) let mapIndexed3 (f: int -> 'T1 -> 'T2 -> 'T3 -> 'U) (source1: 'T1[]) (source2: 'T2[]) (source3: 'T3[]) : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" Native.generate source1.Length (fun i -> f i source1[i] source2[i] source3[i]) let map3 (f: 'T1 -> 'T2 -> 'T3 -> 'U) (source1: 'T1[]) (source2: 'T2[]) (source3: 'T3[]) : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" Native.generate source1.Length (fun i -> f source1[i] source2[i] source3[i]) @@ -344,7 +369,7 @@ let scanBack<'T, 'State> (folder: 'T -> 'State -> 'State) (array: 'T[]) (state: let skip count (array: 'T[]) = if count > array.Length then - invalidArg "count" "count is greater than array length" + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) if count = array.Length then Array.empty @@ -367,7 +392,7 @@ let take count (array: 'T[]) = invalidArg "count" LanguagePrimitives.ErrorStrings.InputMustBeNonNegativeString if count > array.Length then - invalidArg "count" "count is greater than array length" + invalidOp SR.notEnoughElements if count = 0 then Array.empty @@ -399,8 +424,26 @@ let removeAllInPlace (predicate: 'T -> bool) (array: 'T[]) : int = Native.removeWhere array predicate len - array.Length -// TODO: Check array lengths let copyTo (source: 'T[]) (sourceIndex: int) (target: 'T[]) (targetIndex: int) (count: int) : unit = + if count < 0 then + raise (System.ArgumentOutOfRangeException("length", SR.Arg_ArgumentOutOfRangeException)) + + if sourceIndex < 0 then + raise (System.ArgumentOutOfRangeException("sourceIndex", SR.Arg_ArgumentOutOfRangeException)) + + if sourceIndex + count > source.Length then + invalidArg + "sourceArray" + "Source array was not long enough. Check the source index, length, and the array's lower bounds." + + if targetIndex < 0 then + raise (System.ArgumentOutOfRangeException("destinationIndex", SR.Arg_ArgumentOutOfRangeException)) + + if targetIndex + count > target.Length then + invalidArg + "destinationArray" + "Destination array was not long enough. Check the destination index, length, and the array's lower bounds." + Native.copyRange (target, targetIndex, source, sourceIndex, sourceIndex + count) let indexOf (array: 'T[]) (item: 'T) (start: int option) (count: int option) : int = @@ -411,6 +454,50 @@ let indexOf (array: 'T[]) (item: 'T) (start: int option) (count: int option) : i | Some count when i >= start + count -> -1 | _ -> i +// Follows .NET's probe order, so duplicates and comparers with side effects give the same results +let private binarySearchFrom (array: 'T[]) (index: int) (length: int) (value: 'T) (comparer: IComparer<'T>) : int = + let rec loop lo hi = + if lo > hi then + ~~~lo + else + let i = lo + ((hi - lo) >>> 1) + let c = comparer.Compare(array[i], value) + + if c = 0 then + i + elif c < 0 then + loop (i + 1) hi + else + loop lo (i - 1) + + loop index (index + length - 1) + +let private checkBinarySearchRange (lengthName: string) (array: 'T[]) (index: int) (length: int) : unit = + if index < 0 then + raise (System.ArgumentOutOfRangeException("index", "Non-negative number required.")) + + if length < 0 then + raise (System.ArgumentOutOfRangeException(lengthName, "Non-negative number required.")) + + if array.Length - index < length then + raise ( + System.ArgumentException( + "Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection." + ) + ) + +let binarySearch (array: 'T[]) (value: 'T) (comparer: IComparer<'T>) : int = + binarySearchFrom array 0 array.Length value comparer + +let binarySearchRange (array: 'T[]) (index: int) (length: int) (value: 'T) (comparer: IComparer<'T>) : int = + checkBinarySearchRange "length" array index length + binarySearchFrom array index length value comparer + +// ResizeArray's range overload, which names its length "count" +let binarySearchRangeCount (array: 'T[]) (index: int) (count: int) (value: 'T) (comparer: IComparer<'T>) : int = + checkBinarySearchRange "count" array index count + binarySearchFrom array index count value comparer + let partition (f: 'T -> bool) (source: 'T[]) = let res1 = ResizeArray() let res2 = ResizeArray() @@ -423,7 +510,10 @@ let partition (f: 'T -> bool) (source: 'T[]) = Native.asFixed res1, Native.asFixed res2 -let find (predicate: 'T -> bool) (array: 'T[]) : 'T = Native.firstWhere (array, predicate) +let find (predicate: 'T -> bool) (array: 'T[]) : 'T = + match Native.indexWhere (array, predicate) with + | -1 -> indexNotFound () + | index -> array[index] let tryFind (predicate: 'T -> bool) (array: 'T[]) : 'T option = try @@ -546,14 +636,14 @@ let iterateIndexed (action: int -> 'T -> unit) (array: 'T[]) : unit = let iterate2 (action: 'T1 -> 'T2 -> unit) (array1: 'T1[]) (array2: 'T2[]) : unit = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action array1[i] array2[i] let iterateIndexed2 (action: int -> 'T1 -> 'T2 -> unit) (array1: 'T1[]) (array2: 'T2[]) : unit = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action i array1[i] array2[i] @@ -570,7 +660,7 @@ let permute (f: int -> int) (array: 'T[]) : 'T[] = let j = f i if j < 0 || j >= size then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res[j] <- x checkFlags[j] <- 1 @@ -580,7 +670,7 @@ let permute (f: int -> int) (array: 'T[]) : 'T[] = let isValid = checkFlags |> forAll ((=) 1) if not isValid then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res @@ -678,20 +768,20 @@ let unzip3 (array: ('a * 'b * 'c)[]) : 'a[] * 'b[] * 'c[] = let zip (array1: 'T[]) (array2: 'U[]) : ('T * 'U)[] = // Shorthand version: map2 (fun x y -> x, y) array1 array2 if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" Native.generate array1.Length (fun i -> array1[i], array2[i]) let zip3 (array1: 'T1[]) (array2: 'T2[]) (array3: 'T3[]) : ('T1 * 'T2 * 'T3)[] = // Shorthand version: map3 (fun x y z -> x, y, z) array1 array2 array3 if array1.Length <> array2.Length || array2.Length <> array3.Length then - differentLengths () + differentLengths "array1, array2, array3" Native.generate array1.Length (fun i -> array1[i], array2[i], array3[i]) let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = if chunkSize < 1 then - invalidArg "size" "The input must be positive." + invalidArg "chunkSize" "The input must be positive." if Array.isEmpty array then [| [||] |] @@ -708,9 +798,12 @@ let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = Native.asFixed result let splitAt (index: int) (array: 'T[]) : 'T[] * 'T[] = - if index < 0 || index > array.Length then + if index < 0 then invalidArg "index" SR.indexOutOfBounds + if index > array.Length then + invalidOp SR.notEnoughElements + Native.sublist (array, 0, index), Native.sublist (array, index) // Note that, unlike the `compare` operator, Array.compareWith compares elements first, @@ -822,7 +915,7 @@ let fold2<'T1, 'T2, 'State> let mutable acc = state if array1.Length <> array2.Length then - failwith "Arrays have different lengths" + differentLengths "array1" for i = 0 to array1.Length - 1 do acc <- folder acc array1[i] array2[i] @@ -839,7 +932,7 @@ let foldBack2<'T1, 'T2, 'State> let mutable acc = state if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = array1.Length - 1 downto 0 do acc <- folder array1[i] array2[i] acc @@ -848,12 +941,14 @@ let foldBack2<'T1, 'T2, 'State> let reduce (reduction: 'T -> 'T -> 'T) (array: 'T[]) : 'T = // Dart's native reduce will fail if collection is empty - // if Array.isEmpty array then invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + if Array.isEmpty array then + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString + Native.reduce reduction array let reduceBack (reduction: 'T -> 'T -> 'T) (array: 'T[]) : 'T = if Array.isEmpty array then - invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString let mutable i = array.Length - 1 let mutable state = array[i] @@ -884,7 +979,7 @@ let rec existsOffset2 predicate (array1: _[]) (array2: _[]) index = let rec exists2 predicate (array1: _[]) (array2: _[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" existsOffset2 predicate array1 array2 0 @@ -970,14 +1065,14 @@ let averageBy (projection: 'T -> 'T2) (array: 'T[]) ([] averager: IGener let windowed (windowSize: int) (source: 'T[]) : 'T[][] = if windowSize <= 0 then - failwith "windowSize must be positive" + invalidArg "windowSize" "The input must be positive." let len = Operators.max 0 (source.Length - windowSize + 1) Native.generate len (fun i -> source[i .. i + windowSize - 1]) let splitInto (chunks: int) (array: 'T[]) : 'T[][] = if chunks < 1 then - invalidArg "chunks" "The input must be positive." + invalidArg "count" "The input must be positive." if Array.isEmpty array then [| [||] |] @@ -1017,7 +1112,7 @@ let transpose (arrays: 'T[] seq) : 'T[][] = let lenInner = firstArray.Length if arrays |> forAll (fun a -> a.Length = lenInner) |> not then - differentLengths () + differentLengths "array.[0]" Native.generate lenInner (fun i -> Native.generate len (fun j -> arrays[j][i])) @@ -1076,45 +1171,22 @@ let removeAt (index: int) (xs: 'T[]) : 'T[] = ) let removeManyAt (index: int) (count: int) (xs: 'T[]) : 'T[] = - let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 - - let ys = - xs - |> filter (fun _ -> - i <- i + 1 + let len = xs.Length - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true - ) + // Unlike FSharp.Core, a negative count removes nothing instead of duplicating items + if index < 0 || index > len - count || index > len then + invalidArg "index" "index must be within bounds of the array" - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status + let count = Operators.max 0 count - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" + Native.generate + (len - count) + (fun i -> + if i < index then + xs[i] else - "count" - - invalidArg arg SR.indexOutOfBounds - - ys + xs[i + count] + ) let updateAt (index: int) (y: 'T) (xs: 'T[]) : 'T[] = let len = xs.Length @@ -1131,22 +1203,40 @@ let updateAt (index: int) (y: 'T) (xs: 'T[]) : 'T[] = xs[i] ) -let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = +// Like FSharp.Core's Random.next: rejects results outside [0.0, 1.0), NaN included +let private randomNext (randomizer: unit -> float) (minValue: int) (maxValue: int) : int = + let r = randomizer () + + if not (r >= 0.0 && r < 1.0) then + let msg = + "The index is outside the legal range.\nrandomizer returned " + + string r + + ", should be in range [0.0, 1.0)." + + raise (System.ArgumentOutOfRangeException("randomizer", msg)) + + int (r * float (maxValue - minValue)) + minValue + +let private checkRandomCount (count: int) = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + +let private shuffleInPlace (next: int -> int -> int) (xs: 'T[]) : unit = let len = xs.Length - for i = len - 1 downto 1 do - let r = randomizer () + for i = 0 to len - 2 do + let j = next i len - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + if j <> i then + let tmp = xs[i] + xs[i] <- xs[j] + xs[j] <- tmp - let j = int (r * float (i + 1)) - let tmp = xs[i] - xs[i] <- xs[j] - xs[j] <- tmp +let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = + shuffleInPlace (randomNext randomizer) xs let randomShuffleInPlaceWith (random: System.Random) (xs: 'T[]) : unit = - randomShuffleInPlaceBy (fun () -> random.NextDouble()) xs + shuffleInPlace (fun i n -> random.Next(i, n)) xs let randomShuffleInPlace (xs: 'T[]) : unit = randomShuffleInPlaceWith (System.Random()) xs @@ -1157,21 +1247,17 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T[]) : 'T[] = arr let randomShuffleWith (random: System.Random) (xs: 'T[]) : 'T[] = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = copy xs + randomShuffleInPlaceWith random arr + arr let randomShuffle (xs: 'T[]) : 'T[] = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = if xs.Length = 0 then - invalidArg "source" SR.inputSequenceEmpty - - let len = xs.Length - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + invalidArg "source" "The input array was empty." - xs[int (r * float len)] + xs[randomNext randomizer 0 xs.Length] let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = randomChoiceBy (fun () -> random.NextDouble()) xs @@ -1179,24 +1265,13 @@ let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = let randomChoice (xs: 'T[]) : 'T = randomChoiceWith (System.Random()) xs let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative + checkRandomCount count if count > 0 && xs.Length = 0 then invalidArg "source" SR.inputSequenceEmpty let len = xs.Length - - initialize - count - (fun _ -> - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException - - xs[int (r * float len)] - ) + initialize count (fun _ -> xs[randomNext randomizer 0 len]) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = randomChoicesBy (fun () -> random.NextDouble()) count xs @@ -1204,12 +1279,22 @@ let randomChoicesWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = let randomChoices (count: int) (xs: 'T[]) : 'T[] = randomChoicesWith (System.Random()) count xs -let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative +// FSharp.Core's 21 + 4 ** ceil (log4 (3 * count)), kept to integers +let private getMaxSetSizeForSampling (count: int) = + if count > 5 then + let mutable size = 1 - let arr = copy xs - let len = arr.Length + while size < count * 3 do + size <- size * 4 + + 21 + size + else + 21 + +// Picks from a shrinking pool for small inputs, otherwise draws indices until unseen (FSharp.Core, after CPython) +let private randomSampleWithNext (next: int -> int -> int) (count: int) (xs: 'T[]) : 'T[] = + checkRandomCount count + let len = xs.Length if len = 0 && count > 0 then invalidArg "source" SR.inputSequenceEmpty @@ -1217,21 +1302,33 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = if count > len then invalidArg "count" SR.notEnoughElements - for i = 0 to count - 1 do - let r = randomizer () + let result = getSubArray xs 0 count - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + if len <= getMaxSetSizeForSampling count then + let pool = copy xs - let j = i + int (r * float (len - i)) - let tmp = arr[i] - arr[i] <- arr[j] - arr[j] <- tmp + for i = 0 to count - 1 do + let j = next 0 (len - i) + result[i] <- pool[j] + pool[j] <- pool[len - i - 1] + else + let selected = HashSet() - getSubArray arr 0 count + for i = 0 to count - 1 do + let mutable j = next 0 len + + while not (selected.Add j) do + j <- next 0 len + + result[i] <- xs[j] + + result + +let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = + randomSampleWithNext (randomNext randomizer) count xs let randomSampleWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = - randomSampleBy (fun () -> random.NextDouble()) count xs + randomSampleWithNext (fun a b -> random.Next(a, b)) count xs let randomSample (count: int) (xs: 'T[]) : 'T[] = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-dart/Async.dart b/src/fable-library-dart/Async.dart new file mode 100644 index 0000000000..ea4ce13c0a --- /dev/null +++ b/src/fable-library-dart/Async.dart @@ -0,0 +1,323 @@ +import 'dart:async' as dart_async; + +import 'AsyncBuilder.dart' as async_builder; +import 'Choice.dart' as choice; +import 'System.dart' as system; +import 'Types.dart' as types; + +void _emptyContinuation(T _value) {} + +async_builder.Async makeAsync(async_builder.Async body) { + return body; +} + +void invoke( + async_builder.Async computation, + async_builder.IAsyncContext ctx, +) { + computation(ctx); +} + +void callThenInvoke( + async_builder.IAsyncContext ctx, + U result1, + Function part2, +) { + async_builder.invokeBinderAsAsync(part2, result1)(ctx); +} + +void bind( + async_builder.IAsyncContext ctx, + async_builder.Async part1, + Function part2, +) { + async_builder.protectedBind(part1, part2)(ctx); +} + +Duration _cancellationDelay(Object? value) { + if (value is Duration) { + return value; + } + + if (value is num) { + return Duration(milliseconds: value.toInt()); + } + + throw ArgumentError.value( + value, + 'value', + 'Expected Duration or milliseconds.', + ); +} + +async_builder.CancellationToken createCancellationToken([Object? arg]) { + final token = async_builder.CancellationToken(arg is bool ? arg : false); + + if (arg is Duration || arg is num) { + token.cancelAfter(_cancellationDelay(arg)); + } + + return token; +} + +void cancel(async_builder.CancellationToken token) { + token.cancel(); +} + +void cancelAfter(async_builder.CancellationToken token, Object delay) { + token.cancelAfter(_cancellationDelay(delay)); +} + +bool isCancellationRequested(async_builder.CancellationToken? token) { + return token?.isCancelled ?? false; +} + +void throwIfCancellationRequested(async_builder.CancellationToken? token) { + if (token != null && token.isCancelled) { + throw async_builder.OperationCanceledException(); + } +} + +async_builder.Async cancellationToken() { + return async_builder.protectedCont((ctx) => ctx.onSuccess(ctx.cancelToken)); +} + +final defaultCancellationToken = async_builder.CancellationToken(); + +async_builder.Async> catchAsync( + async_builder.Async work, +) { + return async_builder.protectedCont>((ctx) { + work( + async_builder.IAsyncContext( + onSuccess: (value) { + ctx.onSuccess(choice.Choice_makeChoice1Of2(value)); + }, + onError: (error) { + ctx.onSuccess(choice.Choice_makeChoice2Of2(error)); + }, + onCancel: ctx.onCancel, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + ), + ); + }); +} + +async_builder.Async fromContinuations( + void Function(async_builder.Continuations) function, +) { + return async_builder.protectedCont((ctx) { + function( + types.Tuple3< + async_builder.Continuation, + async_builder.Continuation, + async_builder.Continuation + >(ctx.onSuccess, ctx.onError, ctx.onCancel), + ); + }); +} + +async_builder.Async> startChild( + async_builder.Async computation, [ + types.Some? millisecondsTimeout, +]) { + return async_builder.protectedCont>((ctx) { + final future = startAsFuture(computation, types.Some(ctx.cancelToken)); + + var futureToRun = future; + + final timeout = millisecondsTimeout?.value; + + if (timeout != null && timeout > 0) { + futureToRun = future.timeout( + Duration(milliseconds: timeout), + onTimeout: () => throw system.TimeoutException_$ctor(), + ); + } + + // In F# an unobserved child's failure is not propagated. + futureToRun.ignore(); + + async_builder.protectedReturn(awaitFuture(futureToRun))(ctx); + }); +} + +async_builder.Async> parallel( + Iterable> computations, +) { + return async_builder.protectedCont>((ctx) { + final futures = computations + .map((w) => startAsFuture(w, types.Some(ctx.cancelToken))) + .toList(growable: false); + + awaitFuture>(dart_async.Future.wait(futures, eagerError: true))( + ctx, + ); + }); +} + +async_builder.Async> sequential( + Iterable> computations, +) { + dart_async.Future> run( + async_builder.CancellationToken cancelToken, + ) async { + final results = []; + + for (final computation in computations) { + results.add(await startAsFuture(computation, types.Some(cancelToken))); + } + + return results; + } + + return async_builder.protectedCont>((ctx) { + awaitFuture>(run(ctx.cancelToken))(ctx); + }); +} + +async_builder.Async ignore(async_builder.Async computation) { + return async_builder.protectedBind( + computation, + (_) => async_builder.protectedReturn(null), + ); +} + +void start( + async_builder.Async computation, [ + types.Some? cancellationToken, +]) { + startWithContinuations( + computation, + _emptyContinuation, + (dynamic error) { + throw error; + }, + _emptyContinuation, + cancellationToken, + ); +} + +void startImmediate( + async_builder.Async computation, [ + types.Some? cancellationToken, +]) { + start(computation, cancellationToken); +} + +void startWithContinuations( + async_builder.Async computation, + Function continuation, + async_builder.Continuation exceptionContinuation, + async_builder.Continuation + cancellationContinuation, [ + types.Some? cancelToken, +]) { + final trampoline = async_builder.Trampoline(); + + async_builder.Continuation doneSuccess(Function cont) { + return (value) { + trampoline.completed = true; + + if (cont is Function()) { + cont(); + } else { + cont(value); + } + }; + } + + async_builder.Continuation done(async_builder.Continuation cont) { + return (value) { + trampoline.completed = true; + cont(value); + }; + } + + computation( + async_builder.IAsyncContext( + onSuccess: doneSuccess(continuation), + onError: done(exceptionContinuation), + onCancel: done( + cancellationContinuation, + ), + cancelToken: cancelToken?.value ?? defaultCancellationToken, + trampoline: trampoline, + ), + ); +} + +async_builder.Async sleep(Object delay) { + return async_builder.protectedCont((ctx) { + late final dart_async.Timer timer; + int? listenerId; + var completed = false; + + timer = dart_async.Timer(_cancellationDelay(delay), () { + if (completed) { + return; + } + + completed = true; + + final id = listenerId; + + if (id != null) { + ctx.cancelToken.removeListener(id); + } + + ctx.onSuccess(null); + }); + + listenerId = ctx.cancelToken.addListener(() { + if (completed) { + return; + } + + completed = true; + timer.cancel(); + ctx.onCancel(async_builder.OperationCanceledException()); + }); + }); +} + +async_builder.Async awaitFuture(dart_async.Future future) { + return async_builder.protectedCont((ctx) { + future.then( + (value) { + ctx.onSuccess(value); + }, + onError: (Object error, StackTrace stackTrace) { + if (error is async_builder.OperationCanceledException) { + ctx.onCancel(error); + } else { + ctx.onError(error); + } + }, + ); + }); +} + +dart_async.Future startAsFuture( + async_builder.Async computation, [ + types.Some? cancellationToken, +]) { + final completer = dart_async.Completer(); + + startWithContinuations( + computation, + (value) { + completer.complete(value); + }, + (dynamic error) { + completer.completeError(error); + }, + (error) { + completer.completeError(error); + }, + cancellationToken, + ); + + return completer.future; +} diff --git a/src/fable-library-dart/AsyncBuilder.dart b/src/fable-library-dart/AsyncBuilder.dart new file mode 100644 index 0000000000..824d260378 --- /dev/null +++ b/src/fable-library-dart/AsyncBuilder.dart @@ -0,0 +1,334 @@ +import 'dart:async' as dart_async; + +import 'Types.dart' as types; + +typedef Continuation = void Function(T value); + +class OperationCanceledException extends types.ExceptionBase { + OperationCanceledException([String? message]) + : super(message ?? 'The operation was canceled'); +} + +typedef Continuations = + types.Tuple3< + Continuation, + Continuation, + Continuation + >; + +class CancellationToken implements types.IDisposable { + int _id = 0; + bool _cancelled; + dart_async.Timer? _cancelTimer; + final Map _listeners = {}; + + CancellationToken([this._cancelled = false]); + + bool get isCancelled => _cancelled; + + void cancel() { + if (_cancelled) { + return; + } + + _cancelTimer?.cancel(); + _cancelTimer = null; + + _cancelled = true; + + final listeners = _listeners.values.toList(growable: false); + _listeners.clear(); + + for (final listener in listeners) { + listener(); + } + } + + void cancelAfter(Duration delay) { + if (_cancelled) { + return; + } + + _cancelTimer?.cancel(); + + _cancelTimer = dart_async.Timer(delay, () { + _cancelTimer = null; + cancel(); + }); + } + + int addListener(void Function() f) { + if (_cancelled) { + f(); + return -1; + } + + final id = _id++; + _listeners[id] = f; + return id; + } + + bool removeListener(int id) { + return _listeners.remove(id) != null; + } + + types.IDisposable register(dynamic f, [dynamic state]) { + final id = addListener(state == null ? () => f() : () => f(state)); + + return _CancellationRegistration(this, id); + } + + @override + void Dispose() { + // Disposing a CancellationTokenSource does not request cancellation. + } +} + +class _CancellationRegistration implements types.IDisposable { + final CancellationToken _token; + final int _id; + + _CancellationRegistration(this._token, this._id); + + @override + void Dispose() { + _token.removeListener(_id); + } +} + +class Trampoline { + static const int maxTrampolineCallCount = 2000; + + int _callCount = 0; + + bool completed = false; + + bool incrementAndCheck() { + return _callCount++ > maxTrampolineCallCount; + } + + void hijack(void Function() f) { + _callCount = 0; + dart_async.Timer.run(f); + } +} + +class IAsyncContext { + final Continuation onSuccess; + final Continuation onError; + final Continuation onCancel; + + final CancellationToken cancelToken; + final Trampoline trampoline; + + const IAsyncContext({ + required this.onSuccess, + required this.onError, + required this.onCancel, + required this.cancelToken, + required this.trampoline, + }); +} + +typedef Async = void Function(IAsyncContext context); + +// Binders arrive as zero- or one-argument functions with no inferred type. +dynamic invokeBinder(Function binder, T value) { + if (binder is Function()) { + return binder(); + } + + return binder(value); +} + +Async invokeBinderAsAsync(Function binder, T value) { + final dynamic bound = invokeBinder(binder, value); + + return (ctx) => bound(ctx); +} + +Async protectedCont(Async f) { + return (ctx) { + if (ctx.cancelToken.isCancelled) { + ctx.onCancel(OperationCanceledException()); + } else if (ctx.trampoline.incrementAndCheck()) { + ctx.trampoline.hijack(() { + try { + f(ctx); + } catch (error) { + if (ctx.trampoline.completed) { + rethrow; + } + + ctx.onError(error); + } + }); + } else { + try { + f(ctx); + } catch (error) { + if (ctx.trampoline.completed) { + rethrow; + } + + ctx.onError(error); + } + } + }; +} + +Async protectedBind(Async computation, Function binder) { + return protectedCont((ctx) { + computation( + IAsyncContext( + onSuccess: (value) { + final dynamic bound; + + try { + bound = invokeBinder(binder, value); + } catch (error) { + ctx.onError(error); + return; + } + + bound(ctx); + }, + onError: ctx.onError, + onCancel: ctx.onCancel, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + ), + ); + }); +} + +Async protectedReturn(T value) { + return protectedCont((ctx) => ctx.onSuccess(value)); +} + +class AsyncBuilder { + Async Bind(Async computation, Function binder) { + return protectedBind(computation, binder); + } + + Async Combine(Async computation1, Async computation2) { + return Bind(computation1, () => computation2); + } + + Async Delay(Async Function() generator) { + return protectedCont((ctx) => generator()(ctx)); + } + + Async For(Iterable sequence, Function body) { + return Delay(() { + final iterator = sequence.iterator; + + Async loop() { + return Delay(() { + if (!iterator.moveNext()) { + return Zero(); + } + + final current = iterator.current; + + return Bind( + invokeBinderAsAsync(body, current), + loop, + ); + }); + } + + return loop(); + }); + } + + Async Return(T value) { + return protectedReturn(value); + } + + Async ReturnFrom(Async computation) { + return computation; + } + + Async TryFinally(Async computation, void Function() compensation) { + return protectedCont((ctx) { + void finish(void Function() continuation) { + try { + compensation(); + } catch (error) { + ctx.onError(error); + return; + } + + continuation(); + } + + computation( + IAsyncContext( + onSuccess: (value) { + finish(() => ctx.onSuccess(value)); + }, + onError: (error) { + finish(() => ctx.onError(error)); + }, + onCancel: (error) { + finish(() => ctx.onCancel(error)); + }, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + ), + ); + }); + } + + Async TryWith( + Async computation, + Async Function(dynamic error) catchHandler, + ) { + return protectedCont((ctx) { + computation( + IAsyncContext( + onSuccess: ctx.onSuccess, + onCancel: ctx.onCancel, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + onError: (error) { + late final Async handled; + + try { + handled = catchHandler(error); + } catch (handlerError) { + ctx.onError(handlerError); + return; + } + + handled(ctx); + }, + ), + ); + }); + } + + Async Using( + T resource, + Async Function(T value) binder, + ) { + return TryFinally(Delay(() => binder(resource)), resource.Dispose); + } + + Async While(bool Function() guard, Async computation) { + return Delay(() { + if (guard()) { + return Bind(computation, () => While(guard, computation)); + } + + return Return(null); + }); + } + + Async Zero() { + return protectedCont((ctx) => ctx.onSuccess(null)); + } +} + +final singleton = AsyncBuilder(); diff --git a/src/fable-library-dart/FSharp.Core.fs b/src/fable-library-dart/FSharp.Core.fs index 71ae90e717..89cdb1338a 100644 --- a/src/fable-library-dart/FSharp.Core.fs +++ b/src/fable-library-dart/FSharp.Core.fs @@ -14,6 +14,14 @@ module Operators = let nullArg (argumentName: string) = raise (System.ArgumentNullException(argumentName)) + [] + let invalidArg (argumentName: string) (message: string) = + raise (System.ArgumentException(message, argumentName)) + + [] + let invalidOp (message: string) = + raise (System.InvalidOperationException(message)) + [] let using<'T, 'R when 'T :> System.IDisposable and 'T: null> (resource: 'T) (action: 'T -> 'R) = try diff --git a/src/fable-library-dart/Fable.Library.Dart.fsproj b/src/fable-library-dart/Fable.Library.Dart.fsproj index 962c063607..382640cb7d 100644 --- a/src/fable-library-dart/Fable.Library.Dart.fsproj +++ b/src/fable-library-dart/Fable.Library.Dart.fsproj @@ -22,6 +22,8 @@ + + diff --git a/src/fable-library-dart/Global.fs b/src/fable-library-dart/Global.fs index 0fcd96d1d0..ce62f1f3f6 100644 --- a/src/fable-library-dart/Global.fs +++ b/src/fable-library-dart/Global.fs @@ -16,8 +16,9 @@ module SR = let indexOutOfBounds = "The index was outside the range of elements in the collection." - let inputWasEmpty = "Collection was empty." + let inputWasEmpty = "The input list was empty." let inputMustBeNonNegative = "The input must be non-negative." + let inputMustBePositive = "The input must be positive." let inputSequenceEmpty = "The input sequence was empty." let inputSequenceTooLong = "The input sequence contains more than one element." diff --git a/src/fable-library-dart/List.fs b/src/fable-library-dart/List.fs index d0cd36bb3b..aa719947d7 100644 --- a/src/fable-library-dart/List.fs +++ b/src/fable-library-dart/List.fs @@ -27,14 +27,17 @@ and [] LinkedList<'T>(tail_: 'T list member this.Head: 'T = match this with | :? ConsList<'T> as this -> this.GetHead() - | _ -> failwith "Empty list" + | _ -> invalidOp SR.inputWasEmpty member this.TryHead: 'T option = match this with | :? ConsList<'T> as this -> this.GetHead() |> Some | _ -> None - member _.Tail: LinkedList<'T> = Option.get tail + member _.Tail: LinkedList<'T> = + match tail with + | Some t -> t + | None -> invalidOp SR.inputWasEmpty member _.TryTail: LinkedList<'T> option = tail @@ -149,6 +152,9 @@ and List<'T> = LinkedList<'T> let inline indexNotFound () = raise (KeyNotFoundException(SR.keyNotFoundAlt)) +let private differentLengths (paramName: string) = + invalidArg paramName "The lists had different lengths." + let empty () = List.Empty let cons (x: 'T) (xs: 'T list) = List.Cons(x, xs) @@ -159,7 +165,10 @@ let isEmpty (xs: 'T list) = xs.IsEmpty let length (xs: 'T list) = xs.Length -let head (xs: 'T list) = xs.Head +let head (xs: 'T list) = + match xs with + | :? ConsList<'T> as xs -> xs.GetHead() + | _ -> invalidArg "list" SR.inputWasEmpty let tryHead (xs: 'T list) = if xs.IsEmpty then @@ -167,7 +176,10 @@ let tryHead (xs: 'T list) = else Some xs.Head -let tail (xs: 'T list) = xs.Tail +let tail (xs: 'T list) = + match xs.TryTail with + | Some t -> t + | None -> invalidArg "list" SR.inputWasEmpty let rec tryLast (xs: 'T list) = if xs.IsEmpty then @@ -183,7 +195,7 @@ let rec tryLast (xs: 'T list) = let last (xs: 'T list) = match tryLast xs with | Some x -> x - | None -> failwith SR.inputWasEmpty + | None -> invalidArg "list" SR.inputWasEmpty let compareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list) : int = let rec loop (xs: 'T list) (ys: 'T list) = @@ -256,11 +268,29 @@ let fold2<'T1, 'T2, 'State> (folder: 'State -> 'T1 -> 'T2 -> 'State) (state: 'St xs <- xs.Tail ys <- ys.Tail + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc let foldBack2<'T1, 'T2, 'State> (folder: 'T1 -> 'T2 -> 'State -> 'State) (xs: 'T1 list) (ys: 'T2 list) (state: 'State) = // fold2 (fun acc x y -> folder x y acc) state (reverse xs) (reverse ys) - Array.foldBack2 folder (toArray xs) (toArray ys) state + let xs = toArray xs + let ys = toArray ys + + if xs.Length <> ys.Length then + differentLengths ( + if xs.Length = 0 then + "list1" + elif ys.Length = 0 then + "list2" + else + "list1, list2" + ) + + Array.foldBack2 folder xs ys state let unfold<'T, 'State> (generator: 'State -> ('T * 'State) option) (state: 'State) = let rec loop acc (node: 'T list) = @@ -405,6 +435,11 @@ let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let rec loop i (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) = if xs.IsEmpty || ys.IsEmpty then + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc else let node = acc.AppendConsNoTail(mapping i xs.Head ys.Head) @@ -418,6 +453,9 @@ let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = let rec loop (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = if xs.IsEmpty || ys.IsEmpty || zs.IsEmpty then + if not (xs.IsEmpty && ys.IsEmpty && zs.IsEmpty) then + differentLengths "list1, list2, list3" + acc else let node = acc.AppendConsNoTail(mapping xs.Head ys.Head zs.Head) @@ -559,6 +597,9 @@ let contains (value: 'T) (xs: 'T list) ([] eq: IEqualityComparer<'T>) = tryFindIndex (fun v -> eq.Equals(value, v)) xs |> Option.isSome let initialize n (f: int -> 'T) = + if n < 0 then + invalidArg "count" SR.inputMustBeNonNegative + let root = List.Empty let mutable node = root @@ -572,20 +613,24 @@ let replicate n x = initialize n (fun _ -> x) let reduce f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else fold f (head xs) (tail xs) let reduceBack f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else foldBack f (tail xs) (head xs) let forAll f xs = fold (fun acc x -> acc && f x) true xs -let forAll2 f xs ys = - fold2 (fun acc x y -> acc && f x y) true xs ys +let rec forAll2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = + match xs.IsEmpty, ys.IsEmpty with + | true, true -> true + | false, false -> f xs.Head ys.Head && forAll2 f xs.Tail ys.Tail + | true, false -> differentLengths "list1" + | false, true -> differentLengths "list2" let exists f xs = tryFindIndex f xs |> Option.isSome @@ -593,7 +638,7 @@ let rec exists2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = match xs.IsEmpty, ys.IsEmpty with | true, true -> false | false, false -> f xs.Head ys.Head || exists2 f xs.Tail ys.Tail - | _ -> invalidArg "list2" SR.differentLengths + | _ -> differentLengths "list2" let unzip xs = foldBack (fun (x, y) (lacc, racc) -> List.Cons(x, lacc), List.Cons(y, racc)) xs (List.Empty, List.Empty) @@ -672,6 +717,9 @@ let min (xs: 'T list) ([] comparer: IComparer<'T>) : 'T = xs let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let folder acc x = @@ -682,6 +730,9 @@ let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = averager.DivideByInt(total, count) let averageBy (f: 'T -> 'U) (xs: 'T list) ([] averager: IGenericAverager<'U>) : 'U = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let inline folder acc x = @@ -710,7 +761,7 @@ let rec skip count (xs: 'T list) = if count <= 0 then xs elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) else skip (count - 1) xs.Tail @@ -730,7 +781,7 @@ let take count (xs: 'T list) = if i <= 0 then acc elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + invalidOp SR.notEnoughElements else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail @@ -796,7 +847,7 @@ let splitAt index (xs: 'T list) = invalidArg "index" SR.inputMustBeNonNegative if index > xs.Length then - invalidArg "index" SR.notEnoughElements + invalidOp SR.notEnoughElements take index xs, skip index xs @@ -918,43 +969,21 @@ let removeAt (index: int) (xs: 'T list) : 'T list = ys let removeManyAt (index: int) (count: int) (xs: 'T list) : 'T list = + if index < 0 then + invalidArg "index" "index must be within bounds of the list" + + let endIndex = index + count let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 let ys = xs |> filter (fun _ -> i <- i + 1 - - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true + i < index || i >= endIndex ) - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status - - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" - else - "count" - - invalidArg arg SR.indexOutOfBounds + if endIndex > i + 1 then + invalidArg "index" "index must be within bounds of the list" ys @@ -982,11 +1011,16 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T list) : 'T list = ofArray arr let randomShuffleWith (random: System.Random) (xs: 'T list) : 'T list = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = toArray xs + Array.randomShuffleInPlaceWith random arr + ofArray arr let randomShuffle (xs: 'T list) : 'T list = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T list) : 'T = + if isEmpty xs then + invalidArg "source" SR.inputSequenceEmpty + toArray xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T list) : 'T = @@ -1007,7 +1041,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T list) : 'T l toArray xs |> Array.randomSampleBy randomizer count |> ofArray let randomSampleWith (random: System.Random) (count: int) (xs: 'T list) : 'T list = - randomSampleBy (fun () -> random.NextDouble()) count xs + toArray xs |> Array.randomSampleWith random count |> ofArray let randomSample (count: int) (xs: 'T list) : 'T list = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-dart/Map.fs b/src/fable-library-dart/Map.fs index 050ec50915..dc03ad26b2 100644 --- a/src/fable-library-dart/Map.fs +++ b/src/fable-library-dart/Map.fs @@ -607,9 +607,9 @@ module MapTree = started = false } - let notStarted () = failwith "enumeration not started" + let notStarted () = invalidOp SR.enumerationNotStarted - let alreadyFinished () = failwith "enumeration already finished" + let alreadyFinished () = invalidOp SR.enumerationAlreadyFinished let current i = if i.started then diff --git a/src/fable-library-dart/Random.dart b/src/fable-library-dart/Random.dart index d3b47305ad..af92afdef7 100644 --- a/src/fable-library-dart/Random.dart +++ b/src/fable-library-dart/Random.dart @@ -1,8 +1,6 @@ // ignore_for_file: file_names import 'dart:math' as dart_math; - -Exception _outOfRange(String paramName) => - Exception('Specified argument was out of the range of valid values. (Parameter $paramName)'); +import 'System.dart' as system; Exception _nullArg(String paramName) => Exception('Value cannot be null. (Parameter $paramName)'); @@ -16,14 +14,14 @@ class Random { int Next1(int maxValue) { if (maxValue < 0) { - throw _outOfRange('maxValue'); + throw system.ArgumentOutOfRangeException('maxValue', "maxValue ('$maxValue') must be a non-negative value."); } return Next2(0, maxValue); } int Next2(int minValue, int maxValue) { if (maxValue < minValue) { - throw _outOfRange('minValue'); + throw system.ArgumentOutOfRangeException('minValue', "'minValue' cannot be greater than maxValue."); } if (maxValue == minValue) { @@ -45,4 +43,8 @@ class Random { buffer[i] = _random.nextInt(256); } } -} \ No newline at end of file +} + +final Random _shared = Random(); + +Random shared() => _shared; diff --git a/src/fable-library-dart/Range.fs b/src/fable-library-dart/Range.fs index 2e921701bd..75477dbe4e 100644 --- a/src/fable-library-dart/Range.fs +++ b/src/fable-library-dart/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T> (step: 'T) (stop: 'T) (zero: 'T) (add: 'T -> 'T -> let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-dart/RegExp.dart b/src/fable-library-dart/RegExp.dart index 80e8263eb4..5681dceb9d 100644 --- a/src/fable-library-dart/RegExp.dart +++ b/src/fable-library-dart/RegExp.dart @@ -61,9 +61,10 @@ RegExp create(String pattern, [int options = 0]) { // * Compiled: 0x0008 (ignored) // * Singleline: 0x0010 // * ECMAScript: 0x0100 (ignored) - if ((options & ~(1 ^ 2 ^ 8 ^ 16 ^ 256)) != 0) { + // * NonBacktracking: 0x0400 (ignored) + if ((options & ~(1 ^ 2 ^ 8 ^ 16 ^ 256 ^ 1024)) != 0) { throw Exception( - 'RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline and ECMAScript'); + 'RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline, ECMAScript and NonBacktracking'); } return RegExp(pattern, unicode: true, diff --git a/src/fable-library-dart/Seq.fs b/src/fable-library-dart/Seq.fs index 36d5fdfb2f..92628a774f 100644 --- a/src/fable-library-dart/Seq.fs +++ b/src/fable-library-dart/Seq.fs @@ -630,6 +630,9 @@ let tryHead (xs: seq<'T>) : 'T option = None let initialize (count: int) (f: int -> 'a) : seq<'a> = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + unfold (fun i -> if (i < count) then @@ -944,17 +947,22 @@ let reduce (folder: 'T -> 'T -> 'T) (xs: seq<'T>) : 'T = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack (folder: 'T -> 'T -> 'T) (xs: seq<'T>) : 'T = let arr = toArray xs if Array.isEmpty arr then - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty Array.reduceBack folder arr -let replicate (n: int) (x: 'a) : seq<'a> = initialize n (fun _ -> x) +let replicate (n: int) (x: 'a) : seq<'a> = + // FSharp.Core uses Enumerable.Repeat, which raises ArgumentOutOfRangeException + if n < 0 then + raise (ArgumentOutOfRangeException("count", SR.Arg_ArgumentOutOfRangeException)) + + initialize n (fun _ -> x) let reverse (xs: seq<'T>) : seq<'T> = delay (fun () -> xs |> toArray |> Array.rev |> ofArray) @@ -987,7 +995,7 @@ let skip (count: int) (source: seq<'T>) : seq<'T> = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -1009,9 +1017,26 @@ let skipWhile (predicate: 'T -> bool) (xs: seq<'T>) : seq<'T> = ) ) -let tail (xs: seq<'T>) : seq<'T> = skip 1 xs +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException +let tail (xs: seq<'T>) : seq<'T> = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) let take (count: int) (xs: seq<'T>) : seq<'T> = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + Enumerator.generateIndexed (fun () -> ofSeq xs) (fun i e -> @@ -1019,7 +1044,7 @@ let take (count: int) (xs: seq<'T>) : seq<'T> = if e.MoveNext() then Some(e.Current) else - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements else None ) @@ -1060,19 +1085,32 @@ let pairwise (xs: seq<'T>) : seq<'T * 'T> = delay (fun () -> xs |> toArray |> Array.pairwise |> ofArray) let splitInto (chunks: int) (xs: seq<'T>) : 'T[] seq = + if chunks < 1 then + invalidArg "count" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.splitInto chunks |> ofArray) let windowed (windowSize: int) (xs: seq<'T>) : 'T[] seq = + if windowSize <= 0 then + invalidArg "windowSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.windowed windowSize |> ofArray) let transpose (xss: seq<#seq<'T>>) : seq> = delay (fun () -> - xss - |> toArray - |> Array.map toArray - |> Array.transpose - |> Array.map ofArray - |> ofArray + let rows = ResizeArray>() + + for xs in xss do + let mutable i = 0 + + for x in xs do + if i = rows.Count then + rows.Add(ResizeArray()) + + rows[i].Add(x) + i <- i + 1 + + rows |> map (fun row -> row :> seq<'T>) ) let sortWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) = @@ -1172,6 +1210,9 @@ let permute f (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.permute f |> ofArray) let chunkBySize (chunkSize: int) (xs: seq<'T>) : seq<'T[]> = + if chunkSize < 1 then + invalidArg "chunkSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.chunkBySize chunkSize |> ofArray) let insertAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = @@ -1263,28 +1304,28 @@ let removeAt (index: int) (xs: seq<'T>) : seq<'T> = let removeManyAt (index: int) (count: int) (xs: seq<'T>) : seq<'T> = if index < 0 then - invalidArg "index" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - Enumerator.generateIndexed - (fun () -> ofSeq xs) - (fun i e -> + let endIndex = index + count + + delay (fun () -> + let mutable i = -1 + + let rest () = if i < index then - if e.MoveNext() then - Some(e.Current) - else - invalidArg "index" SR.indexOutOfBounds - else - if i = index then - for _ = 1 to count do - if not (e.MoveNext()) then - invalidArg "count" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - if e.MoveNext() then - Some(e.Current) - else - None - ) - (fun e -> e.Dispose()) + empty () + + let ys = + xs + |> filter (fun _ -> + i <- i + 1 + i < index || i >= endIndex + ) + + append ys (delay rest) + ) let updateAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = let mutable isDone = false @@ -1314,20 +1355,36 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T seq) : 'T seq = ofArray arr let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = toArray xs + Array.randomShuffleInPlaceWith random arr + ofArray arr let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceBy randomizer + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceWith random + randomChoiceSource 1 xs |> Array.randomChoiceWith random -let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice +// Draws on enumeration, like FSharp.Core let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = - xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = randomChoicesBy (fun () -> random.NextDouble()) count xs @@ -1339,7 +1396,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T se xs |> toArray |> Array.randomSampleBy randomizer count |> ofArray let randomSampleWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = - randomSampleBy (fun () -> random.NextDouble()) count xs + xs |> toArray |> Array.randomSampleWith random count |> ofArray let randomSample (count: int) (xs: 'T seq) : 'T seq = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-dart/Set.fs b/src/fable-library-dart/Set.fs index 6d24568e3b..0381ed456f 100644 --- a/src/fable-library-dart/Set.fs +++ b/src/fable-library-dart/Set.fs @@ -480,12 +480,12 @@ module SetTree = let minimumElement s = match minimumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." let maximumElement s = match maximumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." // Imperative left-to-right iterators. [] @@ -515,9 +515,9 @@ module SetTree = started = false } - let notStarted () = failwith "Enumeration not started" + let notStarted () = invalidOp SR.enumerationNotStarted - let alreadyFinished () = failwith "Enumeration already started" + let alreadyFinished () = invalidOp SR.enumerationAlreadyFinished let current i = if i.started then diff --git a/src/fable-library-dart/String.dart b/src/fable-library-dart/String.dart index e3d1a7d212..993afe2f3e 100644 --- a/src/fable-library-dart/String.dart +++ b/src/fable-library-dart/String.dart @@ -1,5 +1,7 @@ // ignore_for_file: file_names, constant_identifier_names +import 'System.dart' as system; + class StringComparison { static const CurrentCulture = 0; static const CurrentCultureIgnoreCase = 1; @@ -87,6 +89,33 @@ int getCharAtIndex(String input, int index) { return input.codeUnitAt(index); } +int getRuneAt(String input, int index) { + if (index < 0 || index >= input.length) { + throw Exception('Index was outside the bounds of the array.'); + } + + late final iterator; + try { + iterator = RuneIterator.at(input, index); + } on ArgumentError catch (e) { + throw Exception(e.message); + } + + iterator.moveNext(); + final rune = iterator.current; + + if (rune >= 0xD800 && rune <= 0xDFFF) { + throw Exception('Invalid UTF-16 sequence at index $index.'); + } + return rune; +} + +Iterable enumerateRunes(String input) { + return input.runes.map( + (rune) => rune >= 0xD800 && rune <= 0xDFFF ? 0xFFFD : rune, + ); +} + List splitWithChars(String str, [List? splitters, int? count, int? options]) { splitters ??= []; @@ -253,3 +282,41 @@ String mapIndexed(int Function(int, int) mapping, String str) { return mapping(index, rune); })); } + +String toHexStringLower(List inArray, [int? offset, int? length]) { + offset ??= 0; + length ??= inArray.length - offset; + if (length < 0) { + throw system.ArgumentOutOfRangeException_$ctor_Z384F8060( + 'length', 'Non-negative number required.'); + } + if (offset < 0) { + throw system.ArgumentOutOfRangeException_$ctor_Z384F8060( + 'offset', 'Non-negative number required.'); + } + if (offset > inArray.length - length) { + throw system.ArgumentOutOfRangeException_$ctor_Z721C83C5('offset'); + } + return inArray + .sublist(offset, offset + length) + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); +} + +String toHexString(List inArray, [int? offset, int? length]) { + return toHexStringLower(inArray, offset, length).toUpperCase(); +} + +List fromHexString(String s) { + if (s.length % 2 != 0) { + throw system.FormatException_$ctor_Z721C83C5( + 'The input is not a valid hex string as its length is not a multiple of 2.'); + } + if (!RegExp(r'^[0-9A-Fa-f]*$').hasMatch(s)) { + throw system.FormatException_$ctor_Z721C83C5( + 'The input is not a valid hex string as it contains a non-hex character.'); + } + return List.generate( + s.length ~/ 2, (i) => int.parse(s.substring(2 * i, 2 * i + 2), radix: 16), + growable: false); +} diff --git a/src/fable-library-dart/System.Text.fs b/src/fable-library-dart/System.Text.fs index 44c09f4b1a..0c2bc3a593 100644 --- a/src/fable-library-dart/System.Text.fs +++ b/src/fable-library-dart/System.Text.fs @@ -127,6 +127,14 @@ type StringBuilder(value: string, capacity: int) = let pos = index - len buf[i] <- buf[i][0 .. (pos - 1)] + (string value) + buf[i][(pos + 1) ..] + member x.Length = + let mutable len = 0 + + for i = buf.Count - 1 downto 0 do + len <- len + buf[i].Length + + len + member x.Replace(oldValue: char, newValue: char) = let oldValue = string oldValue let newValue = string newValue @@ -140,16 +148,8 @@ type StringBuilder(value: string, capacity: int) = let str = x.ToString().Replace(oldValue, newValue) x.Clear().Append(str) - member x.Length = - let mutable len = 0 - - for i = buf.Count - 1 downto 0 do - len <- len + buf[i].Length - - len - override _.ToString() = System.String.Concat(buf) - member x.ToString(firstIndex: int, length: int) = + member x.ToString(startIndex: int, length: int) = let str = x.ToString() - str.Substring(firstIndex, length) + str.Substring(startIndex, length) diff --git a/src/fable-library-dart/quotation.dart b/src/fable-library-dart/quotation.dart index 9ad67ebd39..a6c335c232 100644 --- a/src/fable-library-dart/quotation.dart +++ b/src/fable-library-dart/quotation.dart @@ -656,3 +656,70 @@ Expr substitute(Expr expr, dynamic fn) { return sub(expr); } + +// =================================================================== +// DerivedPatterns +// F# defines these on top of Patterns. AndAlso and OrElse recover the +// shape `&&` and `||` desugar into; SpecificCall matches a call by the +// identity of a template quotation rather than by compiled name. +// =================================================================== + +/// a && b ==> if a then b else false +types.Some>? isAndAlso(Expr expr) { + if (expr is ExprIfThenElse) { + final e = expr.elseExpr; + if (e is ExprValue && e.value == false) { + return types.Some>( + types.Tuple2(expr.guard, expr.thenExpr)); + } + } + return null; +} + +/// a || b ==> if a then true else b +types.Some>? isOrElse(Expr expr) { + if (expr is ExprIfThenElse) { + final t = expr.thenExpr; + if (t is ExprValue && t.value == true) { + return types.Some>( + types.Tuple2(expr.guard, expr.elseExpr)); + } + } + return null; +} + +/// A template such as <@ (=) @> quotes as nested lambdas around the call, so +/// the identifying Call node has to be dug out before comparing. +ExprCall? _templateCall(Expr expr) { + Expr current = expr; + while (current is ExprLambda) { + current = current.body; + } + return current is ExprCall ? current : null; +} + +// Takes both arguments; the partial application FSharp.Core expresses by +// currying is built by the replacement instead. +// Dart checks the full generic shape at runtime, so this spells out the type the +// F# signature declares: (Expr option * Type list * Expr list) option. +types.Some?, list.FSharpList, list.FSharpList>>? + isSpecificCall(Expr template, Expr expr) { + final wanted = _templateCall(template); + if (wanted == null || expr is! ExprCall) return null; + if (expr.method != wanted.method || + expr.declaringType != wanted.declaringType) { + return null; + } + final inst = expr.instance; + final normalized = + (inst is ExprValue && inst.type == 'novalue') ? null : inst; + // The middle slot is F#'s generic-argument list. This runtime models types as + // names rather than System.Type, so it is always empty; callers match it with + // a wildcard. + return types.Some< + types.Tuple3?, list.FSharpList, list.FSharpList>>( + types.Tuple3?, list.FSharpList, list.FSharpList>( + normalized == null ? null : types.Some(normalized), + list.ofArray(const []), + list.ofArray(expr.args))); +} diff --git a/src/fable-library-php/Environment.php b/src/fable-library-php/Environment.php new file mode 100644 index 0000000000..a43b598319 --- /dev/null +++ b/src/fable-library-php/Environment.php @@ -0,0 +1,12 @@ + - + diff --git a/src/fable-library-py/fable_library/List.fs b/src/fable-library-py/fable_library/List.fs index 509cf2d126..d1ba04453e 100644 --- a/src/fable-library-py/fable_library/List.fs +++ b/src/fable-library-py/fable_library/List.fs @@ -48,12 +48,12 @@ type LinkedList<'T> = member xs.Head = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some _ -> xs.head member xs.Tail = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some t -> t member xs.Item @@ -167,6 +167,9 @@ and List<'T> = LinkedList<'T> let indexNotFound () = raise (System.Collections.Generic.KeyNotFoundException(SR.keyNotFoundAlt)) +let private differentLengths (paramName: string) = + invalidArg paramName "The lists had different lengths." + let empty () = List.Empty let cons (x: 'T) (xs: 'T list) = List.Cons(x, xs) @@ -177,7 +180,10 @@ let isEmpty (xs: 'T list) = xs.IsEmpty let length (xs: 'T list) = xs.Length -let head (xs: 'T list) = xs.Head +let head (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some _ -> xs.head let tryHead (xs: 'T list) = if xs.IsEmpty then @@ -185,7 +191,10 @@ let tryHead (xs: 'T list) = else Some xs.Head -let tail (xs: 'T list) = xs.Tail +let tail (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some t -> t let rec tryLast (xs: 'T list) = if xs.IsEmpty then @@ -201,7 +210,7 @@ let rec tryLast (xs: 'T list) = let last (xs: 'T list) = match tryLast xs with | Some x -> x - | None -> failwith SR.inputWasEmpty + | None -> invalidArg "list" SR.inputWasEmpty let compareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list) : int = let rec loop (xs: 'T list) (ys: 'T list) = @@ -275,11 +284,29 @@ let fold2<'T1, 'T2, 'State> (folder: 'State -> 'T1 -> 'T2 -> 'State) (state: 'St xs <- xs.Tail ys <- ys.Tail + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc let foldBack2<'T1, 'T2, 'State> (folder: 'T1 -> 'T2 -> 'State -> 'State) (xs: 'T1 list) (ys: 'T2 list) (state: 'State) = // fold2 (fun acc x y -> folder x y acc) state (reverse xs) (reverse ys) - Array.foldBack2 folder (toArray xs) (toArray ys) state + let xs = toArray xs + let ys = toArray ys + + if xs.Length <> ys.Length then + differentLengths ( + if xs.Length = 0 then + "list1" + elif ys.Length = 0 then + "list2" + else + "list1, list2" + ) + + Array.foldBack2 folder xs ys state let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) = let rec loop acc (node: 'T list) = @@ -425,6 +452,11 @@ let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let rec loop i (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) = if xs.IsEmpty || ys.IsEmpty then + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc else let node = acc.AppendConsNoTail(mapping i xs.Head ys.Head) @@ -438,6 +470,9 @@ let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = let rec loop (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = if xs.IsEmpty || ys.IsEmpty || zs.IsEmpty then + if not (xs.IsEmpty && ys.IsEmpty && zs.IsEmpty) then + differentLengths "list1, list2, list3" + acc else let node = acc.AppendConsNoTail(mapping xs.Head ys.Head zs.Head) @@ -583,6 +618,9 @@ let contains (value: 'T) (xs: 'T list) ([] eq: System.Collections.Generi tryFindIndex (fun v -> eq.Equals(value, v)) xs |> Option.isSome let initialize n (f: int -> 'T) = + if n < 0 then + invalidArg "count" SR.inputMustBeNonNegative + let root = List.Empty let mutable node = root @@ -596,20 +634,24 @@ let replicate n x = initialize n (fun _ -> x) let reduce f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else fold f (head xs) (tail xs) let reduceBack f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else foldBack f (tail xs) (head xs) let forAll f xs = fold (fun acc x -> acc && f x) true xs -let forAll2 f xs ys = - fold2 (fun acc x y -> acc && f x y) true xs ys +let rec forAll2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = + match xs.IsEmpty, ys.IsEmpty with + | true, true -> true + | false, false -> f xs.Head ys.Head && forAll2 f xs.Tail ys.Tail + | true, false -> differentLengths "list1" + | false, true -> differentLengths "list2" let exists f xs = tryFindIndex f xs |> Option.isSome @@ -617,7 +659,7 @@ let rec exists2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = match xs.IsEmpty, ys.IsEmpty with | true, true -> false | false, false -> f xs.Head ys.Head || exists2 f xs.Tail ys.Tail - | _ -> invalidArg "list2" SR.differentLengths + | _ -> differentLengths "list2" let unzip xs = foldBack (fun (x, y) (lacc, racc) -> List.Cons(x, lacc), List.Cons(y, racc)) xs (List.Empty, List.Empty) @@ -700,6 +742,9 @@ let min (xs: 'T list) ([] comparer: System.Collections.Generic.IComparer xs let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let folder acc x = @@ -710,6 +755,9 @@ let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = averager.DivideByInt(total, count) let averageBy (f: 'T -> 'U) (xs: 'T list) ([] averager: IGenericAverager<'U>) : 'U = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let inline folder acc x = @@ -738,7 +786,7 @@ let rec skip count (xs: 'T list) = if count <= 0 then xs elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) else skip (count - 1) xs.Tail @@ -758,7 +806,7 @@ let take count (xs: 'T list) = if i <= 0 then acc elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + invalidOp SR.notEnoughElements else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail @@ -824,7 +872,7 @@ let splitAt index (xs: 'T list) = invalidArg "index" SR.inputMustBeNonNegative if index > xs.Length then - invalidArg "index" SR.notEnoughElements + invalidOp SR.notEnoughElements take index xs, skip index xs @@ -946,43 +994,21 @@ let removeAt (index: int) (xs: 'T list) : 'T list = ys let removeManyAt (index: int) (count: int) (xs: 'T list) : 'T list = + if index < 0 then + invalidArg "index" "index must be within bounds of the list" + + let endIndex = index + count let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 let ys = xs |> filter (fun _ -> i <- i + 1 - - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true + i < index || i >= endIndex ) - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status - - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" - else - "count" - - invalidArg arg SR.indexOutOfBounds + if endIndex > i + 1 then + invalidArg "index" "index must be within bounds of the list" ys @@ -1015,6 +1041,9 @@ let randomShuffleWith (random: System.Random) (xs: 'T list) : 'T list = let randomShuffle (xs: 'T list) : 'T list = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T list) : 'T = + if isEmpty xs then + invalidArg "source" SR.inputSequenceEmpty + toArray xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T list) : 'T = diff --git a/src/fable-library-py/fable_library/Map.fs b/src/fable-library-py/fable_library/Map.fs index 6416366633..c3368deb0a 100644 --- a/src/fable-library-py/fable_library/Map.fs +++ b/src/fable-library-py/fable_library/Map.fs @@ -593,9 +593,11 @@ module MapTree = started = false } - let notStarted () = failwith "enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "enumeration already finished" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then @@ -900,14 +902,14 @@ type Map<[] 'Key, [> with member m.Add x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Clear() = - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Remove x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Contains x = m.ContainsKey x.Key && Unchecked.equals m.[x.Key] x.Value diff --git a/src/fable-library-py/fable_library/MutableMap.fs b/src/fable-library-py/fable_library/MutableMap.fs index 502a283cc8..d3344d27e1 100644 --- a/src/fable-library-py/fable_library/MutableMap.fs +++ b/src/fable-library-py/fable_library/MutableMap.fs @@ -72,6 +72,10 @@ type MutableMap<'Key, 'Value when 'Key: equality> match this.TryFindIndex(k) with | true, h, i when i > -1 -> hashMap.[h].RemoveAt(i) + + if hashMap.[h].Count = 0 then + hashMap.Remove(h) |> ignore + true | _, _, _ -> false diff --git a/src/fable-library-py/fable_library/MutableSet.fs b/src/fable-library-py/fable_library/MutableSet.fs index c0d329a001..9a886e2bd4 100644 --- a/src/fable-library-py/fable_library/MutableSet.fs +++ b/src/fable-library-py/fable_library/MutableSet.fs @@ -60,6 +60,10 @@ type MutableSet<'T when 'T: equality>(items: 'T seq, comparer: IEqualityComparer match this.TryFindIndex(k) with | true, h, i when i > -1 -> hashMap.[h].RemoveAt(i) + + if hashMap.[h].Count = 0 then + hashMap.Remove(h) |> ignore + true | _, _, _ -> false diff --git a/src/fable-library-py/fable_library/Native.fs b/src/fable-library-py/fable_library/Native.fs index 7b67f2e35c..1a9ac42bbc 100644 --- a/src/fable-library-py/fable_library/Native.fs +++ b/src/fable-library-py/fable_library/Native.fs @@ -6,6 +6,38 @@ module Native open Fable.Core open Fable.Core.PyInterop +[] +type ValueErrorBase(message: string, innerException: exn | null) = + inherit System.Exception(message, innerException) + +[] +type IndexErrorBase(message: string) = + inherit System.Exception(message) + +[] +type RuntimeErrorBase(message: string) = + inherit System.Exception(message) + +[] +type ZeroDivisionErrorBase(message: string) = + inherit System.Exception(message) + +[] +type OverflowErrorBase(message: string) = + inherit System.Exception(message) + +[] +type NotImplementedErrorBase(message: string) = + inherit System.Exception(message) + +[] +type MemoryErrorBase(message: string) = + inherit System.Exception(message) + +[] +type TimeoutErrorBase(message: string) = + inherit System.Exception(message) + [] [] type Cons<'T>(arrayType: string) = diff --git a/src/fable-library-py/fable_library/Range.fs b/src/fable-library-py/fable_library/Range.fs index db1175c507..617f2472a8 100644 --- a/src/fable-library-py/fable_library/Range.fs +++ b/src/fable-library-py/fable_library/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T when 'T: comparison> (step: 'T) (stop: 'T) (zero: ' let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-py/fable_library/Seq.fs b/src/fable-library-py/fable_library/Seq.fs index 7991b5b6ad..f6dbc827d8 100644 --- a/src/fable-library-py/fable_library/Seq.fs +++ b/src/fable-library-py/fable_library/Seq.fs @@ -14,6 +14,7 @@ type IEnumerable<'T> = System.Collections.Generic.IEnumerable<'T> module SR = let enumerationAlreadyFinished = "Enumeration already finished." let enumerationNotStarted = "Enumeration has not started. Call MoveNext." + let inputMustBePositive = "The input must be positive." let inputSequenceEmpty = "The input sequence was empty." let inputSequenceTooLong = "The input sequence contains more than one element." @@ -650,6 +651,9 @@ let forAll2 predicate xs ys = // `tryHead`/`head`: no F# impl - Replacements.fs redirects both straight to `seq_native`. let initialize count f = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + unfold (fun i -> if (i < count) then @@ -880,7 +884,7 @@ let reduce folder (xs: seq<'T>) = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack folder (xs: seq<'T>) = let arr = toArray xs @@ -888,9 +892,14 @@ let reduceBack folder (xs: seq<'T>) = if arr.Length > 0 then Array.reduceBack folder arr else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty + +let replicate n x = + // FSharp.Core uses Enumerable.Repeat, which raises ArgumentOutOfRangeException + if n < 0 then + raise (System.ArgumentOutOfRangeException("count")) -let replicate n x = initialize n (fun _ -> x) + initialize n (fun _ -> x) let reverse (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.rev |> ofArray) @@ -923,7 +932,7 @@ let skip count (source: seq<'T>) = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -945,7 +954,21 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -let tail (xs: seq<'T>) = skip 1 xs +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException +let tail (xs: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) // `take`/`takeWhile`: no F# impl - Replacements.fs redirects both straight to `seq_native`. @@ -961,19 +984,32 @@ let pairwise (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.pairwise |> ofArray) let splitInto (chunks: int) (xs: seq<'T>) : 'T[] seq = + if chunks < 1 then + invalidArg "count" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.splitInto chunks |> ofArray) let windowed (windowSize: int) (xs: seq<'T>) : 'T[] seq = + if windowSize < 1 then + invalidArg "windowSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.windowed windowSize |> ofArray) let transpose (xss: seq<#seq<'T>>) : seq> = delay (fun () -> - xss - |> toArray - |> Array.map toArray - |> Array.transpose - |> Array.map ofArray - |> ofArray + let rows = ResizeArray>() + + for xs in xss do + let mutable i = 0 + + for x in xs do + if i = rows.Count then + rows.Add(ResizeArray<'T>()) + + rows.[i].Add(x) + i <- i + 1 + + rows |> map (fun row -> row :> seq<'T>) ) let sortWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) = @@ -1077,6 +1113,9 @@ let permute f (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.permute f |> ofArray) let chunkBySize (chunkSize: int) (xs: seq<'T>) : seq<'T[]> = + if chunkSize < 1 then + invalidArg "chunkSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.chunkBySize chunkSize |> ofArray) // let init = initialize @@ -1181,28 +1220,31 @@ let removeAt (index: int) (xs: seq<'T>) : seq<'T> = let removeManyAt (index: int) (count: int) (xs: seq<'T>) : seq<'T> = if index < 0 then - invalidArg "index" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - generateIndexed - (fun () -> ofSeq xs) - (fun i e -> - if i < index then - if e.MoveNext() then - Some e.Current - else - invalidArg "index" SR.indexOutOfBounds - else - if i = index then - for _ = 1 to count do - if not (e.MoveNext()) then - invalidArg "count" SR.indexOutOfBounds + let endIndex = index + count - if e.MoveNext() then - Some e.Current - else - None - ) - (fun e -> e.Dispose()) + mkSeq (fun () -> + let mutable i = -1 + + Enumerator.generateWhileSome + (fun () -> ofSeq xs) + (fun e -> + let mutable res = None + + while Option.isNone res && e.MoveNext() do + i <- i + 1 + + if i < index || i >= endIndex then + res <- Some e.Current + + if Option.isNone res && i < index then + invalidArg "index" "index must be within bounds of the array" + + res + ) + (fun e -> e.Dispose()) + ) let updateAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = let mutable isDone = false @@ -1236,16 +1278,30 @@ let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceBy randomizer + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceWith random + randomChoiceSource 1 xs |> Array.randomChoiceWith random -let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice +// Draws on enumeration, like FSharp.Core let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = - xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = randomChoicesBy (fun () -> random.NextDouble()) count xs diff --git a/src/fable-library-py/fable_library/Set.fs b/src/fable-library-py/fable_library/Set.fs index 7398b1d7de..0e326e35b1 100644 --- a/src/fable-library-py/fable_library/Set.fs +++ b/src/fable-library-py/fable_library/Set.fs @@ -482,12 +482,12 @@ module SetTree = let minimumElement s = match minimumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." let maximumElement s = match maximumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." // Imperative left-to-right iterators. [] @@ -517,9 +517,11 @@ module SetTree = started = false } - let notStarted () = failwith "Enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "Enumeration already started" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then diff --git a/src/fable-library-py/fable_library/System.Text.fs b/src/fable-library-py/fable_library/System.Text.fs index debe8a2cf1..ee4c44a886 100644 --- a/src/fable-library-py/fable_library/System.Text.fs +++ b/src/fable-library-py/fable_library/System.Text.fs @@ -162,6 +162,14 @@ type StringBuilder(value: string, capacity: int) = let pos = index - len buf[i] <- buf[i][0 .. (pos - 1)] + (string value) + buf[i][(pos + 1) ..] + member x.Length = + let mutable len = 0 + + for i = buf.Count - 1 downto 0 do + len <- len + buf[i].Length + + len + member x.Replace(oldValue: char, newValue: char) = for i = buf.Count - 1 downto 0 do buf[i] <- buf[i].Replace(oldValue, newValue) @@ -172,19 +180,11 @@ type StringBuilder(value: string, capacity: int) = let str = x.ToString().Replace(oldValue, newValue) x.Clear().Append(str) - member x.Length = - let mutable len = 0 - - for i = buf.Count - 1 downto 0 do - len <- len + buf[i].Length - - len - override _.ToString() = System.String.Concat(buf) - member x.ToString(firstIndex: int, length: int) = + member x.ToString(startIndex: int, length: int) = let str = x.ToString() - str.Substring(firstIndex, length) + str.Substring(startIndex, length) // ------------------------------------------------------------------------- // Object overrides diff --git a/src/fable-library-py/fable_library/System.fs b/src/fable-library-py/fable_library/System.fs new file mode 100644 index 0000000000..ca77ee92ae --- /dev/null +++ b/src/fable-library-py/fable_library/System.fs @@ -0,0 +1,100 @@ +namespace System + +// decision: keep Python exception ancestry local so other targets retain their existing bases. + +type SystemException(message: string) = + inherit Exception(message) + new() = SystemException(SR.Arg_SystemException) + +type ApplicationException(message: string) = + inherit Exception(message) + new() = ApplicationException(SR.Arg_ApplicationException) + +type ArithmeticException(message: string) = + inherit Exception(message) + new() = ArithmeticException(SR.Arg_ArithmeticException) + +type DivideByZeroException(message: string) = + inherit Native.ZeroDivisionErrorBase(message) + new() = DivideByZeroException(SR.Arg_DivideByZero) + +type FormatException(message: string) = + inherit Native.ValueErrorBase(message, Unchecked.defaultof) + new() = FormatException(SR.Arg_FormatException) + +type IndexOutOfRangeException(message: string) = + inherit Native.IndexErrorBase(message) + new() = IndexOutOfRangeException(SR.Arg_IndexOutOfRangeException) + +type InvalidOperationException(message: string) = + inherit Native.RuntimeErrorBase(message) + new() = InvalidOperationException(SR.Arg_InvalidOperationException) + +type NotFiniteNumberException(message: string) = + inherit Exception(message) + new() = NotFiniteNumberException(SR.Arg_NotFiniteNumberException) + +type NotImplementedException(message: string) = + inherit Native.NotImplementedErrorBase(message) + new() = NotImplementedException(SR.Arg_NotImplementedException) + +type NotSupportedException(message: string) = + inherit Exception(message) + new() = NotSupportedException(SR.Arg_NotSupportedException) + +type NullReferenceException(message: string) = + inherit Exception(message) + new() = NullReferenceException(SR.Arg_NullReferenceException) + +type OutOfMemoryException(message: string) = + inherit Native.MemoryErrorBase(message) + new() = OutOfMemoryException(SR.Arg_OutOfMemoryException) + +type OverflowException(message: string) = + inherit Native.OverflowErrorBase(message) + new() = OverflowException(SR.Arg_OverflowException) + +type RankException(message: string) = + inherit Exception(message) + new() = RankException(SR.Arg_RankException) + +type StackOverflowException(message: string) = + inherit Exception(message) + new() = StackOverflowException(SR.Arg_StackOverflowException) + +type TimeoutException(message: string) = + inherit Native.TimeoutErrorBase(message) + new() = TimeoutException(SR.Arg_TimeoutException) + +type ArgumentException(message: string, paramName: string, innerException: exn | null) = + inherit + Native.ValueErrorBase( + (if System.String.IsNullOrEmpty(paramName) then + message + else + message + SR.Arg_ParamName_Name + paramName + "')"), + innerException + ) + + // Use Unchecked.defaultof rather than a `null` literal for the absent parameter name + // and inner exception: Fable erases nullable reference annotations, so a `null` literal + // would be emitted against a non-nullable parameter type and fail type checking. + new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof, Unchecked.defaultof) + new(message) = ArgumentException(message, Unchecked.defaultof, Unchecked.defaultof) + new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) + + new(message: string, innerException: exn | null) + = + ArgumentException(message, Unchecked.defaultof, innerException) + + member _.ParamName = paramName + +type ArgumentNullException(paramName: string, message: string) = + inherit ArgumentException(message, paramName, Unchecked.defaultof) + new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) + new() = ArgumentNullException(Unchecked.defaultof) + +type ArgumentOutOfRangeException(paramName: string, message: string) = + inherit ArgumentException(message, paramName, Unchecked.defaultof) + new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) + new() = ArgumentOutOfRangeException(Unchecked.defaultof) diff --git a/src/fable-library-py/fable_library/array_.py b/src/fable-library-py/fable_library/array_.py index 72ce5665ef..2b8877c24d 100644 --- a/src/fable-library-py/fable_library/array_.py +++ b/src/fable-library-py/fable_library/array_.py @@ -196,6 +196,7 @@ "UInt64ArrayCons", "add_in_place", "add_range_in_place", + "all_pairs", "append", "average", "average_by", @@ -211,6 +212,7 @@ "create", "empty", "equals_with", + "exactly_one", "exists", "exists2", "exists_offset", @@ -261,8 +263,6 @@ "partition", "permute", "pick", - "reduce", - "reduce_back", "random_choice", "random_choice_by", "random_choice_with", @@ -278,6 +278,8 @@ "random_shuffle_in_place_by", "random_shuffle_in_place_with", "random_shuffle_with", + "reduce", + "reduce_back", "remove_all_in_place", "remove_at", "remove_in_place", @@ -301,8 +303,6 @@ "split_into", "sum", "sum_by", - "all_pairs", - "exactly_one", "tail", "take", "take_while", @@ -320,8 +320,8 @@ "unzip", "unzip3", "update_at", - "windowed", "where", + "windowed", "zero_create", "zip", "zip3", diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index 1b72efd6ee..462b951e8e 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -4,9 +4,10 @@ from asyncio import Future, ensure_future from collections.abc import Awaitable, Callable, Iterable from concurrent.futures import ThreadPoolExecutor -from threading import Timer +from threading import RLock, Timer from typing import ( Any, + cast, ) from .array_ import Array @@ -16,6 +17,7 @@ Continuations, IAsyncContext, OperationCanceledError, + TaskCanceledError, Trampoline, empty_continuation, protected_bind, @@ -32,6 +34,7 @@ ) from .event import IEvent_2 from .protocols import IEnumerable_1 +from .system import InvalidOperationException__ctor_Z721C83C5, TimeoutException__ctor from .task import TaskCompletionSource from .time_span import TimeSpan, to_milliseconds from .types import UNIT, Unit @@ -58,7 +61,7 @@ def cont(ctx: IAsyncContext[T]): def create_cancellation_token(arg: int | bool | None = None) -> CancellationToken: cancelled = arg if isinstance(arg, bool) else False token = CancellationToken(cancelled) - if isinstance(arg, int): + if isinstance(arg, int) and not isinstance(arg, bool): timer = Timer(arg / 1000.0, token.cancel) timer.start() @@ -79,19 +82,70 @@ def is_cancellation_requested(token: CancellationToken) -> bool: return token.is_cancelled -def sleep(milliseconds_duetime: int | TimeSpan) -> Async[None]: - def cont(ctx: IAsyncContext[None]): - def cancel(): - ctx.on_cancel(OperationCanceledError()) +def call_on_owner_loop(loop: asyncio.AbstractEventLoop, action: Callable[[], None]) -> None: + # decision: foreign-thread cancellation settles on the owning loop while it is running + if loop.is_closed() or not loop.is_running(): + action() + return + try: + current_loop = asyncio.get_running_loop() + except RuntimeError: + current_loop = None + if current_loop is loop: + action() + return + try: + loop.call_soon_threadsafe(action) + except RuntimeError: + if not loop.is_closed(): + raise + action() - token_id = ctx.cancel_token.add_listener(cancel) - def timeout(): - ctx.cancel_token.remove_listener(token_id) - ctx.on_success(None) +def sleep(milliseconds_duetime: int | TimeSpan) -> Async[None]: + def cont(ctx: IAsyncContext[None]) -> None: + loop = asyncio.get_running_loop() + gate = RLock() + completed = False + timer: asyncio.TimerHandle | None = None + token_id: int | None = None + + def settle(cancelled: bool) -> None: + nonlocal completed + with gate: + if completed: + return + cancelled = cancelled or ctx.cancel_token.is_cancelled + completed = True + if timer is not None: + timer.cancel() + listener_id = token_id + # Dispose outside the gate: another thread may already be running cancel(). + if listener_id is not None: + ctx.cancel_token.remove_listener(listener_id) + if cancelled: + ctx.on_cancel(OperationCanceledError()) + else: + ctx.on_success(None) + + def cancel() -> None: + call_on_owner_loop(loop, lambda: settle(True)) - due_time = to_milliseconds(milliseconds_duetime) / 1000.0 - ctx.trampoline.run_later(timeout, due_time) + # invariant: installation and settlement cannot interleave before both resources are owned + try: + with gate: + token_id = ctx.cancel_token.add_listener(cancel) + if not completed: + due_time = to_milliseconds(milliseconds_duetime) / 1000.0 + timer = ctx.trampoline.run_later(lambda: settle(False), due_time) + if completed: + timer.cancel() + except Exception: + with gate: + completed = True + if token_id is not None: + ctx.cancel_token.remove_listener(token_id) + raise return protected_cont(cont) @@ -112,6 +166,8 @@ def cancel() -> None: ctx.on_cancel(OperationCanceledError()) token_id[0] = ctx.cancel_token.add_listener(cancel) + if ctx.cancel_token.is_cancelled: + return event.AddHandler(handler) return protected_cont(cont) @@ -159,6 +215,23 @@ def _arrow21(__unit: Unit = UNIT) -> Async[Array[T]]: return delay(delayed) +def try_cancelled[T](computation: Async[T], compensation: Callable[[OperationCanceledError], None]) -> Async[T]: + def cont(ctx: IAsyncContext[T]) -> None: + def on_cancel(error: OperationCanceledError) -> None: + # decision: preserves the original cancellation when compensation throws, matching .NET + try: + compensation(error) + except Exception: + pass + ctx.trampoline.run(lambda: ctx.on_cancel(error)) + + ctx_ = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, ctx.on_success, ctx.on_error, on_cancel) + # invariant: installs the handler without adding a cancellation checkpoint, including for nested handlers + computation(ctx_) + + return cont + + def catch_async[T](work: Async[T]) -> Async[FSharpChoice_2[T, Exception]]: def cont(ctx: IAsyncContext[FSharpChoice_2[T, Exception]]) -> None: def on_success(x: T): @@ -173,16 +246,60 @@ def on_error(err: Exception): return protected_cont(cont) -def from_continuations[T]( +def from_continuations[T](f: Callable[[Continuations[T]], None]) -> Async[T]: + def cont(ctx: IAsyncContext[T]) -> None: + settled = False + gate = RLock() + + def claim() -> None: + nonlocal settled + with gate: + if settled: + raise InvalidOperationException__ctor_Z721C83C5("A continuation was invoked more than once") + settled = True + + # Compiled F# calls the success continuation with no argument when T is unit + def success(value: T = cast(T, None)) -> None: + claim() + ctx.on_success(value) + + def error(value: Exception) -> None: + claim() + ctx.on_error(value) + + def cancelled(value: OperationCanceledError) -> None: + claim() + ctx.on_cancel(value) + + def on_exception(value: Exception) -> None: + with gate: + completed = settled + if completed: + raise value + error(value) + + guarded = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, success, on_exception, cancelled) + protected_cont(lambda _: f((success, error, cancelled)))(guarded) + + return cont + + +def from_continuations_unit( f: Callable[ - [Continuations[T]], + [tuple[Callable[[], None], Callable[[Exception], None], Callable[[OperationCanceledError], None]]], None, ], -) -> Callable[[IAsyncContext[T]], None]: - def cont(ctx: IAsyncContext[T]) -> None: - f((ctx.on_success, ctx.on_error, ctx.on_cancel)) +) -> Async[None]: + def callback(conts: Continuations[None]) -> None: + success, error, cancelled = conts - return protected_cont(cont) + # Generic callers pass None explicitly; specialized F# unit callers erase it. + def unit_success(value: None = None) -> None: + success(value) + + f((unit_success, error, cancelled)) + + return from_continuations(callback) def await_task[T](task: Awaitable[T]) -> Async[T]: @@ -212,6 +329,17 @@ def callback(conts: Continuations[T]) -> None: return from_continuations(callback) +def await_task_with_cancellation[T](task: Awaitable[T]) -> Async[T]: + async def observe() -> T: + try: + return await task + except asyncio.CancelledError as error: + # decision: cancelled tasks are ordinary AwaitTask failures, matching .NET TaskCanceledException + raise TaskCanceledError() from error + + return await_task(observe()) + + def run_in_loop(computation: Callable[..., None]) -> Any: """Run a computation on the event loop. @@ -246,12 +374,19 @@ def start_with_continuations[T]( token is used.""" trampoline = Trampoline() + def done[U](cont: Callable[[U], None]) -> Callable[[U], None]: + def terminal(value: U) -> None: + trampoline.completed = True + cont(value) + + return terminal + ctx = IAsyncContext.create( trampoline, cancellation_token or default_cancellation_token, - continuation or empty_continuation, - exception_continuation or empty_continuation, - cancellation_continuation or empty_continuation, + done(continuation or empty_continuation), + done(exception_continuation or empty_continuation), + done(cancellation_continuation or empty_continuation), ) def runner() -> None: @@ -291,23 +426,29 @@ def cancel(_: OperationCanceledError) -> None: return tcs.get_task() -def start_child[T](computation: Async[T], ms: int | TimeSpan | None = None) -> Async[Async[T]]: - if ms is not None: - # Race the computation against a timeout: whichever settles first wins. - # asyncio.gather (the previous implementation via parallel2) waited for BOTH to settle, - # which meant the timeout always fired even when the computation finished first. - task = start_as_task(computation) +def _start_child_timeout[T](computation: Async[T], ms: int | TimeSpan) -> Async[Async[T]]: + # Return the child result when ready, or raise TimeoutException when the deadline expires. + task = asyncio.ensure_future(start_as_task(computation)) - async def with_timeout() -> T: - try: - return await asyncio.wait_for(task, timeout=to_milliseconds(ms) / 1000.0) - except TimeoutError: - raise TimeoutError() + async def with_timeout() -> T: + try: + return await asyncio.wait_for(task, timeout=to_milliseconds(ms) / 1000.0) + except TimeoutError: + # invariant: a completed child retains its exception; only deadline cancellation is translated + if not task.cancelled(): + raise + raise TimeoutException__ctor() - def cont(ctx: IAsyncContext[Async[T]]) -> None: - protected_return(await_task(with_timeout()))(ctx) + def cont(ctx: IAsyncContext[Async[T]]) -> None: + protected_return(await_task(with_timeout()))(ctx) + + return protected_cont(cont) - return protected_cont(cont) + +def start_child[T](computation: Async[T], ms: int | TimeSpan | None = None) -> Async[Async[T]]: + # decision: share TimeoutException across callers; its native base preserves Python TimeoutError handlers + if ms is not None: + return _start_child_timeout(computation, ms) task = start_as_task(computation) @@ -386,6 +527,7 @@ def exception_handler(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) __all__ = [ "await_event", "await_task", + "await_task_with_cancellation", "cancel", "cancel_after", "cancellation_token", @@ -403,4 +545,5 @@ def exception_handler(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) "start_child", "start_immediate", "start_with_continuations", + "try_cancelled", ] diff --git a/src/fable-library-py/fable_library/async_builder.py b/src/fable-library-py/fable_library/async_builder.py index eb098c1c95..c65ee3e558 100644 --- a/src/fable-library-py/fable_library/async_builder.py +++ b/src/fable-library-py/fable_library/async_builder.py @@ -4,16 +4,15 @@ from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass, field -from threading import Lock, RLock +from threading import Condition, Lock, RLock, get_ident from typing import ( Any, Literal, - Protocol, overload, ) from .protocols import IEnumerable_1 -from .util import Disposable, IDisposable +from .util import IDisposable class OperationCanceledError(Exception): @@ -21,6 +20,12 @@ def __init__(self, msg: str | None = None) -> None: super().__init__(msg or "The operation was canceled") +class TaskCanceledError(OperationCanceledError): + def __init__(self, msg: str | None = None, inner_exception: Exception | None = None) -> None: + Exception.__init__(self, "A task was canceled." if msg is None else msg) + self.inner_exception: Exception | None = inner_exception + + type Continuations[T] = tuple[ Callable[[T], None], Callable[[Exception], None], @@ -28,58 +33,105 @@ def __init__(self, msg: str | None = None) -> None: ] -class _Listener(Protocol): - def __call__(self, __state: Any | None = None) -> None: ... +_NO_STATE = object() + + +class CancellationCallbackError(ExceptionGroup[Exception]): + @property + def inner_exception(self) -> Exception: + # invariant: F# InnerException exposes the first failure without discarding the remaining errors + return self.exceptions[0] class CancellationToken: - __slots__ = "cancelled", "idx", "listeners", "lock" + __slots__ = "_condition", "_running", "cancelled", "idx", "listeners", "lock" - def __init__(self, cancelled: bool = False): + def __init__(self, cancelled: bool = False) -> None: self.cancelled = cancelled self.listeners: dict[int, Callable[[], None]] = {} self.idx = 0 self.lock = RLock() + self._condition = Condition(self.lock) + self._running: dict[int, int] = {} @property - def is_cancelled(self): - return self.cancelled + def is_cancelled(self) -> bool: + with self.lock: + return self.cancelled is_cancellation_requested = is_cancelled def cancel(self) -> None: - cancel = False with self.lock: - if not self.cancelled: - cancel = True - self.cancelled = True + if self.cancelled: + return + self.cancelled = True + # decision: snapshots IDs newest first to match .NET while allowing disposal of pending callbacks + pending = list(reversed(self.listeners)) + + errors: list[Exception] = [] + for listener_id in pending: + try: + self._invoke_listener(listener_id) + except Exception as error: + errors.append(error) + if errors: + raise CancellationCallbackError("Cancellation callbacks failed", errors) - if cancel: - for listener in self.listeners.values(): - listener() + def _invoke_listener(self, listener_id: int) -> None: + with self.lock: + listener = self.listeners.pop(listener_id, None) + if listener is None: + return + self._running[listener_id] = get_ident() + # invariant: user callbacks execute outside the registry lock + try: + listener() + finally: + with self._condition: + del self._running[listener_id] + self._condition.notify_all() def add_listener(self, f: Callable[[], None]) -> int: with self.lock: - id = self.idx - self.idx = self.idx + 1 - self.listeners[id] = f - - return id + listener_id = self.idx + self.idx += 1 + if not self.cancelled: + self.listeners[listener_id] = f + return listener_id + # .NET invokes late registrations synchronously and does not retain them. + f() + return listener_id def remove_listener(self, id: int) -> None: - with self.lock: - del self.listeners[id] + with self._condition: + self.listeners.pop(id, None) + # invariant: disposal waits for a running callback except when that callback disposes itself + while id in self._running and self._running[id] != get_ident(): + self._condition.wait() - def register(self, f: _Listener, state: Any | None = None) -> None: - if state: - id = self.add_listener(lambda: f(state)) - else: - id = self.add_listener(f) + @overload + def register(self, f: Callable[[], None]) -> IDisposable: ... - def dispose(): - self.remove_listener(id) + @overload + def register[S](self, f: Callable[[S], None], state: S) -> IDisposable: ... - Disposable.create(dispose) + def register(self, f: Callable[..., None], state: object = _NO_STATE) -> IDisposable: + listener_id = self.add_listener(f if state is _NO_STATE else lambda: f(state)) + return _CancellationRegistration(self, listener_id) + + def Dispose(self) -> None: + # Source disposal is currently a no-op, as in the compiler replacement and JS runtime. + pass + + +class _CancellationRegistration: + def __init__(self, token: CancellationToken, listener_id: int) -> None: + self._token = token + self._listener_id = listener_id + + def Dispose(self) -> None: + self._token.remove_listener(self._listener_id) class IAsyncContext[T]: @@ -181,7 +233,7 @@ class ScheduledItem: class Trampoline: - __slots__ = "call_count", "lock", "running" + __slots__ = "call_count", "completed", "lock", "running" MaxTrampolineCallCount = 75 # Max recursion depth: 1000 @@ -189,6 +241,8 @@ def __init__(self) -> None: self.call_count: int = 0 self.lock = Lock() self.running = False + # After a terminal continuation runs, exceptions reaching protected_cont come from continuation code + self.completed = False def increment_and_check(self): with self.lock: @@ -199,30 +253,36 @@ def run_later( self, action: Callable[[], None], due_time: float = 0.0, - ): + ) -> asyncio.TimerHandle: loop = asyncio.get_running_loop() - loop.call_later(due_time, action) + return loop.call_later(due_time, action) def run(self, action: Callable[[], None]): - loop = asyncio.get_running_loop() + if not self.increment_and_check(): + action() + return - if self.increment_and_check(): - self.call_count = 0 - loop.call_soon(action) - else: + self.call_count = 0 + try: + loop = asyncio.get_running_loop() + except RuntimeError: action() + return + loop.call_soon(action) def protected_cont[T](f: Async[T]) -> Async[T]: def _protected_cont(ctx: IAsyncContext[T]): if ctx.cancel_token and ctx.cancel_token.is_cancelled: ctx.on_cancel(OperationCanceledError()) + return def fn(): try: return f(ctx) except Exception as err: - # print("Exception: ", err) + if ctx.trampoline.completed: + raise ctx.on_error(err) ctx.trampoline.run(fn) @@ -237,10 +297,11 @@ def protected_bind[T, U]( def cont(ctx: IAsyncContext[U]) -> None: def on_success(x: T) -> None: try: - binder(x)(ctx) + bound = binder(x) except Exception as err: - # print("Exception: ", err) ctx.on_error(err) + return + bound(ctx) ctx_ = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, on_success, ctx.on_error, ctx.on_cancel) return computation(ctx_) @@ -297,30 +358,63 @@ def ReturnFrom[T](self, computation: Async[T]) -> Async[T]: def TryFinally[T](self, computation: Async[T], compensation: Callable[[], None]) -> Async[T]: def cont(ctx: IAsyncContext[T]) -> None: + settled = False + gate = Lock() + + def claim() -> bool: + nonlocal settled + # on_cancel may run on another thread while on_success runs on the loop + with gate: + if settled: + return False + settled = True + return True + def on_success(x: T) -> None: - compensation() + if not claim(): + return + try: + compensation() + except Exception as error: + ctx.on_error(error) + return ctx.on_success(x) def on_error(x: Exception) -> None: - compensation() + if not claim(): + raise x + try: + compensation() + except Exception as error: + ctx.on_error(error) + return ctx.on_error(x) def on_cancel(x: OperationCanceledError) -> None: - compensation() + if not claim(): + return + # decision: ignores compensation failures during cancellation, matching .NET Async.TryFinally + try: + compensation() + except Exception: + pass ctx.on_cancel(x) ctx_ = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, on_success, on_error, on_cancel) - computation(ctx_) + # The cancellation check in protected_cont must see the cleanup continuations + protected_cont(computation)(ctx_) - return protected_cont(cont) + return cont def TryWith[T](self, computation: Async[T], catch_handler: Callable[[Exception], Async[T]]) -> Async[T]: def fn(ctx: IAsyncContext[T]): def on_error(err: Exception) -> None: try: - catch_handler(err)(ctx) + handled = catch_handler(err) except Exception as ex2: ctx.on_error(ex2) + return + handled(ctx) ctx_ = IAsyncContext.create( on_success=ctx.on_success, @@ -338,7 +432,8 @@ def Using[D: IDisposable, U](self, resource: D, binder: Callable[[D], Async[U]]) def compensation() -> None: return resource.Dispose() - return self.TryFinally(binder(resource), compensation) + # Without Delay the binder runs before TryFinally installs Dispose + return self.TryFinally(self.Delay(lambda: binder(resource)), compensation) @overload def While(self, guard: Callable[[], bool], computation: Async[Literal[None]]) -> Async[None]: ... @@ -361,3 +456,23 @@ def Zero(self) -> Async[None]: singleton = AsyncBuilder() + + +__all__ = [ + "AnonymousAsyncContext", + "Async", + "AsyncBuilder", + "CancellationCallbackError", + "CancellationToken", + "Continuations", + "IAsyncContext", + "OperationCanceledError", + "ScheduledItem", + "TaskCanceledError", + "Trampoline", + "empty_continuation", + "protected_bind", + "protected_cont", + "protected_return", + "singleton", +] diff --git a/src/fable-library-py/fable_library/char.py b/src/fable-library-py/fable_library/char.py index 9359054d4a..9be399678e 100644 --- a/src/fable-library-py/fable_library/char.py +++ b/src/fable-library-py/fable_library/char.py @@ -239,7 +239,6 @@ def parse(input: str) -> str: "is_number", "is_punctuation", "is_separator", - "is_separator", "is_surrogate", "is_surrogate_pair", "is_symbol", diff --git a/src/fable-library-py/fable_library/convert.py b/src/fable-library-py/fable_library/convert.py new file mode 100644 index 0000000000..a95a600d6c --- /dev/null +++ b/src/fable-library-py/fable_library/convert.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import SupportsInt + +from .array_ import Array +from .core import byte +from .system import ( + ArgumentNullException__ctor_Z721C83C5, + ArgumentOutOfRangeException__ctor_Z384F8060, + ArgumentOutOfRangeException__ctor_Z721C83C5, + FormatException__ctor_Z721C83C5, +) + + +_HEX_DIGITS = frozenset("0123456789abcdefABCDEF") + + +def _hex_input(in_array: Array[byte], offset: SupportsInt | None, length: SupportsInt | None) -> bytes: + if in_array is None: + raise ArgumentNullException__ctor_Z721C83C5("inArray") + data = bytes(in_array) + start = 0 if offset is None else int(offset) + count = len(data) - start if length is None else int(length) + if count < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060("length", "Non-negative number required.") + if start < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060("offset", "Non-negative number required.") + if start > len(data) - count: + raise ArgumentOutOfRangeException__ctor_Z721C83C5("offset") + return data[start : start + count] + + +def to_hex_string(in_array: Array[byte], offset: SupportsInt | None = None, length: SupportsInt | None = None) -> str: + """Convert a byte array, or length bytes of it from offset, to uppercase hex (Convert.ToHexString).""" + return _hex_input(in_array, offset, length).hex().upper() + + +def to_hex_string_lower( + in_array: Array[byte], offset: SupportsInt | None = None, length: SupportsInt | None = None +) -> str: + """Convert a byte array, or length bytes of it from offset, to lowercase hex (Convert.ToHexStringLower).""" + return _hex_input(in_array, offset, length).hex() + + +def from_hex_string(s: str) -> Array[byte]: + """Convert a hex string of either case to a byte array (Convert.FromHexString).""" + if s is None: + raise ArgumentNullException__ctor_Z721C83C5("s") + if len(s) % 2 != 0: + raise FormatException__ctor_Z721C83C5( + "The input is not a valid hex string as its length is not a multiple of 2." + ) + if not _HEX_DIGITS.issuperset(s): + raise FormatException__ctor_Z721C83C5("The input is not a valid hex string as it contains a non-hex character.") + return Array[byte](byte(x) for x in bytes.fromhex(s)) diff --git a/src/fable-library-py/fable_library/core/__init__.py b/src/fable-library-py/fable_library/core/__init__.py index 042ca692fa..66db388c1e 100644 --- a/src/fable-library-py/fable_library/core/__init__.py +++ b/src/fable-library-py/fable_library/core/__init__.py @@ -1,4 +1,7 @@ -from math import copysign as _copysign, fmod as _fmod, inf as _inf, nan as _nan +from math import copysign as _copysign +from math import fmod as _fmod +from math import inf as _inf +from math import nan as _nan from typing import Literal, SupportsFloat, SupportsInt from ._core import ( diff --git a/src/fable-library-py/fable_library/core/floats.pyi b/src/fable-library-py/fable_library/core/floats.pyi index 0ff33160ad..be85e63968 100644 --- a/src/fable-library-py/fable_library/core/floats.pyi +++ b/src/fable-library-py/fable_library/core/floats.pyi @@ -110,10 +110,10 @@ def log10(x: float) -> float: ... def log2(x: float) -> float: ... def degrees(x: float) -> float: ... def radians(x: float) -> float: ... -def is_nan(x: float) -> bool: ... -def is_infinity(x: float) -> bool: ... -def is_positive_infinity(x: float) -> bool: ... -def is_negative_infinity(x: float) -> bool: ... +def is_nan(x: SupportsFloat) -> bool: ... +def is_infinity(x: SupportsFloat) -> bool: ... +def is_positive_infinity(x: SupportsFloat) -> bool: ... +def is_negative_infinity(x: SupportsFloat) -> bool: ... def floor(x: float) -> float: ... def ceil(x: float) -> float: ... def pow(x: float, y: float | FloatNumeric) -> float: ... diff --git a/src/fable-library-py/fable_library/core/option.pyi b/src/fable-library-py/fable_library/core/option.pyi index b0ec30ff99..ef88a73827 100644 --- a/src/fable-library-py/fable_library/core/option.pyi +++ b/src/fable-library-py/fable_library/core/option.pyi @@ -5,7 +5,8 @@ from typing import overload from . import Array class SomeWrapper[T]: - value: T + @property + def value(self) -> T: ... def __init__(self, value: T) -> None: ... def __repr__(self) -> str: ... def __str__(self) -> str: ... diff --git a/src/fable-library-py/fable_library/date.py b/src/fable-library-py/fable_library/date.py index 08dd229365..d8209c0500 100644 --- a/src/fable-library-py/fable_library/date.py +++ b/src/fable-library-py/fable_library/date.py @@ -50,6 +50,11 @@ ] +def _without_kind(d: datetime) -> datetime: + # .NET compares and subtracts the tick count and ignores DateTimeKind + return d.replace(tzinfo=None) + + @overload def subtract(x: datetime, y: datetime) -> TimeSpan: ... @overload @@ -58,7 +63,7 @@ def subtract(x: datetime, y: TimeSpan) -> datetime: ... def subtract(x: datetime, y: datetime | TimeSpan) -> datetime | TimeSpan: if isinstance(y, datetime): - delta = x - y + delta = _without_kind(x) - _without_kind(y) # ts.microseconds only contains the microseconds provided to the constructor # so we need to calculate the total microseconds ourselves delta_microseconds = delta.days * (24 * 3600 * 10**6) + delta.seconds * 10**6 + delta.microseconds @@ -604,7 +609,7 @@ def now() -> datetime: def utc_now() -> datetime: - return datetime.now(UTC).replace(tzinfo=None) + return datetime.now(UTC) def today() -> datetime: @@ -616,17 +621,19 @@ def to_local_time(date: datetime) -> datetime: def compare(x: datetime, y: datetime) -> int: - if x == y: + _x, _y = _without_kind(x), _without_kind(y) + + if _x == _y: return 0 - if x < y: + if _x < _y: return -1 return 1 def equals(x: datetime, y: datetime) -> bool: - return x == y + return _without_kind(x) == _without_kind(y) def max_value() -> datetime: diff --git a/src/fable-library-py/fable_library/decimal_.py b/src/fable-library-py/fable_library/decimal_.py index d9e92a2861..6d20a9aa42 100644 --- a/src/fable-library-py/fable_library/decimal_.py +++ b/src/fable-library-py/fable_library/decimal_.py @@ -1,6 +1,6 @@ -from decimal import MAX_EMAX, MIN_EMIN, Decimal, getcontext +from decimal import Decimal, getcontext -from .core import FSharpRef, byte, float32, float64, int16, int64, sbyte, uint16, uint32, uint64 +from .core import Array, FSharpRef, array, byte, float32, float64, int16, int64, sbyte, uint16, uint32, uint64 from .types import IntegerTypes @@ -10,8 +10,8 @@ get_one = Decimal(1) get_minus_one = Decimal(-1) -get_max_value = MAX_EMAX -get_min_value = MIN_EMIN +get_max_value = Decimal("79228162514264337593543950335") +get_min_value = Decimal("-79228162514264337593543950335") def compare(x: Decimal, y: Decimal) -> int: @@ -141,6 +141,48 @@ def from_parts( return value +def from_ints(low: IntegerTypes, mid: IntegerTypes, high: IntegerTypes, sign_exp: IntegerTypes) -> Decimal: + _sign_exp = int(sign_exp) + is_negative = 1 if _sign_exp < 0 else 0 + scale = (_sign_exp >> 16) & 0x7F + return from_parts(low, mid, high, is_negative, scale) + + +def from_int_array(bits: Array[int]) -> Decimal: + return from_ints(bits[0], bits[1], bits[2], bits[3]) + + +def _to_int32(value: int) -> int: + masked = value & 0xFFFFFFFF + return masked - 0x100000000 if masked >= 0x80000000 else masked + + +def get_bits(value: Decimal) -> Array[int]: + sign, digits, exponent = value.as_tuple() + + if not isinstance(exponent, int): + raise ValueError(f"The value {value} cannot be represented as a System.Decimal.") + + mantissa = int("".join(str(digit) for digit in digits)) + scale = 0 + + if exponent > 0: + mantissa *= 10**exponent + else: + scale = -exponent + + flags = ((scale & 0x7F) << 16) | (0x80000000 if sign else 0) + + return array.Int32Array( + [ + _to_int32(mantissa), + _to_int32(mantissa >> 32), + _to_int32(mantissa >> 64), + _to_int32(flags), + ] + ) + + def to_string(x: Decimal) -> str: return str(x) @@ -165,14 +207,26 @@ def try_parse(string: str, def_value: FSharpRef[Decimal]) -> bool: return False +def _from_float(value: float, significant_digits: int) -> Decimal: + rounded = Decimal(f"{value:.{significant_digits}G}") + + if rounded.is_zero(): + return get_zero + + # `Decimal` keeps the exponent it was parsed with, which would print as 1E+20 + return Decimal(format(rounded, "f")) + + def create(value: float | float32 | IntegerTypes | str) -> Decimal: match value: - # Int32 and Float64 are plain `int`/`float`, which `Decimal` already accepts - # via the last case + # Int32 is a plain `int`, which `Decimal` already accepts via the last case case sbyte() | byte() | int16() | uint16() | uint32() | int64() | uint64(): return Decimal(int(value)) + # .NET rounds to 7 significant digits for Single and 15 for Double case float32(): - return Decimal(float(value)) + return _from_float(float(value), 7) + case float(): + return _from_float(value, 15) case _: return Decimal(value) @@ -184,7 +238,10 @@ def create(value: float | float32 | IntegerTypes | str) -> Decimal: "compare", "divide", "equals", + "from_int_array", + "from_ints", "from_parts", + "get_bits", "max", "min", "multiply", diff --git a/src/fable-library-py/fable_library/diagnostics.py b/src/fable-library-py/fable_library/diagnostics.py index 03c78829ca..fbe00a6a4e 100644 --- a/src/fable-library-py/fable_library/diagnostics.py +++ b/src/fable-library-py/fable_library/diagnostics.py @@ -54,3 +54,11 @@ def frequency() -> int: def get_timestamp() -> int64: return int64(time.perf_counter_ns()) + + +__all__ = [ + "StopWatch", + "TimerError", + "frequency", + "get_timestamp", +] diff --git a/src/fable-library-py/fable_library/event.py b/src/fable-library-py/fable_library/event.py index 115f2e7f7f..26aebcd35b 100644 --- a/src/fable-library-py/fable_library/event.py +++ b/src/fable-library-py/fable_library/event.py @@ -217,3 +217,25 @@ def create_event[T]( remove_handler: Callable[[DotNetDelegate[T]], None], ) -> IEvent[T]: return AnonymousEvent[T](add_handler, remove_handler) + + +__all__ = [ + "AnonymousEvent", + "Delegate", + "DotNetDelegate", + "Event", + "EventDelegate", + "IDelegateEvent", + "IEvent", + "IEvent_2", + "add", + "choose", + "create_event", + "filter", + "map", + "merge", + "pairwise", + "partition", + "scan", + "split", +] diff --git a/src/fable-library-py/fable_library/exception_bases.py b/src/fable-library-py/fable_library/exception_bases.py new file mode 100644 index 0000000000..817c257320 --- /dev/null +++ b/src/fable-library-py/fable_library/exception_bases.py @@ -0,0 +1,65 @@ +"""Python exception bases for generated .NET exception classes. + +decision: retain ExceptionBase ancestry and payloads while adding one specialized Python exception base. +""" + + +class ExceptionBase(Exception): + """Base class for .NET ``System.Exception`` and its subclasses. + + Subclasses the built-in ``Exception`` so ``raise``/``except``/``isinstance`` + keep working as before. Only the message is forwarded to the built-in + initializer, so ``str(exc)`` still returns the message even when an inner + exception is supplied (the built-in would otherwise stringify the whole + argument tuple). The inner exception is kept on a dedicated attribute so it + can be read back through ``System.Exception.InnerException``. + """ + + def __init__(self, message: str | None = None, inner_exception: Exception | None = None) -> None: + super().__init__(message if message is not None else "") + self.inner_exception: Exception | None = inner_exception + + +class ValueErrorBase(ExceptionBase, ValueError): + """A value error with .NET message and inner-exception support.""" + + +class IndexErrorBase(ExceptionBase, IndexError): + """An index error with .NET message and inner-exception support.""" + + +class RuntimeErrorBase(ExceptionBase, RuntimeError): + """A runtime error with .NET message and inner-exception support.""" + + +class ZeroDivisionErrorBase(ExceptionBase, ZeroDivisionError): + """A division error with .NET message and inner-exception support.""" + + +class OverflowErrorBase(ExceptionBase, OverflowError): + """An overflow error with .NET message and inner-exception support.""" + + +class NotImplementedErrorBase(ExceptionBase, NotImplementedError): + """An unimplemented operation with .NET message and inner-exception support.""" + + +class MemoryErrorBase(ExceptionBase, MemoryError): + """A memory error with .NET message and inner-exception support.""" + + +class TimeoutErrorBase(ExceptionBase, TimeoutError): + """A timeout error with .NET message and inner-exception support.""" + + +__all__ = [ + "ExceptionBase", + "IndexErrorBase", + "MemoryErrorBase", + "NotImplementedErrorBase", + "OverflowErrorBase", + "RuntimeErrorBase", + "TimeoutErrorBase", + "ValueErrorBase", + "ZeroDivisionErrorBase", +] diff --git a/src/fable-library-py/fable_library/file.py b/src/fable-library-py/fable_library/file.py index b2fb8b6ea3..dced0abab9 100644 --- a/src/fable-library-py/fable_library/file.py +++ b/src/fable-library-py/fable_library/file.py @@ -107,3 +107,24 @@ def replace(source_file_name: str, destination_file_name: str, destination_backu with open(destination_backup_file_name, "wb") as f: f.write(data) os.replace(source_file_name, destination_file_name) + + +__all__ = [ + "copy", + "delete", + "exists", + "move", + "read_all_bytes", + "read_all_bytes_async", + "read_all_lines", + "read_all_lines_async", + "read_all_text", + "read_all_text_async", + "replace", + "write_all_bytes", + "write_all_bytes_async", + "write_all_lines", + "write_all_lines_async", + "write_all_text", + "write_all_text_async", +] diff --git a/src/fable-library-py/fable_library/long.py b/src/fable-library-py/fable_library/long.py index 90febfa89f..0936936fbf 100644 --- a/src/fable-library-py/fable_library/long.py +++ b/src/fable-library-py/fable_library/long.py @@ -304,54 +304,29 @@ def to_string(x: int64 | uint64) -> str: "from_value", "get_range", "max", - "max", - "min", "min", "op_addition", - "op_addition", - "op_bitwise_and", "op_bitwise_and", "op_bitwise_or", - "op_bitwise_or", - "op_division", "op_division", "op_equality", - "op_equality", "op_exclusive_or", - "op_exclusive_or", - "op_greater_than", "op_greater_than", "op_greater_than_or_equal", - "op_greater_than_or_equal", "op_inequality", - "op_inequality", - "op_left_shift", "op_left_shift", "op_less_than", - "op_less_than", "op_less_than_or_equal", - "op_less_than_or_equal", - "op_logical_not", "op_logical_not", "op_modulus", - "op_modulus", "op_multiply", - "op_multiply", - "op_right_shift", "op_right_shift", "op_subtraction", - "op_subtraction", "op_unary_negation", - "op_unary_negation", - "op_unary_plus", "op_unary_plus", "parse", - "parse", - "sign", "sign", "to_number", "to_string", - "to_string", - "try_parse", "try_parse", ] diff --git a/src/fable-library-py/fable_library/mailbox_processor.py b/src/fable-library-py/fable_library/mailbox_processor.py index 663635f589..5762d22f28 100644 --- a/src/fable-library-py/fable_library/mailbox_processor.py +++ b/src/fable-library-py/fable_library/mailbox_processor.py @@ -1,16 +1,19 @@ from __future__ import annotations +import asyncio from collections.abc import Callable from queue import SimpleQueue -from threading import RLock +from threading import Lock, RLock from typing import Any -from .async_ import from_continuations, start_immediate +from .async_ import call_on_owner_loop, from_continuations, start_immediate from .async_builder import ( Async, CancellationToken, Continuations, + IAsyncContext, OperationCanceledError, + protected_cont, ) @@ -70,18 +73,30 @@ def post_and_async_reply[Reply](self, build_message: Callable[[AsyncReplyChannel # Track whether `reply` was actually called, since the reply value itself # may legitimately be `None` (e.g. `AsyncReplyChannel[None]` for unit). replied = False + gate = Lock() continuation: Continuations[Any] | None = ( None # This is the continuation for the `done` callback of the awaiting poster. ) def check_completion() -> None: - if replied and continuation is not None: - continuation[0](result) + nonlocal continuation + # reply may run on another thread before the poster's continuation is installed + with gate: + if not replied or continuation is None: + return + deliver = continuation[0] + # Consume this waiter once, retaining the reply for later executions. + continuation = None + deliver(result) def reply_callback(res: Reply): nonlocal result, replied - result = res - replied = True + # .NET AsyncReplyChannel.Reply ignores a second reply + with gate: + if replied: + return + result = res + replied = True check_completion() reply_channel: AsyncReplyChannel[Reply] = AsyncReplyChannel(reply_callback) @@ -90,7 +105,8 @@ def reply_callback(res: Reply): def callback(conts: Continuations[Any]) -> None: nonlocal continuation - continuation = conts + with gate: + continuation = conts check_completion() return from_continuations(callback) @@ -101,34 +117,44 @@ def receive(self) -> Async[Msg]: Returns: An asynchronous computation which will consume the first message in arrival order. No thread is blocked while - waiting for further messages. Raises a TimeoutException if - the timeout is exceeded. + waiting for further messages. Cancelling the token of the + receiving computation cancels a pending receive. """ - def callback(conts: Continuations[Any]): - if self.continuation: - raise Exception("Receive can only be called once!") - - self.continuation = conts - + def cont(ctx: IAsyncContext[Msg]) -> None: + loop = asyncio.get_running_loop() + token_id: int | None = None + + def on_message(msg: Msg) -> None: + if token_id is not None: + ctx.cancel_token.remove_listener(token_id) + ctx.on_success(msg) + + continuation: Continuations[Any] = (on_message, ctx.on_error, ctx.on_cancel) + + def on_cancel() -> None: + with self.lock: + if self.continuation is not continuation: + return + self.continuation = None + call_on_owner_loop(loop, lambda: ctx.on_cancel(OperationCanceledError())) + + with self.lock: + if self.continuation: + raise Exception("Receive can only be called once!") + self.continuation = continuation + + token_id = ctx.cancel_token.add_listener(on_cancel) + with self.lock: + if self.continuation is not continuation: + return self.__process_events() - return from_continuations(callback) + return protected_cont(cont) def __process_events(self) -> None: - if not self.continuation: - return - - # Cancellation of async workflows is more tricky in Python than - # with F# so we check the cancellation token for each process. - if self.token.is_cancellation_requested: - self.continuation, cont = None, self.continuation - if cont: - cont[2](OperationCanceledError("Mailbox was cancelled")) - return - with self.lock: - if self.messages.empty(): + if not self.continuation or self.messages.empty(): return msg = self.messages.get() self.continuation, cont = None, self.continuation @@ -143,7 +169,7 @@ def start( cancellation_token: CancellationToken | None = None, ) -> MailboxProcessor[Msg]: mbox: MailboxProcessor[Msg] = MailboxProcessor(body, cancellation_token) - start_immediate(body(mbox)) + start_immediate(body(mbox), mbox.token) return mbox @@ -163,7 +189,7 @@ def post_and_async_reply[Msg, Reply]( def start_instance(mbox: MailboxProcessor[Any]) -> None: body = mbox.body(mbox) - return start_immediate(body) + return start_immediate(body, mbox.token) def start[Msg]( diff --git a/src/fable-library-py/fable_library/map_util.py b/src/fable-library-py/fable_library/map_util.py index d33794bb5b..69ea4909a6 100644 --- a/src/fable-library-py/fable_library/map_util.py +++ b/src/fable-library-py/fable_library/map_util.py @@ -272,3 +272,31 @@ def assign(key: str, case_rule: CaseRules, value: Any): fail(kv_pair) return obj + + +__all__ = [ + "CaseRules", + "add_to_dict", + "add_to_set", + "change_case", + "contains_in_set", + "contains_value", + "copy_to_array_from_set", + "dashify", + "except_with_set", + "get_item_from_dict", + "intersect_with_set", + "is_proper_subset_of_set", + "is_proper_superset_of_set", + "is_subset_of_set", + "is_superset_of_set", + "key_value_list", + "make_dict", + "overlaps_with_set", + "remove_from_dict", + "remove_from_set", + "set_equals_set", + "symmetric_except_with_set", + "try_get_value", + "union_with_set", +] diff --git a/src/fable-library-py/fable_library/observable.py b/src/fable-library-py/fable_library/observable.py index 9c5cef70ae..7cb80d1fe1 100644 --- a/src/fable-library-py/fable_library/observable.py +++ b/src/fable-library-py/fable_library/observable.py @@ -216,3 +216,22 @@ def split[T, U, V]( choose(lambda v: Choice_tryValueIfChoice1Of2(splitter(v)), source), choose(lambda v: Choice_tryValueIfChoice2Of2(splitter(v)), source), ) + + +__all__ = [ + "IObservable", + "IObserver", + "Observable", + "Observer", + "add", + "choose", + "filter", + "map", + "merge", + "pairwise", + "partition", + "protect", + "scan", + "split", + "subscribe", +] diff --git a/src/fable-library-py/fable_library/protocols.py b/src/fable-library-py/fable_library/protocols.py index 49bd980945..a123a17f49 100644 --- a/src/fable-library-py/fable_library/protocols.py +++ b/src/fable-library-py/fable_library/protocols.py @@ -442,3 +442,31 @@ def keys(self, __unit: Unit = None) -> Iterable[K]: def __getitem__(self, key: K, /) -> V: """Get the value for a key.""" ... + + +__all__ = [ + "HashCode", + "ICollection", + "IComparable", + "IComparable_1", + "IComparer", + "IComparer_1", + "IDictionary_2", + "IDisposable", + "IEnumerable", + "IEnumerable_1", + "IEnumerator", + "IEqualityComparer", + "IEqualityComparer_1", + "IEquatable", + "IGenericAdder", + "IGenericAverager", + "IReadOnlyCollection_1", + "IReadOnlyDictionary", + "IReadOnlyDictionary_2", + "IReadOnlySet_1", + "ISet", + "IStructuralComparable", + "IStructuralEquatable", + "SupportsLessThan", +] diff --git a/src/fable-library-py/fable_library/quotation.py b/src/fable-library-py/fable_library/quotation.py index 1063607ad5..df41ab4a0a 100644 --- a/src/fable-library-py/fable_library/quotation.py +++ b/src/fable-library-py/fable_library/quotation.py @@ -653,3 +653,117 @@ def sub(e: Expr) -> Expr: return e return sub(expr) + + +# =================================================================== +# DerivedPatterns +# F# defines these on top of Patterns. AndAlso and OrElse recover the +# shape `&&` and `||` desugar into; SpecificCall matches a call by the +# identity of a template quotation rather than by compiled name. +# =================================================================== + + +def is_and_also(expr: Expr) -> tuple[Expr, Expr] | None: + """Match `a && b`, represented as `if a then b else false`.""" + if isinstance(expr, ExprIfThenElse) and isinstance(expr.else_expr, ExprValue) and expr.else_expr.value is False: + return (expr.guard, expr.then_expr) + return None + + +def is_or_else(expr: Expr) -> tuple[Expr, Expr] | None: + """Match `a || b`, represented as `if a then true else b`.""" + if isinstance(expr, ExprIfThenElse) and isinstance(expr.then_expr, ExprValue) and expr.then_expr.value is True: + return (expr.guard, expr.else_expr) + return None + + +def _template_call(expr: Expr) -> ExprCall | None: + """Find the call identifying a template quotation under its lambdas.""" + current = expr + while isinstance(current, ExprLambda): + current = current.body + return current if isinstance(current, ExprCall) else None + + +def is_specific_call(template: Expr, expr: Expr) -> tuple[Expr | None, FSharpList[str], FSharpList[Expr]] | None: + """Match a call by the method identity represented by a template quotation.""" + wanted = _template_call(template) + if not isinstance(expr, ExprCall) or wanted is None: + return None + if expr.method != wanted.method or expr.declaring_type != wanted.declaring_type: + return None + + instance = None if isinstance(expr.instance, ExprValue) and expr.instance.type == "novalue" else expr.instance + # The middle slot is F#'s generic-argument list. This runtime models types as + # names rather than System.Type, so it is always empty; callers match it with + # a wildcard. + return (instance, of_array(Array([])), of_array(expr.args)) + + +__all__ = [ + "Expr", + "ExprApplication", + "ExprCall", + "ExprFieldGet", + "ExprFieldSet", + "ExprIfThenElse", + "ExprLambda", + "ExprLet", + "ExprNewList", + "ExprNewRecord", + "ExprNewTuple", + "ExprNewUnion", + "ExprSequential", + "ExprTupleGet", + "ExprUnionField", + "ExprUnionTag", + "ExprValue", + "ExprVarExpr", + "ExprVarSet", + "Var", + "call_declaring_type", + "evaluate", + "expr_to_string", + "get_free_vars", + "get_type", + "is_and_also", + "is_application", + "is_call", + "is_field_get", + "is_if_then_else", + "is_lambda", + "is_let", + "is_new_record", + "is_new_tuple", + "is_new_union_case", + "is_or_else", + "is_sequential", + "is_specific_call", + "is_tuple_get", + "is_value", + "is_var", + "mk_application", + "mk_call", + "mk_field_get", + "mk_field_set", + "mk_if_then_else", + "mk_lambda", + "mk_let", + "mk_new_list", + "mk_new_record", + "mk_new_tuple", + "mk_new_union", + "mk_null", + "mk_quot_var", + "mk_sequential", + "mk_tuple_get", + "mk_union_field", + "mk_union_tag", + "mk_value", + "mk_var", + "mk_var_set", + "substitute", + "var_get_is_mutable", + "var_get_name", + "var_get_type", +] diff --git a/src/fable-library-py/fable_library/random_.py b/src/fable-library-py/fable_library/random_.py new file mode 100644 index 0000000000..b0c82a691a --- /dev/null +++ b/src/fable-library-py/fable_library/random_.py @@ -0,0 +1,30 @@ +"""System.Random methods that check their arguments like .NET (util.py cannot import the exception types).""" + +from __future__ import annotations + +import random + +from .system import ArgumentOutOfRangeException__ctor_Z384F8060 +from .util import random_int + + +def next_max(rand: random.Random, max_value: int) -> int: + """Random.Next(maxValue).""" + if max_value < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060( + "maxValue", f"maxValue ('{max_value}') must be a non-negative value." + ) + return random_int(rand, 0, max_value) + + +def next_range(rand: random.Random, min_value: int, max_value: int) -> int: + """Random.Next(minValue, maxValue).""" + if min_value > max_value: + raise ArgumentOutOfRangeException__ctor_Z384F8060("minValue", "'minValue' cannot be greater than maxValue.") + return random_int(rand, min_value, max_value) + + +__all__ = [ + "next_max", + "next_range", +] diff --git a/src/fable-library-py/fable_library/record.py b/src/fable-library-py/fable_library/record.py index 12313ed374..6a195f3ad2 100644 --- a/src/fable-library-py/fable_library/record.py +++ b/src/fable-library-py/fable_library/record.py @@ -79,15 +79,23 @@ def _field_to_string(value: object) -> str: def record_to_string(self: Record) -> str: - if hasattr(self, "__slots__"): - return ( - "{ " - + "\n ".join(map(lambda slot: slot + " = " + _field_to_string(getattr(self, slot)), self.__slots__)) - + " }" - ) - else: + if not hasattr(self, "__slots__"): return "{ " + "\n ".join(map(lambda kv: kv[0] + " = " + _field_to_string(kv[1]), self.__dict__.items())) + " }" + slots = self.__slots__ + # Newer generated records retain their source-level F# field names here. + # Fall back to slots so the current runtime remains compatible with code + # compiled before that metadata was emitted. + field_names = getattr(type(self), "__fable_field_names__", slots) + return ( + "{ " + + "\n ".join( + f"{field_name} = {_field_to_string(getattr(self, slot))}" + for slot, field_name in zip(slots, field_names, strict=True) + ) + + " }" + ) + def record_get_hashcode(self: Record) -> int: # Hash each field with `structural_hash` rather than hashing a Python tuple, diff --git a/src/fable-library-py/fable_library/reflection.py b/src/fable-library-py/fable_library/reflection.py index 071b0636d8..b9e1fef101 100644 --- a/src/fable-library-py/fable_library/reflection.py +++ b/src/fable-library-py/fable_library/reflection.py @@ -500,3 +500,90 @@ def get_case_name(x: Any) -> str: def get_case_fields(x: Any) -> list[Any]: assert_union(x) return x.fields + + +__all__ = [ + "CaseInfo", + "Constructor", + "EnumCase", + "FieldInfo", + "MethodInfo", + "ParameterInfo", + "PropertyInfo", + "TypeInfo", + "anon_record_type", + "array_type", + "assert_union", + "bigint_type", + "bool_type", + "char_type", + "class_type", + "create_instance", + "decimal_type", + "delegate_type", + "enum_type", + "equals", + "float32_type", + "float64_type", + "full_name", + "get_case_fields", + "get_case_name", + "get_case_tag", + "get_element_type", + "get_enum_case", + "get_enum_name", + "get_enum_names", + "get_enum_underlying_type", + "get_enum_values", + "get_function_elements", + "get_generic_type_definition", + "get_generics", + "get_record_elements", + "get_record_field", + "get_record_fields", + "get_tuple_elements", + "get_tuple_field", + "get_tuple_fields", + "get_union_case_fields", + "get_union_cases", + "get_union_fields", + "get_value", + "int8_type", + "int16_type", + "int32_type", + "int64_type", + "is_array", + "is_enum", + "is_enum_defined", + "is_erased_to_number", + "is_function", + "is_generic_type", + "is_instance_of_type", + "is_record", + "is_subclass_of", + "is_tuple", + "is_union", + "lambda_type", + "list_type", + "make_generic_type", + "make_record", + "make_tuple", + "make_union", + "name", + "namespace", + "nativeint_type", + "obj_type", + "option_type", + "parse_enum", + "record_type", + "string_type", + "try_parse_enum", + "tuple_type", + "uint8_type", + "uint16_type", + "uint32_type", + "uint64_type", + "unativeint_type", + "union_type", + "unit_type", +] diff --git a/src/fable-library-py/fable_library/reg_exp.py b/src/fable-library-py/fable_library/reg_exp.py index aef8cc9f99..982731b75a 100644 --- a/src/fable-library-py/fable_library/reg_exp.py +++ b/src/fable-library-py/fable_library/reg_exp.py @@ -58,6 +58,10 @@ def GetEnumerator(self, __unit: Unit = UNIT) -> IEnumerator[Match[str]]: def _options_to_flags(options: int) -> int: """Convert .NET options to Python flags.""" + if options & ~(1 | 2 | 8 | 16 | 256 | 1024): + raise Exception( + "RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline, ECMAScript and NonBacktracking" + ) flags = 256 if options & 1: flags |= re.IGNORECASE diff --git a/src/fable-library-py/fable_library/resize_array.py b/src/fable-library-py/fable_library/resize_array.py index e31c625c5f..d2b405f733 100644 --- a/src/fable-library-py/fable_library/resize_array.py +++ b/src/fable-library-py/fable_library/resize_array.py @@ -1,11 +1,12 @@ from __future__ import annotations -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Sequence from typing import Any, cast from .core import int32 from .option import Option, some -from .protocols import IEnumerable_1 +from .protocols import IComparer_1, IEnumerable_1 +from .system import ArgumentException__ctor_Z721C83C5, ArgumentOutOfRangeException__ctor_Z384F8060 from .util import to_iterable @@ -100,6 +101,52 @@ def index_of[T](value: T, start: int, count: int | None, xs: list[T]) -> int: return -1 +def _check_range(size: int, index: int, count: int, count_name: str) -> None: + """Raise like .NET when index and count do not describe a range of a collection of the given size.""" + if index < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060("index", "Non-negative number required.") + if count < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060(count_name, "Non-negative number required.") + if size - index < count: + raise ArgumentException__ctor_Z721C83C5( + "Offset and length were out of bounds for the array or count is greater than " + + "the number of elements from index to the end of the source collection." + ) + + +def _binary_search[T](array: Sequence[T], index: int, length: int, value: T, comparer: IComparer_1[T]) -> int: + """Search array[index:index + length] in .NET's probe order. Returns ~insertion point if not found.""" + lo = index + hi = index + length - 1 + while lo <= hi: + i = lo + ((hi - lo) >> 1) + c = comparer.Compare(array[i], value) + if c == 0: + return int32(i) + if c < 0: + lo = i + 1 + else: + hi = i - 1 + return int32(~lo) + + +def binary_search[T](array: Sequence[T], value: T, comparer: IComparer_1[T]) -> int: + """Binary search a sorted array or ResizeArray (System.Array.BinarySearch).""" + return _binary_search(array, 0, len(array), value, comparer) + + +def binary_search_range[T](array: Sequence[T], index: int, length: int, value: T, comparer: IComparer_1[T]) -> int: + """Binary search a sorted range of an array (System.Array.BinarySearch with index and length).""" + _check_range(len(array), index, length, "length") + return _binary_search(array, index, length, value, comparer) + + +def binary_search_range_count[T](xs: list[T], index: int, count: int, value: T, comparer: IComparer_1[T]) -> int: + """Binary search a sorted range of a ResizeArray (ResizeArray.BinarySearch with index and count).""" + _check_range(len(xs), index, count, "count") + return _binary_search(xs, index, count, value, comparer) + + def insert_range_in_place[T](index: int, items: Iterable[T] | IEnumerable_1[T], xs: list[T]) -> None: """Insert a range of items into xs at the given index.""" xs[index:index] = to_iterable(items) @@ -141,6 +188,9 @@ def contains[T](value: T, xs: list[T], cons: Any | None = None) -> bool: "add_in_place", "add_range", "add_range_in_place", + "binary_search", + "binary_search_range", + "binary_search_range_count", "contains", "exists", "filter", diff --git a/src/fable-library-py/fable_library/seq_native.py b/src/fable-library-py/fable_library/seq_native.py index 1e2342442b..cbdcdf2f9a 100644 --- a/src/fable-library-py/fable_library/seq_native.py +++ b/src/fable-library-py/fable_library/seq_native.py @@ -26,9 +26,11 @@ from .list import try_item as list_try_item from .option import Option, some, value from .protocols import IEnumerable_1, IEnumerator +from .system import ArgumentException__ctor_Z384F8060, InvalidOperationException__ctor_Z721C83C5 from .types import UNIT, Unit from .util import Enumerator, get_enumerator, to_iterator + _SENTINEL: Any = object() @@ -159,12 +161,17 @@ def gen() -> Iterator[T]: def take[T](count: int, xs: IEnumerable_1[T]) -> NativeSeq[T]: + if count < 0: + raise ArgumentException__ctor_Z384F8060("The input must be non-negative.", "count") + def gen() -> Iterator[T]: it = _source_iter(xs) for _ in range(count): v = next(it, _SENTINEL) if v is _SENTINEL: - raise Exception("The input sequence has an insufficient number of elements. (Parameter 'source')") + raise InvalidOperationException__ctor_Z721C83C5( + "The input sequence has an insufficient number of elements." + ) yield v return NativeSeq(gen) @@ -186,7 +193,7 @@ def try_head[T](xs: IEnumerable_1[T]) -> Option[T]: def head[T](xs: IEnumerable_1[T]) -> T: result = try_head(xs) if result is None: - raise Exception("The input sequence was empty. (Parameter 'source')") + raise ArgumentException__ctor_Z384F8060("The input sequence was empty.", "source") return value(result) @@ -212,7 +219,7 @@ def try_item[T](index: int, xs: IEnumerable_1[T]) -> Option[T]: def item[T](index: int, xs: IEnumerable_1[T]) -> T: result = try_item(index, xs) if result is None: - raise Exception("The input sequence has an insufficient number of elements. (Parameter 'index')") + raise ArgumentException__ctor_Z384F8060("The input sequence has an insufficient number of elements.", "index") return value(result) diff --git a/src/fable-library-py/fable_library/thread.py b/src/fable-library-py/fable_library/thread.py index b704c56ba4..4f0a17b78f 100644 --- a/src/fable-library-py/fable_library/thread.py +++ b/src/fable-library-py/fable_library/thread.py @@ -6,3 +6,8 @@ def sleep(milliseconds: int | TimeSpan) -> None: milliseconds_value = to_milliseconds(milliseconds) time.sleep(milliseconds_value / 1000.0) + + +__all__ = [ + "sleep", +] diff --git a/src/fable-library-py/fable_library/time_span.py b/src/fable-library-py/fable_library/time_span.py index 3467ce29ad..3e5de67fde 100644 --- a/src/fable-library-py/fable_library/time_span.py +++ b/src/fable-library-py/fable_library/time_span.py @@ -340,14 +340,10 @@ def to_milliseconds(value: int | TimeSpan) -> float: "subtract", "to_milliseconds", "total_days", - "total_days", - "total_hours", "total_hours", "total_microseconds", "total_milliseconds", "total_minutes", - "total_minutes", - "total_seconds", "total_seconds", "try_parse", ] diff --git a/src/fable-library-py/fable_library/types.py b/src/fable-library-py/fable_library/types.py index 1d140768dd..33af68d883 100644 --- a/src/fable-library-py/fable_library/types.py +++ b/src/fable-library-py/fable_library/types.py @@ -25,6 +25,7 @@ uint32, uint64, ) +from .exception_bases import ExceptionBase as ExceptionBase # Unit type for F# unit-typed parameters. Using None mirrors F#'s @@ -44,22 +45,6 @@ class Attribute: ... -class ExceptionBase(Exception): - """Base class for .NET ``System.Exception`` and its subclasses. - - Subclasses the built-in ``Exception`` so ``raise``/``except``/``isinstance`` - keep working as before. Only the message is forwarded to the built-in - initializer, so ``str(exc)`` still returns the message even when an inner - exception is supplied (the built-in would otherwise stringify the whole - argument tuple). The inner exception is kept on a dedicated attribute so it - can be read back through ``System.Exception.InnerException``. - """ - - def __init__(self, message: str | None = None, inner_exception: Exception | None = None) -> None: - super().__init__(message if message is not None else "") - self.inner_exception: Exception | None = inner_exception - - # We don't use type aliases here because we need to do isinstance checks. # # Generated code represents Int32 as a plain `int` and Float64 as a plain `float`, so diff --git a/src/fable-library-py/fable_library/uri.py b/src/fable-library-py/fable_library/uri.py index 1b71567eb5..1925728356 100644 --- a/src/fable-library-py/fable_library/uri.py +++ b/src/fable-library-py/fable_library/uri.py @@ -108,3 +108,9 @@ def try_create(uri: str | Uri, kind_or_uri: UriKind | Uri | str | int, out: FSha return True except Exception: return False + + +__all__ = [ + "Uri", + "UriKind", +] diff --git a/src/fable-library-py/fable_library/util.py b/src/fable-library-py/fable_library/util.py index c2abf502e5..eda490d628 100644 --- a/src/fable-library-py/fable_library/util.py +++ b/src/fable-library-py/fable_library/util.py @@ -29,6 +29,7 @@ overload, ) from urllib.parse import quote, unquote +from weakref import WeakValueDictionary from .array_ import Array from .bases import ( @@ -55,6 +56,10 @@ from .types import UNIT, Unit +def erase_unit_callable[**P, R](callback: Callable[P, R], /) -> Callable[..., R]: + return callback + + class nullable: """Returns a tuple of None values with the specified types. @@ -351,16 +356,21 @@ def assert_not_equal[T](actual: T, expected: T, msg: str | None = None) -> None: raise Exception(msg or f"Expected not equal to: {expected} - Actual: {actual}") -MAX_LOCKS = 1024 +# decision: weak values give each active object its own lock without retaining idle locks or their objects +_locks: WeakValueDictionary[int, RLock] = WeakValueDictionary() +_lock_registry_guard = RLock() -def lock[T](lock_obj: Any, fn: Callable[[], T]) -> T: - @functools.lru_cache(maxsize=MAX_LOCKS) - def get_lock(n: int) -> RLock: - return RLock() +def lock[T](lock_obj: object, fn: Callable[[], T]) -> T: + key = id(lock_obj) + with _lock_registry_guard: + object_lock = _locks.get(key) + if object_lock is None: + object_lock = RLock() + _locks[key] = object_lock - lock = get_lock(id(lock_obj)) - with lock: + # invariant: each holder and waiter keeps a strong lock reference outside the registry guard + with object_lock: return fn() @@ -745,6 +755,18 @@ def create_random(seed: int | None = None) -> random.Random: return random.Random(None if seed is None else int(seed)) +_shared_random = random.Random() + + +def random_shared() -> random.Random: + return _shared_random + + +def slice_index(index: int) -> int: + """Clamp a slice bound to 0, as .NET does where Python would count from the end.""" + return index if index > 0 else 0 + + def random_int(rand: random.Random, a: int, b: int) -> int: if a == b: return int32(a) @@ -957,20 +979,23 @@ def range(start: int, stop: int, step: int = 1) -> Iterable[int]: "Unit", "array_hash", "copy_to_array", + "create_random", + "erase_unit_callable", "escape_data_string", "escape_uri_string", - "create_random", "get_platform", "identity_hash", "ignore", "nullable", "number_hash", "physical_hash", - "random_int", "random_bytes", "random_double", + "random_int", + "random_shared", "range", "round", + "slice_index", "to_iterable", "unescape_data_string", ] diff --git a/src/fable-library-py/src/array.rs b/src/fable-library-py/src/array.rs index 4e7ff4e4d8..5a43882be3 100644 --- a/src/fable-library-py/src/array.rs +++ b/src/fable-library-py/src/array.rs @@ -665,8 +665,6 @@ impl FSharpArray { /// For Int8/UInt8 arrays, the result is a direct byte representation. /// For larger types (Int16, Int32, Float64, etc.), each element occupies /// multiple bytes in native byte order. - /// - /// Returns NotImplemented for String and PyObject arrays. pub fn __bytes__(&self, py: Python<'_>) -> PyResult> { match &self.storage { // For UInt8/Int8 arrays, we can create bytes directly @@ -756,8 +754,15 @@ impl FSharpArray { }); Ok(bytes.into()) } - // For non-numeric types, return NotImplemented - _ => Ok(py.NotImplemented()), + NativeArray::PyObject(vec) => { + let list = PyList::new(py, vec.iter().map(|item| item.bind(py)))?; + Ok(py.get_type::().call1((list,))?.unbind()) + } + NativeArray::Bool(vec) => { + let bytes = + PyBytes::new(py, &vec.iter().map(|b| u8::from(*b)).collect::>()); + Ok(bytes.into()) + } } } @@ -3375,16 +3380,11 @@ impl FSharpArray { randomizer: &Bound<'_, PyAny>, ) -> PyResult<()> { let len = self.storage.len(); - for i in (1..len).rev() { - let r: f64 = randomizer.call0()?.extract()?; - if r < 0.0 || r >= 1.0 { - return Err(PyErr::new::(format!( - "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", - r - ))); + for i in 0..len.saturating_sub(1) { + let j = random_next(randomizer, i, len)?; + if j != i { + self.swap_items(i, j); } - let j = (r * (i + 1) as f64) as usize; - self.swap_items(i, j); } Ok(()) } @@ -3392,10 +3392,9 @@ impl FSharpArray { pub fn random_shuffle_in_place_with( &mut self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, ) -> PyResult<()> { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_shuffle_in_place_by(py, &randomizer) } @@ -3418,10 +3417,9 @@ impl FSharpArray { pub fn random_shuffle_with( &self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, ) -> PyResult { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_shuffle_by(py, &randomizer) } @@ -3439,26 +3437,18 @@ impl FSharpArray { let len = self.storage.len(); if len == 0 { return Err(PyErr::new::( - "The input sequence was empty.", + "The input array was empty.", )); } - let r: f64 = randomizer.call0()?.extract()?; - if r < 0.0 || r >= 1.0 { - return Err(PyErr::new::(format!( - "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", - r - ))); - } - self.storage.get(py, (r * len as f64) as usize) + self.storage.get(py, random_next(randomizer, 0, len)?) } pub fn random_choice_with( &self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, ) -> PyResult> { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_choice_by(py, &randomizer) } @@ -3474,11 +3464,7 @@ impl FSharpArray { randomizer: &Bound<'_, PyAny>, count: isize, ) -> PyResult { - if count < 0 { - return Err(PyErr::new::( - "The input must be non-negative.", - )); - } + check_random_count(count)?; let count = count as usize; let xs_len = self.storage.len(); if count > 0 && xs_len == 0 { @@ -3496,14 +3482,7 @@ impl FSharpArray { let first = self.storage.get(py, 0)?; let mut result = FSharpArray::create(py, count, first.bind(py))?; for i in 0..count { - let r: f64 = randomizer.call0()?.extract()?; - if r < 0.0 || r >= 1.0 { - return Err(PyErr::new::(format!( - "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", - r - ))); - } - let src_idx = (r * xs_len as f64) as usize; + let src_idx = random_next(randomizer, 0, xs_len)?; let val = self.storage.get(py, src_idx)?; result.__setitem__(i as isize, val.bind(py), py)?; } @@ -3513,11 +3492,10 @@ impl FSharpArray { pub fn random_choices_with( &self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, count: isize, ) -> PyResult { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_choices_by(py, &randomizer, count) } @@ -3533,14 +3511,9 @@ impl FSharpArray { randomizer: &Bound<'_, PyAny>, count: isize, ) -> PyResult { - if count < 0 { - return Err(PyErr::new::( - "The input must be non-negative.", - )); - } + check_random_count(count)?; let count = count as usize; - let mut arr = self.clone(); - let length = arr.storage.len(); + let length = self.storage.len(); if length == 0 && count > 0 { return Err(PyErr::new::( "The input sequence was empty.", @@ -3551,28 +3524,40 @@ impl FSharpArray { "The input sequence has an insufficient number of elements.", )); } - for i in 0..count { - let r: f64 = randomizer.call0()?.extract()?; - if r < 0.0 || r >= 1.0 { - return Err(PyErr::new::(format!( - "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", - r - ))); + if length == 0 { + return FSharpArray::empty(py, None); + } + // FSharp.Core's algorithm: a shrinking pool for small inputs, otherwise redraw seen indices + let mut result = self.get_sub_array(py, 0, count, None)?; + if length <= max_set_size_for_sampling(count) { + let mut pool: Vec = (0..length).collect(); + for i in 0..count { + let j = random_next(randomizer, 0, length - i)?; + let item = self.storage.get(py, pool[j])?; + result.__setitem__(i as isize, item.bind(py), py)?; + pool[j] = pool[length - i - 1]; + } + } else { + let mut selected = std::collections::HashSet::new(); + for i in 0..count { + let mut j = random_next(randomizer, 0, length)?; + while !selected.insert(j) { + j = random_next(randomizer, 0, length)?; + } + let item = self.storage.get(py, j)?; + result.__setitem__(i as isize, item.bind(py), py)?; } - let j = i + (r * (length - i) as f64) as usize; - arr.swap_items(i, j); } - arr.get_sub_array(py, 0, count, None) + Ok(result) } pub fn random_sample_with( &self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, count: isize, ) -> PyResult { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_sample_by(py, &randomizer, count) } @@ -3583,6 +3568,40 @@ impl FSharpArray { } } +// FSharp.Core's Random.next, which rejects a randomizer result outside [0.0, 1.0), NaN included +fn random_next( + randomizer: &Bound<'_, PyAny>, + min_value: usize, + max_value: usize, +) -> PyResult { + let r: f64 = randomizer.call0()?.extract()?; + if !(0.0..1.0).contains(&r) { + return Err(PyErr::new::(format!( + "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", + r + ))); + } + Ok((r * (max_value - min_value) as f64) as usize + min_value) +} + +fn check_random_count(count: isize) -> PyResult<()> { + if count < 0 { + return Err(PyErr::new::(format!( + "The input must be non-negative.\ncount = {}", + count + ))); + } + Ok(()) +} + +fn max_set_size_for_sampling(count: usize) -> usize { + if count > 5 { + 21 + 4f64.powf((count as f64 * 3.0).log(4.0).ceil()) as usize + } else { + 21 + } +} + // Loose functions that delegate to member functions #[pyfunction] #[pyo3(signature = (array1, array2, cons=None))] diff --git a/src/fable-library-py/src/ints.rs b/src/fable-library-py/src/ints.rs index 6580ed1b2f..258c558b8e 100644 --- a/src/fable-library-py/src/ints.rs +++ b/src/fable-library-py/src/ints.rs @@ -1311,6 +1311,12 @@ pub fn parse_int64( // value, not a two's complement bit pattern, so u64 cannot parse it i64::from_str_radix(&final_string, actual_radix as u32) .map_err(|_| create_parse_error(string))? + } else if unsigned { + // The upper half of the u64 range does not fit in an i64, so it is carried + // as its two's complement bit pattern and cast back by the caller + u64::from_str_radix(&final_string, actual_radix as u32) + .map(|u_val| u_val as i64) + .map_err(|_| create_parse_error(string))? } else { // For decimal, use standard i64 parsing i64::from_str_radix(&final_string, actual_radix as u32) diff --git a/src/fable-library-py/src/options.rs b/src/fable-library-py/src/options.rs index 9d59505f12..5121e8ce2a 100644 --- a/src/fable-library-py/src/options.rs +++ b/src/fable-library-py/src/options.rs @@ -67,9 +67,12 @@ impl SomeWrapper { op: CompareOp, py: Python<'py>, ) -> PyResult { + if other.is_none() { + return Ok(matches!(op, CompareOp::Ne | CompareOp::Gt | CompareOp::Ge)); + } + let other_value = extract_value(py, other)?; - let result = self.value.bind(py).rich_compare(other_value, op)?; - result.is_truthy() + compare_values(self.value.bind(py), other_value.bind(py), op) } pub fn __repr__(&self, py: Python<'_>) -> PyResult { @@ -167,6 +170,16 @@ impl SomeWrapper { } } +// A `None` argument here is the inner option's `None` case, not an absent value. +fn compare_values(a: &Bound<'_, PyAny>, b: &Bound<'_, PyAny>, op: CompareOp) -> PyResult { + match (a.is_none(), b.is_none()) { + (true, true) => Ok(matches!(op, CompareOp::Eq | CompareOp::Le | CompareOp::Ge)), + (true, false) => Ok(matches!(op, CompareOp::Ne | CompareOp::Lt | CompareOp::Le)), + (false, true) => Ok(matches!(op, CompareOp::Ne | CompareOp::Gt | CompareOp::Ge)), + (false, false) => a.rich_compare(b, op)?.is_truthy(), + } +} + // Helper function to check if an object is a Some wrapper fn is_some_wrapper(py: Python<'_>, obj: &Bound<'_, PyAny>) -> bool { obj.is_instance(&py.get_type::()) diff --git a/src/fable-library-py/tests/test_array.py b/src/fable-library-py/tests/test_array.py index cefdcf7d79..15224ef478 100644 --- a/src/fable-library-py/tests/test_array.py +++ b/src/fable-library-py/tests/test_array.py @@ -1646,3 +1646,38 @@ def test_generic_copy_round_trip(elements: list[int]) -> None: if elements: copied[0] = "changed" assert list(arr) == list(elements) + + +def test_bytes_from_generic_array() -> None: + arr = Array(array_type="Generic", elements=[byte(1), byte(2), byte(255)]) + assert bytes(arr) == b"\x01\x02\xff" + + +def test_bytes_from_empty_generic_array() -> None: + assert bytes(Array(array_type="Generic", elements=[])) == b"" + + +@pytest.mark.parametrize( + "elements, error", + [ + (["a"], TypeError), + ([None], TypeError), + ([1.5], TypeError), + ([300], ValueError), + ([-1], ValueError), + ], +) +def test_bytes_from_generic_array_rejects_non_bytes(elements: list[Any], error: type[Exception]) -> None: + arr = Array(array_type="Generic", elements=elements) + with pytest.raises(error): + bytes(arr) + + +def test_bytes_from_bool_array() -> None: + arr = Array(array_type="Bool", elements=[True, False, True]) + assert bytes(arr) == b"\x01\x00\x01" + + +def test_bytes_from_bool_array_matches_generic_array() -> None: + elements = [True, False, True] + assert bytes(Array(array_type="Bool", elements=elements)) == bytes(Array(array_type="Generic", elements=elements)) diff --git a/src/fable-library-py/tests/test_ints.py b/src/fable-library-py/tests/test_ints.py index 828de2a5b0..cb30093192 100644 --- a/src/fable-library-py/tests/test_ints.py +++ b/src/fable-library-py/tests/test_ints.py @@ -2,7 +2,6 @@ from decimal import Decimal import pytest - from fable_library.core import FSharpRef, Int32, byte, float32, int16, int32, int64, sbyte, uint16, uint32, uint64 from fable_library.int32 import parse as int32_parse from fable_library.int32 import try_parse as int32_try_parse diff --git a/src/fable-library-py/tests/test_option.py b/src/fable-library-py/tests/test_option.py new file mode 100644 index 0000000000..fc6323fb98 --- /dev/null +++ b/src/fable-library-py/tests/test_option.py @@ -0,0 +1,46 @@ +"""Tests for nested options, represented by `fable_library.core.option.SomeWrapper`. + +Only nesting materializes a wrapper: `Some 42` and `Some (Some 42)` are the bare value, +`Some None` is `SomeWrapper(None)`. Two options of the same F# type can therefore put a +wrapper next to `None` or next to a bare value. +""" + +from fable_library.option import some +from fable_library.util import compare, compare_arrays, equal_arrays, equals + + +def test_equality_between_different_nesting_depths() -> None: + assert (some(None) == some(some(None))) is False + assert (some(some(None)) == some(None)) is False + assert (some(None) != some(some(None))) is True + assert equals(some(None), some(some(None))) is False + + +def test_equality_between_equal_nesting_depths() -> None: + assert (some(None) == some(None)) is True + assert (some(some(None)) == some(some(None))) is True + assert equals(some(some(None)), some(some(None))) is True + + +def test_equality_against_none_and_bare_values() -> None: + assert (some(None) == None) is False # noqa: E711 + assert (None == some(None)) is False # noqa: E711 + assert (some(None) == 42) is False + assert (42 == some(None)) is False + + +def test_equality_is_inherited_by_containers() -> None: + assert equal_arrays([some(None)], [some(some(None))]) is False + assert equal_arrays([some(None)], [some(None)]) is True + assert compare_arrays([some(None)], [some(some(None))]) == -1 + + +def test_comparison_orders_none_before_some() -> None: + assert compare(some(None), some(some(None))) == -1 + assert compare(some(some(None)), some(None)) == 1 + assert compare(some(None), some(None)) == 0 + assert compare(None, some(None)) == -1 + assert compare(some(None), None) == 1 + # `int option option`: `Some None` against `Some (Some 42)` + assert compare(some(None), 42) == -1 + assert compare(42, some(None)) == 1 diff --git a/src/fable-library-py/tests/test_reg_exp.py b/src/fable-library-py/tests/test_reg_exp.py new file mode 100644 index 0000000000..0f15d2e01b --- /dev/null +++ b/src/fable-library-py/tests/test_reg_exp.py @@ -0,0 +1,18 @@ +import pytest +from fable_library.reg_exp import create + + +def test_accepted_options_compile() -> None: + ignore_case = 1 + compiled = 8 + non_backtracking = 1024 + regex = create("ab", ignore_case | compiled | non_backtracking) + + assert regex.search("xABx") is not None + + +def test_unsupported_option_raises() -> None: + right_to_left = 64 + + with pytest.raises(Exception, match="RegexOptions only supports"): + create("ab", right_to_left) diff --git a/src/fable-library-py/uv.lock b/src/fable-library-py/uv.lock index c74e4a75ee..2a3891657b 100644 --- a/src/fable-library-py/uv.lock +++ b/src/fable-library-py/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10, <4.0" [[package]] @@ -61,94 +61,98 @@ dev = [ [[package]] name = "hypothesis" -version = "6.165.9" +version = "6.168.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/a8/f7afb4b55a7f00307f246fb7c3cf2abdc8f8e0ad7eb81bf23f8025480da6/hypothesis-6.165.9.tar.gz", hash = "sha256:a5d1fa312020b499c517e1217ef4440c56a9b52e20b47b420b48a583a81c412b", size = 503594, upload-time = "2026-08-15T02:50:07.518Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/a8/bd70d7c2966e561228b9fdc075ee77c0ba577dcbbfbf921edf614db14f6a/hypothesis-6.168.5.tar.gz", hash = "sha256:76b9226962fe11d40858253a967eda95bb65811365286317e0118f4ec8f808c7", size = 511115, upload-time = "2026-10-05T23:26:35.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/c7/da4298709de649a01c73ce93c4ee5b5651433217e17c494dc91baded86b0/hypothesis-6.165.9-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:01a2a4106fa90396ffa1c402968a8bd25af056e1fdf7974a369c4057cc4035f1", size = 783107, upload-time = "2026-08-15T02:49:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/58/97/d9f265174464d05a4d818d98dcd4e870ad2c2b60e9844033c23b3eb21eda/hypothesis-6.165.9-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7812f1ce9e2bfc0958f2a96975f16a1e1f2400f9d8b7043723ff5811647fe0f6", size = 778677, upload-time = "2026-08-15T02:48:16.984Z" }, - { url = "https://files.pythonhosted.org/packages/ae/fe/d3364f99dd76f1275b430b2fc2818119df84e71f0657ad7f89b922e90d55/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e547823eca16eab9c9569f292e5ce8f95d8a59f1d67e504e97a841d17eb0b562", size = 1107895, upload-time = "2026-08-15T02:47:42.954Z" }, - { url = "https://files.pythonhosted.org/packages/71/5d/42b524303219351add359a05463df067be126c9ca576b0547218cf8aa54b/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e50c687c0e86bb5898f36b845ab84439f44ef2369f0f9bbcbf6aa6860640324d", size = 1136455, upload-time = "2026-08-15T02:49:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/af/d5/91948f68b5007a40b2cc356d9365de0f5cb08c200e383b38259ad75130ae/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f82aed6f0631b5073bd87789aa56609e7c83bd762384fd24c84f3435816b2569", size = 1135112, upload-time = "2026-08-15T02:48:51.211Z" }, - { url = "https://files.pythonhosted.org/packages/55/3c/6e04ac80c5a63da9ecff00199851433437f617da9cd1b5208f11af3337b8/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488b88b6856e430763a7ac5a5fc6e20a798766dca8df8f94576de2281ee60afd", size = 1157397, upload-time = "2026-08-15T02:49:27.874Z" }, - { url = "https://files.pythonhosted.org/packages/12/40/4a1aeddc254d8d6a8f43bbeea928ef5b576e041eb0c9459a91fce884f4cd/hypothesis-6.165.9-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:22dd9e264f2a79a1b63a5fae13351e3cf0088f584fb05bb0e23f21b8fba9df7c", size = 1112748, upload-time = "2026-08-15T02:48:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/35/bc/dde8231f1d10ab0c60627c40757b9b832605028ef0780420fcfc3ccba664/hypothesis-6.165.9-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:431ab894062666c93157234f9949a4db3549fbf3c220777abae3482e1b43f9bc", size = 1149484, upload-time = "2026-08-15T02:48:53.088Z" }, - { url = "https://files.pythonhosted.org/packages/1b/b2/cb29a983c7f06770a6b348f76cb33f90a70b0594a1e61c4a803aedfb1d1f/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c7edcc3dcec42b413c900f8e660f3741c21f0055c53bde21d728f72a21713247", size = 1283267, upload-time = "2026-08-15T02:47:55.688Z" }, - { url = "https://files.pythonhosted.org/packages/88/c0/6bfcf3fb379aba87f3d09073b589c7b7e932ad047ed7bc0124300a51f431/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0d4a613404ba216b6f698ce9df6f9c22d04d8e8a571c42525e83905cf1b1cd10", size = 1409841, upload-time = "2026-08-15T02:48:23.939Z" }, - { url = "https://files.pythonhosted.org/packages/98/c0/0c73a985144717272d2afec377a45c149688a477af946ae24c16b59c9788/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:658c33d8a54cead6081f73fcc4e2c0fbe43d251d3f4170630748d8e5dcbd542f", size = 1264878, upload-time = "2026-08-15T02:50:05.056Z" }, - { url = "https://files.pythonhosted.org/packages/56/91/50d4a6df07a305f5f17578443c93b618a49e8db68c01de8870e539139eb9/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:7d3297fe95a253cea81b48e581e456105016ebe782d823dfb57ec09c3854301f", size = 1282519, upload-time = "2026-08-15T02:48:22.42Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5f/5a54f4ca30a669a247df7c467aea7bb854d9b4de7e1dc51edb2f91ec603f/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a9a1e17200a2b369a38c19c2184efbc041a40b784675ba72ac6800d8c1260a54", size = 1324669, upload-time = "2026-08-15T02:48:29.144Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b2/6ef8eac3b30d6c8225fc405be3b99790701a942706ec93a3bd638a0b614a/hypothesis-6.165.9-cp310-abi3-win32.whl", hash = "sha256:5d40a92c6d025725da7827431a71b7657ce2e3923ff8f1773d6b15d717f77ea0", size = 668903, upload-time = "2026-08-15T02:48:34.033Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7f/b9514f65d2cc97277f6442e3b05164177a742489940efa53e794a54b62f6/hypothesis-6.165.9-cp310-abi3-win_amd64.whl", hash = "sha256:59f29ef2965caf1ae367ee0598ac0d293551b95744a5d49888737a3b3d9dd944", size = 675051, upload-time = "2026-08-15T02:47:49.697Z" }, - { url = "https://files.pythonhosted.org/packages/96/01/110123106a3702d2f3ffc47b9bc6a1a29c19cd2d15fcf356430777a4883a/hypothesis-6.165.9-cp310-abi3-win_arm64.whl", hash = "sha256:f52fd4f1b95f614c3fec53840d603f0a3b19e68eaa91c8d609222999c3ee5506", size = 673402, upload-time = "2026-08-15T02:47:48.364Z" }, - { url = "https://files.pythonhosted.org/packages/09/c0/42f101a02ba2593c61ca4242b8a228d31599bfc2ab1bc5da3542fbdba9c2/hypothesis-6.165.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d7871212e7a1e9d317b2cc11934c6f28aaaa48b594231165a0963d3b3f5159b2", size = 783820, upload-time = "2026-08-15T02:49:19.948Z" }, - { url = "https://files.pythonhosted.org/packages/09/49/e49c94c9f09542e95785dbdaeb8aeac901571a26497dbeef1eedd8c95e20/hypothesis-6.165.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b80ffe65bed7f42681b845bdfde445c3da46a84eb2c4fdbc928521786b551c65", size = 779547, upload-time = "2026-08-15T02:48:06.225Z" }, - { url = "https://files.pythonhosted.org/packages/a3/74/173a59310db80f896059f96259c336c6a773c07d6298645a83ef8e1353fd/hypothesis-6.165.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7186be7b5ce1aee61614ede14fc6f3f072d94ab3c3549613903cd1c9a37848ea", size = 1108411, upload-time = "2026-08-15T02:49:04.88Z" }, - { url = "https://files.pythonhosted.org/packages/08/94/4f4091f4d8522fa6c9a7ee8ad71b6db30a7820faf18e5736318fac946569/hypothesis-6.165.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0a64a05255caa9a5ad90e1ab57eb26d64123b005234e22c80e2f723832e047", size = 1157885, upload-time = "2026-08-15T02:49:06.742Z" }, - { url = "https://files.pythonhosted.org/packages/bc/4f/7b5afb5037df3f1c5e327824f3589ca94c4584cbeace0f6cae68f9148163/hypothesis-6.165.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4d7bbb89c256d02406b7875dbe4058ab79fba6d8e6b56dd1d771e0c9800a02d4", size = 1284010, upload-time = "2026-08-15T02:48:01.796Z" }, - { url = "https://files.pythonhosted.org/packages/7b/cf/8a98ab9d9476cf282054c399577ab7aba2984d2989dc907f20016b633e09/hypothesis-6.165.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:740074d1b6ae4a5335b7a17f44f776c3952d6c7f4a6c84f23ae93e1907834692", size = 1324995, upload-time = "2026-08-15T02:48:57.245Z" }, - { url = "https://files.pythonhosted.org/packages/54/07/e0a0d86029f6bc709be2cf9bb77c7661d0e644fb631c9835116d96ce0e0c/hypothesis-6.165.9-cp310-cp310-win_amd64.whl", hash = "sha256:3295f35299c6d4f7bc12dc583bdca776a5e41cf22a185e5209800885950bb163", size = 674965, upload-time = "2026-08-15T02:48:14.04Z" }, - { url = "https://files.pythonhosted.org/packages/de/1d/1c2dea64a3b00b9f83ca57bacaf4982255da7797d6c65fc0301f21a0ca94/hypothesis-6.165.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a9995cbf91e0b22cb8d9aaaf6b10994b2ac1ebc3b01b4a91d3e82761a2737043", size = 783582, upload-time = "2026-08-15T02:49:29.881Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8c/d8f51166ef9feaeb08d33737b2874d1effca4ada8bf6598e6f2a634a3ca1/hypothesis-6.165.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f94e4456f7b0dd04f4f80f26a2c499370f6048f393ff61436de4e192e278e2df", size = 779367, upload-time = "2026-08-15T02:48:47.639Z" }, - { url = "https://files.pythonhosted.org/packages/8c/27/749aa12ec03fdcec1e4fa7289145f03ded46e892ae89613e333dd7090a7e/hypothesis-6.165.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bab95b4ef980cbc2b4632e49057588766cda930ffbd727d70edfea289da98dd3", size = 1108251, upload-time = "2026-08-15T02:47:59.983Z" }, - { url = "https://files.pythonhosted.org/packages/2b/cb/a1c417cd39f87ca11e2b5b4a7da0446ae316b43b9baee23e113a59113759/hypothesis-6.165.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8795161db0a7c270c4f3e0eb754bd0ed96b17133d6f851c7444f9733c4c9d37f", size = 1157689, upload-time = "2026-08-15T02:49:40.288Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d5/82abf4d3b171f087d323b179a422472e1c1621ed47adf88f0fcdf7be7e35/hypothesis-6.165.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8c7dcb0a21dab077e65bcb082ee321b7e31402cf980da648b4369c71086ad53c", size = 1283592, upload-time = "2026-08-15T02:48:04.698Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ee/f156d374fd7f49220a789a231cd980ffe53bf1eec11a75306ded76265306/hypothesis-6.165.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92b264bef7860bf37b5f22b5949083d26f113351da8b99e93adb0554d3e2c110", size = 1324910, upload-time = "2026-08-15T02:48:11.037Z" }, - { url = "https://files.pythonhosted.org/packages/23/57/d14969c84e59356103c0b5e1a27623a4149d5d371799757552b2ec344da2/hypothesis-6.165.9-cp311-cp311-win_amd64.whl", hash = "sha256:ae34d13d8438ea7f3ee18292290b9f608de6070309f3c5826644a88d197c319d", size = 674776, upload-time = "2026-08-15T02:48:59.008Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7e/03a84262b74300714af486e037403db8f5e19170f01bc32605300896b19d/hypothesis-6.165.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:41082aff3e438ac1eeb86002c269b9b5d86bab563e911614c2c8d34f405e02db", size = 784696, upload-time = "2026-08-15T02:48:09.314Z" }, - { url = "https://files.pythonhosted.org/packages/49/50/c77dca38074e38e99373ccd5f361bb5ad251e8f2de92dff4d7035111595d/hypothesis-6.165.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67a0c0346ac0e8e106fad0224269a87c665768cc8d45de6e367e9b260ef36d5a", size = 776265, upload-time = "2026-08-15T02:49:36.232Z" }, - { url = "https://files.pythonhosted.org/packages/1b/80/9de2e558cd966ab1b1d014718c7aaab7d2d8f2d591ccadb27c1ffe639f6d/hypothesis-6.165.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10e826a708456c965e125f15dbf5a6f463e41dcf230a930b7a3e0ce7036f5176", size = 1106706, upload-time = "2026-08-15T02:49:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/04/78/dc89684de1a739347b22beadf3e53a64b8b7eef287dfa34b150432e13d4e/hypothesis-6.165.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fba20d9c60d6b8aec76f32cd22109e1b1f19a9985317255e2b43940d27996097", size = 1156753, upload-time = "2026-08-15T02:48:20.512Z" }, - { url = "https://files.pythonhosted.org/packages/b9/bc/495988f0fc788d33a24849e9ce7303a2c646f48baf3f5f010d9850bcc5a7/hypothesis-6.165.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6564fb9bbe1760719070a6fd37335fbf43696fe2911c386ba55fb3cbaaafb82a", size = 1280667, upload-time = "2026-08-15T02:48:30.763Z" }, - { url = "https://files.pythonhosted.org/packages/36/08/9758ba15cc7dcb408d2699081ee0acbd96842f9c5778c84fd145d19953ba/hypothesis-6.165.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:126e7bf873892649cc5f697c3b3a6548f077a56ae3601b22fddef2ea1ba6f64d", size = 1324018, upload-time = "2026-08-15T02:48:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/97/09/eaef6ee89a792ea82064cef66726023a5933f8704c0d3dbfc5a451829428/hypothesis-6.165.9-cp312-cp312-win_amd64.whl", hash = "sha256:c908edd69e4308c3cb194f0853aa7dfaa15e905242e81646b9f12881cd2683d3", size = 672207, upload-time = "2026-08-15T02:49:50.121Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1f/99069da612aee3a51519f96f4370c4ba42055b57eae92926353c63e68617/hypothesis-6.165.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:11bcdfb14b52d41384cc14ecd3f6d6152735bc2f7af0b9f3fa8685d630a1e98d", size = 784591, upload-time = "2026-08-15T02:48:42.765Z" }, - { url = "https://files.pythonhosted.org/packages/94/10/cabf6a8782253e6b547ea8bdc6a48a9eeab415f1036c749b40ca59df32f8/hypothesis-6.165.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8c3d7d54802641c2f8b49f478f153567770d299d639ee38fc0a0fa3d95ee07cb", size = 776216, upload-time = "2026-08-15T02:49:34.179Z" }, - { url = "https://files.pythonhosted.org/packages/15/5f/da966f98d69a8ee4b4fac509d92f7ab18cb4505b8191acc23bbb52f8bb17/hypothesis-6.165.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8f5b63a8fba74d19a12ad4abaa95afb7466d66d1adc90ba3026e40b7924452c", size = 1106621, upload-time = "2026-08-15T02:49:08.529Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5d/1fffd1031b4281e53fff02ad7adeda31db8218e91b82e7a98c7957541e76/hypothesis-6.165.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c01b77160328d8fce22bb5121fe1f5fee36b434a1220603500605b88ab854d7a", size = 1156613, upload-time = "2026-08-15T02:47:45.457Z" }, - { url = "https://files.pythonhosted.org/packages/b1/30/8e93a02016689c0b5cf0c35f8f9199b745601b3db9723296ca6d7366dcd5/hypothesis-6.165.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88bde76aaab7f8abcd9412d135eaa49e8f7d89d068188fae2768a7803c80df89", size = 1280650, upload-time = "2026-08-15T02:49:44.026Z" }, - { url = "https://files.pythonhosted.org/packages/c0/48/2b0987389f141bd7bccc0785af4af86de9dfe9a6bc1b2b5dc612b083ae8a/hypothesis-6.165.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0df8f8736d176d3a5092890b6fb9030f9c726d272b52af0e1ce887d62fc21699", size = 1323749, upload-time = "2026-08-15T02:48:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a2/0a2380fdedfe0e16a09a09401b86bdc1eb049b3b6256fd837424d652c88e/hypothesis-6.165.9-cp313-cp313-win_amd64.whl", hash = "sha256:7fc9f46d88732fc74539ca7748aedc736c5e111431b06bd8ef2534440379aff2", size = 672195, upload-time = "2026-08-15T02:49:02.806Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d0/95075d5ae3263e1c9143a3e89d9f19b864b1da303fb426964d9f7c08ce69/hypothesis-6.165.9-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c3432a48e5895cba6b36c3e5abb915163d5169dc755685b4cd4720b710d77e3c", size = 784690, upload-time = "2026-08-15T02:49:23.922Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d9/4a8b58237a36a839abefac22905ce9172b4c5195344708690413c6ef900d/hypothesis-6.165.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:70fe20939f63eb661227fd2b05193eba1e5ea6e547a2fbe158839f2acc519f4f", size = 776366, upload-time = "2026-08-15T02:48:27.261Z" }, - { url = "https://files.pythonhosted.org/packages/74/8d/91d1261f09a87299dc0ba89e12d8f9bbfd454e8922560e332b190a5c0a9f/hypothesis-6.165.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b177ef8aae2a2817a2223d103353d120118e686a152bcd01832a27ddc9388e", size = 1107128, upload-time = "2026-08-15T02:49:10.617Z" }, - { url = "https://files.pythonhosted.org/packages/2b/01/0198547c8b73d73ee5b885d8f73931c4dd3c62055dc93a80b5e18590efeb/hypothesis-6.165.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d755b56098801a28a419172e57beb536b8c32929e7a37bb5ef12f2c00abd284", size = 1156757, upload-time = "2026-08-15T02:48:25.44Z" }, - { url = "https://files.pythonhosted.org/packages/21/3c/a3bde8c9630d361d85570ed5a6248fec0360a3874e26fdb04d62c8ebabf9/hypothesis-6.165.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d49496271282bf424ddd728108f49b92e1d32066a3d0ba18990d5b01c40c3175", size = 1281013, upload-time = "2026-08-15T02:48:44.399Z" }, - { url = "https://files.pythonhosted.org/packages/f7/2d/017895202e6f2d75928b8e66bb8efe28c5c834771f6a3d118fc936d811d0/hypothesis-6.165.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:be54673f76785ff954b511745d4a7a18c7d7fe1d893cb81a8917cd28878fbd55", size = 1324110, upload-time = "2026-08-15T02:49:18.05Z" }, - { url = "https://files.pythonhosted.org/packages/40/f8/02f9be17e21b55513da624bac18a21c29011351d4e5d089ca5095555d1b4/hypothesis-6.165.9-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:af42ad4c24b69d4482e4813e148f458fc090911c0cf9055225c62fbd1f0b102e", size = 616257, upload-time = "2026-08-15T02:49:00.997Z" }, - { url = "https://files.pythonhosted.org/packages/e9/be/c94631e799bb4ee87c8c80de72f2eb44e93e7d0bff45b22b8801c78e5895/hypothesis-6.165.9-cp314-cp314-win_amd64.whl", hash = "sha256:9bc2d40dd2a71ba3971b902d31c8b1b0c40d3d465591fd71bb8ce6f541add530", size = 672006, upload-time = "2026-08-15T02:48:32.418Z" }, - { url = "https://files.pythonhosted.org/packages/43/15/d23548a12e5670513903b73a42fbd4f5ab7b1287bc6fd5a4a890c5746a62/hypothesis-6.165.9-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fb16358e29f3090a9ab529e84df52923ddcd8505785c019bf48cdcd45e094a", size = 783152, upload-time = "2026-08-15T02:50:02.576Z" }, - { url = "https://files.pythonhosted.org/packages/0d/cd/37b5e5c042c557aa50db8c7f554b655b51c83e0b50306fb6ec8b05b03bfe/hypothesis-6.165.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed61ad45184997f97c3baf255f34aaaf4edff63f1a61aad87919034b4ab9f64e", size = 774782, upload-time = "2026-08-15T02:49:47.988Z" }, - { url = "https://files.pythonhosted.org/packages/56/ed/43b6db3681937ffa191148533d37cdd3c091f707ecb92281424078876d45/hypothesis-6.165.9-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f96f36c3d3677f22ff990d7aab4235576a8429e72dab5382c45bf489630972f5", size = 1105367, upload-time = "2026-08-15T02:49:42.041Z" }, - { url = "https://files.pythonhosted.org/packages/0d/dd/eaab3c678a23ab434c65b4ea5de5b302267ce68aa47b996524d2f34dcd13/hypothesis-6.165.9-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69ab6500e47d7c21310360d1ee5cb6441660a10408e856d3738f07536732ed8", size = 1155499, upload-time = "2026-08-15T02:48:49.363Z" }, - { url = "https://files.pythonhosted.org/packages/10/fa/27049cbfb3e66cc1b68b32cbe137ac69a91cf09ac8520c4eb8ae9b3e5cbe/hypothesis-6.165.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cc98fa897f48127fdf2e7d1b2428f839656cd52a6344095e501371d222b9c19d", size = 1279038, upload-time = "2026-08-15T02:49:26.001Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d4/ab591a445dfeb7fa59b3c12de3eb9ff09901b4ede4025061b2fe1bc302af/hypothesis-6.165.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a8b2d7415b1f7ceec6e689c992b919af41e339eafb35d6367a3abbb5b6db9753", size = 1322697, upload-time = "2026-08-15T02:48:39.467Z" }, - { url = "https://files.pythonhosted.org/packages/40/fd/4ee3b14d2d73a1d9442ac161b953dea2522fdac8ef7ca265fe5dd205982d/hypothesis-6.165.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1000afaf05be59c88c13b458fd74d123e3361ca3d14fe127814a1231cb9cb7df", size = 672011, upload-time = "2026-08-15T02:48:07.583Z" }, - { url = "https://files.pythonhosted.org/packages/81/a1/20b63e3d17e05e82ef47b93ea3fa9a823297249ed7953e7e38f1b57a1e91/hypothesis-6.165.9-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:5f6b4ae650480368e2477e8f875cdbfe8533a7b2e09f049fe206c48b20174622", size = 782729, upload-time = "2026-08-15T02:49:52.008Z" }, - { url = "https://files.pythonhosted.org/packages/7f/13/b43cea9578820a3e41cd70e8395068a0f07ef11c393ab09ff5aeb5d44d7f/hypothesis-6.165.9-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:1aaba05b943345d2a448b8c1c795bb191adf842b450bfc7207bdd1bf139a9b58", size = 774415, upload-time = "2026-08-15T02:47:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/ea/08/222a8f2aeb8429b54cd53624add076ae5c9b39b0b170afd2435d0e81a8a9/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:306c1efc0f01f3d5c29c45996e055437971337aa24b6c6735b0514cfb5df60ce", size = 1104761, upload-time = "2026-08-15T02:47:46.841Z" }, - { url = "https://files.pythonhosted.org/packages/68/6b/09943bad5c1c5ea689a1be6376bcb7c68c34f3e3db5b792a1c0bb4edd79c/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1551df11534d192616dc1b4539b6e526c4704d08b22d2ffb6ea3479ce6b87d94", size = 1133212, upload-time = "2026-08-15T02:47:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ee/8fa90ca7a7d8b2362f3aaa0b2117405abf7b104fa01b52d5fecc4bb3c3fc/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afd99a069df90c73c15d53e275d11a782fd5f17f5423002868ce9d2438ab567f", size = 1132156, upload-time = "2026-08-15T02:47:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c3/6dae83eccd7260f5cf3468f145c663ce9b22579a9d9c80e9e7be49bec8b2/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c3e8c45ca5e2e4f519021836e5d86fb2229915c3f92fb50db7bb7364dfb2c1", size = 1155040, upload-time = "2026-08-15T02:49:16.085Z" }, - { url = "https://files.pythonhosted.org/packages/50/90/25eaabe2fecb3af3bcb9b4c3261ba9ffe0e092a3199232adce15399c040b/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:676a3bd04261b7bdd35374a3f4dabd82025fcead110a067a76328799f394a4d0", size = 1109751, upload-time = "2026-08-15T02:50:00.293Z" }, - { url = "https://files.pythonhosted.org/packages/1f/81/e8117e7ed3ff1ef7a36f023469efc925458b52c1243c539566a5308f5d77/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7f51c2297d116c2c025643762269eed0767589b6fc858d8de69d9f80cf33a23", size = 1144793, upload-time = "2026-08-15T02:49:38.317Z" }, - { url = "https://files.pythonhosted.org/packages/c6/6a/da2ea69d1fed780d86895f1ef7f2811240ed810487536ad22298629cc630/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e500b97f899e375e47b407a48f3450cb1dd44bca757529c0490cb4533d878b30", size = 1278531, upload-time = "2026-08-15T02:48:37.581Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6e/b3b21f15fe9debb9c448b03aaeb725ee007ba68518e55a6810878d7b9765/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:8401f55aba2d51f20b02c42e9e08b7eff2018639bdb1af16a4c279ba7e129c81", size = 1407073, upload-time = "2026-08-15T02:48:41.256Z" }, - { url = "https://files.pythonhosted.org/packages/da/fe/55f8e9c34f2b0a5ccf895a527a499f8562e9a14f014dd018b2535e8aa1c4/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:34a4591db1dcf6aa544b4f884520ff0f1bc02ac3d11c76f78c73535b6b672272", size = 1261243, upload-time = "2026-08-15T02:49:58.076Z" }, - { url = "https://files.pythonhosted.org/packages/0b/37/49fb2374bff4e66d3d8264d083ddbb83269ec13903b441ebbc0d3c51502a/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:0bd589a011b3d0c4b98d78f467e5a5a6b96bf5be5d15b39ef47e65c225e8d790", size = 1279060, upload-time = "2026-08-15T02:48:54.843Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b7/c826e67f5d553d20644ac3da5d25de7ff9d50a1f41467981b00da7b2ab9b/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:84a93a152ffaf92fa6b3f94d366f4c866e26f3f1b643fcc6da4ee2ddfa038aa9", size = 1322247, upload-time = "2026-08-15T02:47:54.398Z" }, - { url = "https://files.pythonhosted.org/packages/24/ec/e70e440805820cfd662d04d05aa34b5d29e46d5a49ee871d6937721b2c17/hypothesis-6.165.9-cp315-abi3.abi3t-win32.whl", hash = "sha256:4a2081699e0b16b35c7b34cf0ff01997e8fc6038b520b9f29c106764158c411a", size = 665906, upload-time = "2026-08-15T02:49:53.901Z" }, - { url = "https://files.pythonhosted.org/packages/23/79/968b149b572e1d4284b04280cc181449090c8bd03e55b24d57b64443978e/hypothesis-6.165.9-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:e1d9492193b52087df4d264edd77618869d447b5c7f6a022c7bb11b88d577cb8", size = 671817, upload-time = "2026-08-15T02:49:46.053Z" }, - { url = "https://files.pythonhosted.org/packages/c2/72/0f951d4f0e07720583e4773638cd556d2b6585605f8a3df54d2fecf2e2ff/hypothesis-6.165.9-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:f67414675f2263948e5a5977b636b5cb1cc5f33b82a9be34f8a6b9a8293d4707", size = 669795, upload-time = "2026-08-15T02:48:03.176Z" }, - { url = "https://files.pythonhosted.org/packages/43/7d/1b6b48629032b22960a5b010a791467670c08d9413a3c6c15a72f7c1da09/hypothesis-6.165.9-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f6caa4194f37a02232ecb964600d17facc23a6aa5b0295008989f31e329eb837", size = 784507, upload-time = "2026-08-15T02:48:45.961Z" }, - { url = "https://files.pythonhosted.org/packages/95/a6/c29939a55cde73886cee7a3975148e132a9ed6881d39b7c9b8c19e3a854b/hypothesis-6.165.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0a4a32dc8e8728417aa13055903dbf742dd0b5cedda7c2031b4b2a0bcc1d463d", size = 780353, upload-time = "2026-08-15T02:49:14.307Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f9/ac8cd423b1c7506dae01c4a9295c434b53525401a182a116aecaa27332cf/hypothesis-6.165.9-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f255b389364f71e6e9305ba99bf13733354cb0dc4a97d07db8aef76278a345e", size = 1109224, upload-time = "2026-08-15T02:48:15.557Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/64c4dfa2836ec421e35a167a3fba92018950a8c7551a268a5fc799329455/hypothesis-6.165.9-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7f0507d8224e5b44c2651d7ef8df15ac6d38eb56c98e7f451c3090804ce109", size = 1158929, upload-time = "2026-08-15T02:49:21.922Z" }, - { url = "https://files.pythonhosted.org/packages/88/8e/fccbea8a0a03532762fdd56d3809fd8398f46af1ec9018f694d4ec5c22cf/hypothesis-6.165.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fd9e242fabb05715ec2d3b9b96dcddf683fc3891a04b6685b79e400e35ccbe18", size = 675885, upload-time = "2026-08-15T02:47:50.986Z" }, + { url = "https://files.pythonhosted.org/packages/98/0c/7f04c8d277dfc828ba584b7d9d10dbac5e91fce673fa5328f7bd5bf64609/hypothesis-6.168.5-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ca43a751410a9c6685f029fd5126cc5507664cafaa76017922aa8ae2e17b6620", size = 791539, upload-time = "2026-10-05T23:24:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/11/5c/660906d83db74eb86feda715d0f2df14836205b14a183332116676733e6f/hypothesis-6.168.5-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b98707cbe9f430d100a945bbe17612fd3aa44eac1b0ac5299669fe3b8e4128", size = 787208, upload-time = "2026-10-05T23:25:14.028Z" }, + { url = "https://files.pythonhosted.org/packages/01/85/36e19492bc4ff354c2be9c8fa7c6ace0c65f9d2c7116656b741680c6ca55/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dde52a0b696c642e7f988a03026c7c29f90daf21e74507b6f865c3ccc9d536e", size = 1115200, upload-time = "2026-10-05T23:25:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/d4/82/3273fb0a3567c09b767bb8fe2824d65e16ae2abb92cf1f43762df723df94/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42f02e4541fe0c17a1320617effc0ab8a8aca2a9af15e3358d4150acf3bbdc00", size = 1145113, upload-time = "2026-10-05T23:25:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/74/59/5c5904555a0bbd4b2898d73ea90c6d03f5be0d8ff0756ac1d519ace6ae66/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf6dd7e537a12763c9afa017f7a6159e5cda608e98670621fa44596a1e8e9288", size = 1139564, upload-time = "2026-10-05T23:25:56.681Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/55654ff9575587a401e304f08ad1d43b7e6318f81c66bd866fdc5ab4665b/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df2c04cd30abf42c52580184216162a75b5508b214a472b86670f6dd50659a3b", size = 1192142, upload-time = "2026-10-05T23:26:06.565Z" }, + { url = "https://files.pythonhosted.org/packages/48/91/4cc9d6e8a950473e07e3ebf00cbb8ee0d76b14d193f94c3de20f1c09e2b1/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278662eb21aaec9eaae71ea4dabd4fe390c2af11ec58a6a0606687cf6d7689b0", size = 1157262, upload-time = "2026-10-05T23:24:59.229Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/4b8aa3be788ea81b9a7bc6b673ed89edd72fd0645c6aa691d4c159ff971a/hypothesis-6.168.5-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:6bcedc4ab8ab92dd0f3af0cfe24dce184d225751d7bc870a9cddb9a557de847f", size = 1113779, upload-time = "2026-10-05T23:24:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/f9/98/2eb4c79d1851195e6a083568b065235680ab984e984bbd472f2a7d02ba33/hypothesis-6.168.5-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b58097cc3b98d8616f635ac73888fc9f859311875f2adc043f1544c40c3c466", size = 1152639, upload-time = "2026-10-05T23:25:43.635Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9c/68f7e99b43c6f37c077669a4d3bd88f48c042444ced9e7cff0eaf44bc70a/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f8a387d9ee7f804e830b31f2e2e339ab5731665e922cfda4f6f6fbdb05e191b4", size = 1290068, upload-time = "2026-10-05T23:25:28.45Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/d4f87164a0d028ab102cea345b601d9dafb3196358df5448caa88ac3c1e2/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:326f6383fdf2e37ac69773589a8238a3bf396ca8ac8efacb0fb9ed42dd08e426", size = 1418551, upload-time = "2026-10-05T23:24:51.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/6b518a25514f0e643f95610c77e279bfbf0e0b3bd423aac0187d6f039b9a/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d33fc74e43bbd7c3a8f6f7161a8b93b676924286e97e70e828c6e0dcee5c01f", size = 1372130, upload-time = "2026-10-05T23:25:32.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/c2/32538e14e63193ca894ba584696805d1eb45cfc27e15fccd47acfb87531c/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:1994923cf5e5220ae6bf19645302504b27c0289d83e5d8690df71dcae63d8416", size = 1268950, upload-time = "2026-10-05T23:25:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/e50e7e98af9489aa05203c2ab38c95d891dd8d1ed08fad972dcdb6955332/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:501038fd24d3bc95239cfd093a23cf1151f29dd82382a3554dac5dfdab9729ae", size = 1283907, upload-time = "2026-10-05T23:24:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/41c460a7d2148a04b212b2d594d39992fb52e0b844e13bf6784573fc8dea/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2292ddc24fe6d04b7d30fa6a7e2c9e280ad5078fe671d0bf4aa6df6e143b5ac", size = 1323762, upload-time = "2026-10-05T23:24:18.984Z" }, + { url = "https://files.pythonhosted.org/packages/68/4f/37a7fc1fe445e3589e0f56ff4573c28de1d6e6a03009cba2f99f04e46ffa/hypothesis-6.168.5-cp310-abi3-win32.whl", hash = "sha256:925d67c69b719d416334aa961c0cdfc4a58a471af1ebd2d7101bd515a70f4e5f", size = 677903, upload-time = "2026-10-05T23:25:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/7b25ca7845a60522ebc5f8054f6bba68d47126fb5d940c784fc528a4be4a/hypothesis-6.168.5-cp310-abi3-win_amd64.whl", hash = "sha256:2311590eccba452de863dfe3466daa86a05c25f072ab31ed8bb4d3313ee68439", size = 684616, upload-time = "2026-10-05T23:25:04.028Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/40e7c36b46c8788eddc7a322ad324e6db53c8ab9a8b9a95d6535ee7bdaaf/hypothesis-6.168.5-cp310-abi3-win_arm64.whl", hash = "sha256:222a6d23a2a824b0f9f73761c2fb9cd2aca96cf3e5b441617625bce4f7eb4fd4", size = 683061, upload-time = "2026-10-05T23:25:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/04/0a/3b3414124055ac49c2478cb49add90eb3b727508b2aa54a4fc50de88f98a/hypothesis-6.168.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8dfead3a6b2e2ceb6165505885b81396b0e3fe8a556bd941d88fa43cd8daff2f", size = 792187, upload-time = "2026-10-05T23:25:51.287Z" }, + { url = "https://files.pythonhosted.org/packages/a1/60/90ccc9e18d831480920dc0f1d33a9af142e796d67dbe6a760e93d0122587/hypothesis-6.168.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:658563b8f2782a0577a4d8d195e31f29b18f3f3b61ba58c4dcbd8e6ac502d14d", size = 788228, upload-time = "2026-10-05T23:24:57.84Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/8257699b8456241b8348fe0071c29912aeeaf5d16ef97a45e9c1d3170ca6/hypothesis-6.168.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f40be9b9c6b7b058ff56b0b18a91ff4cfa57a7c7756043eabaa094a0a162c9", size = 1115527, upload-time = "2026-10-05T23:24:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/42/42/31e66ce21aa6ea030ace8874269e5a169b0c69d8a3043042e315bd64c6ad/hypothesis-6.168.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30208c44364b6fe1f70c74b45f3f1f8a173a749d876294a80fe88c9cf16ab6d0", size = 1157678, upload-time = "2026-10-05T23:25:54.904Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2a/b46ea00cb1cb9930b9cf7f844673913bf8bfc34f38c031d39ede6f649c59/hypothesis-6.168.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09ca5b2f45786feb93ab41c16de602de4a54f42f35985565423417f4ed9d5b6b", size = 1290397, upload-time = "2026-10-05T23:25:34.184Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e8/eb50f72257f8b00f950da99c7ee444aae5f7c6364fce4ffbe82dd550ffdf/hypothesis-6.168.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:257175b2800cb3073f21041d174e67db7613dc64cc79f3f09f93cfecf7cfeb68", size = 1324447, upload-time = "2026-10-05T23:26:32.767Z" }, + { url = "https://files.pythonhosted.org/packages/35/88/cbb53055091323c186752b437024ff6cd95564af4389bfd1b36900aa459d/hypothesis-6.168.5-cp310-cp310-win_amd64.whl", hash = "sha256:3cacf8e84badb92e34336a6b6b95e2135ad248f870382daf56fe471d6c6e794a", size = 684598, upload-time = "2026-10-05T23:24:40.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/f1149d913d685809c016b2a3ae9d727741ae22f52376c6d0ed51eecb5ac8/hypothesis-6.168.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8c35e5d4a85d0d6071cc267a6cbb8fd7ae23ca8a0f745ea5a52c0064d7c1c4b8", size = 792108, upload-time = "2026-10-05T23:25:12.323Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/7e5ffb6bbfc033c85746243dc4d1541876082e136ee44c02f843bb77427e/hypothesis-6.168.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:244a8d14c0a8a3be0345ad0b120deafb94517cc1d74a961d14b5b5eb041b4c0c", size = 788035, upload-time = "2026-10-05T23:26:26.557Z" }, + { url = "https://files.pythonhosted.org/packages/38/df/022129d3e16d19a84e7a5a35ebf7baca07d3482fb34f0faaab865b14fe66/hypothesis-6.168.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e68e1d43b7c9c7a1aa659dfe1c0ecc2de79391b20db853c1e18ea7e3d2ce31f", size = 1115438, upload-time = "2026-10-05T23:24:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/b3e45b0386d8f643a304105883c5bfce79fd530b2dfe3a70564e1d7aa0bd/hypothesis-6.168.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01a4d3773f285e75551eeef12df058e6316b666bcc3ec187c5eb52a893fbb015", size = 1157503, upload-time = "2026-10-05T23:25:05.609Z" }, + { url = "https://files.pythonhosted.org/packages/ee/4a/aba5a74ddb20c9f41ba5b8f2918c5a12660146cab2120f14122122715060/hypothesis-6.168.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc327005f2fbb55db81d132948ee7c6cec0589694bed04b1e45fc8fc317e12bd", size = 1290235, upload-time = "2026-10-05T23:25:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/34/f4/7204aa6117a38085e6f1dbefd5cd98050a58c847f2bdecc917422cdb2b1c/hypothesis-6.168.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f21c74ad83fe77abc72e82c54114148fb01396769c234e26c9b9dbc21344a9", size = 1324204, upload-time = "2026-10-05T23:26:16.239Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4b/15a46ced6d999148d1b718c5488c243bd56dfcd687a61404fe371192dfd5/hypothesis-6.168.5-cp311-cp311-win_amd64.whl", hash = "sha256:bd3ff6e53e29b86ec6078f123284e65e1c678fe7b30c2b52512244faf266502c", size = 684414, upload-time = "2026-10-05T23:26:18.231Z" }, + { url = "https://files.pythonhosted.org/packages/90/43/a04a727578cbef9f75c11fa6fbad66d13aaffc354f4f979506219814c7d4/hypothesis-6.168.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ddee1ef4bab47e315b705e42d2f4354e789973d11f9620d2df242aef4cfa42b2", size = 793156, upload-time = "2026-10-05T23:25:49.433Z" }, + { url = "https://files.pythonhosted.org/packages/f4/91/55de4e2a12fe98ebd5bc8f35e59870c897ab360cbfe5aa63862cdbef56ad/hypothesis-6.168.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:81ceb49b0dc3a4b6126cd0d3bf2b634af4e91513c8f1e2daee16041414ed8e3d", size = 784788, upload-time = "2026-10-05T23:26:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f4/61/230abc6320540bdf73baf9a1c025fb0aa27cfd5a3791a2e0c95114239a70/hypothesis-6.168.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a09caa95d2d7e6546f727f703de606145835d9ca215fb3134a21353c69afaac", size = 1113853, upload-time = "2026-10-05T23:25:30.593Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4d/3bf0a7806b3fa12ed076f2daeb3db0e6f9738994e879432ffd8dbcffd634/hypothesis-6.168.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97ac1d516a42a3b1f13b36a1aa6a5f842e43d67e69d4dc664a9645b28de411ef", size = 1156695, upload-time = "2026-10-05T23:24:28.607Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a0/603f918fcf8f74f81ea593b04e3a9a9fcd426bbf389ed52cb340249bdc14/hypothesis-6.168.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e4819fba78c6cbaa6e2f9fd5a69a413817446943f286763819b5ac52391bff3e", size = 1288792, upload-time = "2026-10-05T23:25:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/69/7c/711ef5be6e889dcd40d9b03cdd85cd42ae39af75835bced3c374730291a9/hypothesis-6.168.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87334b95dfbc101652fa48a427a742b0715b814506d9a10f621c29e476b4a2c1", size = 1323090, upload-time = "2026-10-05T23:25:58.753Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/0377d7d13ff3e2c16efd141942649edcdb568caec4576f86ac779545dd85/hypothesis-6.168.5-cp312-cp312-win_amd64.whl", hash = "sha256:2fcec23ff4eb526ee85d3510f564b938ca74f6011f1eec1050e4eb55280b0468", size = 681999, upload-time = "2026-10-05T23:25:41.86Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b3/1f7f72cd28d02a5ca99c432fbffe4b750a375df2284af9d916943dd3aa4f/hypothesis-6.168.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:714337b25ca9137bc359c570b868269462307e120999412ca1946f997f4b9db5", size = 793075, upload-time = "2026-10-05T23:25:15.905Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ba/5b0874828695c4d49e3858d0967254f783e563cd0e211a6db27d11d48a1f/hypothesis-6.168.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f1c3617155fcf5b5259a1f2e4c775d3eec7bfa80b162b2f6f145b08f871ab08", size = 784693, upload-time = "2026-10-05T23:24:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5f/ca777becba5251b0d778bb9d83d15524c559a07e4b5d4e6211473855bae2/hypothesis-6.168.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebee70b7a026210bb47c86c89e5bfb42effd5bd630080e76bc084f29c01c7f7a", size = 1113693, upload-time = "2026-10-05T23:24:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/5a74bf329e405db3edc5639a2595eccf33ad6f5aaa191019e9f824d630f4/hypothesis-6.168.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cfb06b31cca005345b8ad63f88986d21fd359a7dc3dba2965dd3515b720e5c9", size = 1156530, upload-time = "2026-10-05T23:24:47.153Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/e79cf67f03f212a1394abac21053bd6887aa70f557be1da3f9c9c73e58ae/hypothesis-6.168.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a4c244d7ab64963fb575f0ec2d813630e1d14cefc39e7c460d5d778e5af4118", size = 1288713, upload-time = "2026-10-05T23:25:22.763Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/df452159ac8d7b278071a3e81fafc69da833ec4302b8c85f5b6e530aea21/hypothesis-6.168.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e59d519f6fb38b3fa4fcde046767b03a24740fe827d261ee7ff9a721c06169b", size = 1323134, upload-time = "2026-10-05T23:26:02.485Z" }, + { url = "https://files.pythonhosted.org/packages/af/fb/f07d8d09fb57eb14555cad64dfbe29bfdcecff3806f1e01268258088e741/hypothesis-6.168.5-cp313-cp313-win_amd64.whl", hash = "sha256:c103f655644afa4ef6bf7efbf86e44b78ee475fd0691da2db86e2cfe72c07234", size = 682019, upload-time = "2026-10-05T23:24:22.888Z" }, + { url = "https://files.pythonhosted.org/packages/de/e9/7c3c2262b8cfa825c4c1764d62aa15e628bae257ccfd2ee4f3ffa4f81eaa/hypothesis-6.168.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c4dc037d8001bc6eccb8636f4a38d16ea6b250d6bf0a89075aaa5e5069f751cc", size = 793253, upload-time = "2026-10-05T23:26:14.331Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/7909ed7d29302491e9b7bc0e7ac3287c20736c05a0cc35bae65024aeec3b/hypothesis-6.168.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c90743321f29b65491d146adfc2ece85869bacb71ce18b47674795e896c81ee3", size = 784837, upload-time = "2026-10-05T23:25:24.728Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/57742c459349052e6a3e0c011855840f8cbbbadca91079d5b591f08b25ae/hypothesis-6.168.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09debb7f7f0f229da5f7e2ad515a5be7a8dc607ec204074775f8ab6731a447f0", size = 1114268, upload-time = "2026-10-05T23:24:27.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/07bd2449f91f9426f705fb689429bab6e26d1365f8ac4ef7d7c1cec9055e/hypothesis-6.168.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d227f8ac497eca0bde4e8562d32dd4e82fc9566526020bbd567f76b833b923b0", size = 1156838, upload-time = "2026-10-05T23:24:35.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/75/7f3dda517e5134f73e2ae41821bf40b3fd3ac6551a9a43ea9287471738a1/hypothesis-6.168.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc6ebd35601c72c842e5899c3f760f9ed26c69e786ee40a9a64fb5a4a3058315", size = 1288864, upload-time = "2026-10-05T23:24:42.645Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cd/4b1364140642cf3f1431ca59b5841fc322872dfa7197b2facb97692da234/hypothesis-6.168.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:503e103ad49e702bad200157d82778eebbc14d3045e9700a8e8fe5db40912953", size = 1323300, upload-time = "2026-10-05T23:24:14.826Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/47d7cffcaf15318a4308516b6b3d2fd0db599f18eacc0f2dc553be2206a7/hypothesis-6.168.5-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:bc5cc310f9f86ec62f0d0dd7eea5a4788f18ec793b70ee2c7163b916768e1057", size = 624239, upload-time = "2026-10-05T23:24:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/97/6e/2ca0f68150be175b7cfa7bfb6692260638d86aeb9313478ba82e198186e6/hypothesis-6.168.5-cp314-cp314-win_amd64.whl", hash = "sha256:71ce0599e806ce3a68f9f118edf450bf091e11b134f6bcc5f8dd706b42c91ebc", size = 681884, upload-time = "2026-10-05T23:25:00.757Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/4fc2b5970df0c27668ec08abc505f1d01314a69953f89dc0edc6528ff5a0/hypothesis-6.168.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f66b02c9e95e916a2c58f725a92377ec988146ed7b5aeccd5e78ceecac1eae6f", size = 791636, upload-time = "2026-10-05T23:26:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/03cdf5f052dcb441e045be1fd0aa531e85cde1a1cbabab60968625c570a3/hypothesis-6.168.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bab27926e1d1575fb43b70d4aeece05b74a5e477af0509b56cb6fd778070dd93", size = 783280, upload-time = "2026-10-05T23:26:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/615557af244e2f3ce4763029c03a62ed82dcbbd646b72a8c479ef0408b33/hypothesis-6.168.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edeb42c3009b5652dc1c44907ec91bfe9284100ad5e57993dfebabb76f2961a1", size = 1112149, upload-time = "2026-10-05T23:24:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/67/a6707fcd51dc5f2531bf88ac072e99f31ab9d8020488b01349a6d2981081/hypothesis-6.168.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8977456328147c521a16a089325017b2c728fddc23351693a4fd924cc7fc7001", size = 1155637, upload-time = "2026-10-05T23:24:24.047Z" }, + { url = "https://files.pythonhosted.org/packages/85/d4/ac2e852d2f163afd398854662bcbb0b849a767abbf2f95a75de6f685f821/hypothesis-6.168.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:36ecf7ac351f9c0b5489ba800884b607da754e88ef40713fbfcc170d2151e6eb", size = 1287040, upload-time = "2026-10-05T23:25:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/f77b1602704bda6ff3d9d0817120bd7fb94fd792bd25508b36ce4b8bd2a2/hypothesis-6.168.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0333aa5129ba3019a83fb81a7f0fc238180e415a9edddd9a15101f8deaaa517e", size = 1322166, upload-time = "2026-10-05T23:25:45.39Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2e/94138a73e0906b31cb5968d20be58688f582a09e5958f2c75d45a7049545/hypothesis-6.168.5-cp314-cp314t-win_amd64.whl", hash = "sha256:2fcb87341d76ae0183e8219c9a14d55957c50d14973879db5fea3e81da45ba1a", size = 681599, upload-time = "2026-10-05T23:24:37.827Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/92cb8092b680be2b6ec5ffe83b9f1a98dbf414117566f9e3ba4e8b569214/hypothesis-6.168.5-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:453ab7d0a1fadbaa54ae8722d22463cc2046fa8ef25b9b88715d28279bf79fc1", size = 790950, upload-time = "2026-10-05T23:24:45.852Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/78aea12694e3d1177e2980d44798b6d93e191faf59155b18bf5ae315f6a2/hypothesis-6.168.5-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:bbdbc43d1f9dad595b249b7bbe8ee5102bc94a4fcb0a79ff76d20e41fcfe342a", size = 782939, upload-time = "2026-10-05T23:25:39.961Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/5ef9947b2d149f773428e555bdf66688405aa5167510bdbe97c8ec5c6090/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bc36194d7b6083591060836c7872711a6820217b325bf432dd7e10b3d4af5cb", size = 1111380, upload-time = "2026-10-05T23:24:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/e1/65/7e668e203fb2659c6214dc0c24cc09b7dea8a02c7c8d0ad338f644a054c4/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22425e2b1543a43c157a81472c713ba8f291cbaf054c70ffe128e2cacc294f65", size = 1142644, upload-time = "2026-10-05T23:26:30.697Z" }, + { url = "https://files.pythonhosted.org/packages/c0/77/b112978676e795658d58c4294bf90cdbb8cb56cb8292c8c4874650468cf9/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eea0bc513d0e38d1d5ddfb581132928871cd02dc54dfe4511a5396727c48e9d0", size = 1135966, upload-time = "2026-10-05T23:24:49.806Z" }, + { url = "https://files.pythonhosted.org/packages/e6/27/cd3bf01e8246c4318ec3df15f5eeeee3214f444df0129a6c7f9a62859ee8/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb142bc70bbf6645e15c7ca72de3f7c8dae198aa2743a609f4f3e3bb4f9c3a52", size = 1189646, upload-time = "2026-10-05T23:26:04.471Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a6/4d3e882f31c289e432dfec34dbb9029296038a8c69e8b28cebb0a5fb7ea8/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a27b758707bd37f5a1759cca6eef83fe1a212c38dc4ca0a203434004c5647d15", size = 1154770, upload-time = "2026-10-05T23:25:10.814Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/96f5455e1b3d0409cbbb1434c98e792bcceefd614a4b11e600072520b487/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:77a111cb50c330fa7098f65852fa17a01ecd781a85be3cf5e5871bdeeeb0ecbc", size = 1109925, upload-time = "2026-10-05T23:25:26.369Z" }, + { url = "https://files.pythonhosted.org/packages/3d/64/0758985d9d36f0c5ec981a1457ea1c8173f62d46a94531417aec117df4d7/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdd0afc13e86ec76cae3d3659569c1f601f4e9ca52b5cf91c1685979eae64d7b", size = 1148476, upload-time = "2026-10-05T23:24:54.552Z" }, + { url = "https://files.pythonhosted.org/packages/43/5c/a9b8953e1d8aefcd3c22cf8d10dd8acf93e602b903278e2e51cf8544ccea/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:5fefb02035864c3d322e3b0969b296250923fdcfb574ea1ad4374f1a6333f663", size = 1285856, upload-time = "2026-10-05T23:25:38.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/66098444dc832523ddc4f2e05723662834e5f99bba3c759615d059f6420e/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:9db8aa1f5529e1b577ec18b775c2fb4225821712e946f7762b90c966604faf83", size = 1415500, upload-time = "2026-10-05T23:24:21.682Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/e971b6fe20ef8d7c2019cbf24b4f6149468efc88c42a744f5bc99e6ca0ed/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:59e07d2f62b5ff573b0059959ae9cef9edfb0f5393fdb35ea81fce1ee77b27ac", size = 1368623, upload-time = "2026-10-05T23:26:11.292Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ac/b279dfbd2c06cdb3030ba7eea042cb2cf0171d0013563d103d5207dde63b/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:8a03ca128bea29d6826fc545f1f6289fb1ea2e83a5bb811321761b2d515ca575", size = 1264457, upload-time = "2026-10-05T23:25:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/55/57/16ac9f8ddfada1cd278bd2185234d0d36ebd304926b69ad0497c210c6fed/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:5c03f2d3f84f626f3fd07f54573ab40455e1a1996e98a4f4971caf8b7e796afe", size = 1281744, upload-time = "2026-10-05T23:24:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d1/99a44430b82998fdef0ffd7d353f64ee5f078c2805ff70f8677ee102cb6c/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2bdf8ce9b72a620cd5ec4dd6b1c1837ff6971489a863851d11d9b0f58dd4062a", size = 1321504, upload-time = "2026-10-05T23:26:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/58ef2564d1985a5c1a1dc57906b8363a767094abca180e80a0aca4cb635f/hypothesis-6.168.5-cp315-abi3.abi3t-win32.whl", hash = "sha256:5c3abbef7b17571fd713b0922407d9cd8cbc652254c0f462875f15199fcb29f7", size = 674959, upload-time = "2026-10-05T23:24:36.482Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/153414f55eb0c85bd9d891bd7811d746978c7ad3de81ea79eeb4e62e088b/hypothesis-6.168.5-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:38172199abab94a04bc017613e055faa796d7175fbc6221aac504d406c960b60", size = 681329, upload-time = "2026-10-05T23:26:08.897Z" }, + { url = "https://files.pythonhosted.org/packages/6d/63/117c82f08ab3ba1dcfbf6562ac43b8deb8efa8106646494fadd15122cc1b/hypothesis-6.168.5-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:0600ddc24c32dab5ca8e780630ab6e2561df6d7f594f781d0608b38e04c4da91", size = 679632, upload-time = "2026-10-05T23:24:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/73/25/5c38b739fb778d4de48aab6509b9cf0afd0317bb0459741afdcd0ad44aed/hypothesis-6.168.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6786049db92275e0c5cfac7dfcda6d4bbc80bdf84cbc8c9c7171ca17f47b5aac", size = 792028, upload-time = "2026-10-05T23:24:56.365Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3f/91071d53240f5f13ab1dda286e3ddb33177537dbf55cede76e7f4a3856db/hypothesis-6.168.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ffbde24430dcd73231fd03324a934e0f638f7c0899fc566f3ef8c851534f8030", size = 787324, upload-time = "2026-10-05T23:24:17.822Z" }, + { url = "https://files.pythonhosted.org/packages/10/ef/eb262e50d7741de6c49d27923e2c282d079273b8bcdacde33165ea39488d/hypothesis-6.168.5-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea967baaedfd532f1a521aaedafc66bb9de09795071492b0e7252139df38479f", size = 1115118, upload-time = "2026-10-05T23:24:20.43Z" }, + { url = "https://files.pythonhosted.org/packages/87/67/a655a8666164aa896516f919af272fa3a3a00d2786be880c31bb638e79e2/hypothesis-6.168.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f98289a5da876c08b9eeb68d1cfdfbd0fcc110cf364d33c3cc32cf229ffe8", size = 1157894, upload-time = "2026-10-05T23:26:28.641Z" }, + { url = "https://files.pythonhosted.org/packages/57/4d/71c422a29446c03e9a052f10b8ee527044242e71e3c0139100991f721e16/hypothesis-6.168.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e313a01ce580180dc3bb8fa98ddd0ffb20e51e108d9fa747ba6c1596790dc3fa", size = 685030, upload-time = "2026-10-05T23:24:09.964Z" }, ] [[package]] @@ -162,26 +166,26 @@ wheels = [ [[package]] name = "maturin" -version = "1.14.1" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/71/eb078174335ff30bde1be2ad54fb6256db4b160eb3efa81163fa30f8c814/maturin-1.16.0.tar.gz", hash = "sha256:13e69ca12542999d8c5467131de72a49bb76e4c284b9a45c5783df7c0fcb3a8a", size = 396641, upload-time = "2026-10-29T14:09:43.096Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, - { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, - { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, - { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, - { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, - { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, - { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f8/235d4a8b3d61af3c7744582ae9e6ccb8f096e9f68f06fe62d7d03e5d7b2d/maturin-1.16.0-py3-none-linux_armv6l.whl", hash = "sha256:deacbb204a20326862e194c7df4958e0514a0411c9a3ccf228a00067c11b5c7e", size = 10425070, upload-time = "2026-10-29T14:09:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d5/f49a90d8c489dc5f134654ec91d12a78a51599fdc8c2b5a1bc2391bca0ac/maturin-1.16.0-py3-none-macosx_10_12_universal2.macosx_10_12_x86_64.macosx_11_0_arm64.whl", hash = "sha256:131fcc17ddb3ccc48aa183d2172ebf994c322b858e1b9694fb8744d783df66f6", size = 19839189, upload-time = "2026-10-29T14:09:09.441Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8e/d65633d7c25484cb82735a51e15bc969f9874a13557e2b352d27df574c20/maturin-1.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2216d731c9324869d3bff72c0c73009bb6e640b3fa7d340174ae70dc874d1d9f", size = 10271659, upload-time = "2026-10-29T14:09:12.596Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dc/e77f490557cb5e045a75c3d18a7965425ac576235c970dfff4d903eb2c74/maturin-1.16.0-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:837db2f4d93d4899c3ac393fb3cc9eebf35b6002336e5d574f4a4fb6c49fe283", size = 10763361, upload-time = "2026-10-29T14:09:16.11Z" }, + { url = "https://files.pythonhosted.org/packages/fa/eb/535564a4211105fd64611fa4f9437c8925eded26a97d58de84565f5aebfa/maturin-1.16.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:399fadcd4af39532a173e010240c34930eacc1bb1746bb179863481c68bcf1ab", size = 10442856, upload-time = "2026-10-29T14:09:18.64Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/570e13ce81f103be6609c067a06c5bac08da5af0cf7ea700c9ae43419be9/maturin-1.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e285ec9eb10243d45d10417178cb2eb76e0cbb184d0aa4378b503489fa12ee78", size = 10298796, upload-time = "2026-10-29T14:09:21.385Z" }, + { url = "https://files.pythonhosted.org/packages/88/a6/26b40c31c354359fe4e49398113d018694c7035968fe1b569bb13c3905af/maturin-1.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:609228d639c5b888dc41364cbd318547a59f027481f53cba85838f46da4b3f02", size = 10268023, upload-time = "2026-10-29T14:09:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/2b/19/faca1784d917c2eecd498648dff6a0179b299c8142cc448ea5b1183545ec/maturin-1.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:7001fd3ac591820d01f2fa041552c367ec587c98649d9f10a817cc7f28e59062", size = 13739038, upload-time = "2026-10-29T14:09:26.662Z" }, + { url = "https://files.pythonhosted.org/packages/36/c1/d67e77aa07100e3b2d4c0d9ef6922646d9c4bab25cc04f91023aefc72418/maturin-1.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b0573a146803f24566f5b301a4572a8e49bcab4917175407c730da13b46e134", size = 11100193, upload-time = "2026-10-29T14:09:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/76/44/ccef52f02ab0e1f7258bc8fe0140b4991c6287458ef883af1027f946a253/maturin-1.16.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:c16d3589cbddda9420237fdb1394b1a9b36483c0bce83b23de6118c908a8e940", size = 10663849, upload-time = "2026-10-29T14:09:32.183Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9b/3104f62dbb428f9e26e35cba2c80cfc42ba9bb1dc4968882c19d1bfe594b/maturin-1.16.0-py3-none-win32.whl", hash = "sha256:9e221c047931caa79283cc0c2278904ae1b25880dde3574b0447657d72907689", size = 9124894, upload-time = "2026-10-29T14:09:34.857Z" }, + { url = "https://files.pythonhosted.org/packages/63/b8/c25ce706c4797ee1663bcf1f386ffe91d8a3b3589700a135a204b49bc4b0/maturin-1.16.0-py3-none-win_amd64.whl", hash = "sha256:9d43a2adf51a2794e907a8f7dbad06eb114d5edb0909c73c1fa9040f15e584e9", size = 10575645, upload-time = "2026-10-29T14:09:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/f69aba8b22539e5ed58323d4a6ee974afd9edda238489c73926cc0fc9d51/maturin-1.16.0-py3-none-win_arm64.whl", hash = "sha256:95f34ce53249fb7095ff4f2aff64d2562c511d6edf3e216c15dbffcd82b72b4f", size = 9886475, upload-time = "2026-10-29T14:09:40.603Z" }, ] [[package]] @@ -212,17 +216,17 @@ wheels = [ ] [[package]] -name = "py-cpuinfo" -version = "9.0.0" +name = "py-cpuinfo2" +version = "10.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/97/a8b1ddada14c8280a047c0746f95cb05d94a31b1a331cea22bcdc2b2a82d/py_cpuinfo2-10.1.1.tar.gz", hash = "sha256:7861133863663f16e06eca63b12904ef100b5760415e92372dac0162799a4771", size = 100840, upload-time = "2026-03-25T21:49:40.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, + { url = "https://files.pythonhosted.org/packages/23/0a/ba69d2dde1ae12ef1d389ea5a216384c5ff6ef7a1e7a48d1e9b6686f6790/py_cpuinfo2-10.1.1-py3-none-any.whl", hash = "sha256:adc53396bfb206e6498d078ec2ab407f85799ecd819584ac36a8f80a2d4d762d", size = 23791, upload-time = "2026-03-25T21:49:39.574Z" }, ] [[package]] name = "pydantic" -version = "2.13.4" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -230,125 +234,160 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/d5/b4867fa7c7f04c34997d7d8aad744f64d210bb99d67cf18bf3fb1eea1aed/pydantic-2.14.2.tar.gz", hash = "sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d", size = 911596, upload-time = "2026-10-29T16:37:12.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/97/01/c71a71258b58659c72504e5fd65b451f6e36e34dc931d0b237ded213c814/pydantic-2.14.2-py3-none-any.whl", hash = "sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118", size = 490403, upload-time = "2026-10-29T16:37:10.181Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.50.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/8e/23ec1021834e35041fbb2f10946141e6acdfd39379c45cc4d65a4e8a1d98/pydantic_core-2.50.2.tar.gz", hash = "sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775", size = 518711, upload-time = "2026-10-29T16:34:57.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, + { url = "https://files.pythonhosted.org/packages/00/46/5fabd45007e53349fa9ff22c95968cb451acbc7d912dd8a9980ffa2325c1/pydantic_core-2.50.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6258754c473b3045a7aa30b833a8a331a1ab36c6147cb230789861d80bb26f83", size = 2163662, upload-time = "2026-10-29T16:31:28.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d5/6ec0fb1ccf75a8d6b42164f8e9440012416546f9badb683850c58167a5a8/pydantic_core-2.50.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a786cf782ebb2e45a43c7118d75d452aac982b79577dfe804d8dbf2878f0b589", size = 2119060, upload-time = "2026-10-29T16:31:29.959Z" }, + { url = "https://files.pythonhosted.org/packages/64/54/39be898ef4811df54687e71f1bdb77095c7d792c36f98b20027602ab012a/pydantic_core-2.50.2-cp310-cp310-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:ea71b94a2eb5d2fc102926321f56d2ee8eddfad2673eadb1d23e9f287a492715", size = 2276387, upload-time = "2026-10-29T16:31:31.239Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9d/99fcf55e44eab0436cf0e49e34eee7393a9f737ab830018d67cdda7a02bc/pydantic_core-2.50.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:71615edeaf1ae70f5f3f558426072eede0168b7e5190fd9633878e29a0b85732", size = 2119441, upload-time = "2026-10-29T16:31:32.577Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fe/b51d611621963172900b8c2a6242c2d7a1ffb2a2026f5798449da6b211f3/pydantic_core-2.50.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1a5e859f5a8a6d834b0a13acaa0eaddf1a9b4b015fcda13bab7b471e18299d32", size = 2206739, upload-time = "2026-10-29T16:31:33.758Z" }, + { url = "https://files.pythonhosted.org/packages/95/08/cd920fdfd19d32aa93a815bd81a43cb892cf38ce96420157092935e1633c/pydantic_core-2.50.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d71a9a9352acbcb5c73d39dd003383599428067b6eda5cf7c5d335a7a0da37e1", size = 2394581, upload-time = "2026-10-29T16:31:35.306Z" }, + { url = "https://files.pythonhosted.org/packages/be/cf/40f2b1a7c5cee35a59fac898bc39e43283547cdedf4ffe152f1504104843/pydantic_core-2.50.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:bf7f99a0e6616cea754e2334d129091f3260d011369d9b8c658522bfb2427619", size = 2446325, upload-time = "2026-10-29T16:31:36.498Z" }, + { url = "https://files.pythonhosted.org/packages/30/b0/5aae06bbe58fac7cf5c6aac6dc11b04a9beeb5e50bd74f6e4f724ac91c4e/pydantic_core-2.50.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c12a5a080570a91bbcbf6d10db008b80e3f5dea015ef2f0137bbec70f24f3f3", size = 2238710, upload-time = "2026-10-29T16:31:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/33/41/fa31929c6d3b9c91048abd486ac94ceb402a8a3e145b5abf1ef87300b781/pydantic_core-2.50.2-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:89213368eef163350af37127939ace099010e8102757905c3e329f1175950ed8", size = 2274879, upload-time = "2026-10-29T16:31:39.349Z" }, + { url = "https://files.pythonhosted.org/packages/27/00/f9cf3893c6a4c5f785888c3842d837a01067f4795978c6d56c798d0434ca/pydantic_core-2.50.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d93c066a38f83376b5bc020e563666784d1b680a70d6ceb625fdb541af7a312e", size = 2313505, upload-time = "2026-10-29T16:31:40.742Z" }, + { url = "https://files.pythonhosted.org/packages/9a/48/c780d27b9d622301bac0700746ceb65f8e647d8319b509fea3acdf85b329/pydantic_core-2.50.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:eec187d2876abb6a74203efbe2aa796455c1809334acf4e739dde35da8cfb69f", size = 2483718, upload-time = "2026-10-29T16:31:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fe/8acdea280a53ac5e9fefdcd6b4f01fed612980029d343d0202c044e245a3/pydantic_core-2.50.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:701bc55d08c22f981973ca4f6fdf4ea102ee1ee3a21b818b66fdb161ea711a4a", size = 2490576, upload-time = "2026-10-29T16:31:43.836Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b7/48d74ed20d415e43ead485f7b149270afa98fe6acf3d4c895100c9332073/pydantic_core-2.50.2-cp310-cp310-win32.whl", hash = "sha256:7f9821dfc0c473c96ad55ce4189c7cb4f9c94e7d4f563afb874173243dec1162", size = 2123629, upload-time = "2026-10-29T16:31:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/77d3eca0db83968efe5142dce85fdf050cf4b3a3b1e92ea3eb7525cbe143/pydantic_core-2.50.2-cp310-cp310-win_amd64.whl", hash = "sha256:3939538e640c94ab779a05f2d45dafedd01d65d93e1290797f3ac5b59fcb6455", size = 2242044, upload-time = "2026-10-29T16:31:46.664Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/1043d3840b7859cc19abede795e058a785ebc1fa011b022b0a69514dd5c5/pydantic_core-2.50.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2115d7ca6d58554af8a14da3dd32d361a4fe999fae52f115a6155679b5b00e72", size = 2160886, upload-time = "2026-10-29T16:31:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/6211dfe1794bde5ce14f3de2212551196784c67411ac5249d8aa627e3e23/pydantic_core-2.50.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2dd65864aad38dbcc57c3d0e2c978d4286e063378137d8fc0c9fcd558943285", size = 2115730, upload-time = "2026-10-29T16:31:49.449Z" }, + { url = "https://files.pythonhosted.org/packages/67/42/50a1a9721cfc21d360916754096c90467fa21386ef16875445ea06a4cf6e/pydantic_core-2.50.2-cp311-cp311-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:26b778d4ebb75f181d36220ff0f54a513fc46f053d9b09ce79993c2f00bc45cc", size = 2274234, upload-time = "2026-10-29T16:31:50.668Z" }, + { url = "https://files.pythonhosted.org/packages/69/0b/22349c3114a3b3c400333ba6ec1d2e127fc9a4e4682440d8e23d954df0d1/pydantic_core-2.50.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e80c47314c7233e7105a7efcfffd89d56e479e558189d1959eeeaf2639a27680", size = 2116001, upload-time = "2026-10-29T16:31:51.887Z" }, + { url = "https://files.pythonhosted.org/packages/d8/2e/3b88612239e87cfddd370362e93b18a50d5075883817d6b35e8e3871af75/pydantic_core-2.50.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:3bfe12243b3ded517c6734d91f127489e2f51bf95cb533b53130fe360333db80", size = 2204318, upload-time = "2026-10-29T16:31:53.342Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/a735bf3967d0fd73b512705d6f80fbe179d3b6208779721e86613cdf736d/pydantic_core-2.50.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:01eb0282d0b3a065d55956d1be9ce398c46db3dd6d277a6cb2358c8aa8c53179", size = 2392365, upload-time = "2026-10-29T16:31:54.721Z" }, + { url = "https://files.pythonhosted.org/packages/9d/16/0a0febc06b311d6ca43dd7fa068273d2c4d3c4ad86ac4a6c2f2cbe924c23/pydantic_core-2.50.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:196b87b48b2695426920e8470d20f9e99f019ebd86f15808aa018b68086840cd", size = 2443963, upload-time = "2026-10-29T16:31:56.455Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/22bd04c22adae266ae5e99e4c34bd882daf6c93c35bda2f9674421c1e7c3/pydantic_core-2.50.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86e7abd24a67f61a4bfe8df059b67a2bdd8bf33705178e98463195d140bd3780", size = 2236349, upload-time = "2026-10-29T16:31:57.837Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/77b12031179047a292128eee12cd7aad899d82391157356a581117e232e7/pydantic_core-2.50.2-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:aff31602e8518594e6f6bc670cdb27e37cb7dbeb2d5d0b76a7707b70fe951ddf", size = 2272702, upload-time = "2026-10-29T16:31:59.287Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/41f674077b08b79bf2f2ae3c777a5b7b8758647433c6c621580b612e8c1e/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a88851e8e332588cb8cced8b5318d1163c7d0bb87830aa8dcafbc4ae9602b73c", size = 2311296, upload-time = "2026-10-29T16:32:00.722Z" }, + { url = "https://files.pythonhosted.org/packages/61/af/527d1d29cfb8efc7c7f9ed9f35ab5b16a33b1837b186c6e42ecbca699817/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3b1bb3950e0794a795c9344d818d5b476189b0c9c9755e8eb8fde87c3acee17a", size = 2481042, upload-time = "2026-10-29T16:32:02.423Z" }, + { url = "https://files.pythonhosted.org/packages/57/35/79877aaf083424e67c870ba07c0b5ed36a2096a0cfc8b058c1d921797723/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:928a44ad1e21a7e923eb924d601605f0694cd187d2ce5d40bf7ad76650f4627d", size = 2488007, upload-time = "2026-10-29T16:32:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a627e40690c21be388c4abfddaefbf589e359614456156c90432ac0a76a/pydantic_core-2.50.2-cp311-cp311-win32.whl", hash = "sha256:9a87744e683d9a13f60b7fa5242c17f8d283ffa94ad727cb19063b48c3d34960", size = 2121376, upload-time = "2026-10-29T16:32:05.053Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9b/d21478003682505aad9619ff9e086bd29aced2615b23d5315cb37de14cad/pydantic_core-2.50.2-cp311-cp311-win_amd64.whl", hash = "sha256:b958c217c6e56bef261f516e4caf5d490b079ad6a428752afada5a764cca6c71", size = 2238388, upload-time = "2026-10-29T16:32:06.811Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/670a52ea740fd60f5e0a873b74505714c4bfd04c4344f789310005c4cbbd/pydantic_core-2.50.2-cp311-cp311-win_arm64.whl", hash = "sha256:b0e6a2152706d84f7da8b9d34dcecc7e9bde678af32c40bd5b405f427ae0fbdb", size = 2185763, upload-time = "2026-10-29T16:32:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/6609a66cda909d414c58438f9bd70b1e947fe12c25f6bd40c93121afd7ef/pydantic_core-2.50.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738", size = 2220341, upload-time = "2026-10-29T16:32:10.193Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/92463ae7be78d73885487f72c4599902c2cc190378bccaad5718b7495250/pydantic_core-2.50.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9", size = 2093701, upload-time = "2026-10-29T16:32:11.502Z" }, + { url = "https://files.pythonhosted.org/packages/62/d3/24147a1aa22f7eade546172750e1110fd7b2793998057809800d3ebeae4e/pydantic_core-2.50.2-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756", size = 2253622, upload-time = "2026-10-29T16:32:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/2628ca3c83a0d9a6fda3305fe59b1b28e40c0221e8f234cd1ef86fb64745/pydantic_core-2.50.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6", size = 2114101, upload-time = "2026-10-29T16:32:14.702Z" }, + { url = "https://files.pythonhosted.org/packages/21/c8/cc6d7304a93f91f5e06260ccad71cc7d882489663a37dec009532379fc46/pydantic_core-2.50.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234", size = 2199541, upload-time = "2026-10-29T16:32:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/18f44ac5696b3e2b2384cb89c9fbe6ed69860f528e232b9fb46cc5b45a17/pydantic_core-2.50.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4", size = 2368597, upload-time = "2026-10-29T16:32:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/1d/22/5f2a48ef819a7e1b74fb003a85d27576ad2f9ddf3901b88998ac63ce1e00/pydantic_core-2.50.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a", size = 2451701, upload-time = "2026-10-29T16:32:18.956Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/2a6556bbdb8e918175af9d849a5d0b367b5644e41939d040f3a7215b1339/pydantic_core-2.50.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a", size = 2242782, upload-time = "2026-10-29T16:32:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/90/ed/dd9f29a92257f32eb5b99d5120826186ee0632ecc3aaf6c23e2fab89ed6d/pydantic_core-2.50.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe", size = 2253706, upload-time = "2026-10-29T16:32:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/11/19/1ccf3bf38eb4ed97d8aeefc558458834ae0ad53fdbb1edf61af7228ea500/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798", size = 2288603, upload-time = "2026-10-29T16:32:23.465Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cd/bae2320fc89a65bcd87bf66ec83aac51b3b43b970a2edece15feb8af64dd/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3", size = 2476597, upload-time = "2026-10-29T16:32:24.824Z" }, + { url = "https://files.pythonhosted.org/packages/db/35/1bdb8d52c8e562e95c5e6ebb5784792af568ac6a81470dd546d95f392b60/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2", size = 2478664, upload-time = "2026-10-29T16:32:26.197Z" }, + { url = "https://files.pythonhosted.org/packages/12/72/0179b40ac6a6b0899c61826f2e3e80f88063f6d821e0dd9641927de46700/pydantic_core-2.50.2-cp312-cp312-win32.whl", hash = "sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f", size = 2108820, upload-time = "2026-10-29T16:32:27.736Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fb/679144362d7e5162519d9f6631ec982521417de3196351c6c56485f5b29f/pydantic_core-2.50.2-cp312-cp312-win_amd64.whl", hash = "sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314", size = 2252238, upload-time = "2026-10-29T16:32:29.572Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/e3ccc885c3e2086ff84d7ca496f45b974a9f0a49ce2da8973aaa3a369a4b/pydantic_core-2.50.2-cp312-cp312-win_arm64.whl", hash = "sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db", size = 2162565, upload-time = "2026-10-29T16:32:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fc/ac3ae9d934932ce3c380e7fe53e01372b49ef5dcb49db56f1a1f005b4d8c/pydantic_core-2.50.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437", size = 2222463, upload-time = "2026-10-29T16:32:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/7605223654ed4e777d05668e914d32b476b1fa2bb756bff9820fe270456b/pydantic_core-2.50.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e", size = 2093995, upload-time = "2026-10-29T16:32:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/29/4a1a653cfedd3f5e1ccd3b1e919fe324fb45607d12c30fdd7ca5af16ba8d/pydantic_core-2.50.2-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4", size = 2253178, upload-time = "2026-10-29T16:32:34.77Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/03ce5ed01c98b60272cdee8ac1abb819b64ef347717c11d96215e32d9952/pydantic_core-2.50.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23", size = 2113894, upload-time = "2026-10-29T16:32:39.241Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bc/f7644474ac0faad9a7fdf5179239893b21339169b8b4f89e3075c4be16b2/pydantic_core-2.50.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d", size = 2198248, upload-time = "2026-10-29T16:32:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8f/3d3a1c1d313e02774f6c034eb1910eb6e795922e66e81f0f053f98afe866/pydantic_core-2.50.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057", size = 2368505, upload-time = "2026-10-29T16:32:43.755Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/38fb4545c00ce2bfebc2a0ffd99f7f580f1e1efb8db3b2faf88def640435/pydantic_core-2.50.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63", size = 2451704, upload-time = "2026-10-29T16:32:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/79/be/b559a1ceaff8b1d4581733d47d2ffa785635ac05716eae4c964466aac6e8/pydantic_core-2.50.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b", size = 2241760, upload-time = "2026-10-29T16:32:46.382Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7c/3c8ab7cb45e4dc7687a4f16a953fee14d2eca18dd90d6be8bd470ef06962/pydantic_core-2.50.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432", size = 2254115, upload-time = "2026-10-29T16:32:47.818Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/3f3471206224cdac2af3492a3f7c724c47a82f5f8850ff364c2e36023497/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c", size = 2288598, upload-time = "2026-10-29T16:32:49.404Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/2070b388b1d8633cfb72e3964ac3c95aaeb195c240209894ff386847b494/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601", size = 2475116, upload-time = "2026-10-29T16:32:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/96/e96ff19527c73b9aaddce2a4424006dc15439fd89ef2d39641048c5ee85a/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544", size = 2478926, upload-time = "2026-10-29T16:32:52.856Z" }, + { url = "https://files.pythonhosted.org/packages/35/10/2fefc082eb46f959e80735e859161305c0c2fbb3d5d20ac5547c49a84a3b/pydantic_core-2.50.2-cp313-cp313-win32.whl", hash = "sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d", size = 2108354, upload-time = "2026-10-29T16:32:54.276Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/14eeec394ec1422f9c4e9743cd746f4502232a7c65eb235d1a03729d94ab/pydantic_core-2.50.2-cp313-cp313-win_amd64.whl", hash = "sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45", size = 2250556, upload-time = "2026-10-29T16:32:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/75/b7/a014acf1ae4b4aa7d32ff957e2250698637daa3a29448444180360b92284/pydantic_core-2.50.2-cp313-cp313-win_arm64.whl", hash = "sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67", size = 2162633, upload-time = "2026-10-29T16:32:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/fc525bb981a9f287cfa34346e7ed1d5ef7c043c2b9b517f54c6136032d10/pydantic_core-2.50.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329", size = 2226414, upload-time = "2026-10-29T16:32:58.559Z" }, + { url = "https://files.pythonhosted.org/packages/db/68/315701095f25687938ab79d263a6d939337456c2840fe69038fd38000338/pydantic_core-2.50.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02", size = 2095041, upload-time = "2026-10-29T16:32:59.873Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/efd3abf31aa24bc97b311735f418886f947b4f193df02a1977643963b03e/pydantic_core-2.50.2-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2", size = 2257635, upload-time = "2026-10-29T16:33:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6c/146b36eed4de8115a0fda76840703e75f5f8b36aeb3af86976bf094d1a17/pydantic_core-2.50.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7", size = 2116807, upload-time = "2026-10-29T16:33:02.692Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/db006ea79bbe405cc38b25899319c7d461ff1813cbdfe6aa90a65ed1dfff/pydantic_core-2.50.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9", size = 2201037, upload-time = "2026-10-29T16:33:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/41/f9/ea2bc1ec13a67af4a63c9a887fa0aa6ef4ade9018e2bb412d4fc80110e19/pydantic_core-2.50.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108", size = 2374904, upload-time = "2026-10-29T16:33:05.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/8e/26a6a075301f0e563646037ea69bef8c63911d38fbd257f94cec69cdf985/pydantic_core-2.50.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92", size = 2457626, upload-time = "2026-10-29T16:33:07.271Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/8eeabaf1d394f0d3b9a27129ac21556eabee6f2ddcb418069ef4037e00b0/pydantic_core-2.50.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51", size = 2245505, upload-time = "2026-10-29T16:33:08.824Z" }, + { url = "https://files.pythonhosted.org/packages/bd/54/449778cd397e2682b30d1bcfab0872f6ac36702b59cc326296b7f4f14928/pydantic_core-2.50.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a", size = 2259563, upload-time = "2026-10-29T16:33:10.107Z" }, + { url = "https://files.pythonhosted.org/packages/26/c2/2f65691d25488ee0f3c17861d079e8ccb7817955522d07cd3c3970d30af5/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d", size = 2290470, upload-time = "2026-10-29T16:33:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/aa7c40234cd24536afa0895ff1d26f9f15638004e25242c5193ce053b96b/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee", size = 2477272, upload-time = "2026-10-29T16:33:12.771Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/c347c68e040cf93d8487a8f4fc28f2768312834c7d21c301538f416f9955/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58", size = 2484352, upload-time = "2026-10-29T16:33:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/72556126c8b6154fe7acaf757b96b2daa37aeffc3a517deb76e272f5ad09/pydantic_core-2.50.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3", size = 1491226, upload-time = "2026-10-29T16:33:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/66/004c85511812964a36b3535af022c5c0dfcb760a31e4905b96310e8dd6c8/pydantic_core-2.50.2-cp314-cp314-win32.whl", hash = "sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99", size = 2113134, upload-time = "2026-10-29T16:33:16.87Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/dcd3df20ab2a7279d42a8415fc184e8e77bcf9aa3c9ac3e938c1300a242c/pydantic_core-2.50.2-cp314-cp314-win_amd64.whl", hash = "sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31", size = 2253407, upload-time = "2026-10-29T16:33:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fb/0685895f5d199b215011dc4eb8ccea032a64ec23dfa88eeb5acca939c2d2/pydantic_core-2.50.2-cp314-cp314-win_arm64.whl", hash = "sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3", size = 2160456, upload-time = "2026-10-29T16:33:19.782Z" }, + { url = "https://files.pythonhosted.org/packages/84/2f/4c8f67efe62837a508b74cf910904f73029cb211371351c5ceb36d56e85e/pydantic_core-2.50.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1", size = 2217558, upload-time = "2026-10-29T16:33:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/e0ef88cfbcca5409f57751289bc20be63a58af596a36e94b8cd795e59747/pydantic_core-2.50.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c", size = 2074335, upload-time = "2026-10-29T16:33:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cc/47844a3179ebce27064e13619594b3fc1a9b4509d8dec1e3960c5f87f5bb/pydantic_core-2.50.2-cp314-cp314t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40", size = 2264201, upload-time = "2026-10-29T16:33:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/30e9d3c86d81884ee3199560ddfa2c1a66a8dc0736441748eae43064abbc/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b", size = 2094353, upload-time = "2026-10-29T16:33:26.739Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/392a5999480d0bcab4539db0dafa93265152396fd63e87be4d24e13d6d82/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d", size = 2203119, upload-time = "2026-10-29T16:33:28.154Z" }, + { url = "https://files.pythonhosted.org/packages/cb/80/6e4e276a3fde5500224a39150e06a4f134f136165908be69402720718a36/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b", size = 2392881, upload-time = "2026-10-29T16:33:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/d5/5eb56c6317ab4abf33ef9519a50bfac0c96f7f408bad1bc36da6f27d43e4/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910", size = 2457290, upload-time = "2026-10-29T16:33:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f1fe9384bb37e4e728c51c79151808deacf7b2502e245f50b57c85eeb199/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11", size = 2241568, upload-time = "2026-10-29T16:33:32.04Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f1/cffc4f8456c35690bcb65a8f2304afbf2f519766345a28aba553d5a55cc8/pydantic_core-2.50.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead", size = 2257185, upload-time = "2026-10-29T16:33:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/ccd4df1cc39cb1e42fb7ec86a0dc0a23e8479de04d3012ca6b0bf2279a03/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c", size = 2299384, upload-time = "2026-10-29T16:33:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/83/60/404b4b0c416a61867ee642cdc1450bca4d8a9a10e5f5f21e1ecaad05fdc8/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef", size = 2479384, upload-time = "2026-10-29T16:33:36.106Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/57c81418a2fdfde0578c71396df773965adebd4421d9985a0f8dcbe9ec4d/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636", size = 2504616, upload-time = "2026-10-29T16:33:37.423Z" }, + { url = "https://files.pythonhosted.org/packages/08/66/dec78e28a604f3f4985f5a795d4da3e5fef78b0921aeb7f614ed7c780a32/pydantic_core-2.50.2-cp314-cp314t-win32.whl", hash = "sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d", size = 2093179, upload-time = "2026-10-29T16:33:38.705Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ea/79d9ee60648463804145d44b08f1e7e2474427d9704e3331d814483908b3/pydantic_core-2.50.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977", size = 2220575, upload-time = "2026-10-29T16:33:39.929Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/aa6a7bd017c3228a41942da4fd788b86fb4aad9677212a4737a0c157fe74/pydantic_core-2.50.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb", size = 2160866, upload-time = "2026-10-29T16:33:41.237Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7f/ec2974015b4be5e50266ce732b8942d3d0b3bff751ace8900999c6577bce/pydantic_core-2.50.2-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5", size = 2230936, upload-time = "2026-10-29T16:33:43.656Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/af99a09eddc348c1d0744c0df28dfcd4839ca497067f0fa2168208b41b1b/pydantic_core-2.50.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd", size = 2100409, upload-time = "2026-10-29T16:33:44.946Z" }, + { url = "https://files.pythonhosted.org/packages/84/75/8db3c46f02d6005277e5c2ff5933c85e7e1eb8798aa334c676c828650c3f/pydantic_core-2.50.2-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b", size = 2259728, upload-time = "2026-10-29T16:33:46.461Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9d/98c8d524921ca24e5c8a81a4b8f12209ac6ab05d0dd58b374e357da0ffc5/pydantic_core-2.50.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97", size = 2121771, upload-time = "2026-10-29T16:33:47.952Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/68c3b1fa919946012c940dea88e94e33e7dde46abe3e3d57053793c9bab8/pydantic_core-2.50.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7", size = 2196328, upload-time = "2026-10-29T16:33:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9b/cfb6312a5b65b02eab6bc00fb0d258d0e2e49468dab29b430e7cb49b2ceb/pydantic_core-2.50.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca", size = 2380152, upload-time = "2026-10-29T16:33:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/7fcced1d84ea294200abfa93e488f36d00732101d17bf5e1f1389e66de26/pydantic_core-2.50.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc", size = 2462299, upload-time = "2026-10-29T16:33:51.884Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d0/dd0e355694094f0045922f4da60ac9bc7a314babc494e46089fb00640fd3/pydantic_core-2.50.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986", size = 2249999, upload-time = "2026-10-29T16:33:53.333Z" }, + { url = "https://files.pythonhosted.org/packages/3a/79/f5f4b36434604d22aacb7152667f87a470e68847c89b136c68a278cf409d/pydantic_core-2.50.2-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f", size = 2262745, upload-time = "2026-10-29T16:33:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/a4c47a286745857758a4bb8d4d5e260c736a7bd6fab5da618f5711d1a3f1/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd", size = 2293663, upload-time = "2026-10-29T16:33:56.132Z" }, + { url = "https://files.pythonhosted.org/packages/0d/db/25330e609fd219ce16d0a089af1b4b62095f01595dbe6972f7a7dfc313b9/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46", size = 2473247, upload-time = "2026-10-29T16:33:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/53/16/704ca88b82156dc485da2dfb11d5d52a10b9ef15a294639c0aa5ebd1643e/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f", size = 2490536, upload-time = "2026-10-29T16:33:59.297Z" }, + { url = "https://files.pythonhosted.org/packages/50/34/1fdd0eebee9f8b6c4ec3c2490f49dc59a652c8c5a7fa0cbcaada0d91cda9/pydantic_core-2.50.2-cp315-cp315-win32.whl", hash = "sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8", size = 2117204, upload-time = "2026-10-29T16:34:00.514Z" }, + { url = "https://files.pythonhosted.org/packages/24/6b/702adc91486fb96ba55675d58ee15ddc38fb48553fb35e26fd005e303206/pydantic_core-2.50.2-cp315-cp315-win_amd64.whl", hash = "sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41", size = 2261064, upload-time = "2026-10-29T16:34:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/7f117432b8614526aa0d96fadd2b7fb7393156ddf2caf26a6fe606e6cfb6/pydantic_core-2.50.2-cp315-cp315-win_arm64.whl", hash = "sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a", size = 2161783, upload-time = "2026-10-29T16:34:02.966Z" }, + { url = "https://files.pythonhosted.org/packages/45/1c/2ba7a34fe8da4c83ec200fb9f85ab4d54b961be08367922743623f987849/pydantic_core-2.50.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f", size = 2223184, upload-time = "2026-10-29T16:34:04.214Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/f71a8da1f53a5be1787d2a97ba6fd9adba115305c7f15ea0bf733ed8336c/pydantic_core-2.50.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3", size = 2078174, upload-time = "2026-10-29T16:34:05.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0a/847e1f5427372692f6eb5c38261fed6b8cedf379e37d9f6134d48b9c8ee9/pydantic_core-2.50.2-cp315-cp315t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0", size = 2268813, upload-time = "2026-10-29T16:34:06.733Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bb/12966dc82b5e9c347981658b52e24349b6242902b644725c00ee7bdf4267/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd", size = 2098331, upload-time = "2026-10-29T16:34:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/340416b7ebe664524e7ada16ad2e3e069888b168010d75bda6a502bf2bf8/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a", size = 2197432, upload-time = "2026-10-29T16:34:10.091Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4d/425162c3d88af0d190c4dc4f940402cc6dd44d934359a37f50da6b7e4672/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e", size = 2399300, upload-time = "2026-10-29T16:34:11.462Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/2882bddf2e2038c8f41fa69f16ddeb6eeb1c002cc0cc0eb2157f73bb34e9/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b", size = 2462672, upload-time = "2026-10-29T16:34:12.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/d7e6bfc6a95573647d1a699e19228dd99914ae6aab79a2bec293a038e980/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853", size = 2246293, upload-time = "2026-10-29T16:34:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/64/97473ccaeb4ae5eb338162bd8479af121484e67f7078499730e3ee0a5592/pydantic_core-2.50.2-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376", size = 2259507, upload-time = "2026-10-29T16:34:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/78a9b7a6a0d0679ce1e46cdd38ff95afd18dd2d6a01ed87c80a01bc003f7/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04", size = 2303550, upload-time = "2026-10-29T16:34:17.162Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/b697173f82e99cdcf5ec6654b2edb66a00f2f92ef9d9a168861fca3fcded/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b", size = 2474919, upload-time = "2026-10-29T16:34:18.841Z" }, + { url = "https://files.pythonhosted.org/packages/71/10/986be76723784ae87f9664658fae39df340eaa21681cea95b6339d2e1b3e/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f", size = 2509095, upload-time = "2026-10-29T16:34:20.16Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0b/38dbd38c67de7642f87348da70122b89ca4c5fe8227cfdd5cc00c1cdb454/pydantic_core-2.50.2-cp315-cp315t-win32.whl", hash = "sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773", size = 2096972, upload-time = "2026-10-29T16:34:21.715Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/154baf8413b08d6daf2f5dec211c303ec6bed50c032717f354f8dd85c994/pydantic_core-2.50.2-cp315-cp315t-win_amd64.whl", hash = "sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6", size = 2226659, upload-time = "2026-10-29T16:34:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/eea7a1e7d98a79ef11b6d31748b70c58f8723f87d7ee42edbb6a101f6245/pydantic_core-2.50.2-cp315-cp315t-win_arm64.whl", hash = "sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02", size = 2166387, upload-time = "2026-10-29T16:34:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/f6/82/ec0384d2a74415aeef7fcd032d29af84a98da0ca25cfb4937b10a2ac78eb/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:adec010c824290d4ca87047e8d8bfa4f85ac29f545f326cd5321696805d311ed", size = 2154528, upload-time = "2026-10-29T16:34:28.022Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b4/46483477921934aa31eec9cb86ba95d2f620f4c71da749024c2e3b6e8203/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:f47c1349c3b4274aeb18459d54244881a5c0dee0fdccedffca45704ba5c9e0a0", size = 2072173, upload-time = "2026-10-29T16:34:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/4a/16/c1eb6bdd67c9d5f42b290abb86948d8de288bdf4aeca06c588c3cfe6100b/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ee64baeab403b9de9c92846c5c1abec28ffb046cbc023bfae059897841f88af", size = 2116695, upload-time = "2026-10-29T16:34:31.103Z" }, + { url = "https://files.pythonhosted.org/packages/ae/67/ccd8294730d9335c687facbdfd77515fa3b323e9bbb042c5d63addc6f5f8/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16ec8e07c37f02dcf68939cc7f202ba9a148cc155ee2acf22d337e10b24c2cb7", size = 2263561, upload-time = "2026-10-29T16:34:32.901Z" }, + { url = "https://files.pythonhosted.org/packages/64/4d/23560b11366b547e3e68a3b8f9c02557cee707effa54fd39dbb3c4fff1fa/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b", size = 2204868, upload-time = "2026-10-29T16:34:34.745Z" }, + { url = "https://files.pythonhosted.org/packages/06/58/b9ddfaad189143d6568627166a730b6624ca90bd9a9a4af4854df88d444b/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d", size = 2049162, upload-time = "2026-10-29T16:34:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/8b/20/99d259bf7835b2f13c6d0b3eb7bcf669fb6349986736fa7a8308a9e620b0/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371", size = 2112135, upload-time = "2026-10-29T16:34:37.777Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/652c8fe599abacafd852479059d209e608596ad3de82f1bff22e9f0f02fe/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324", size = 2270339, upload-time = "2026-10-29T16:34:39.039Z" }, + { url = "https://files.pythonhosted.org/packages/18/9b/d240307fd6882a93a88bf038ff07e1a6dcac0e88f41951fe92c9b614f0b6/pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b583ddeee6a6bb5bbc04462167c68034913221139d52d9f155ca6d1637a2fdcf", size = 2157909, upload-time = "2026-10-29T16:34:40.355Z" }, + { url = "https://files.pythonhosted.org/packages/30/e5/d336fb654f8f1584b4a20d71f92c2a966419bc5eafffd36848d8cb1a916d/pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5ab5738c39b25b5dc5a721270e2269a2f5e86e614375e6c07ee6caab8cbc0f2f", size = 2075773, upload-time = "2026-10-29T16:34:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/38/08/18203f64a405f11c3e4d569c9f49ad7a0b5901cd5fa2dff2436d28531c60/pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21a902e546032c82b7275552345a2342d862e0541aea7226c91067c396e68b3f", size = 2277290, upload-time = "2026-10-29T16:34:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c4/a4e0ae45ad1554c38caa93e200fdd08e74d2c0149b4242c78b2475903379/pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92ada606285dab054e40848a951f583c537cde1e54701ea1305ec0096877012d", size = 2265597, upload-time = "2026-10-29T16:34:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/99/20/304baf7724ac137b921ce793e4b686cf4cddc431da4888e5419e7bc7374f/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c1b65a42afc73b39e6ae908ec7b9c3b9898676a7b15ce4c66d4965f7071007c8", size = 2289092, upload-time = "2026-10-29T16:34:45.55Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/0fe5ac160718d75f2f9729bb31aa9a349dadd0d32886b8c0ecc253629c46/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c324a7e0c0012e3974053f81a270d8f1aa23227571f4ddd3254c911bb356459b", size = 2458417, upload-time = "2026-10-29T16:34:46.827Z" }, + { url = "https://files.pythonhosted.org/packages/43/44/042cbd7e46f00f45aea36496ca8bdb6b1136c2ec4063875a25286c2ba8fc/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:cb8d2d7b07cbe2d7488de7e53671f836c4f4a8032dcda079df33dc1b7e2397c0", size = 2492296, upload-time = "2026-10-29T16:34:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8e/e8aef16e587215a1795dbe991e41006003aef1f1b6f554361f3a6edd585a/pydantic_core-2.50.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7c6e1bd6cadae13958e52897ef020e85c9916a95917149fca463bb0c0421a7c6", size = 2323090, upload-time = "2026-10-29T16:34:50.008Z" }, + { url = "https://files.pythonhosted.org/packages/21/36/d083f973e28c5718f9d9d819c0b8171df1526124e8c3709bd55ddf1d4244/pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:f078e276848bc23d1f92f46fc88d5b9cbb824b9acc60d9e07116620e502e16a9", size = 2157909, upload-time = "2026-10-29T16:34:51.579Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4b/e2eb5b51f46411ceb80451a7bf4aee215fd84012eeba22a183b4699006b5/pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3435c443da7510a6de1a30c68beae338852a11f0440cc8320ea756a162f38dbe", size = 2075771, upload-time = "2026-10-29T16:34:53.469Z" }, + { url = "https://files.pythonhosted.org/packages/d5/99/e9703785468d1e644f19d602985b722ff97cc8ca67389e2d79f8fb562cfa/pydantic_core-2.50.2-pp311-pypy311_pp80-manylinux_2_28_x86_64.whl", hash = "sha256:b5675a12a7b67063fe3ca187e735b0463cf13fabc10166ae4d4b2f775fd0632d", size = 2265708, upload-time = "2026-10-29T16:34:54.662Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d7/079d3d36aa6ee0333847d968d91ff185837541741d3c58ed0316103309bf/pydantic_core-2.50.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:f5ba7ef10ce82737e173fa95cafa1c8210918f7e653715b2c7a7a31877a74848", size = 2323087, upload-time = "2026-10-29T16:34:56.176Z" }, ] [[package]] @@ -362,20 +401,20 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.411" +version = "1.1.414" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, ] [[package]] name = "pytest" -version = "9.1.1" +version = "9.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -386,22 +425,22 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, ] [[package]] name = "pytest-benchmark" -version = "5.2.3" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "py-cpuinfo" }, + { name = "py-cpuinfo2" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/d1/1bc61155de46f25473a06476ad1b3a94f96d40d49cfa98f82c1184117808/pytest_benchmark-5.3.1.tar.gz", hash = "sha256:c1ee70a58f9bfb1ac955cc634247a340b0df7bcb949071cfca396f16c6fc3516", size = 385467, upload-time = "2026-11-01T03:32:10.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, + { url = "https://files.pythonhosted.org/packages/78/75/919ac85ad3f762e45aa369d2f68d40390e316474b6bb8434aeb6e3c8bf8b/pytest_benchmark-5.3.1-py3-none-any.whl", hash = "sha256:d7732a9952b1c4a4a32277adc693739126eab37afc3a711847f53805ccadc020", size = 49142, upload-time = "2026-11-01T03:32:08.359Z" }, ] [[package]] @@ -478,12 +517,12 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] diff --git a/src/fable-library-rust/Cargo.toml b/src/fable-library-rust/Cargo.toml index 992dafb80f..67c7a7578a 100644 --- a/src/fable-library-rust/Cargo.toml +++ b/src/fable-library-rust/Cargo.toml @@ -14,10 +14,12 @@ guid = ["dep:uuid"] lrc_ptr = [] no_std = ["dep:hashbrown"] random = ["dep:rand", "dep:chrono"] +reflection = [] regexp = ["dep:regex"] static_do_bindings = ["dep:startup"] threaded = ["atomic", "dep:futures", "dep:futures-timer"] -default = ["bigint", "datetime", "decimal", "enum_func", "enum_string", "guid", "random", "regexp"] +unicode = ["dep:unicode-general-category"] +default = ["bigint", "datetime", "decimal", "enum_func", "enum_string", "guid", "random", "regexp", "unicode"] [dependencies] chrono = { version = "0.4", optional = true } @@ -29,9 +31,10 @@ num-bigint = { version = "0.5", optional = true } num-integer = { version = "0.1", optional = true } num-traits = { version = "0.2", optional = true } rand = { version = "0.10", features = ["std_rng"], default-features = false, optional = true } -regex = { version = "1.12", optional = true } -rust_decimal = { version = "1.42", features = ["maths"], default-features = false, optional = true } +regex = { version = "1.13", optional = true } +rust_decimal = { version = "1.43", features = ["maths"], default-features = false, optional = true } startup = { version = "0.1", path = "vendored/startup", optional = true } +unicode-general-category = { version = "1.1", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] uuid = { version = "1.23", features = ["v4", "v7"], default-features = false, optional = true } diff --git a/src/fable-library-rust/src/Array.fs b/src/fable-library-rust/src/Array.fs index 13c8f44d5b..b75ac417dd 100644 --- a/src/fable-library-rust/src/Array.fs +++ b/src/fable-library-rust/src/Array.fs @@ -985,22 +985,40 @@ let distinctBy<'T, 'Key when 'Key: equality and 'Key: not null> (projection: 'T let hashSet = System.Collections.Generic.HashSet<'Key>() xs |> filter (fun x -> hashSet.Add(projection x)) -let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = +// Like FSharp.Core's Random.next: rejects results outside [0.0, 1.0), NaN included +let private randomNext (randomizer: unit -> float) (minValue: int) (maxValue: int) : int = + let r = randomizer () + + if not (r >= 0.0 && r < 1.0) then + let msg = + "The index is outside the legal range.\nrandomizer returned " + + string r + + ", should be in range [0.0, 1.0)." + + invalidArg "randomizer" msg + + int (r * float (maxValue - minValue)) + minValue + +let private checkRandomCount (count: int) = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + +let private shuffleInPlace (next: int -> int -> int) (xs: 'T[]) : unit = let len = xs.Length - for i = len - 1 downto 1 do - let r = randomizer () + for i = 0 to len - 2 do + let j = next i len - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + if j <> i then + let tmp = xs[i] + xs[i] <- xs[j] + xs[j] <- tmp - let j = int (r * float (i + 1)) - let tmp = xs[i] - xs[i] <- xs[j] - xs[j] <- tmp +let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = + shuffleInPlace (randomNext randomizer) xs let randomShuffleInPlaceWith (random: System.Random) (xs: 'T[]) : unit = - randomShuffleInPlaceBy (fun () -> random.NextDouble()) xs + shuffleInPlace (fun i n -> random.Next(i, n)) xs let randomShuffleInPlace (xs: 'T[]) : unit = randomShuffleInPlaceWith (System.Random()) xs @@ -1011,21 +1029,18 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T[]) : 'T[] = arr let randomShuffleWith (random: System.Random) (xs: 'T[]) : 'T[] = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = copy xs + randomShuffleInPlaceWith random arr + arr let randomShuffle (xs: 'T[]) : 'T[] = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = if isEmpty xs then - invalidArg "source" SR.inputSequenceEmpty + invalidArg "source" "The input array was empty." let len = xs.Length - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException - - xs[int (r * float len)] + xs[randomNext randomizer 0 len] let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = randomChoiceBy (fun () -> random.NextDouble()) xs @@ -1033,24 +1048,13 @@ let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = let randomChoice (xs: 'T[]) : 'T = randomChoiceWith (System.Random()) xs let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative + checkRandomCount count if count > 0 && isEmpty xs then invalidArg "source" SR.inputSequenceEmpty let len = xs.Length - - initialize - count - (fun _ -> - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException - - xs[int (r * float len)] - ) + initialize count (fun _ -> xs[randomNext randomizer 0 len]) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = randomChoicesBy (fun () -> random.NextDouble()) count xs @@ -1058,12 +1062,22 @@ let randomChoicesWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = let randomChoices (count: int) (xs: 'T[]) : 'T[] = randomChoicesWith (System.Random()) count xs -let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative +// FSharp.Core's 21 + 4 ** ceil (log4 (3 * count)), kept to integers +let private getMaxSetSizeForSampling (count: int) = + if count > 5 then + let mutable size = 1 - let arr = copy xs - let len = arr.Length + while size < count * 3 do + size <- size * 4 + + 21 + size + else + 21 + +// Picks from a shrinking pool for small inputs, otherwise draws indices until unseen (FSharp.Core, after CPython) +let private randomSampleWithNext (next: int -> int -> int) (count: int) (xs: 'T[]) : 'T[] = + checkRandomCount count + let len = xs.Length if len = 0 && count > 0 then invalidArg "source" SR.inputSequenceEmpty @@ -1071,21 +1085,33 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = if count > len then invalidArg "count" SR.notEnoughElements - for i = 0 to count - 1 do - let r = randomizer () + let result = getSubArray xs 0 count - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + if len <= getMaxSetSizeForSampling count then + let pool = copy xs + + for i = 0 to count - 1 do + let j = next 0 (len - i) + result[i] <- pool[j] + pool[j] <- pool[len - i - 1] + else + let selected = System.Collections.Generic.HashSet() - let j = i + int (r * float (len - i)) - let tmp = arr[i] - arr[i] <- arr[j] - arr[j] <- tmp + for i = 0 to count - 1 do + let mutable j = next 0 len - getSubArray arr 0 count + while not (selected.Add j) do + j <- next 0 len + + result[i] <- xs[j] + + result + +let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = + randomSampleWithNext (randomNext randomizer) count xs let randomSampleWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = - randomSampleBy (fun () -> random.NextDouble()) count xs + randomSampleWithNext (fun a b -> random.Next(a, b)) count xs let randomSample (count: int) (xs: 'T[]) : 'T[] = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-rust/src/Async.rs b/src/fable-library-rust/src/Async.rs index bd539baeaa..7cce6256ab 100644 --- a/src/fable-library-rust/src/Async.rs +++ b/src/fable-library-rust/src/Async.rs @@ -12,12 +12,25 @@ pub mod Async_ { use futures_timer::Delay; use super::Task_::Task; + use crate::Native_::{Func0, Seq}; + use crate::NativeArray_::{Array, array_from}; use crate::System::Threading::CancellationToken; pub struct Async { pub future: Arc + Send + Sync>>>>, } + impl Async { + /// Wraps a future as a single-shot computation. + pub fn from_future(fut: impl Future + Send + Sync + 'static) -> Arc> { + let b: Pin + Send + Sync + 'static>> = Box::pin(fut); + + Arc::from(Async { + future: Arc::from(Mutex::from(b)), + }) + } + } + impl Future for &Async { type Output = T; @@ -25,27 +38,20 @@ pub mod Async_ { self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { - let p = self - .future - .try_lock() - .map(|mut f| f.poll_unpin(cx)) - .unwrap_or_else(|| { - //Again blocking wait, not good - thread::sleep(Duration::from_millis(10)); - cx.waker().wake_by_ref(); - - std::task::Poll::Pending - }); - p + // Poll the lock's own future so contention parks on its waker. + // try_lock plus a 10ms sleep spent that long doing nothing whenever + // two pollers met. + let mut lock = self.future.lock(); + + match lock.poll_unpin(cx) { + std::task::Poll::Ready(mut guard) => guard.as_mut().poll(cx), + std::task::Poll::Pending => std::task::Poll::Pending, + } } } pub fn sleep(milliseconds: i32) -> Arc> { - let fut = Delay::new(Duration::from_millis(milliseconds as u64)); - let a: Pin + Send + Sync + 'static>> = Box::pin(fut); - Arc::from(Async { - future: Arc::from(Mutex::from(a)), - }) + Async::from_future(Delay::new(Duration::from_millis(milliseconds as u64))) } pub fn startAsTask( @@ -73,33 +79,249 @@ pub mod Async_ { let res = res.as_mut().await; res }; - executor::block_on(unitFut) + if executor::enter().is_err() { + thread::spawn(|| executor::block_on(unitFut)) + .join() + .expect("nested async computation panicked") + } else { + executor::block_on(unitFut) + } + } + + pub fn startImmediate( + a: Arc>, + cancellationToken: Option, + ) { + let _ = runSynchronously(a, None, cancellationToken); + } + + /// Async.Parallel. Runs every computation on one executor and collects the + /// results in order. Previously missing entirely, so any use of it failed to + /// resolve. + pub fn parallel( + computations: Seq>>, + ) -> Arc>> { + // F# hands this a seq, so materialise it before anything is awaited. + let items: Vec>> = + crate::Seq_::toArray(computations).iter().cloned().collect(); + + let fut = async move { + let futures = items.into_iter().map(|a| async move { + let mut guard = a.future.lock().await; + guard.as_mut().await + }); + + let results: Vec = futures::future::join_all(futures).await; + array_from(results) + }; + + Async::from_future(fut) + } + + /// Async.Sequential. Runs each computation to completion before starting + /// the next one and collects the results in order. + pub fn sequential( + computations: Seq>>, + ) -> Arc>> { + let items: Vec>> = + crate::Seq_::toArray(computations).iter().cloned().collect(); + + let fut = async move { + let mut results = Vec::with_capacity(items.len()); + + for a in items { + let mut guard = a.future.lock().await; + results.push(guard.as_mut().await); + } + + array_from(results) + }; + + Async::from_future(fut) + } + + /// Async.StartImmediateAsTask. Runs on the calling thread rather than + /// queueing onto the thread pool, so it does not pay for a hand-off. The + /// returned task is therefore already complete: there is no scheduler here + /// to leave it pending on, so this trades .NET's concurrency for the speed + /// the name promises. Use startAsTask when the work should overlap. + pub fn startImmediateAsTask( + a: Arc>, + cancellationToken: Option, + ) -> Arc> { + let result = runSynchronously(a, None, cancellationToken); + Arc::from(Task::from_result(result)) } pub fn awaitTask(a: Arc>) -> Arc> { - let fut = async move { (&*a).await }; - let a: Pin + Send + Sync + 'static>> = Box::pin(fut); - Arc::from(Async { - future: Arc::from(Mutex::from(a)), - }) + Async::from_future(async move { (&*a).await }) } } #[cfg(feature = "threaded")] pub mod AsyncBuilder_ { use std::future::{Future, ready}; + use std::panic::AssertUnwindSafe; use std::pin::Pin; use std::sync::Arc; + use futures::FutureExt; use futures::lock::Mutex; use super::Async_::Async; - use crate::Native_::{Func0, Func1}; + use crate::Exception_::get_exception; + use crate::Native_::{EraseUnitArg, Func0, Func1, LrcPtr, Seq, eraseUnitArg}; + use crate::System::{Exception, IDisposable}; + + /// Awaits one computation to completion. + /// + /// Every method here goes through this rather than reaching into `future` + /// itself, so the locking is stated once. + async fn run(computation: Arc>) -> T { + let mut fut = computation.future.lock().await; + fut.as_mut().await + } + + /// The `async` builder value used to resolve `DefaultAsyncBuilder`. + pub struct AsyncBuilder {} + + pub static singleton: AsyncBuilder = AsyncBuilder {}; + + /// Two computations in sequence, discarding the first result. This is + /// what `if cond then do! x` followed by anything else compiles to. + pub fn combine( + computation1: Arc>, + computation2: Arc>, + ) -> Arc> { + Async::from_future(async move { + run(computation1).await; + run(computation2).await + }) + } + + /// `for` inside an async block. + /// + /// The sequence is materialised before anything is awaited, the same + /// way `Async.Parallel` does it: a Fable `Seq`'s iterator is not `Send` + /// and so cannot be held across an await. A lazy, side-effecting + /// sequence therefore runs to completion up front instead of + /// interleaving with the body. + pub fn for_loop( + sequence: Seq, + body: Func1>>, + ) -> Arc> { + let items: Vec = crate::Seq_::toArray(sequence).iter().cloned().collect(); + + Async::from_future(async move { + for item in items { + run(body(item)).await; + } + }) + } + + /// `try/with` inside an async block. + /// + /// Exceptions are panics on this target, and the body spans await + /// points, so the catch has to wrap the whole computation rather than a + /// synchronous call the way `Exception_::try_catch` does. That also + /// means the panic hook is left alone: it is process-global, and + /// swapping it for the duration of an await would silence unrelated + /// panics on other threads. A caught exception therefore still prints + /// the default panic message. + pub fn try_with( + computation: Arc>, + catchHandler: Func1, Arc>>, + ) -> Arc> { + Async::from_future(async move { + // The panic payload is only Send, never Sync, so it must not + // still be alive at the await below -- that would make this + // future non-Sync and `Async` requires Sync. Turning it into + // the handler's computation here ends its scope first. + let outcome = match AssertUnwindSafe(run(computation)).catch_unwind().await { + Ok(value) => Ok(value), + Err(payload) => Err(catchHandler(get_exception(&*payload))), + }; + + match outcome { + Ok(value) => value, + Err(handled) => run(handled).await, + } + }) + } + + /// `try/finally` inside an async block. The compensation runs on both + /// paths, and a failure keeps unwinding with its original payload. + pub fn try_finally( + computation: Arc>, + compensation: Func0<()>, + ) -> Arc> { + Async::from_future(async move { + let outcome = AssertUnwindSafe(run(computation)).catch_unwind().await; + compensation(); + + match outcome { + Ok(value) => value, + Err(payload) => std::panic::resume_unwind(payload), + } + }) + } + + /// `use` inside an async block. Dispose the resource on both success and + /// failure, preserving the original panic after cleanup. + pub fn using( + resource: D, + binder: Func1>>, + ) -> Arc> { + Async::from_future(async move { + let outcome = AssertUnwindSafe(async { run(binder(resource.clone())).await }) + .catch_unwind() + .await; + resource.Dispose(); + + match outcome { + Ok(result) => result, + Err(payload) => std::panic::resume_unwind(payload), + } + }) + } + + /// `Delay` is what makes an async block a description rather than a running + /// computation, so the builder is called when the result is awaited and not + /// before. + /// + /// The builder is taken as `EraseUnitArg` rather than plainly as `Func0` + /// because the unit parameter of a `unit -> Async` lambda is not + /// always erased: `discardUnitArg` keeps it when the lambda's own generic + /// argument is unit, which is what a `try/finally` nested inside a + /// `try/with` produces. Accepting both shapes and normalising here is + /// contained to this file; changing when the compiler erases a unit + /// parameter would reach every lambda on the target. + /// + /// It used to be called immediately, which meant any part of the block + /// evaluated eagerly -- `return i` compiles to `r_return(i)`, and `r_return` + /// takes its argument by value -- captured state from before the block ran. + /// Nothing noticed while every async block was a straight line, because the + /// value was read at construction and the block did nothing in between. + /// `combine` makes it visible: in `while .. done; return i` the `return i` + /// half was read before the loop had touched `i`. + pub fn delay>>>( + binder: F, + ) -> Arc> { + let build = eraseUnitArg(binder); - pub fn delay(binder: Func0>>) -> Arc> { - let pr = binder(); Arc::from(Async { - future: pr.future.clone(), + future: Arc::from(Mutex::from(Box::pin(async move { run(build()).await }) + as Pin + Send + Sync + 'static>>)), + }) + } + + /// `while` inside an async block. The body is supplied as a factory so a + /// fresh one-shot future is built for every iteration. + pub fn while_loop(guard: Func0, body: Func0>>) -> Arc> { + Async::from_future(async move { + while guard() { + run(body()).await; + } }) } @@ -115,18 +337,11 @@ pub mod AsyncBuilder_ { next.as_mut().await }; - let b: Pin + Send + Sync + 'static>> = Box::pin(next); - Arc::from(Async { - future: Arc::from(Mutex::from(b)), - }) + Async::from_future(next) } pub fn r_return(item: T) -> Arc> { - let r = ready(item); - let b: Pin + Send + Sync + 'static>> = Box::pin(r); - Arc::from(Async { - future: Arc::from(Mutex::from(b)), - }) + Async::from_future(ready(item)) } pub fn return_from(computation: Arc>) -> Arc> { @@ -201,21 +416,24 @@ pub mod Monitor_ { #[cfg(feature = "threaded")] pub mod Task_ { + use std::panic::AssertUnwindSafe; use std::pin::Pin; - use std::sync::{Arc, RwLock}; - use std::task::Poll; + use std::sync::{Arc, Mutex, RwLock}; + use std::task::{Poll, Waker}; use std::thread::{self, JoinHandle}; - use std::time::Duration; use futures::{Future, FutureExt}; use super::ThreadPool::try_init_and_get_pool; - use crate::Native_::{Func0, Func1}; + use crate::Exception_::get_exception; + use crate::Native_::LrcPtr; + use crate::System::Exception; pub enum TaskState { New(Pin + Send + Sync>>), Running, Complete(T), + Faulted(LrcPtr), } impl TaskState { @@ -255,6 +473,10 @@ pub mod Task_ { #[derive(Clone)] pub struct Task { result: Arc>>, + // Whoever is awaiting this task. Polling used to sleep the thread for + // 100ms and try again, which made awaiting a task cost far more than + // the work it was waiting for. + wakers: Arc>>, } impl Future for &Task { @@ -264,51 +486,78 @@ pub mod Task_ { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { - //eprintln!("{:?} Polling task for result", thread::current().id()); - let m = self.result.read().unwrap(); + match &*self.result.read().unwrap() { + TaskState::Complete(res) => return Poll::Ready(res.clone()), + TaskState::Faulted(error) => std::panic::panic_any(error.clone()), + _ => {} + } + + // Register before re-reading the state: set_result may complete the + // task between the check above and this push, and the re-read below + // is what stops that racing completion from being missed. + self.wakers.lock().unwrap().push(cx.waker().clone()); - match &*m { + match &*self.result.read().unwrap() { + TaskState::Complete(res) => Poll::Ready(res.clone()), + // Not started yet. start() flips New to Running synchronously + // before spawning, so this window is short. TaskState::New(_) => { - //schedule? - //eprintln!("{:?} poll: task is new, waking up", thread::current().id()); cx.waker().wake_by_ref(); Poll::Pending } - TaskState::Running => { - //eprintln!("{:?} poll: pending, nothing to do", thread::current().id()); - - //todo - this is no good as it blocks the thread. It needs to be non-blocking and delegate out - thread::sleep(Duration::from_millis(100)); - cx.waker().wake_by_ref(); - - Poll::Pending - } - TaskState::Complete(res) => { - //eprintln!("{:?} Poll succeeded", thread::current().id()); - Poll::Ready(res.clone()) - } + // Running: set_result wakes us. + TaskState::Running => Poll::Pending, + TaskState::Faulted(error) => std::panic::panic_any(error.clone()), } } } + pub fn from_result(value: T) -> Arc> { + let task = Task::from_result(value); + Arc::from(task) + } + impl Task { pub fn new(fut: impl Future + Send + Sync + 'static) -> Task { Task { result: Arc::from(RwLock::from(TaskState::New(Box::pin(fut)))), + wakers: Arc::from(Mutex::from(Vec::new())), } } pub fn from_result(value: T) -> Task { Task { result: Arc::from(RwLock::from(TaskState::Complete(value))), + wakers: Arc::from(Mutex::from(Vec::new())), } } pub fn set_result(&self, value: T) { - let mut m = self.result.write().unwrap(); - //eprintln!("{:?} set task result", thread::current().id()); - (*m) = TaskState::Complete(value); - //eprintln!("{:?} set task result completed", thread::current().id()); + { + let mut m = self.result.write().unwrap(); + (*m) = TaskState::Complete(value); + } + + self.wake_waiters(); + } + + pub fn set_fault(&self, error: LrcPtr) { + { + let mut m = self.result.write().unwrap(); + (*m) = TaskState::Faulted(error); + } + + self.wake_waiters(); + } + + fn wake_waiters(&self) { + // Drain first, then wake outside the lock: a waker may poll straight + // back into this task. + let wakers: Vec = self.wakers.lock().unwrap().drain(..).collect(); + + for w in wakers { + w.wake(); + } } pub fn is_new(&self) -> bool { @@ -320,13 +569,14 @@ pub mod Task_ { } pub fn get_result(&self) -> T { - while !self.is_complete() { - //eprintln!("{:?} try get result", thread::current().id()); - thread::sleep(Duration::from_millis(10)); + match &*self.result.read().unwrap() { + TaskState::Complete(res) => return res.clone(), + TaskState::Faulted(error) => std::panic::panic_any(error.clone()), + _ => {} } - //eprintln!("{:?} has result", thread::current().id()); - let t = self.result.read().unwrap().unwrap(); - t + + // Parks on the waker rather than waking every 10ms to look again. + futures::executor::block_on(self) } pub fn start(t: Arc>) { @@ -337,8 +587,10 @@ pub mod Task_ { match ts { TaskState::New(mut fut) => { let f2 = async move { - let res = fut.as_mut().await; - t.set_result(res); + match AssertUnwindSafe(fut.as_mut()).catch_unwind().await { + Ok(res) => t.set_result(res), + Err(payload) => t.set_fault(get_exception(&*payload)), + } }; let pool = super::ThreadPool::try_init_and_get_pool(); //eprintln!("{:?} new task added to queue", thread::current().id()); @@ -347,6 +599,56 @@ pub mod Task_ { _ => {} } } + + pub async fn run(t: Arc>) -> T { + let state = t.result.write().unwrap().replace(TaskState::Running); + match state { + TaskState::New(mut fut) => { + match AssertUnwindSafe(fut.as_mut()).catch_unwind().await { + Ok(result) => { + t.set_result(result.clone()); + result + } + Err(payload) => { + t.set_fault(get_exception(&*payload)); + std::panic::resume_unwind(payload) + } + } + } + TaskState::Running => (&*t).await, + TaskState::Complete(value) => { + t.set_result(value.clone()); + value + } + TaskState::Faulted(error) => std::panic::panic_any(error), + } + } + } +} + +#[cfg(feature = "threaded")] +pub mod TaskBuilder_ { + use std::panic::AssertUnwindSafe; + use super::super::Native_::LrcPtr; + use super::Task_::Task; + use std::sync::Arc; + use crate::Native_::{Func0, Func1, Seq}; + use crate::Exception_::get_exception; + use crate::System::{Exception, IDisposable}; + use futures::FutureExt; + + pub struct TaskBuilder {} + + impl TaskBuilder { + pub fn run(&self, task: Arc>) -> Arc> { + Task::start(task.clone()); + task + } + + } + + pub fn new() -> LrcPtr { + LrcPtr::new(TaskBuilder {}) } pub fn bind( @@ -355,13 +657,10 @@ pub mod Task_ { ) -> Arc> { let next = async move { //eprintln!("{:?} begin await source fut", thread::current().id()); - let m = opt.as_ref().await; + let m = run(opt).await; //eprintln!("{:?} awaiting source future success", thread::current().id()); let nextAsync = binder(m); - if nextAsync.is_new() { - Task::start(nextAsync.clone()); - } - let next = nextAsync.as_ref().await; + let next = run(nextAsync).await; //eprintln!("{:?} setting result", thread::current().id()); next }; @@ -370,9 +669,84 @@ pub mod Task_ { Arc::from(task) } + async fn run(task: Arc>) -> T { + if task.is_new() { + Task::run(task).await + } else { + (&*task).await + } + } + + pub fn combine( + computation1: Arc>, + computation2: Arc>, + ) -> Arc> { + let task = Task::new(async move { + run(computation1).await; + run(computation2).await + }); + Arc::from(task) + } + + pub fn return_from(computation: Arc>) -> Arc> { + computation + } + + pub fn try_with( + computation: Arc>, + catchHandler: Func1, Arc>>, + ) -> Arc> { + let task = Task::new(async move { + let outcome = match AssertUnwindSafe(run(computation)).catch_unwind().await { + Ok(value) => Ok(value), + Err(payload) => Err(get_exception(&*payload)), + }; + + match outcome { + Ok(value) => value, + Err(exception) => run(catchHandler(exception)).await, + } + }); + Arc::from(task) + } + + pub fn try_finally( + computation: Arc>, + compensation: Func0<()>, + ) -> Arc> { + let task = Task::new(async move { + let outcome = AssertUnwindSafe(run(computation)).catch_unwind().await; + compensation(); + + match outcome { + Ok(value) => value, + Err(payload) => std::panic::resume_unwind(payload), + } + }); + Arc::from(task) + } + + pub fn using( + resource: D, + binder: Func1>>, + ) -> Arc> { + let task = Task::new(async move { + let outcome = AssertUnwindSafe(async { run(binder(resource.clone())).await }) + .catch_unwind() + .await; + resource.Dispose(); + + match outcome { + Ok(result) => result, + Err(payload) => std::panic::resume_unwind(payload), + } + }); + Arc::from(task) + } + pub fn delay(binder: Func0>>) -> Arc> { - let pr = binder(); - pr + let task = Task::new(async move { run(binder()).await }); + Arc::from(task) } pub fn r_return(item: T) -> Arc> { @@ -380,35 +754,35 @@ pub mod Task_ { Arc::from(t) } - pub fn zero() -> Arc> { + pub fn zero() -> Arc> { r_return(()) } - pub fn from_result(t: T) -> Arc> { - let t = Task { - result: Arc::from(RwLock::from(TaskState::Complete(t))), - }; - Arc::from(t) - } -} - -#[cfg(feature = "threaded")] -pub mod TaskBuilder_ { - use super::super::Native_::LrcPtr; - use super::Task_::Task; - use std::sync::Arc; - - pub struct TaskBuilder {} + pub fn for_loop( + sequence: Seq, + body: Func1>>, + ) -> Arc> { + let arr = crate::Seq_::toArray(sequence); + let task = Task::new(async move { + let items = arr.iter().cloned(); + for item in items { + let next = body(item); + run(next).await; + } + }); - impl TaskBuilder { - pub fn run(&self, task: Arc>) -> Arc> { - Task::start(task.clone()); - task - } + Arc::from(task) } - pub fn new() -> LrcPtr { - LrcPtr::new(TaskBuilder {}) + pub fn while_loop(guard: Func0, body: Func0>>) -> Arc> { + let task = Task::new(async move { + while guard() { + let next = body(); + run(next).await; + } + }); + + Arc::from(task) } } diff --git a/src/fable-library-rust/src/BigInt.rs b/src/fable-library-rust/src/BigInt.rs index 871f4828b5..8f3ed81113 100644 --- a/src/fable-library-rust/src/BigInt.rs +++ b/src/fable-library-rust/src/BigInt.rs @@ -2,7 +2,7 @@ pub mod BigInt_ { use crate::Decimal_::{decimal, truncate}; use crate::NativeArray_::{array_from, Array}; - use crate::Native_::{compare, getHashCode, DivideByInt, Hashable, Lrc, MutCell, ToString, Vec}; + use crate::Native_::{compare, hash_code, DivideByInt, Hashable, Lrc, MutCell, ToString, Vec}; use crate::String_::{string, toString as toString_1}; use num_bigint::*; @@ -43,7 +43,7 @@ pub mod BigInt_ { impl Hashable for bigint { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } @@ -103,7 +103,8 @@ pub mod BigInt_ { pub fn one() -> bigint { BigInt::one().into() } pub fn minusOne() -> bigint { (-BigInt::one()).into() } - // pub fn getHashCode(x: bigint) = x.GetHashCode() + // pub fn getHashCode(x: bigint) = hash_code(&x) + pub fn equals(x: bigint, y: bigint) -> bool { x.eq(&y) } pub fn compareTo(x: bigint, y: bigint) -> i32 { compare(&x, &y) } diff --git a/src/fable-library-rust/src/Char.rs b/src/fable-library-rust/src/Char.rs index de0e15da59..2018027443 100644 --- a/src/fable-library-rust/src/Char.rs +++ b/src/fable-library-rust/src/Char.rs @@ -1,94 +1,139 @@ pub mod Char_ { - use crate::Native_::{MutCell, ToString, compare}; - use crate::NativeArray_::{Array, array_from}; + use crate::NativeArray_::{array_from, Array}; + use crate::Native_::{compare, MutCell, ToString}; use crate::String_::{getCharAt, length, string, toString}; - - // https://docs.microsoft.com/en-us/dotnet/api/system.globalization.unicodecategory?view=net-6.0 - pub mod UnicodeCategory { - pub const UppercaseLetter: u8 = 0; - pub const LowercaseLetter: u8 = 1; - pub const TitlecaseLetter: u8 = 2; - pub const ModifierLetter: u8 = 3; - pub const OtherLetter: u8 = 4; - pub const NonSpacingMark: u8 = 5; - pub const SpacingCombiningMark: u8 = 6; - pub const EnclosingMark: u8 = 7; - pub const DecimalDigitNumber: u8 = 8; - pub const LetterNumber: u8 = 9; - pub const OtherNumber: u8 = 10; - pub const SpaceSeparator: u8 = 11; - pub const LineSeparator: u8 = 12; - pub const ParagraphSeparator: u8 = 13; - pub const Control: u8 = 14; - pub const Format: u8 = 15; - pub const Surrogate: u8 = 16; - pub const PrivateUse: u8 = 17; - pub const ConnectorPunctuation: u8 = 18; - pub const DashPunctuation: u8 = 19; - pub const OpenPunctuation: u8 = 20; - pub const ClosePunctuation: u8 = 21; - pub const InitialQuotePunctuation: u8 = 22; - pub const FinalQuotePunctuation: u8 = 23; - pub const OtherPunctuation: u8 = 24; - pub const MathSymbol: u8 = 25; - pub const CurrencySymbol: u8 = 26; - pub const ModifierSymbol: u8 = 27; - pub const OtherSymbol: u8 = 28; - pub const OtherNotAssigned: u8 = 29; + #[cfg(feature = "unicode")] + use unicode_general_category::{get_general_category, GeneralCategory}; + + // https://docs.microsoft.com/en-us/dotnet/api/system.globalization.unicodecategory + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + #[repr(u8)] + pub enum UnicodeCategory { + UppercaseLetter = 0, + LowercaseLetter = 1, + TitlecaseLetter = 2, + ModifierLetter = 3, + OtherLetter = 4, + NonSpacingMark = 5, + SpacingCombiningMark = 6, + EnclosingMark = 7, + DecimalDigitNumber = 8, + LetterNumber = 9, + OtherNumber = 10, + SpaceSeparator = 11, + LineSeparator = 12, + ParagraphSeparator = 13, + Control = 14, + Format = 15, + Surrogate = 16, + PrivateUse = 17, + ConnectorPunctuation = 18, + DashPunctuation = 19, + OpenPunctuation = 20, + ClosePunctuation = 21, + InitialQuotePunctuation = 22, + FinalQuotePunctuation = 23, + OtherPunctuation = 24, + MathSymbol = 25, + CurrencySymbol = 26, + ModifierSymbol = 27, + OtherSymbol = 28, + OtherNotAssigned = 29, + } + + impl core::fmt::Display for UnicodeCategory { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:?}", self) + } } + // map GeneralCategory to UnicodeCategory + const UnicodeCategoryMap: [UnicodeCategory; 30] = [ + UnicodeCategory::ClosePunctuation, + UnicodeCategory::ConnectorPunctuation, + UnicodeCategory::Control, + UnicodeCategory::CurrencySymbol, + UnicodeCategory::DashPunctuation, + UnicodeCategory::DecimalDigitNumber, + UnicodeCategory::EnclosingMark, + UnicodeCategory::FinalQuotePunctuation, + UnicodeCategory::Format, + UnicodeCategory::InitialQuotePunctuation, + UnicodeCategory::LetterNumber, + UnicodeCategory::LineSeparator, + UnicodeCategory::LowercaseLetter, + UnicodeCategory::MathSymbol, + UnicodeCategory::ModifierLetter, + UnicodeCategory::ModifierSymbol, + UnicodeCategory::NonSpacingMark, + UnicodeCategory::OpenPunctuation, + UnicodeCategory::OtherLetter, + UnicodeCategory::OtherNumber, + UnicodeCategory::OtherPunctuation, + UnicodeCategory::OtherSymbol, + UnicodeCategory::ParagraphSeparator, + UnicodeCategory::PrivateUse, + UnicodeCategory::SpaceSeparator, + UnicodeCategory::SpacingCombiningMark, + UnicodeCategory::Surrogate, + UnicodeCategory::TitlecaseLetter, + UnicodeCategory::OtherNotAssigned, + UnicodeCategory::UppercaseLetter, + ]; + // The maximum character value. pub const MaxValue: char = '\u{FFFF}'; // The minimum character value. pub const MinValue: char = '\u{0000}'; - // const IsWhiteSpaceFlag: u8 = 0x80; - // const IsUpperCaseLetterFlag: u8 = 0x40; - // const IsLowerCaseLetterFlag: u8 = 0x20; + const IsWhiteSpaceFlag: u8 = 0x80; + const IsUpperCaseLetterFlag: u8 = 0x40; + const IsLowerCaseLetterFlag: u8 = 0x20; const UnicodeCategoryMask: u8 = 0x1F; - // const isControlMask: u32 = 0 | 1 << UnicodeCategory::Control; - // const isDigitMask: u32 = 0 | 1 << UnicodeCategory::DecimalDigitNumber; - // const isLetterMask: u32 = 0 - // | 1 << UnicodeCategory::UppercaseLetter - // | 1 << UnicodeCategory::LowercaseLetter - // | 1 << UnicodeCategory::TitlecaseLetter - // | 1 << UnicodeCategory::ModifierLetter - // | 1 << UnicodeCategory::OtherLetter; - // const isLetterOrDigitMask: u32 = 0 | isLetterMask | isDigitMask; - // const isUpperMask: u32 = 0 | 1 << UnicodeCategory::UppercaseLetter; - // const isLowerMask: u32 = 0 | 1 << UnicodeCategory::LowercaseLetter; - // const isNumberMask: u32 = 0 - // | 1 << UnicodeCategory::DecimalDigitNumber - // | 1 << UnicodeCategory::LetterNumber - // | 1 << UnicodeCategory::OtherNumber; - // const isPunctuationMask: u32 = 0 - // | 1 << UnicodeCategory::ConnectorPunctuation - // | 1 << UnicodeCategory::DashPunctuation - // | 1 << UnicodeCategory::OpenPunctuation - // | 1 << UnicodeCategory::ClosePunctuation - // | 1 << UnicodeCategory::InitialQuotePunctuation - // | 1 << UnicodeCategory::FinalQuotePunctuation - // | 1 << UnicodeCategory::OtherPunctuation; - // const isSeparatorMask: u32 = 0 - // | 1 << UnicodeCategory::SpaceSeparator - // | 1 << UnicodeCategory::LineSeparator - // | 1 << UnicodeCategory::ParagraphSeparator; - // const isSymbolMask: u32 = 0 - // | 1 << UnicodeCategory::MathSymbol - // | 1 << UnicodeCategory::CurrencySymbol - // | 1 << UnicodeCategory::ModifierSymbol - // | 1 << UnicodeCategory::OtherSymbol; - // const isWhiteSpaceMask: u32 = 0 - // | 1 << UnicodeCategory::SpaceSeparator - // | 1 << UnicodeCategory::LineSeparator - // | 1 << UnicodeCategory::ParagraphSeparator; + const isControlMask: u32 = 0 | 1 << UnicodeCategory::Control as u8; + const isDigitMask: u32 = 0 | 1 << UnicodeCategory::DecimalDigitNumber as u8; + const isLetterMask: u32 = 0 + | 1 << UnicodeCategory::UppercaseLetter as u8 + | 1 << UnicodeCategory::LowercaseLetter as u8 + | 1 << UnicodeCategory::TitlecaseLetter as u8 + | 1 << UnicodeCategory::ModifierLetter as u8 + | 1 << UnicodeCategory::OtherLetter as u8; + const isLetterOrDigitMask: u32 = 0 | isLetterMask | isDigitMask; + const isUpperMask: u32 = 0 | 1 << UnicodeCategory::UppercaseLetter as u8; + const isLowerMask: u32 = 0 | 1 << UnicodeCategory::LowercaseLetter as u8; + const isNumberMask: u32 = 0 + | 1 << UnicodeCategory::DecimalDigitNumber as u8 + | 1 << UnicodeCategory::LetterNumber as u8 + | 1 << UnicodeCategory::OtherNumber as u8; + const isPunctuationMask: u32 = 0 + | 1 << UnicodeCategory::ConnectorPunctuation as u8 + | 1 << UnicodeCategory::DashPunctuation as u8 + | 1 << UnicodeCategory::OpenPunctuation as u8 + | 1 << UnicodeCategory::ClosePunctuation as u8 + | 1 << UnicodeCategory::InitialQuotePunctuation as u8 + | 1 << UnicodeCategory::FinalQuotePunctuation as u8 + | 1 << UnicodeCategory::OtherPunctuation as u8; + const isSeparatorMask: u32 = 0 + | 1 << UnicodeCategory::SpaceSeparator as u8 + | 1 << UnicodeCategory::LineSeparator as u8 + | 1 << UnicodeCategory::ParagraphSeparator as u8; + const isSymbolMask: u32 = 0 + | 1 << UnicodeCategory::MathSymbol as u8 + | 1 << UnicodeCategory::CurrencySymbol as u8 + | 1 << UnicodeCategory::ModifierSymbol as u8 + | 1 << UnicodeCategory::OtherSymbol as u8; + const isWhiteSpaceMask: u32 = 0 + | 1 << UnicodeCategory::SpaceSeparator as u8 + | 1 << UnicodeCategory::LineSeparator as u8 + | 1 << UnicodeCategory::ParagraphSeparator as u8; // Contains information about the C0, Basic Latin, C1, and Latin-1 Supplement ranges [ U+0000..U+00FF ], with: // - 0x80 bit if set means 'is whitespace' // - 0x40 bit if set means 'is uppercase letter' // - 0x20 bit if set means 'is lowercase letter' // - bottom 5 bits are the of: UnicodeCategory the character + #[cfg(not(feature = "unicode"))] #[cfg_attr(rustfmt, rustfmt::skip)] const Latin1CharInfo: &[u8; 256] = &[ // 0 1 2 3 4 5 6 7 8 9 A B C D E F @@ -116,27 +161,48 @@ pub mod Char_ { c as u32 <= 0xFF } - // fn isUnicodeCategory(c: char, uc_mask: u32) -> bool { - // let uc = GetUnicodeCategory(c); - // ((1 << uc) & uc_mask) != 0 - // } + #[inline] + fn isUnicodeCategory(c: char, uc_mask: u32) -> bool { + ((1u32 << (GetUnicodeCategory(c) as u32)) & uc_mask) != 0 + } + + #[cfg(feature = "unicode")] + pub fn GetUnicodeCategory(c: char) -> i32 { + let category = get_general_category(c); + UnicodeCategoryMap[category as usize] as i32 + } + #[cfg(not(feature = "unicode"))] pub fn GetUnicodeCategory(c: char) -> i32 { - let category = if (isLatin1(c)) { - (Latin1CharInfo[c as usize] & UnicodeCategoryMask) + if (isLatin1(c)) { + // complete, but only for Latin1 char set + let cat = (Latin1CharInfo[c as usize] & UnicodeCategoryMask); + // let uc: UnicodeCategory = unsafe { std::mem::transmute(cat) }; + cat as i32 } else { - match c { - // very incomplete, TODO: get real unicode categories - c if IsUpper(c) => UnicodeCategory::UppercaseLetter, - c if IsLower(c) => UnicodeCategory::LowercaseLetter, - c if IsLetter(c) => UnicodeCategory::OtherLetter, - c if IsDigit(c) => UnicodeCategory::DecimalDigitNumber, - c if IsControl(c) => UnicodeCategory::Control, + (match c { + // very incomplete, enable the unicode feature to get all unicode categories + c if c.is_uppercase() => UnicodeCategory::UppercaseLetter, + c if c.is_lowercase() => UnicodeCategory::LowercaseLetter, + c if c.is_alphabetic() => match c { + c if c.is_numeric() => UnicodeCategory::LetterNumber, + _ => UnicodeCategory::OtherLetter, + }, + c if c.is_ascii_digit() => UnicodeCategory::DecimalDigitNumber, // incomplete + c if c.is_ascii_punctuation() => UnicodeCategory::ConnectorPunctuation, // incomplete + c if c.is_numeric() => UnicodeCategory::OtherNumber, + c if c.is_control() => UnicodeCategory::Control, + c if c.is_whitespace() => match c { + // only whitespace characters that are not control chars + '\u{2028}' => UnicodeCategory::LineSeparator, + '\u{2029}' => UnicodeCategory::ParagraphSeparator, + _ => UnicodeCategory::SpaceSeparator, + }, c if IsSurrogate(c) => UnicodeCategory::Surrogate, + // other categories are incomplete c => UnicodeCategory::OtherNotAssigned, - } - }; - category as i32 + }) as i32 + } } pub fn GetUnicodeCategory_2(s: string, index: i32) -> i32 { @@ -171,12 +237,26 @@ pub mod Char_ { c as i32 } + pub fn ConvertToUtf32(c1: char, c2: char) -> i32 { + let first = c1 as u32; + let second = c2 as u32; + if (0xD800..=0xDBFF).contains(&first) && (0xDC00..=0xDFFF).contains(&second) { + (0x10000 + ((first - 0xD800) << 10) + (second - 0xDC00)) as i32 + } else { + panic!("The values do not form a valid UTF-16 surrogate pair") + } + } + pub fn GetHashCode(c: char) -> i32 { // Calculate a hashcode for a 2 byte Unicode character. // c as i32 | ((c as i32) << 16) c as i32 } + pub fn GetTypeCode(_c: char) -> i32 { + 4 // TypeCode.Char + } + pub fn Equals(c: char, v: char) -> bool { c == v } @@ -257,88 +337,111 @@ pub mod Char_ { c.is_ascii_hexdigit() && c.is_ascii_uppercase() } + // ---------------------------------------------------- + + #[cfg(feature = "unicode")] + pub fn IsControl(c: char) -> bool { + isUnicodeCategory(c, isControlMask) + } + + #[cfg(not(feature = "unicode"))] pub fn IsControl(c: char) -> bool { - // isUnicodeCategory(c, isControlMask) c.is_control() } + #[cfg(feature = "unicode")] pub fn IsDigit(c: char) -> bool { - // if (IsLatin1(c)) { - // matches!(c, '0'..='9') - // } else { - // isUnicodeCategory(c, isDigitMask) - // } - c.is_ascii_digit() + isUnicodeCategory(c, isDigitMask) + } + + #[cfg(not(feature = "unicode"))] + pub fn IsDigit(c: char) -> bool { + c.is_ascii_digit() // TODO: very incomplete } + #[cfg(feature = "unicode")] pub fn IsLetter(c: char) -> bool { - // if (IsAscii(c)) { - // // For the version of the Unicode standard the type: char is locked to, the - // // ASCII range doesn't include letters in categories other than "upper" and "lower". - // (Latin1CharInfo[c as usize] & (IsUpperCaseLetterFlag | IsLowerCaseLetterFlag)) != 0 - // } else { - // isUnicodeCategory(c, isLetterMask) - // } - c.is_alphabetic() + isUnicodeCategory(c, isLetterMask) + } + + #[cfg(not(feature = "unicode"))] + pub fn IsLetter(c: char) -> bool { + c.is_alphabetic() // TODO: not precise, includes Marks + } + + #[cfg(feature = "unicode")] + pub fn IsLetterOrDigit(c: char) -> bool { + isUnicodeCategory(c, isLetterOrDigitMask) } + #[cfg(not(feature = "unicode"))] pub fn IsLetterOrDigit(c: char) -> bool { - // isUnicodeCategory(c, LetterOrDigitMask) - c.is_ascii_digit() || c.is_alphabetic() + IsLetter(c) || IsDigit(c) } + #[cfg(feature = "unicode")] + pub fn IsLower(c: char) -> bool { + isUnicodeCategory(c, isLowerMask) + } + + #[cfg(not(feature = "unicode"))] pub fn IsLower(c: char) -> bool { - // if (IsLatin1(c)) { - // (Latin1CharInfo[c as usize] & IsLowerCaseLetterFlag) != 0 - // } else { - // isUnicodeCategory(c, isLowerMask) - // } c.is_lowercase() } + #[cfg(feature = "unicode")] + pub fn IsUpper(c: char) -> bool { + isUnicodeCategory(c, isUpperMask) + } + + #[cfg(not(feature = "unicode"))] pub fn IsUpper(c: char) -> bool { - // if (IsLatin1(c)) { - // (Latin1CharInfo[c as usize] & IsUpperCaseLetterFlag) != 0 - // } else { - // isUnicodeCategory(c, isUpperMask) - // } c.is_uppercase() } + #[cfg(feature = "unicode")] + pub fn IsNumber(c: char) -> bool { + isUnicodeCategory(c, isNumberMask) + } + + #[cfg(not(feature = "unicode"))] pub fn IsNumber(c: char) -> bool { - // if (IsAscii(c)) { - // matches!(c, '0'..='9') - // } else { - // isUnicodeCategory(c, isNumberMask) - // } c.is_numeric() } - pub fn IsPunctuation(c: char) -> bool { - if (IsAscii(c)) { - c.is_ascii_punctuation() - } else { - c.is_ascii_punctuation() //TODO: imprecise, fix this - // isUnicodeCategory(c, isPunctuationMask) - } + #[cfg(feature = "unicode")] + pub fn IsSeparator(c: char) -> bool { + isUnicodeCategory(c, isSeparatorMask) } + #[cfg(not(feature = "unicode"))] pub fn IsSeparator(c: char) -> bool { - if (isLatin1(c)) { - c == '\u{0020}' || c == '\u{00a0}' - } else { - // isUnicodeCategory(c, isSeparatorMask) - c.is_whitespace() //TODO: imprecise, fix this - } + c.is_whitespace() && !c.is_control() + } + + #[cfg(feature = "unicode")] + pub fn IsPunctuation(c: char) -> bool { + isUnicodeCategory(c, isPunctuationMask) + } + + #[cfg(not(feature = "unicode"))] + pub fn IsPunctuation(c: char) -> bool { + c.is_ascii_punctuation() // TODO: very incomplete + } + + #[cfg(feature = "unicode")] + pub fn IsSymbol(c: char) -> bool { + isUnicodeCategory(c, isSymbolMask) } + #[cfg(not(feature = "unicode"))] pub fn IsSymbol(c: char) -> bool { - // isUnicodeCategory(c, isSymbolMask) - c.is_ascii_punctuation() //TODO: imprecise, fix this + c.is_ascii_punctuation() // TODO: very incomplete } + #[cfg(feature = "unicode")] pub fn IsWhiteSpace(c: char) -> bool { - // if (IsLatin1(c)) { + // if (isLatin1(c)) { // (Latin1CharInfo[c as usize] & IsWhiteSpaceFlag) != 0 // } else { // isUnicodeCategory(c, isWhiteSpaceMask) @@ -346,6 +449,11 @@ pub mod Char_ { c.is_whitespace() } + #[cfg(not(feature = "unicode"))] + pub fn IsWhiteSpace(c: char) -> bool { + c.is_whitespace() + } + // ---------------------------------------------------- pub fn IsControl_2(s: string, index: i32) -> bool { @@ -420,7 +528,7 @@ pub mod Char_ { } pub fn ToUpperInvariant(c: char) -> char { - ToUpper(c) //TODO: use invariant culture + ToUpper(c) // TODO: use invariant culture } pub fn ToLower(c: char) -> char { @@ -438,7 +546,7 @@ pub mod Char_ { } pub fn ToLowerInvariant(c: char) -> char { - ToLower(c) //TODO: use invariant culture + ToLower(c) // TODO: use invariant culture } // ---------------------------------------------------- diff --git a/src/fable-library-rust/src/Convert.rs b/src/fable-library-rust/src/Convert.rs index 7aaf7b3e01..3531d764c5 100644 --- a/src/fable-library-rust/src/Convert.rs +++ b/src/fable-library-rust/src/Convert.rs @@ -268,6 +268,9 @@ pub mod Convert_ { } } let chars = s.as_bytes(); + if chars.len() % 2 != 0 { + panic!("The input is not a valid hex string as its length is not a multiple of 2."); + } let bytes = chars .chunks_exact(2) .map(|x| decode(x[0]) << 4 | decode(x[1])) diff --git a/src/fable-library-rust/src/DateOnly.rs b/src/fable-library-rust/src/DateOnly.rs index 1e028bbbb2..057d6ccd8e 100644 --- a/src/fable-library-rust/src/DateOnly.rs +++ b/src/fable-library-rust/src/DateOnly.rs @@ -3,7 +3,7 @@ pub mod DateOnly_ { use crate::{ DateTime_::{DateTime, DateTimeKind, ticks_to_duration}, Format::DateTime::{format_dotnet_custom, try_parse_date_str}, - Native_::{Hashable, MutCell, ToString, compare, getHashCode}, + Native_::{Hashable, MutCell, ToString, compare, hash_code}, String_::{fromString, string}, TimeOnly_::TimeOnly, TimeSpan_::ticks_per_day, @@ -23,7 +23,7 @@ pub mod DateOnly_ { impl Hashable for DateOnly { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } diff --git a/src/fable-library-rust/src/DateTime.rs b/src/fable-library-rust/src/DateTime.rs index c736d73443..62e884a1df 100644 --- a/src/fable-library-rust/src/DateTime.rs +++ b/src/fable-library-rust/src/DateTime.rs @@ -6,7 +6,7 @@ pub mod DateTime_ { format_dotnet_custom, format_offset_token, format_roundtrip_datetime, try_parse_datetime_str, }, - Native_::{Hashable, MutCell, String, ToString, compare, getHashCode}, + Native_::{Hashable, MutCell, String, ToString, compare, hash_code}, String_::{fromString, string}, TimeOnly_::TimeOnly, TimeSpan_::{TimeSpan, nanoseconds_per_tick, ticks_per_second}, @@ -64,6 +64,7 @@ pub mod DateTime_ { } impl Hash for DateTime { + #[inline] fn hash(&self, state: &mut H) { self.ticks().hash(state) } @@ -72,7 +73,7 @@ pub mod DateTime_ { impl Hashable for DateTime { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(&self.ticks()) + hash_code(&self.ticks()) } } diff --git a/src/fable-library-rust/src/DateTimeOffset.rs b/src/fable-library-rust/src/DateTimeOffset.rs index a1e5f9d112..23be6e963e 100644 --- a/src/fable-library-rust/src/DateTimeOffset.rs +++ b/src/fable-library-rust/src/DateTimeOffset.rs @@ -6,7 +6,7 @@ pub mod DateTimeOffset_ { Format::DateTime::{ format_dotnet_custom, format_roundtrip_datetime, try_parse_datetime_offset_str, }, - Native_::{Hashable, MutCell, ToString, compare, format, getHashCode}, + Native_::{Hashable, MutCell, ToString, compare, format, hash_code}, String_::{fromString, string}, TimeOnly_::TimeOnly, TimeSpan_::{ @@ -57,6 +57,7 @@ pub mod DateTimeOffset_ { } impl Hash for DateTimeOffset { + #[inline] fn hash(&self, state: &mut H) { self.utcTicks().hash(state) } @@ -65,7 +66,7 @@ pub mod DateTimeOffset_ { impl Hashable for DateTimeOffset { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(&self.utcTicks()) + hash_code(&self.utcTicks()) } } diff --git a/src/fable-library-rust/src/Decimal.rs b/src/fable-library-rust/src/Decimal.rs index f1d6bc2226..82c5434e39 100644 --- a/src/fable-library-rust/src/Decimal.rs +++ b/src/fable-library-rust/src/Decimal.rs @@ -1,8 +1,8 @@ #[cfg(feature = "decimal")] pub mod Decimal_ { - use crate::Native_::{compare, getHashCode, DivideByInt, Hashable, Lrc, MutCell, Vec}; + use crate::Native_::{compare, format, hash_code, DivideByInt, Hashable, Lrc, MutCell, NullableRef, String}; use crate::NativeArray_::{new_array, Array}; - use crate::String_::{string, toString as toString_1}; + use crate::String_::{fromString as fromString_1, string, toString as toString_1}; use core::cmp::Ordering; use rust_decimal::prelude::*; @@ -24,10 +24,12 @@ pub mod Decimal_ { impl Hashable for decimal { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } + // pub fn getHashCode(x: decimal) -> i32 { hash_code(&x) } + pub fn equals(x: decimal, y: decimal) -> bool { x.eq(&y) } pub fn compareTo(x: decimal, y: decimal) -> i32 { compare(&x, &y) } @@ -36,11 +38,27 @@ pub mod Decimal_ { pub fn abs(x: decimal) -> decimal { x.abs() } pub fn sign(x: decimal) -> i32 { x.signum().to_i32().unwrap() } - pub fn max(x: decimal, y: decimal) -> decimal { x.max(y) } - pub fn min(x: decimal, y: decimal) -> decimal { x.min(y) } + pub fn copySign(x: decimal, sign: decimal) -> decimal { + if x.is_sign_negative() == sign.is_sign_negative() { x } else { -x } + } - pub fn maxMagnitude(x: decimal, y: decimal) -> decimal { if abs(x) > abs(y) {x} else {y} } - pub fn minMagnitude(x: decimal, y: decimal) -> decimal { if abs(x) < abs(y) {x} else {y} } + pub fn max(x: decimal, y: decimal) -> decimal { if x < y { y } else { x } } + pub fn min(x: decimal, y: decimal) -> decimal { if x < y { x } else { y } } + + pub fn maxMagnitude(x: decimal, y: decimal) -> decimal { + match abs(x).cmp(&abs(y)) { + Ordering::Less => y, + Ordering::Greater => x, + Ordering::Equal => if x.is_sign_negative() { y } else { x }, + } + } + pub fn minMagnitude(x: decimal, y: decimal) -> decimal { + match abs(x).cmp(&abs(y)) { + Ordering::Less => x, + Ordering::Greater => y, + Ordering::Equal => if x.is_sign_negative() { x } else { y }, + } + } pub fn clamp(x: decimal, min: decimal, max: decimal) -> decimal { x.clamp(min, max).into() @@ -51,38 +69,72 @@ pub mod Decimal_ { pub fn truncate(x: decimal) -> decimal { x.trunc() } pub fn pown(x: decimal, n: i32) -> decimal { x.powi(n as i64) } - pub fn add(x: decimal, y: decimal) -> decimal { x + y } - pub fn subtract(x: decimal, y: decimal) -> decimal { x - y } - pub fn multiply(x: decimal, y: decimal) -> decimal { x * y } - pub fn divide(x: decimal, y: decimal) -> decimal { x / y } + #[inline] + fn preserveScale(mut value: decimal, scale: u32) -> decimal { + if value.scale() < scale { + value.rescale(scale); + } + value + } + + pub fn add(x: decimal, y: decimal) -> decimal { + let value = x.checked_add(y).unwrap_or_else(|| panic!("")); + preserveScale(value, x.scale().max(y.scale())) + } + pub fn subtract(x: decimal, y: decimal) -> decimal { + let value = x.checked_sub(y).unwrap_or_else(|| panic!("")); + preserveScale(value, x.scale().max(y.scale())) + } + pub fn multiply(x: decimal, y: decimal) -> decimal { + let value = x.checked_mul(y).unwrap_or_else(|| panic!("")); + preserveScale(value, (x.scale() + y.scale()).min(28)) + } + pub fn divide(x: decimal, y: decimal) -> decimal { + let quotient = x.checked_div(y).unwrap_or_else(|| panic!("")); + if quotient.checked_mul(y) == Some(x) { + let quotient = quotient.normalize(); + let scale = quotient.scale().max(x.scale().saturating_sub(y.scale())); + preserveScale(quotient, scale) + } else { + quotient + } + } pub fn remainder(x: decimal, y: decimal) -> decimal { x % y } pub fn negate(x: decimal) -> decimal { -x } pub fn isNegative(x: decimal) -> bool { x.is_sign_negative() } pub fn isPositive(x: decimal) -> bool { x.is_sign_positive() } - pub fn isInteger(x: decimal) -> bool { x.fract() == Decimal::ZERO } - pub fn isEvenInteger(x: decimal) -> bool { isInteger(x) && (x % Decimal::from(2)) == Decimal::ZERO } - pub fn isOddInteger(x: decimal) -> bool { isInteger(x) && (x % Decimal::from(2)) != Decimal::ZERO } + pub fn isInteger(x: decimal) -> bool { x.scale() == 0 || x.fract() == Decimal::ZERO } + pub fn isEvenInteger(x: decimal) -> bool { isInteger(x) && x.trunc().mantissa() % 2 == 0 } + pub fn isOddInteger(x: decimal) -> bool { isInteger(x) && x.trunc().mantissa() % 2 != 0 } // Canonical means the stored scale is already minimal (no representable trailing // zero digits), e.g. 1.0M and 1.20M are non-canonical, but 1M and 3.14M are. pub fn isCanonical(x: decimal) -> bool { x.scale() == x.normalize().scale() } - pub fn toInt8(x: decimal) -> i8 { x.to_i8().unwrap() } - pub fn toUInt8(x: decimal) -> u8 { x.to_u8().unwrap() } - pub fn toInt16(x: decimal) -> i16 { x.to_i16().unwrap() } - pub fn toUInt16(x: decimal) -> u16 { x.to_u16().unwrap() } - pub fn toInt32(x: decimal) -> i32 { x.to_i32().unwrap() } - pub fn toUInt32(x: decimal) -> u32 { x.to_u32().unwrap() } - pub fn toInt64(x: decimal) -> i64 { x.to_i64().unwrap() } - pub fn toUInt64(x: decimal) -> u64 { x.to_u64().unwrap() } - pub fn toNativeInt(x: decimal) -> isize { x.to_isize().unwrap() } - pub fn toUNativeInt(x: decimal) -> usize { x.to_usize().unwrap() } + pub fn toInt8(x: decimal) -> i8 { x.trunc().to_i8().unwrap() } + pub fn toUInt8(x: decimal) -> u8 { x.trunc().normalize().to_u8().unwrap() } + pub fn toInt16(x: decimal) -> i16 { x.trunc().to_i16().unwrap() } + pub fn toUInt16(x: decimal) -> u16 { x.trunc().normalize().to_u16().unwrap() } + pub fn toInt32(x: decimal) -> i32 { x.trunc().to_i32().unwrap() } + pub fn toUInt32(x: decimal) -> u32 { x.trunc().normalize().to_u32().unwrap() } + pub fn toInt64(x: decimal) -> i64 { x.trunc().to_i64().unwrap() } + pub fn toUInt64(x: decimal) -> u64 { x.trunc().normalize().to_u64().unwrap() } + pub fn toNativeInt(x: decimal) -> isize { x.trunc().to_isize().unwrap() } + pub fn toUNativeInt(x: decimal) -> usize { x.trunc().normalize().to_usize().unwrap() } pub fn toFloat32(x: decimal) -> f32 { x.to_f32().unwrap() } pub fn toFloat64(x: decimal) -> f64 { x.to_f64().unwrap() } pub fn toDecimal(x: decimal) -> decimal { x } + pub fn toOACurrency(x: decimal) -> i64 { + let scaled = x * Decimal::from_i32(10000).unwrap(); + scaled + .round_dp_with_strategy(0, RoundingStrategy::MidpointNearestEven) + .to_i64() + .unwrap() + } + pub fn toBoolean(x: decimal) -> bool { !x.is_zero() } pub fn toChar(x: decimal) -> char { @@ -90,23 +142,489 @@ pub mod Decimal_ { } pub fn toString(x: decimal) -> string { - toString_1(&x) + toString_1(&if x.is_zero() { x.abs() } else { x }) } - pub fn tryParse(s: string, res: &MutCell) -> bool { - match Decimal::from_str(s.trim()) { - Ok(d) => { res.set(d); true }, - Err(e) => false, + fn exponentialFormatString(x: decimal, precision: usize, lowercase: bool) -> String { + if x.is_zero() { + return format!( + "{:.*}{}+000", + precision, + x.normalize(), + if lowercase { 'e' } else { 'E' } + ); + } + + let mantissa = x.mantissa().unsigned_abs(); + let digitCount = mantissa.ilog10() as usize + 1; + let mut exponent = digitCount as i32 - x.scale() as i32 - 1; + let significantDigits = precision.saturating_add(1); + let rounded = if significantDigits < digitCount { + let divisor = 10u128.pow((digitCount - significantDigits) as u32); + let mut rounded = mantissa / divisor + u128::from(mantissa % divisor >= divisor / 2); + if rounded.ilog10() as usize + 1 > significantDigits { + rounded /= 10; + exponent += 1; + } + rounded + } else { + mantissa + }; + let digits = format!("{rounded}"); + let mut formatted = String::new(); + if x.is_sign_negative() { + formatted.push('-'); + } + formatted.push(digits.as_bytes()[0] as char); + if precision > 0 { + formatted.push('.'); + formatted.push_str(&digits[1..]); + for _ in digits.len()..significantDigits { + formatted.push('0'); + } } + formatted.push(if lowercase { 'e' } else { 'E' }); + formatted.push(if exponent < 0 { '-' } else { '+' }); + formatted.push_str(&format!("{:03}", exponent.unsigned_abs())); + formatted } - pub fn parse(s: string) -> decimal { - match Decimal::from_str(s.trim()) { - Ok(d) => d, - Err(e) => panic!("The input string {} was not in a correct format.", s), + fn exponentialFormat(x: decimal, precision: usize, lowercase: bool) -> string { + fromString_1(exponentialFormatString(x, precision, lowercase)) + } + + fn generalFormat(x: decimal, precision: usize, lowercase: bool) -> string { + if x.is_zero() { + return toString_1(&Decimal::ZERO); + } + + let normalized = x.abs().normalize(); + let mantissa = normalized.mantissa() as u128; + let digitCount = mantissa.ilog10() + 1; + let exponent = digitCount as i32 - normalized.scale() as i32 - 1; + let rounded = if exponent < precision as i32 { + let decimalPlaces = (precision as i32 - exponent - 1).clamp(0, 28) as u32; + x.round_dp_with_strategy(decimalPlaces, RoundingStrategy::MidpointAwayFromZero).normalize() + } else { + x.normalize() + }; + let magnitude = rounded.abs(); + let exponent = (magnitude.mantissa() as u128).ilog10() as i32 - magnitude.scale() as i32; + + if exponent >= precision as i32 || exponent < -4 { + let mut formatted = exponentialFormatString(rounded, precision - 1, lowercase); + if let Some(point) = formatted.find('.') { + let exponentIndex = formatted.find(['e', 'E']).unwrap(); + let mut end = exponentIndex; + while end > point + 1 && formatted.as_bytes()[end - 1] == b'0' { + end -= 1; + } + if end == point + 1 { + end = point; + } + formatted.replace_range(end..exponentIndex, ""); + } + let exponentIndex = formatted.find(['e', 'E']).unwrap(); + if formatted.as_bytes().get(exponentIndex + 2) == Some(&b'0') { + formatted.remove(exponentIndex + 2); + } + fromString_1(formatted) + } else { + toString_1(&rounded) + } + } + + pub fn toStringWithFormat(x: decimal, formatSpec: string) -> string { + let formatSpec = formatSpec.as_str(); + let Some(token) = formatSpec.chars().next() else { + return toString(x); + }; + let specifiedPrecision = formatSpec + .get(1..) + .and_then(|value| value.parse::().ok()); + let precision = specifiedPrecision.unwrap_or(0); + + match token { + 'G' | 'g' if precision > 0 => generalFormat(x, precision, token == 'g'), + 'G' | 'g' | 'R' | 'r' => toString(x), + 'E' | 'e' => exponentialFormat(x, specifiedPrecision.unwrap_or(6), token == 'e'), + 'F' | 'f' => { + let precision = specifiedPrecision.unwrap_or(2); + let rounded = x.round_dp_with_strategy(precision.min(28) as u32, RoundingStrategy::MidpointAwayFromZero); + let rounded = if rounded.is_zero() { rounded.abs() } else { rounded }; + fromString_1(format!("{:.*}", precision, rounded)) + }, + _ => toString(x), + } + } + + pub fn getTypeCode() -> i32 { 15 } + + // System.Globalization.NumberStyles + #[allow(dead_code)] + mod NumberStyles { + pub const None: i32 = 0x000; + pub const AllowLeadingWhite: i32 = 0x001; + pub const AllowTrailingWhite: i32 = 0x002; + pub const AllowLeadingSign: i32 = 0x004; + pub const AllowTrailingSign: i32 = 0x008; + pub const AllowParentheses: i32 = 0x010; + pub const AllowDecimalPoint: i32 = 0x020; + pub const AllowThousands: i32 = 0x040; + pub const AllowExponent: i32 = 0x080; + pub const AllowCurrencySymbol: i32 = 0x100; + pub const AllowHexSpecifier: i32 = 0x200; + pub const AllowBinarySpecifier: i32 = 0x400; + + pub const Integer: i32 = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign; + pub const HexNumber: i32 = AllowLeadingWhite | AllowTrailingWhite | AllowHexSpecifier; + pub const BinaryNumber: i32 = AllowLeadingWhite | AllowTrailingWhite | AllowBinarySpecifier; + pub const Number: i32 = + AllowLeadingWhite + | AllowTrailingWhite + | AllowLeadingSign + | AllowTrailingSign + | AllowDecimalPoint + | AllowThousands; + pub const Float: i32 = + AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowDecimalPoint | AllowExponent; + pub const Currency: i32 = + AllowLeadingWhite + | AllowTrailingWhite + | AllowLeadingSign + | AllowTrailingSign + | AllowParentheses + | AllowDecimalPoint + | AllowThousands + | AllowCurrencySymbol; + pub const Any: i32 = Currency | AllowExponent; + } + + mod parsing { + use super::decimal; + use super::NumberStyles; + use rust_decimal::Decimal; + + #[derive(Clone, Copy, PartialEq, Eq)] + enum DecimalParseError { + Format, + Overflow, + } + + fn parseFailure(error: DecimalParseError, value: &str) -> ! { + match error { + DecimalParseError::Format => panic!("The input string '{}' was not in a correct format.", value), + DecimalParseError::Overflow => panic!("Value was either too large or too small for a Decimal."), + } + } + + pub(super) fn overflowFailure() -> ! { parseFailure(DecimalParseError::Overflow, "") } + + const MAX_MANTISSA: u128 = (1_u128 << 96) - 1; + const SIGNIFICANT_WINDOW: usize = 30; + const EXPONENT_LIMIT: i128 = 1_i128 << 62; + + // The significant digits of a number, without leading zeros. Parsing never needs more than the first + // SIGNIFICANT_WINDOW of them: a mantissa has at most 29 digits and one more is needed for rounding. + // `hasTail` records whether any later digit is non-zero, which is all the rounding needs from them. + struct SignificantDigits { + window: [u8; SIGNIFICANT_WINDOW], + length: usize, + hasTail: bool, + } + + impl SignificantDigits { + fn push(&mut self, digit: u8) { + if self.length < SIGNIFICANT_WINDOW { + self.window[self.length] = digit; + } else if digit != 0 { + self.hasTail = true; + } + self.length += 1; + } + + // Index 0 is the first significant digit. Negative indexes are the zeros between the decimal point + // and the first significant digit. + fn digitAt(&self, index: i128) -> u128 { + if index < 0 { + 0 + } else { + u128::from(self.window[index as usize]) + } + } + + // `start` is never past SIGNIFICANT_WINDOW, so every digit recorded in `hasTail` is at or after it. + fn hasNonZeroFrom(&self, start: usize) -> bool { + let stored = self.length.min(SIGNIFICANT_WINDOW); + self.hasTail || self.window[start.min(stored)..stored].iter().any(|digit| *digit != 0) + } + } + + // Reads the mantissa and the exponent in a single pass. Returns the significant digits and the power of + // ten of their last digit, so the value is digits * 10^power. + fn scanNumber( + value: &str, + allowDecimalPoint: bool, + allowThousands: bool, + allowExponent: bool, + ) -> Result<(SignificantDigits, i128), DecimalParseError> { + let bytes = value.as_bytes(); + let mut digits = SignificantDigits { + window: [0; SIGNIFICANT_WINDOW], + length: 0, + hasTail: false, + }; + let mut digitCount = 0_usize; + let mut fractionLength = 0_i128; + let mut hasDecimalPoint = false; + let mut hasIntegerDigit = false; + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + byte @ b'0'..=b'9' => { + digitCount += 1; + if hasDecimalPoint { + fractionLength += 1; + } else { + hasIntegerDigit = true; + } + if digits.length > 0 || byte != b'0' { + digits.push(byte - b'0'); + } + } + b'.' if allowDecimalPoint && !hasDecimalPoint => hasDecimalPoint = true, + b',' if allowThousands && !hasDecimalPoint && hasIntegerDigit => {} + b'e' | b'E' => break, + _ => return Err(DecimalParseError::Format), + } + index += 1; + } + if digitCount == 0 { + return Err(DecimalParseError::Format); + } + + let exponent = if index < bytes.len() { + if !allowExponent { + return Err(DecimalParseError::Format); + } + parseExponent(&bytes[index + 1..])? + } else { + 0 + }; + Ok((digits, exponent - fractionLength)) + } + + // Parses the text after the exponent marker. Magnitudes are clamped to EXPONENT_LIMIT so the arithmetic + // cannot overflow; any exponent beyond the clamp gives the same result. + fn parseExponent(text: &[u8]) -> Result { + let (isNegative, digits) = match text.first() { + Some(b'-') => (true, &text[1..]), + Some(b'+') => (false, &text[1..]), + _ => (false, text), + }; + if digits.is_empty() || !digits.iter().all(u8::is_ascii_digit) { + return Err(DecimalParseError::Format); + } + let magnitude = digits.iter().fold(0_i128, |magnitude, digit| { + (magnitude * 10 + i128::from(*digit - b'0')).min(EXPONENT_LIMIT) + }); + Ok(if isNegative { -magnitude } else { magnitude }) + } + + fn fromMantissa(mantissa: u128, scale: u32, isNegative: bool) -> decimal { + let mut parsed = Decimal::from_parts( + mantissa as u32, + (mantissa >> 32) as u32, + (mantissa >> 64) as u32, + isNegative, + scale, + ); + parsed.set_sign_negative(isNegative); + parsed + } + + // Rounds digits * 10^power to a Decimal with at most 28 fraction digits, rounding half to even. + fn roundDigits(digits: &SignificantDigits, power: i128, isNegative: bool) -> Result { + if digits.length == 0 { + return Ok(fromMantissa(0, (-power).clamp(0, 28) as u32, isNegative)); + } + // Common case: every digit fits the mantissa and the scale, so nothing is rounded. + if digits.length <= 28 && (-28..=0).contains(&power) { + let mantissa = digits.window[..digits.length] + .iter() + .fold(0_u128, |mantissa, digit| mantissa * 10 + u128::from(*digit)); + return Ok(fromMantissa(mantissa, (-power) as u32, isNegative)); + } + let position = digits.length as i128 + power; + if position > 29 { + return Err(DecimalParseError::Overflow); + } + if position < -28 { + return Ok(fromMantissa(0, 28, isNegative)); + } + // The mantissa takes the digits up to the decimal point (zeros past the last digit when power >= 0), + // then as many fraction digits as still fit. + let fractionLength = -power; + let mut mantissa = 0_u128; + if position > 0 { + for index in 0..position { + mantissa = mantissa * 10 + digits.digitAt(index); + } + if mantissa > MAX_MANTISSA { + return Err(DecimalParseError::Overflow); + } + } + let mut scale = 0_i128; + while scale < fractionLength.min(28) { + let next = mantissa * 10 + digits.digitAt(position + scale); + if next > MAX_MANTISSA { + break; + } + mantissa = next; + scale += 1; + } + + let roundUp = scale < fractionLength + && match digits.digitAt(position + scale) { + 0..=4 => false, + 5 => mantissa % 2 == 1 || digits.hasNonZeroFrom((position + scale + 1) as usize), + _ => true, + }; + if !roundUp || mantissa < MAX_MANTISSA { + return Ok(fromMantissa(mantissa + u128::from(roundUp), scale as u32, isNegative)); + } + if scale == 0 { + return Err(DecimalParseError::Overflow); + } + // Rounding up would exceed MAX_MANTISSA, so the result keeps one fraction digit fewer. + Ok(fromMantissa(MAX_MANTISSA / 10 + 1, scale as u32 - 1, isNegative)) + } + + // Space and U+0009..U+000D, the whitespace .NET accepts around a number. + fn isParseWhitespace(c: char) -> bool { matches!(c, ' ' | '\t'..='\r') } + + pub(super) fn validateStyle(style: i32) { + if style & !NumberStyles::Any != 0 { + panic!("An undefined NumberStyles value is being used."); + } + } + + fn parseDecimalWithStyle(value: &str, style: i32) -> Result { + validateStyle(style); + let has = |flag: i32| style & flag != 0; + let allowLeadingWhite = has(NumberStyles::AllowLeadingWhite); + let allowTrailingWhite = has(NumberStyles::AllowTrailingWhite); + let allowLeadingSign = has(NumberStyles::AllowLeadingSign); + let allowTrailingSign = has(NumberStyles::AllowTrailingSign); + let allowParentheses = has(NumberStyles::AllowParentheses); + let allowCurrencySymbol = has(NumberStyles::AllowCurrencySymbol); + + // Disallowed whitespace is left in place and rejected by scanNumber. + let mut value = value.trim_end_matches('\0'); + + let mut isNegative = false; + let mut hasSign = false; + let mut hasParentheses = false; + let mut hasCurrencySymbol = false; + loop { + if allowLeadingWhite && (!hasSign || hasCurrencySymbol) { + value = value.trim_start_matches(isParseWhitespace); + } + if allowLeadingSign && !hasSign && matches!(value.as_bytes().first(), Some(b'+' | b'-')) { + isNegative = value.starts_with('-'); + hasSign = true; + value = &value[1..]; + } else if allowParentheses && !hasSign && value.starts_with('(') { + isNegative = true; + hasSign = true; + hasParentheses = true; + value = &value[1..]; + } else if allowCurrencySymbol && !hasCurrencySymbol && value.starts_with('\u{00a4}') { + hasCurrencySymbol = true; + value = &value['\u{00a4}'.len_utf8()..]; + } else { + break; + } + } + + loop { + if allowTrailingWhite { + value = value.trim_end_matches(isParseWhitespace); + } + if allowTrailingSign && !hasSign && matches!(value.as_bytes().last(), Some(b'+' | b'-')) { + isNegative = value.ends_with('-'); + hasSign = true; + value = &value[..value.len() - 1]; + } else if hasParentheses && value.ends_with(')') { + hasParentheses = false; + value = &value[..value.len() - 1]; + } else if allowCurrencySymbol && !hasCurrencySymbol && value.ends_with('\u{00a4}') { + hasCurrencySymbol = true; + value = &value[..value.len() - '\u{00a4}'.len_utf8()]; + } else { + break; + } + } + if hasParentheses { + return Err(DecimalParseError::Format); + } + + let (digits, power) = scanNumber( + value, + has(NumberStyles::AllowDecimalPoint), + has(NumberStyles::AllowThousands), + has(NumberStyles::AllowExponent), + )?; + roundDigits(&digits, power, isNegative) + } + + pub(super) fn tryParseDecimal(value: &str, style: i32) -> Option { + parseDecimalWithStyle(value, style).ok() + } + + pub(super) fn parseDecimal(value: &str, style: i32) -> decimal { + match parseDecimalWithStyle(value, style) { + Ok(d) => d, + Err(error) => parseFailure(error, value), + } + } + + // Parses the text produced by float formatting and drops insignificant trailing zeros. + pub(super) fn fromFloatString(value: &str) -> decimal { + let parsed = parseDecimal(value, NumberStyles::Float); + if parsed.is_zero() { Decimal::ZERO } else { parsed.normalize() } } } + pub fn tryParseWithStyle(s: string, style: i32, res: &MutCell) -> bool { + if s.is_null() { + parsing::validateStyle(style); + res.set(Decimal::ZERO); + return false; + } + match parsing::tryParseDecimal(&s, style) { + Some(d) => { res.set(d); true }, + None => { res.set(Decimal::ZERO); false }, + } + } + + pub fn tryParse(s: string, res: &MutCell) -> bool { + tryParseWithStyle(s, NumberStyles::Number, res) + } + + pub fn parseWithStyle(s: string, style: i32) -> decimal { + if s.is_null() { panic!("Value cannot be null."); } + parsing::parseDecimal(&s, style) + } + + pub fn parse(s: string) -> decimal { + parseWithStyle(s, NumberStyles::Number) + } + + pub fn convert(s: string) -> decimal { + if s.is_null() { Decimal::ZERO } else { parse(s) } + } + pub fn fromInt8(n: i8) -> decimal { Decimal::from_i8(n).unwrap() } pub fn fromUInt8(n: u8) -> decimal { Decimal::from_u8(n).unwrap() } pub fn fromInt16(n: i16) -> decimal { Decimal::from_i16(n).unwrap() } @@ -118,11 +636,26 @@ pub mod Decimal_ { pub fn fromNativeInt(n: isize) -> decimal { Decimal::from_isize(n).unwrap() } pub fn fromUNativeInt(n: usize) -> decimal { Decimal::from_usize(n).unwrap() } - pub fn fromFloat32(n: f32) -> decimal { Decimal::from_f32(n).unwrap() } - pub fn fromFloat64(n: f64) -> decimal { Decimal::from_f64(n).unwrap() } + pub fn fromFloat32(n: f32) -> decimal { + if !n.is_finite() { + parsing::overflowFailure(); + } + parsing::fromFloatString(&format!("{:.6e}", n)) + } + + pub fn fromFloat64(n: f64) -> decimal { + if !n.is_finite() { + parsing::overflowFailure(); + } + parsing::fromFloatString(&format!("{:.14e}", n)) + } pub fn fromDecimal(d: decimal) -> decimal { d } + pub fn fromOACurrency(value: i64) -> decimal { + Decimal::from_i64(value).unwrap() / Decimal::from_i32(10000).unwrap() + } + pub fn fromBoolean(b: bool) -> decimal { Decimal::from_u32(b as u32).unwrap() } @@ -131,21 +664,27 @@ pub mod Decimal_ { Decimal::from_u32(c as u32).unwrap() } + // F# `decimal` on strings uses NumberStyles.Float by default. pub fn fromString(s: string) -> decimal { - Decimal::from_str(s.trim()).unwrap() + parseWithStyle(s, NumberStyles::Float) } pub fn fromParts(low: i32, mid: i32, high: i32, isNegative: bool, scale: u8) -> decimal { - Decimal::from_parts(low as u32, mid as u32, high as u32, isNegative, scale as u32) + assert!(scale <= 28, "Invalid decimal scale"); + let mut value = Decimal::from_parts(low as u32, mid as u32, high as u32, isNegative, scale as u32); + value.set_sign_negative(isNegative); + value } pub fn fromInts(low: i32, mid: i32, high: i32, signExp: i32) -> decimal { + assert!(signExp as u32 & 0x7F00_FFFF == 0, "Invalid decimal flags"); let isNegative = signExp < 0; - let scale = ((signExp >> 16) & 0x7F) as u8; + let scale = ((signExp >> 16) & 0xFF) as u8; fromParts(low, mid, high, isNegative, scale) } pub fn fromIntArray(bits: Array) -> decimal { + assert!(bits.len() == 4, "Invalid decimal bits length"); fromInts(bits[0], bits[1], bits[2], bits[3]) } @@ -155,48 +694,45 @@ pub mod Decimal_ { let mid = du.mid as i32; let high = du.hi as i32; let scale = du.scale as i32; - let signExp = - if du.negative { -(scale << 16) } else { scale << 16 }; + let signExp = (scale << 16) | if du.negative { i32::MIN } else { 0 }; new_array(&[low, mid, high, signExp]) } pub fn round(x: decimal) -> decimal { x.round() } pub fn roundTo(x: decimal, dp: i32) -> decimal { + assert!((0..=28).contains(&dp), "Invalid decimal precision"); x.round_dp(dp as u32) } - pub enum MidpointRounding { - ToEven = 0, - AwayFromZero = 1, - ToZero = 2, - ToNegativeInfinity = 3, - ToPositiveInfinity = 4 + pub mod MidpointRounding { + pub const ToEven: i32 = 0; + pub const AwayFromZero: i32 = 1; + pub const ToZero: i32 = 2; + pub const ToNegativeInfinity: i32 = 3; + pub const ToPositiveInfinity: i32 = 4; } - pub fn roundToMode(x: decimal, dp: i32, mode: MidpointRounding) -> decimal { + pub fn roundToMode(x: decimal, dp: i32, mode: i32) -> decimal { + assert!((0..=28).contains(&dp), "Invalid decimal precision"); let strategy = match mode { MidpointRounding::ToEven => RoundingStrategy::MidpointNearestEven, MidpointRounding::AwayFromZero => RoundingStrategy::MidpointAwayFromZero, MidpointRounding::ToZero => RoundingStrategy::ToZero, MidpointRounding::ToNegativeInfinity => RoundingStrategy::ToNegativeInfinity, MidpointRounding::ToPositiveInfinity => RoundingStrategy::ToPositiveInfinity, - // _ => RoundingStrategy::MidpointNearestEven, + _ => panic!("Invalid decimal rounding mode"), }; x.round_dp_with_strategy(dp as u32, strategy) } - pub fn roundMode(x: decimal, mode: MidpointRounding) -> decimal { + pub fn roundMode(x: decimal, mode: i32) -> decimal { roundToMode(x, 0, mode) } - // pub fn copySign // pub fn createChecked // pub fn createSaturating // pub fn createTruncating - // pub fn fromOACurrency - // pub fn getTypeCode - // pub fn toOACurrency // pub fn tryFormat // pub fn tryGetBits } diff --git a/src/fable-library-rust/src/Event.rs b/src/fable-library-rust/src/Event.rs index 7f873bfce6..865aef440e 100644 --- a/src/fable-library-rust/src/Event.rs +++ b/src/fable-library-rust/src/Event.rs @@ -11,7 +11,8 @@ pub mod Event_ { use crate::Choice_::Choice_2; use crate::Microsoft::FSharp::Control::{IDelegateEvent_1, IEvent_2}; use crate::Native_::{ - getZeroObj, referenceEquals, refCell, Any, Func0, Func1, Func2, LrcPtr, MutCell, Vec, + getZeroObj, interface_cast, referenceEquals, refCell, Any, Func0, Func1, Func2, Lrc, LrcPtr, + MutCell, Vec, }; use crate::Observable_::{mkDisposable, mkObserver, noCompleted, noError}; use crate::System::{IDisposable, IObservable_1, IObserver_1}; @@ -20,9 +21,7 @@ pub mod Event_ { pub type Handler = Func2, T, ()>; // ----- AnonymousEvent: the published IEvent ----- - // A concrete type so the Publish field can be used through IEvent_2 - // (AddHandler/RemoveHandler) *and* unsize-coerced to `dyn IObservable_1` - // (in F#/JS an IEvent is also an IObservable). + // A concrete implementation of IEvent_2 and IObservable_1. #[derive(Clone)] pub struct AnonymousEvent { add_handler: Func1, ()>, @@ -91,11 +90,43 @@ pub mod Event_ { } } + #[derive(Clone)] + struct EventObservable { + source: LrcPtr, T>>, + } + + impl core::fmt::Debug for EventObservable { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl core::fmt::Display for EventObservable { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl IObservable_1 for EventObservable { + fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { + self.source.Subscribe(observer) + } + } + + pub fn asObservable( + source: LrcPtr, T>>, + ) -> LrcPtr> { + interface_cast!( + LrcPtr::new(EventObservable { source }), + Lrc> + ) + } + // ----- FSharpEvent`1 ----- #[derive(Clone)] pub struct FSharpEvent_1 { handlers: LrcPtr>>>, - pub Publish: LrcPtr>, + pub Publish: LrcPtr, T>>, } impl core::fmt::Debug for FSharpEvent_1 { @@ -140,10 +171,10 @@ pub mod Event_ { } } }); - let publish = LrcPtr::new(AnonymousEvent { + let publish = interface_cast!(LrcPtr::new(AnonymousEvent { add_handler, remove_handler, - }); + }), Lrc, T>>); LrcPtr::new(FSharpEvent_1 { handlers, Publish: publish, @@ -161,6 +192,57 @@ pub mod Event_ { }) } + // ----- FSharpEvent`2 ----- + // Sender-aware events use the same normalized Handler shape as one-generic events. + #[derive(Clone)] + pub struct FSharpEvent_2 { + handlers: LrcPtr>>>, + pub Publish: LrcPtr, T>>, + } + + impl core::fmt::Debug for FSharpEvent_2 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl core::fmt::Display for FSharpEvent_2 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl FSharpEvent_2 { + pub fn Trigger(&self, sender: LrcPtr, value: T) { + let hs = self.handlers.get().clone(); + for handler in hs.iter() { + handler(sender.clone(), value.clone()); + } + } + } + + pub fn default2() -> LrcPtr> { + let handlers: LrcPtr>>> = refCell(Vec::new()); + let add_handler = Func1::new({ + let handlers = handlers.clone(); + move |handler: Handler| handlers.get_mut().push(handler) + }); + let remove_handler = Func1::new({ + let handlers = handlers.clone(); + move |handler: Handler| { + let vec = handlers.get_mut(); + if let Some(pos) = vec.iter().position(|existing| referenceEquals(&**existing, &*handler)) { + vec.remove(pos); + } + } + }); + let publish = interface_cast!(LrcPtr::new(AnonymousEvent { + add_handler, + remove_handler, + }), Lrc, T>>); + LrcPtr::new(FSharpEvent_2 { handlers, Publish: publish }) + } + // ----- module functions ----- pub fn add( @@ -275,14 +357,14 @@ pub mod Event_ { )> { let sp = splitter.clone(); let first = choose( - Func1::new(move |v: T| match &*sp(v) { + Func1::new(move |v: T| match sp(v).as_ref() { Choice_2::Choice1Of2(x) => Some(x.clone()), _ => None, }), source.clone(), ); let second = choose( - Func1::new(move |v: T| match &*splitter(v) { + Func1::new(move |v: T| match splitter(v).as_ref() { Choice_2::Choice2Of2(x) => Some(x.clone()), _ => None, }), diff --git a/src/fable-library-rust/src/Exception.rs b/src/fable-library-rust/src/Exception.rs index e9a35df832..ce1809f83a 100644 --- a/src/fable-library-rust/src/Exception.rs +++ b/src/fable-library-rust/src/Exception.rs @@ -13,31 +13,37 @@ pub mod Exception_ { try_f() // no catching when no_std } + /// Turns a caught panic payload into the exception a `with` handler sees. + /// + /// Taken by reference so a caller that has to re-raise (the async builder's + /// TryFinally) still owns the payload to resume unwinding with. + #[cfg(not(feature = "no_std"))] + pub fn get_exception(err: &dyn Any) -> LrcPtr { + match err.downcast_ref::<&'static str>() { + Some(s) => new_Exception(string(*s)), + None => match err.downcast_ref::() { + Some(s) => new_Exception(fromSlice(s)), + None => match err.downcast_ref::>() { + Some(ex) => ex.clone(), + None => new_Exception(string("Unknown error")), + }, + }, + } + } + #[cfg(not(feature = "no_std"))] pub fn try_catch(try_f: F, catch_f: G) -> R where F: FnOnce() -> R, G: FnOnce(LrcPtr) -> R, { - fn get_ex(err: Box) -> LrcPtr { - match err.downcast_ref::<&'static str>() { - Some(s) => new_Exception(string(*s)), - None => match err.downcast_ref::() { - Some(s) => new_Exception(fromSlice(s)), - None => match err.downcast_ref::>() { - Some(ex) => ex.clone(), - None => new_Exception(string("Unknown error")), - }, - }, - } - } let prev_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(|_| {})); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(try_f)); std::panic::set_hook(prev_hook); match result { Ok(res) => res, - Err(err) => catch_f(get_ex(err)), + Err(err) => catch_f(get_exception(&*err)), } } diff --git a/src/fable-library-rust/src/Fable.Library.Rust.fsproj b/src/fable-library-rust/src/Fable.Library.Rust.fsproj index 30ccc1242e..42644799ea 100644 --- a/src/fable-library-rust/src/Fable.Library.Rust.fsproj +++ b/src/fable-library-rust/src/Fable.Library.Rust.fsproj @@ -23,7 +23,9 @@ + + diff --git a/src/fable-library-rust/src/Guid.rs b/src/fable-library-rust/src/Guid.rs index 87b97f0ee3..a631589bd4 100644 --- a/src/fable-library-rust/src/Guid.rs +++ b/src/fable-library-rust/src/Guid.rs @@ -1,7 +1,7 @@ #[cfg(feature = "guid")] pub mod Guid_ { use crate::DateTimeOffset_::DateTimeOffset; - use crate::Native_::{Hashable, MutCell, ToString, compare, format, getHashCode}; + use crate::Native_::{Hashable, MutCell, ToString, compare, format, hash_code}; use crate::NativeArray_::{Array, new_array}; use crate::String_::{fromString, string}; use uuid::{NoContext, Timestamp, Uuid}; @@ -22,7 +22,7 @@ pub mod Guid_ { #[inline] fn getHashCode(&self) -> i32 { let bytes = self.toByteArray(); - getHashCode(&bytes) + hash_code(&bytes) } } diff --git a/src/fable-library-rust/src/Interop.rs b/src/fable-library-rust/src/Interop.rs index 1d7633dce5..3a9bd22bca 100644 --- a/src/fable-library-rust/src/Interop.rs +++ b/src/fable-library-rust/src/Interop.rs @@ -1,19 +1,25 @@ pub mod ListExt { // use core::ops::Deref; use crate::List_::{List, cons, empty, isEmpty, reverse}; - use crate::Native_::{NullableRef, Vec, seq_to_iter}; + use crate::Native_::{NullableRef, Vec}; use crate::Seq_::ofList; - impl List { - //todo - non-consuming iter by ref - // pub fn iter<'a>(&self) -> impl Iterator { - // let s = ofList(self.clone()); - // seq_to_iter(s) - // } + pub struct ListIterator { + list: List, + } - pub fn into_iter(&self) -> impl Iterator { - let s = ofList(self.clone()); - seq_to_iter(s) + impl Iterator for ListIterator { + type Item = T; + + fn next(&mut self) -> Option { + match self.list.root.as_ref() { + Some(node) => { + let head = node.head.clone(); + self.list.root = node.tail.root.clone(); + Some(head) + } + None => None, + } } } @@ -67,6 +73,15 @@ pub mod ListExt { } } + impl IntoIterator for List { + type Item = T; + type IntoIter = ListIterator; + + fn into_iter(self) -> Self::IntoIter { + ListIterator { list: self } + } + } + impl Into> for List { fn into(self) -> Vec { self.into_iter().collect() @@ -75,24 +90,13 @@ pub mod ListExt { } pub mod SetExt { - use crate::Native_::{Func2, Hashable, NullableRef, Vec, combineHashCodes, make_compare, seq_to_iter}; + use crate::Native_::{ + Func2, Hashable, NullableRef, SeqIterator, Vec, combineHashCodes, make_compare, + }; use crate::Set_::{Set, add, compareTo, empty, equals, isEmpty, toSeq}; use core::cmp::Ordering; use core::hash::{Hash, Hasher}; - impl Set { - //todo - non-consuming iter by ref - // pub fn iter<'a>(&self) -> impl Iterator { - // let s = toSeq(self.clone()); - // seq_to_iter(s) - // } - - pub fn into_iter(&self) -> impl Iterator { - let s = toSeq(self.clone()); - seq_to_iter(s) - } - } - impl NullableRef for Set { #[inline] fn null() -> Self { @@ -115,9 +119,8 @@ pub mod SetExt { impl Hashable for Set { fn getHashCode(&self) -> i32 { - let s = toSeq(self.clone()); let mut res = 0_i32; - for value in seq_to_iter(s) { + for value in self.clone().into_iter() { res = combineHashCodes(res, value.getHashCode()); } res @@ -125,9 +128,9 @@ pub mod SetExt { } impl Hash for Set { + #[inline] fn hash(&self, state: &mut H) { - let s = toSeq(self.clone()); - seq_to_iter(s).for_each(|x| x.hash(state)) + self.clone().into_iter().for_each(|x| x.hash(state)) } } @@ -173,6 +176,15 @@ pub mod SetExt { } } + impl IntoIterator for Set { + type Item = T; + type IntoIter = SeqIterator; + + fn into_iter(self) -> Self::IntoIter { + SeqIterator::new(toSeq(self)) + } + } + impl Into> for Set { fn into(self) -> Vec { self.into_iter().collect() @@ -181,24 +193,13 @@ pub mod SetExt { } pub mod MapExt { - use crate::Map_::{Map, add, compareTo, empty, equals, isEmpty, iterate, toSeq}; - use crate::Native_::{Func2, Hashable, NullableRef, Vec, combineHashCodes, make_compare, seq_to_iter}; + use crate::Map_::{Map, add, compareTo, empty, equals, isEmpty, iterate, toEnumerable}; + use crate::Native_::{ + Func2, Hashable, LrcPtr, NullableRef, SeqIterator, Vec, combineHashCodes, make_compare, + }; use core::cmp::Ordering; use core::hash::{Hash, Hasher}; - impl Map { - //todo - non-consuming iter by ref - // pub fn iter<'a>(&self) -> impl Iterator { - // let s = toSeq(self.clone()); - // seq_to_iter(s).map(|kvp| kvp.as_ref().clone()) - // } - - pub fn into_iter(&self) -> impl Iterator { - let s = toSeq(self.clone()); - seq_to_iter(s).map(|kvp| kvp.as_ref().clone()) - } - } - impl NullableRef for Map { #[inline] fn null() -> Self { @@ -221,10 +222,8 @@ pub mod MapExt { impl Hashable for Map { fn getHashCode(&self) -> i32 { - let s = toSeq(self.clone()); let mut res = 0_i32; - for kvp in seq_to_iter(s) { - let (key, value) = kvp.as_ref(); + for (key, value) in self.clone().into_iter() { res = combineHashCodes(res, key.getHashCode()); res = combineHashCodes(res, value.getHashCode()); } @@ -233,9 +232,9 @@ pub mod MapExt { } impl Hash for Map { + #[inline] fn hash(&self, state: &mut H) { - let s = toSeq(self.clone()); - seq_to_iter(s).for_each(|kvp| kvp.hash(state)) + self.clone().into_iter().for_each(|kvp| kvp.hash(state)) } } @@ -271,6 +270,15 @@ pub mod MapExt { } } + impl IntoIterator for Map { + type Item = (K, V); + type IntoIter = SeqIterator; + + fn into_iter(self) -> Self::IntoIter { + SeqIterator::new(toEnumerable(self)) + } + } + impl Into> for Map { fn into(self) -> Vec<(K, V)> { self.into_iter().collect() diff --git a/src/fable-library-rust/src/List.fs b/src/fable-library-rust/src/List.fs index 4ce0353ec0..16fc523498 100644 --- a/src/fable-library-rust/src/List.fs +++ b/src/fable-library-rust/src/List.fs @@ -1009,11 +1009,16 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T list) : 'T list = ofArray arr let randomShuffleWith (random: System.Random) (xs: 'T list) : 'T list = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = toArray xs + Array.randomShuffleInPlaceWith random arr + ofArray arr let randomShuffle (xs: 'T list) : 'T list = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T list) : 'T = + if isEmpty xs then + invalidArg "source" SR.inputSequenceEmpty + toArray xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T list) : 'T = @@ -1034,7 +1039,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T list) : 'T l toArray xs |> Array.randomSampleBy randomizer count |> ofArray let randomSampleWith (random: System.Random) (count: int) (xs: 'T list) : 'T list = - randomSampleBy (fun () -> random.NextDouble()) count xs + toArray xs |> Array.randomSampleWith random count |> ofArray let randomSample (count: int) (xs: 'T list) : 'T list = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-rust/src/LrcPtr.rs b/src/fable-library-rust/src/LrcPtr.rs index 397917494d..95debbe676 100644 --- a/src/fable-library-rust/src/LrcPtr.rs +++ b/src/fable-library-rust/src/LrcPtr.rs @@ -51,7 +51,7 @@ impl Clone for LrcPtr { impl Hashable for LrcPtr { #[inline] fn getHashCode(&self) -> i32 { - self.0.as_ref().map_or(0, |value| value.getHashCode()) + self.0.getHashCode() } } diff --git a/src/fable-library-rust/src/Map.fs b/src/fable-library-rust/src/Map.fs index e7e09ed839..35cf8659a8 100644 --- a/src/fable-library-rust/src/Map.fs +++ b/src/fable-library-rust/src/Map.fs @@ -561,7 +561,7 @@ let current i = | { root = Some t } :: _ -> if t.Height = 1 then // KeyValuePair<_, _>(t.Key, t.Value) - (t.Key, t.Value) + struct (t.Key, t.Value) else unexpectedStackForCurrent () | _ -> alreadyFinished () @@ -582,7 +582,7 @@ let rec moveNext i = i.started <- true // The first call to MoveNext "starts" the enumeration. not i.stack.IsEmpty -let toSeq (m: Map<'K, 'V>) = +let toEnumerable (m: Map<'K, 'V>) = Seq.delay (fun () -> mkIterator m |> Seq.unfold (fun i -> @@ -593,6 +593,9 @@ let toSeq (m: Map<'K, 'V>) = ) ) +let toSeq (m: Map<'K, 'V>) = + Seq.map (fun struct (k, v) -> (k, v)) (toEnumerable m) + let compareTo (m1: Map<'K, 'V>) (m2: Map<'K, 'V>) = // LanguagePrimitives.GenericComparison m1 m2 Seq.compareWith compare (toSeq m1) (toSeq m2) diff --git a/src/fable-library-rust/src/Microsoft.FSharp.Core.fs b/src/fable-library-rust/src/Microsoft.FSharp.Core.fs new file mode 100644 index 0000000000..6d7f12d28a --- /dev/null +++ b/src/fable-library-rust/src/Microsoft.FSharp.Core.fs @@ -0,0 +1,12 @@ +namespace Microsoft.FSharp.Core + +// F# emits `raise (MatchFailureException(file, line, col))` for any match it +// cannot prove exhaustive. The other targets import this type from their runtime +// library (Replacements.fs maps Types.matchFail to it); Rust had no definition, +// so such a match referenced a type that did not exist and the generated crate +// failed with `unresolved import ...Microsoft::FSharp::Core::MatchFailureException`. +// +// Kept in its own file rather than appended to System.fs: a second namespace in +// that file makes the generated module glob-import System's own Exception +// alongside its definition, which Rust rejects as a duplicate name. +exception MatchFailureException of string * int * int diff --git a/src/fable-library-rust/src/Mutable.rs b/src/fable-library-rust/src/Mutable.rs index 43d64ec40b..08293f9df6 100644 --- a/src/fable-library-rust/src/Mutable.rs +++ b/src/fable-library-rust/src/Mutable.rs @@ -17,7 +17,7 @@ mod NonSyncCell { use core::convert::{AsMut, AsRef}; use core::fmt::{Debug, Display, Formatter, Result}; use core::hash::{Hash, Hasher}; - use core::ops::{Deref, DerefMut, Index}; + use core::ops::{Deref, DerefMut}; #[repr(transparent)] pub struct MutCell { @@ -95,20 +95,12 @@ mod NonSyncCell { } impl Hash for MutCell { + #[inline] fn hash(&self, state: &mut H) { self.get().hash(state); } } - impl Index for MutCell> { - type Output = T; - - #[inline] - fn index(&self, idx: i32) -> &Self::Output { - &self.as_ref()[idx as usize] - } - } - impl PartialEq for MutCell { #[inline] fn eq(&self, other: &MutCell) -> bool { diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index a6c2383003..655c1cb9b9 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -68,6 +68,7 @@ pub mod Native_ { use crate::System::Collections::Generic::EqualityComparer_1; use crate::System::Collections::Generic::IEnumerable_1; + use crate::System::Collections::Generic::IEnumerator_1; use crate::System::Collections::Generic::IEqualityComparer_1; // TODO: use these types in generated code @@ -100,6 +101,8 @@ pub mod Native_ { // impl Downcast for T {} + static NULL: OnceInit> = OnceInit::new(); + pub trait NullableRef { fn null() -> Self; fn is_null(&self) -> bool; @@ -118,6 +121,19 @@ pub mod Native_ { } } + #[cfg(not(feature = "lrc_ptr"))] + impl NullableRef for Lrc { + #[inline] + fn null() -> Self { + NULL.get_or_init(|| Lrc::new(())).clone() + } + + #[inline] + fn is_null(&self) -> bool { + Lrc::ptr_eq(self, &Self::null()) + } + } + impl NullableRef for Option { #[inline] fn null() -> Self { @@ -242,7 +258,7 @@ pub mod Native_ { fn getHashCode(&self) -> i32 { #[allow(non_snake_case)] let ($($ty,)+) = self; - getHashCodeFromHashCodes([$($ty.getHashCode(),)+]) + appendHashCodes([$($ty.getHashCode(),)+]) } } }; @@ -262,7 +278,7 @@ pub mod Native_ { impl Hashable for $ident { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } }; @@ -300,8 +316,7 @@ pub mod Native_ { } pub fn null_ptr() -> Lrc { - static NULL: OnceInit> = OnceInit::new(); - let null_rc = NULL.get_or_init(move || Lrc::new(())); + let null_rc = NULL.get_or_init(|| Lrc::new(())); unsafe { Lrc::from_raw(Lrc::into_raw(null_rc.clone()) as *const T) } } @@ -314,11 +329,10 @@ pub mod Native_ { unsafe { core::mem::zeroed() } // will panic on Rc/Arc/Box } - // A valid placeholder value of type `LrcPtr`. Used to pre-declare - // reference-typed (`obj`) match bindings, as `getZero`/`mem::zeroed` panics on - // the `Rc` fat pointer and `null` is not available for the unsized `dyn Any`. + // A valid null value of type `LrcPtr`, used for `obj` match bindings + // and event sender arguments. pub fn getZeroObj() -> LrcPtr { - box_(()) + null::>() } pub fn defaultOf() -> T { @@ -358,28 +372,34 @@ pub mod Native_ { ((h >> 32) ^ h) as i32 } - pub fn getHashCode(x: &T) -> i32 { + #[inline] + pub fn hash_code(x: &T) -> i32 { hash_to_i32(|hasher| x.hash(hasher)) } + #[inline] + pub fn getHashCode(x: &T) -> i32 { + x.getHashCode() + } + #[inline] pub fn combineHashCodes(x: i32, y: i32) -> i32 { x.wrapping_shl(1).wrapping_add(y).wrapping_add(631) } - pub fn getHashCodeFromHashCodes(hash_codes: I) -> i32 + pub fn appendHashCodes(hash_codes: I) -> i32 where I: IntoIterator, { hash_to_i32(|hasher| { - for hash_code in hash_codes { - hash_code.hash(hasher); + for code in hash_codes { + code.hash(hasher); } }) } pub fn referenceHash(p: &T) -> i32 { - getHashCode(&(p as *const T)) + hash_code(&(p as *const T)) } pub fn compare(x: T, y: T) -> i32 { @@ -418,7 +438,7 @@ pub mod Native_ { { interface_cast!( EqualityComparer_1::::get_Default(), - Lrc>, + Lrc> ) } @@ -479,11 +499,14 @@ pub mod Native_ { // Type testing // ----------------------------------------------------------- - pub fn try_downcast(value: &T) -> Option<&U> { + // Returns an owned value rather than a reference: `match o with :? string as s` + // binds `s` as the tested type, and codegen emits `if let Some(s) = try_downcast(..)`, + // so a borrow here made `s` a `&U` wherever a `U` was expected. + pub fn try_downcast(value: &T) -> Option { if let Some(o) = (value as &dyn Any).downcast_ref::>() { - o.downcast_ref::() + o.downcast_ref::().cloned() } else { - (value as &dyn Any).downcast_ref::() + (value as &dyn Any).downcast_ref::().cloned() } } @@ -578,7 +601,7 @@ pub mod Native_ { #[cfg(not(feature = "lrc_ptr"))] #[macro_export] macro_rules! interface_cast { - ($value:expr, $ifc:ty,) => { + ($value:expr, $ifc:ty) => { ($value as $ifc) }; } @@ -586,7 +609,7 @@ pub mod Native_ { #[cfg(feature = "lrc_ptr")] #[macro_export] macro_rules! interface_cast { - ($value:expr, $ifc:ty,) => { + ($value:expr, $ifc:ty) => { LrcPtr::from((*$value).clone() as $ifc) }; } @@ -620,8 +643,8 @@ pub mod Native_ { #[macro_export] macro_rules! un_op { - ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $($args:ty),*) => { - impl<$($args),*> core::ops::$op_trait for $obj { + ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty $(,$args:ty)*) => { + impl<$($args)*> core::ops::$op_trait for $obj { type Output = Self; #[inline] fn $op_fn(self) -> Self::Output { @@ -633,8 +656,8 @@ pub mod Native_ { #[macro_export] macro_rules! bin_op { - ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $rhs:ty, $($args:ty,)*) => { - impl<$($args),*> core::ops::$op_trait<$rhs> for $obj { + ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $rhs:ty $(,$args:ty,)*) => { + impl<$($args)*> core::ops::$op_trait<$rhs> for $obj { type Output = Self; #[inline] fn $op_fn(self, rhs: $rhs) -> Self::Output { @@ -646,8 +669,8 @@ pub mod Native_ { #[macro_export] macro_rules! div_int_op { - ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $rhs:ty, $($args:ty,)*) => { - impl<$($args),*> $crate::Native_::DivideByInt for $obj { + ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $rhs:ty $(,$args:ty,)*) => { + impl<$($args)*> $crate::Native_::DivideByInt for $obj { #[inline] fn divide_by_int(self, rhs: i32) -> Self { <$obj>::$op(self, rhs) @@ -725,6 +748,18 @@ pub mod Native_ { LrcPtr::from(coerced) } + pub fn box_rc(o: Rc) -> LrcPtr { + box_((*o).clone()) + } + + pub fn box_arc(o: Arc) -> LrcPtr { + box_((*o).clone()) + } + + pub fn box_box(o: Box) -> LrcPtr { + box_((*o).clone()) + } + pub fn unbox_lrc(o: LrcPtr) -> LrcPtr { #[cfg(not(feature = "lrc_ptr"))] let inner: Lrc = o; @@ -746,6 +781,18 @@ pub mod Native_ { } } + pub fn unbox_rc(o: LrcPtr) -> Rc { + Rc::new(unbox(o)) + } + + pub fn unbox_arc(o: LrcPtr) -> Arc { + Arc::new(unbox(o)) + } + + pub fn unbox_box(o: LrcPtr) -> Box { + Box::new(unbox(o)) + } + pub fn ofObj(value: T) -> Option { if is_null(&value) { None:: @@ -765,6 +812,30 @@ pub mod Native_ { // Sequences // ----------------------------------------------------------- + pub struct SeqIterator { + en: LrcPtr>, + } + + impl SeqIterator { + pub fn new(seq: Seq) -> Self { + SeqIterator { + en: seq.GetEnumerator(), + } + } + } + + impl Iterator for SeqIterator { + type Item = T; + + fn next(&mut self) -> Option { + if self.en.MoveNext() { + Some(self.en.get_Current().clone()) + } else { + None + } + } + } + pub fn seq_to_iter(seq: Seq) -> impl Iterator where T: Clone + 'static, diff --git a/src/fable-library-rust/src/NativeArray.rs b/src/fable-library-rust/src/NativeArray.rs index c82b69b89b..ecc0a7f269 100644 --- a/src/fable-library-rust/src/NativeArray.rs +++ b/src/fable-library-rust/src/NativeArray.rs @@ -14,6 +14,25 @@ pub mod NativeArray_ { pub type Array = LrcPtr>; + pub struct ArrayIterator { + arr: MutArray, + pos: usize, + } + + impl Iterator for ArrayIterator { + type Item = T; + + fn next(&mut self) -> Option { + if self.pos < self.arr.len() { + let item = self.arr[self.pos as i32].clone(); + self.pos += 1; + Some(item) + } else { + None + } + } + } + impl core::ops::Deref for MutArray { type Target = MutCell>; fn deref(&self) -> &Self::Target { @@ -47,6 +66,24 @@ pub mod NativeArray_ { } } + impl core::ops::Index for MutArray { + type Output = T; + + #[inline] + fn index(&self, idx: i32) -> &Self::Output { + &self.0.get()[idx as usize] + } + } + + impl IntoIterator for MutArray { + type Item = T; + type IntoIter = ArrayIterator; + + fn into_iter(self) -> Self::IntoIter { + ArrayIterator { arr: self, pos: 0 } + } + } + impl Into> for MutArray { fn into(self) -> Vec { self.0.get().iter().cloned().collect() diff --git a/src/fable-library-rust/src/Observable.rs b/src/fable-library-rust/src/Observable.rs index 826370874e..39fbc7e661 100644 --- a/src/fable-library-rust/src/Observable.rs +++ b/src/fable-library-rust/src/Observable.rs @@ -1,6 +1,7 @@ pub mod Observable_ { use crate::Choice_::Choice_2; - use crate::Native_::{interface_cast, refCell, Func0, Func1, Func2, Lrc, LrcPtr}; + use crate::Microsoft::FSharp::Control::IEvent_2; + use crate::Native_::{interface_cast, refCell, Func0, Func1, Func2, Lrc, LrcPtr, RefCell}; use crate::System::{Exception, IDisposable, IObservable_1, IObserver_1}; // ----------------------------------------------------------- @@ -92,6 +93,22 @@ pub mod Observable_ { } } + pub trait ObservableSource: Clone + 'static { + fn subscribe_source(&self, observer: LrcPtr>) -> LrcPtr; + } + + impl + ?Sized + 'static, T: Clone + 'static> ObservableSource for LrcPtr { + fn subscribe_source(&self, observer: LrcPtr>) -> LrcPtr { + self.Subscribe(observer) + } + } + + impl IObservable_1 for dyn IEvent_2 { + fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { + IEvent_2::Subscribe(self, observer) + } + } + // ----------------------------------------------------------- // Constructors // ----------------------------------------------------------- @@ -107,7 +124,7 @@ pub mod Observable_ { on_error, on_completed, }), - Lrc>, + Lrc> ) } @@ -116,12 +133,12 @@ pub mod Observable_ { ) -> LrcPtr> { interface_cast!( LrcPtr::new(Observable { subscribe }), - Lrc>, + Lrc> ) } pub fn mkDisposable(dispose: Func0<()>) -> LrcPtr { - interface_cast!(LrcPtr::new(Disposable { dispose }), Lrc,) + interface_cast!(LrcPtr::new(Disposable { dispose }), Lrc) } // A no-op error handler (errors are propagated by combinators when present, @@ -146,46 +163,111 @@ pub mod Observable_ { Func0::new(move || observer.OnCompleted()) } + fn protect(action: F, on_error: Func1, ()>) { + crate::Exception_::try_catch( + || action(), + |error| { + on_error(error); + }, + ); + } + + struct StoppedObserver { + stopped: RefCell, + on_next: Func1, + on_error: Func1, ()>, + on_completed: Func0<()>, + } + + fn stoppedObserver( + observer: LrcPtr>, + ) -> StoppedObserver { + let stopped = refCell(false); + let on_next = { + let stopped = stopped.clone(); + let observer = observer.clone(); + Func1::new(move |value: T| { + if !*stopped.get() { + observer.OnNext(value); + } + }) + }; + let on_error = { + let stopped = stopped.clone(); + let observer = observer.clone(); + Func1::new(move |error: LrcPtr| { + if !*stopped.get() { + stopped.set(true); + observer.OnError(error); + } + }) + }; + let on_completed = { + let stopped = stopped.clone(); + Func0::new(move || { + if !*stopped.get() { + stopped.set(true); + observer.OnCompleted(); + } + }) + }; + StoppedObserver { + stopped, + on_next, + on_error, + on_completed, + } + } + // ----------------------------------------------------------- // Combinators // ----------------------------------------------------------- - pub fn add(callback: Func1, source: LrcPtr>) { - source.Subscribe(mkObserver(callback, noError(), noCompleted())); + pub fn add, T: Clone + 'static>(callback: Func1, source: S) { + source.subscribe_source(mkObserver(callback, noError(), noCompleted())); } - pub fn subscribe( + pub fn subscribe, T: Clone + 'static>( callback: Func1, - source: LrcPtr>, + source: S, ) -> LrcPtr { - source.Subscribe(mkObserver(callback, noError(), noCompleted())) + source.subscribe_source(mkObserver(callback, noError(), noCompleted())) } - pub fn choose( + pub fn choose, T: Clone + 'static, U: Clone + 'static>( chooser: Func1>, - source: LrcPtr>, + source: S, ) -> LrcPtr> { mkObservable(Func1::new( move |observer: LrcPtr>| -> LrcPtr { let chooser = chooser.clone(); - let obs = observer.clone(); + let guard = stoppedObserver(observer.clone()); + let on_next = guard.on_next.clone(); + let errorObserver = observer.clone(); + let on_error = guard.on_error.clone(); + let on_completed = guard.on_completed.clone(); let inner = mkObserver( Func1::new(move |t: T| { - if let Some(u) = chooser(t) { - obs.OnNext(u); - } + protect( + || { + if let Some(u) = chooser(t) { + on_next(u); + } + }, + fwdError(errorObserver.clone()), + ); }), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), + on_error, + on_completed, ); - source.Subscribe(inner) + source.subscribe_source(inner) }, )) } - pub fn filter( + pub fn filter, T: Clone + 'static>( predicate: Func1, - source: LrcPtr>, + source: S, ) -> LrcPtr> { choose( Func1::new(move |x: T| if predicate(x.clone()) { Some(x) } else { None }), @@ -193,55 +275,51 @@ pub mod Observable_ { ) } - pub fn map( + pub fn map, T: Clone + 'static, U: Clone + 'static>( mapping: Func1, - source: LrcPtr>, + source: S, ) -> LrcPtr> { mkObservable(Func1::new( move |observer: LrcPtr>| -> LrcPtr { let mapping = mapping.clone(); - let obs = observer.clone(); + let guard = stoppedObserver(observer.clone()); + let on_next = guard.on_next.clone(); + let errorObserver = observer.clone(); + let on_error = guard.on_error.clone(); + let on_completed = guard.on_completed.clone(); let inner = mkObserver( - Func1::new(move |t: T| obs.OnNext(mapping(t))), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), + Func1::new(move |t: T| { + protect(|| on_next(mapping(t)), fwdError(errorObserver.clone())); + }), + on_error, + on_completed, ); - source.Subscribe(inner) + source.subscribe_source(inner) }, )) } - pub fn merge( - source1: LrcPtr>, - source2: LrcPtr>, + pub fn merge, S2: ObservableSource, T: Clone + 'static>( + source1: S1, + source2: S2, ) -> LrcPtr> { mkObservable(Func1::new( move |observer: LrcPtr>| -> LrcPtr { - let stopped = refCell(false); + let guard = stoppedObserver(observer.clone()); + let stopped = guard.stopped.clone(); + let on_next = guard.on_next.clone(); + let on_error = guard.on_error.clone(); let completed1 = refCell(false); let completed2 = refCell(false); let h1 = { - let stopped = stopped.clone(); let completed1 = completed1.clone(); let completed2 = completed2.clone(); - let obs_n = observer.clone(); - let obs_e = observer.clone(); let obs_c = observer.clone(); - let se = stopped.clone(); let sc = stopped.clone(); - source1.Subscribe(mkObserver( - Func1::new(move |v: T| { - if !*stopped.get() { - obs_n.OnNext(v); - } - }), - Func1::new(move |e: LrcPtr| { - if !*se.get() { - se.set(true); - obs_e.OnError(e); - } - }), + source1.subscribe_source(mkObserver( + on_next.clone(), + on_error.clone(), Func0::new(move || { if !*sc.get() { completed1.set(true); @@ -255,26 +333,13 @@ pub mod Observable_ { }; let h2 = { - let stopped = stopped.clone(); let completed1 = completed1.clone(); let completed2 = completed2.clone(); - let obs_n = observer.clone(); - let obs_e = observer.clone(); let obs_c = observer.clone(); - let se = stopped.clone(); let sc = stopped.clone(); - source2.Subscribe(mkObserver( - Func1::new(move |v: T| { - if !*stopped.get() { - obs_n.OnNext(v); - } - }), - Func1::new(move |e: LrcPtr| { - if !*se.get() { - se.set(true); - obs_e.OnError(e); - } - }), + source2.subscribe_source(mkObserver( + on_next, + on_error, Func0::new(move || { if !*sc.get() { completed2.set(true); @@ -295,8 +360,8 @@ pub mod Observable_ { )) } - pub fn pairwise( - source: LrcPtr>, + pub fn pairwise, T: Clone + 'static>( + source: S, ) -> LrcPtr>> { mkObservable(Func1::new( move |observer: LrcPtr>>| -> LrcPtr { @@ -312,14 +377,14 @@ pub mod Observable_ { fwdError(observer.clone()), fwdCompleted(observer.clone()), ); - source.Subscribe(inner) + source.subscribe_source(inner) }, )) } - pub fn partition( + pub fn partition, T: Clone + 'static>( predicate: Func1, - source: LrcPtr>, + source: S, ) -> LrcPtr<(LrcPtr>, LrcPtr>)> { let pred = predicate.clone(); let first = filter(predicate, source.clone()); @@ -327,44 +392,53 @@ pub mod Observable_ { LrcPtr::new((first, second)) } - pub fn scan( + pub fn scan, U: Clone + 'static, T: Clone + 'static>( collector: Func2, state: U, - source: LrcPtr>, + source: S, ) -> LrcPtr> { mkObservable(Func1::new( move |observer: LrcPtr>| -> LrcPtr { let collector = collector.clone(); let st = refCell(state.clone()); - let obs = observer.clone(); + let guard = stoppedObserver(observer.clone()); + let on_next = guard.on_next.clone(); + let errorObserver = observer.clone(); + let on_error = guard.on_error.clone(); + let on_completed = guard.on_completed.clone(); let inner = mkObserver( Func1::new(move |t: T| { - let u = collector(st.get().clone(), t); - st.set(u.clone()); - obs.OnNext(u); + protect( + || { + let u = collector(st.get().clone(), t); + st.set(u.clone()); + on_next(u); + }, + fwdError(errorObserver.clone()), + ); }), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), + on_error, + on_completed, ); - source.Subscribe(inner) + source.subscribe_source(inner) }, )) } - pub fn split( + pub fn split, T: Clone + 'static, U1: Clone + 'static, U2: Clone + 'static>( splitter: Func1>>, - source: LrcPtr>, + source: S, ) -> LrcPtr<(LrcPtr>, LrcPtr>)> { let sp = splitter.clone(); let first = choose( - Func1::new(move |v: T| match &*sp(v) { + Func1::new(move |v: T| match sp(v).as_ref() { Choice_2::Choice1Of2(x) => Some(x.clone()), _ => None, }), source.clone(), ); let second = choose( - Func1::new(move |v: T| match &*splitter(v) { + Func1::new(move |v: T| match splitter(v).as_ref() { Choice_2::Choice2Of2(x) => Some(x.clone()), _ => None, }), diff --git a/src/fable-library-rust/src/Path.rs b/src/fable-library-rust/src/Path.rs index d40f9d547e..39d4129bea 100644 --- a/src/fable-library-rust/src/Path.rs +++ b/src/fable-library-rust/src/Path.rs @@ -1,5 +1,6 @@ #[cfg(not(feature = "no_std"))] pub mod Path_ { + use crate::Native_::NullableRef; use crate::String_::{fromString, string}; fn is_sep(c: char) -> bool { @@ -24,12 +25,24 @@ pub mod Path_ { // or an empty string when there is no separator. Trailing separators on // the directory portion are trimmed, matching .NET semantics. pub fn getDirectoryName(path: string) -> string { + if path.is_null() { + return ::null(); + } + let s = path.as_str(); + if s.is_empty() || (s.len() == 1 && is_sep(s.chars().next().unwrap())) { + return ::null(); + } + match s.rfind(is_sep) { Some(i) => { let dir = &s[..i]; let dir = dir.trim_end_matches(is_sep); - fromString(dir.to_string()) + if dir.is_empty() && !s.is_empty() { + fromString(s[..1].to_string()) + } else { + fromString(dir.to_string()) + } } None => string(""), } diff --git a/src/fable-library-rust/src/Quotation.fs b/src/fable-library-rust/src/Quotation.fs index f10f2e517c..9cdebc2d7e 100644 --- a/src/fable-library-rust/src/Quotation.fs +++ b/src/fable-library-rust/src/Quotation.fs @@ -201,9 +201,140 @@ let isFieldGet (e: FSharpExpr) = Some(inst, pi, ([]: FSharpExpr list)) | _ -> None +let private joinStrings (separator: string) (parts: string list) = + match parts with + | [] -> "" + | first :: rest -> List.fold (fun result part -> result + separator + part) first rest + +let private operatorSymbol (methodName: string) = + match methodName with + | "op_Addition" -> Some "+" + | "op_Subtraction" -> Some "-" + | "op_Multiply" -> Some "*" + | "op_Division" -> Some "/" + | "op_Modulus" -> Some "%" + | "op_Exponentiation" -> Some "**" + | "op_Equality" -> Some "=" + | "op_Inequality" -> Some "<>" + | "op_LessThan" -> Some "<" + | "op_LessThanOrEqual" -> Some "<=" + | "op_GreaterThan" -> Some ">" + | "op_GreaterThanOrEqual" -> Some ">=" + | "op_BooleanAnd" -> Some "&&" + | "op_BooleanOr" -> Some "||" + | "op_UnaryNegation" -> Some "-" + | "op_LogicalNot" -> Some "not" + | _ -> None + +let private valueToString (value: obj) (typ: string) = + match typ with + | "string" -> "\"" + unbox value + "\"" + | "unit" -> "()" + | "bool" -> + if unbox value then + "true" + else + "false" + | "char" -> sprintf "%O" (unbox value) + | "int8" -> sprintf "%O" (unbox value) + | "uint8" -> sprintf "%O" (unbox value) + | "int16" -> sprintf "%O" (unbox value) + | "uint16" -> sprintf "%O" (unbox value) + | "int32" -> sprintf "%O" (unbox value) + | "uint32" -> sprintf "%O" (unbox value) + | "int64" -> sprintf "%O" (unbox value) + | "uint64" -> sprintf "%O" (unbox value) + | "nativeint" -> sprintf "%O" (unbox value) + | "unativeint" -> sprintf "%O" (unbox value) + | "float32" -> sprintf "%O" (unbox value) + | "float64" -> sprintf "%O" (unbox value) + | _ -> "" + +let rec exprToString (e: FSharpExpr) : string = + match e with + | ExprValue(value, typ) -> valueToString value typ + | ExprVarExpr v -> v.Name + | ExprLambda(v, body) -> "fun " + v.Name + " -> " + exprToString body + | ExprApplication(func, arg) -> exprToString func + " " + exprToString arg + | ExprLet(v, value, body) -> "let " + v.Name + " = " + exprToString value + " in " + exprToString body + | ExprIfThenElse(guard, thenExpr, elseExpr) -> + "if " + + exprToString guard + + " then " + + exprToString thenExpr + + " else " + + exprToString elseExpr + | ExprCall(_, methodName, args, _) -> + let renderedArgs = args |> List.ofArray |> List.map exprToString + + match operatorSymbol methodName, renderedArgs with + | Some symbol, [ left; right ] -> "(" + left + " " + symbol + " " + right + ")" + | Some symbol, [ operand ] -> symbol + operand + | _ -> methodName + "(" + joinStrings ", " renderedArgs + ")" + | ExprSequential(first, second) -> exprToString first + "; " + exprToString second + | ExprNewTuple elements -> + "(" + + (elements |> List.ofArray |> List.map exprToString |> joinStrings ", ") + + ")" + | ExprTupleGet(inner, index) -> "Item" + string (index + 1) + "(" + exprToString inner + ")" + | ExprFieldGet(inner, fieldName) -> exprToString inner + "." + fieldName + | _ -> "" + +let substitute (e: FSharpExpr) (substitution: FSharpVar -> FSharpExpr option) : FSharpExpr = + let rec substituteExpr expression = + match expression with + | ExprValue _ -> expression + | ExprVarExpr variable -> + match substitution variable with + | Some replacement -> replacement + | None -> expression + | ExprLambda(variable, body) -> ExprLambda(variable, substituteExpr body) + | ExprApplication(func, arg) -> ExprApplication(substituteExpr func, substituteExpr arg) + | ExprLet(variable, value, body) -> ExprLet(variable, substituteExpr value, substituteExpr body) + | ExprIfThenElse(guard, thenExpr, elseExpr) -> + ExprIfThenElse(substituteExpr guard, substituteExpr thenExpr, substituteExpr elseExpr) + | ExprCall(instance, methodName, args, declaringType) -> + let substitutedArgs = ResizeArray() + + for argument in args do + substitutedArgs.Add(substituteExpr argument) + + ExprCall(substituteExpr instance, methodName, substitutedArgs.ToArray(), declaringType) + | ExprSequential(first, second) -> ExprSequential(substituteExpr first, substituteExpr second) + | ExprNewTuple elements -> + let substitutedElements = ResizeArray() + + for element in elements do + substitutedElements.Add(substituteExpr element) + + ExprNewTuple(substitutedElements.ToArray()) + | ExprNewUnion(typeName, tag, caseName, fields) -> + let substitutedFields = ResizeArray() + + for field in fields do + substitutedFields.Add(substituteExpr field) + + ExprNewUnion(typeName, tag, caseName, substitutedFields.ToArray()) + | ExprNewRecord(typeName, fieldNames, values) -> + let substitutedValues = ResizeArray() + + for value in values do + substitutedValues.Add(substituteExpr value) + + ExprNewRecord(typeName, fieldNames, substitutedValues.ToArray()) + | ExprNewList(head, tail) -> ExprNewList(substituteExpr head, substituteExpr tail) + | ExprTupleGet(inner, index) -> ExprTupleGet(substituteExpr inner, index) + | ExprUnionTag inner -> ExprUnionTag(substituteExpr inner) + | ExprUnionField(inner, fieldIndex) -> ExprUnionField(substituteExpr inner, fieldIndex) + | ExprFieldGet(inner, fieldName) -> ExprFieldGet(substituteExpr inner, fieldName) + | ExprFieldSet(inner, fieldName, value) -> ExprFieldSet(substituteExpr inner, fieldName, substituteExpr value) + | ExprVarSet(target, value) -> ExprVarSet(substituteExpr target, substituteExpr value) + + substituteExpr e + // --- Free variables --- -let getFreeVars (e: FSharpExpr) : FSharpVar list = +let getFreeVars (e: FSharpExpr) : FSharpVar seq = let seen = System.Collections.Generic.HashSet() let acc = ResizeArray() @@ -223,7 +354,9 @@ let getFreeVars (e: FSharpExpr) : FSharpVar list = walk bound g walk bound t walk bound el - | ExprCall(_, _, args, _) -> + | ExprCall(instance, _, args, _) -> + walk bound instance + for a in args do walk bound a | ExprSequential(f, s) -> @@ -232,12 +365,29 @@ let getFreeVars (e: FSharpExpr) : FSharpVar list = | ExprNewTuple els -> for el in els do walk bound el + | ExprNewUnion(_, _, _, fields) -> + for field in fields do + walk bound field + | ExprNewRecord(_, _, values) -> + for value in values do + walk bound value + | ExprNewList(head, tail) -> + walk bound head + walk bound tail | ExprTupleGet(inner, _) -> walk bound inner + | ExprUnionTag inner -> walk bound inner + | ExprUnionField(inner, _) -> walk bound inner | ExprFieldGet(inner, _) -> walk bound inner + | ExprFieldSet(inner, _, value) -> + walk bound inner + walk bound value + | ExprVarSet(target, value) -> + walk bound target + walk bound value | _ -> () walk Set.empty e - List.ofSeq acc + acc.ToArray() |> Seq.ofArray // --- Evaluation (structural cases + common operators; SQL translation deconstructs // rather than evaluates, so this covers the tested subset). --- @@ -248,8 +398,8 @@ let private applyOperator (method: string) (args: obj list) : obj = // invalid at runtime. Matching only on the method string avoids that. // if/elif (not match): Fable-Rust would emit string literals as match patterns, // which isn't valid Rust; an if-chain compiles to string == comparisons. - let i (n: int) : int = unbox (List.item n args) - let b (n: int) : bool = unbox (List.item n args) + let i (n: int) : int = unbox(List.item n args) + let b (n: int) : bool = unbox(List.item n args) if method = "op_Addition" then box (i 0 + i 1) @@ -285,24 +435,64 @@ let private applyOperator (method: string) (args: obj list) : obj = failwithf "Cannot evaluate method: %s" method let evaluate (e: FSharpExpr) : obj = - let rec eval (env: Map) (e: FSharpExpr) : obj = + let rec eval (env: Map ref) (e: FSharpExpr) : obj = + let evalArrayField (values: obj[]) (index: int) = + if index < 0 || index >= values.Length then + failwith "Quotation field index is out of range" + + values.[index] + match e with - | ExprValue(v, _) -> v - | ExprVarExpr v -> Map.find v.Name env - | ExprLambda(v, body) -> box (fun (arg: obj) -> eval (Map.add v.Name arg env) body) + | ExprValue(v, typ) -> + match typ with + | "unit" -> v + | "list" -> box ([||]: obj[]) + | _ -> v + | ExprVarExpr v -> Map.find v.Name env.Value + | ExprLambda(v, body) -> + let capturedEnv = env.Value + box (fun (arg: obj) -> eval (ref (Map.add v.Name arg capturedEnv)) body) | ExprApplication(f, a) -> - let func = unbox obj> (eval env f) + let func = unbox obj>(eval env f) func (eval env a) - | ExprLet(v, value, body) -> eval (Map.add v.Name (eval env value) env) body + | ExprLet(v, value, body) -> + let value = eval env value + eval (ref (Map.add v.Name value env.Value)) body | ExprIfThenElse(g, t, el) -> - if unbox (eval env g) then + if unbox(eval env g) then eval env t else eval env el | ExprSequential(a, b) -> eval env a |> ignore eval env b - | ExprCall(_, method, args, _) -> applyOperator method [ for a in args -> eval env a ] + | ExprCall(instance, method, args, _) -> + let evaluatedArgs = [ for argument in args -> eval env argument ] + + match method with + | "get_IsSome" + | "get_IsNone" -> + let values = unbox(eval env instance) + let isSome = values.Length > 0 && unbox values.[0] = 1 + + box ( + if method = "get_IsSome" then + isSome + else + not isSome + ) + | "get_IsCons" + | "get_IsEmpty" -> + let values = unbox(eval env instance) + let isCons = values.Length > 0 + + box ( + if method = "get_IsCons" then + isCons + else + not isCons + ) + | _ -> applyOperator method evaluatedArgs // A tuple literal evaluates to a boxed obj[] of its evaluated elements, mirroring // PHP (which produces a plain array). TupleGet then indexes into that array; without // this arm `eval inner` on a tuple literal would fall through to failwith and there @@ -313,7 +503,129 @@ let evaluate (e: FSharpExpr) : obj = // operator wants an owned i32; `index + 0` forces an owned i32 rvalue (the // arithmetic dereferences the borrow) so the indexing type-checks. let i = index + 0 - (unbox (eval env inner)).[i] - | _ -> failwith "Cannot evaluate expression" + evalArrayField (unbox(eval env inner)) i + | ExprNewUnion(_, tag, _, fields) -> + let values = ResizeArray() + values.Add(box tag) + + for field in fields do + values.Add(eval env field) + + box (values.ToArray()) + | ExprNewRecord(_, fieldNames, values) -> + let result = ResizeArray() + + for i = 0 to fieldNames.Length - 1 do + result.Add(box fieldNames.[i]) + result.Add(eval env values.[i]) + + box (result.ToArray()) + | ExprNewList(head, tail) -> + let result = ResizeArray() + result.Add(eval env head) + + for value in unbox(eval env tail) do + result.Add(value) + + box (result.ToArray()) + | ExprUnionTag inner -> + let values = unbox(eval env inner) + evalArrayField values 0 + | ExprUnionField(inner, fieldIndex) -> + let values = unbox(eval env inner) + evalArrayField values (fieldIndex + 1) + | ExprFieldGet(inner, fieldName) -> + let values = unbox(eval env inner) + + if fieldName = "Head" then + evalArrayField values 0 + elif fieldName = "Tail" then + box values.[1..] + elif fieldName = "Value" && values.Length > 1 && unbox values.[0] = 1 then + values.[1] + else + let mutable result = None + let mutable index = 0 + + while index + 1 < values.Length && result.IsNone do + if unbox values.[index] = fieldName then + result <- Some values.[index + 1] + + index <- index + 2 + + match result with + | Some value -> value + | None -> failwithf "Quotation field not found: %s" fieldName + | ExprFieldSet(target, fieldName, value) -> + let target = unbox(eval env target) + let value = eval env value + + let rec findFieldIndex index = + if index + 1 >= target.Length then + None + elif unbox target.[index] = fieldName then + Some index + else + findFieldIndex (index + 2) + + match findFieldIndex 0 with + | Some index -> target.[index + 1] <- value + | None -> failwithf "Quotation field not found: %s" fieldName + + box 0 + | ExprVarSet(target, value) -> + let value = eval env value + + match target with + | ExprVarExpr variable -> env.Value <- Map.add variable.Name value env.Value + | _ -> failwith "Quotation VarSet target must be a variable" + + box 0 - eval Map.empty e + eval (ref Map.empty) e + +// --- DerivedPatterns ------------------------------------------------------ +// F# defines these on top of Patterns. AndAlso and OrElse recover the shape +// `&&` and `||` desugar into; SpecificCall matches a call by the identity of a +// template quotation rather than by compiled name. + +let private isBoolLit (want: bool) (e: FSharpExpr) = + match e with + | ExprValue(value, "bool") -> unbox value = want + | _ -> false + +/// a && b ==> if a then b else false +let isAndAlso (e: FSharpExpr) = + match e with + | ExprIfThenElse(g, t, el) when isBoolLit false el -> Some(g, t) + | _ -> None + +/// a || b ==> if a then true else b +let isOrElse (e: FSharpExpr) = + match e with + | ExprIfThenElse(g, t, el) when isBoolLit true t -> Some(g, el) + | _ -> None + +/// A template such as <@ (=) @> quotes as nested lambdas around the call, so +/// the identifying Call node has to be dug out before comparing. +let rec private templateCall (e: FSharpExpr) = + match e with + | ExprLambda(_, body) -> templateCall body + | ExprCall(_, m, _, dt) -> Some(m, dt) + | _ -> None + +/// Takes both arguments; the partial application that FSharp.Core expresses by +/// currying is built by the replacement instead (see quotationDerivedPatterns). +let isSpecificCall (template: FSharpExpr) (e: FSharpExpr) = + match templateCall template, e with + | Some(wantedName, wantedType), ExprCall(instance, m, args, dt) when m = wantedName && dt = wantedType -> + let inst = + match instance with + | ExprValue(_, "novalue") -> None + | _ -> Some instance + + // The middle slot is F#'s generic-argument list (Type list), which erases + // to obj on targets with no System.Type runtime, so the element type has + // to be obj rather than anything narrower. + Some(inst, (List.empty), List.ofArray args) + | _ -> None diff --git a/src/fable-library-rust/src/Reflection.rs b/src/fable-library-rust/src/Reflection.rs index 713b62ecc2..ae1ec58daa 100644 --- a/src/fable-library-rust/src/Reflection.rs +++ b/src/fable-library-rust/src/Reflection.rs @@ -2,14 +2,16 @@ pub mod Reflection_ { pub use core::any::TypeId; // re-export use crate::Microsoft::FSharp::Quotations::FSharpPropertyInfo; - use crate::Native_::{box_, Any, Func1, LrcPtr, Vec}; use crate::NativeArray_::{array_from, Array}; + use crate::Native_::{box_, box_lrc, unbox_lrc, Any, Func1, LrcPtr, MutCell, OnceInit, Vec}; use crate::String_::string; + #[cfg(feature = "no_std")] + use hashbrown as collections; #[cfg(not(feature = "no_std"))] - use core::cell::RefCell; - #[cfg(not(feature = "no_std"))] - use std::collections::HashMap; + use std::collections; + #[cfg(feature = "threaded")] + use std::sync::{OnceLock, RwLock}; pub fn name() -> string { // TODO: map some common type names to .NET type names @@ -19,8 +21,91 @@ pub mod Reflection_ { // The object (System.Object / obj) representation on the Rust target. type obj = LrcPtr; + #[cfg(not(feature = "lrc_ptr"))] + fn object_type_id(value: &obj) -> TypeId { + (&**value).type_id() + } + + #[cfg(feature = "lrc_ptr")] + fn object_type_id(value: &obj) -> TypeId { + (***value).type_id() + } + + #[cfg(not(feature = "threaded"))] + static TYPE_NAME_REGISTRY: OnceInit>> = + OnceInit::new(); + + #[cfg(feature = "threaded")] + static TYPE_NAME_REGISTRY: OnceLock>> = + OnceLock::new(); + + #[cfg(not(feature = "threaded"))] + fn register_type_name_value(tid: TypeId, name: string) { + TYPE_NAME_REGISTRY + .get_or_init(|| MutCell::new(collections::HashMap::new())) + .get_mut() + .insert(tid, name); + } + + #[cfg(feature = "threaded")] + fn register_type_name_value(tid: TypeId, name: string) { + TYPE_NAME_REGISTRY + .get_or_init(|| RwLock::new(collections::HashMap::new())) + .write() + .unwrap() + .insert(tid, name); + } + + #[cfg(not(feature = "threaded"))] + fn registered_type_name(tid: &TypeId) -> Option { + TYPE_NAME_REGISTRY + .get_or_init(|| MutCell::new(collections::HashMap::new())) + .get() + .get(tid) + .cloned() + } + + #[cfg(feature = "threaded")] + fn registered_type_name(tid: &TypeId) -> Option { + TYPE_NAME_REGISTRY + .get_or_init(|| RwLock::new(collections::HashMap::new())) + .read() + .unwrap() + .get(tid) + .cloned() + } + + fn canonical_type_name() -> string { + let rust_name = core::any::type_name::(); + let name = if TypeId::of::() == TypeId::of::() { + "System.String" + } else { + match rust_name { + "bool" => "System.Boolean", + "char" => "System.Char", + "i8" => "System.SByte", + "u8" => "System.Byte", + "i16" => "System.Int16", + "u16" => "System.UInt16", + "i32" => "System.Int32", + "u32" => "System.UInt32", + "i64" => "System.Int64", + "u64" => "System.UInt64", + "f32" => "System.Single", + "f64" => "System.Double", + _ => rust_name, + } + }; + string(name) + } + + pub fn register_type_name() { + register_type_name_value(TypeId::of::(), canonical_type_name::()); + } + // Compile-time helper emitted by `typeof` to obtain a concrete type id. pub fn type_id() -> TypeId { + register_type_name::(); TypeId::of::() } @@ -46,43 +131,59 @@ pub mod Reflection_ { } // Registry mapping a record's *concrete* TypeId to its reflection info. - // Populated when `typeof` is evaluated. This is what makes + // Populated by `typeof` and generated record reflection calls. This is what makes // value-based reflection (GetRecordFields(record)) possible: from a bare // boxed record we can recover its runtime TypeId and look the info up. // - // `thread_local!` is std-only, so under `no_std` there is no registry and - // the value-first entry points degrade (like exception catching does). + // Under `no_std`, the registry uses a non-threaded OnceInit-backed map. // Type-first reflection (typeof, MakeRecord, GetRecordElements) is // unaffected, as those carry the info in the `System.Type` value itself. - #[cfg(not(feature = "no_std"))] - thread_local! { - static RECORD_REGISTRY: RefCell> = - RefCell::new(HashMap::new()); - } - #[cfg(not(feature = "no_std"))] - fn registry_insert(tid: TypeId, info: RecordTypeInfo) { - RECORD_REGISTRY.with(|r| { - r.borrow_mut().insert(tid, info); - }); + #[cfg(not(feature = "threaded"))] + static RECORD_REGISTRY: OnceInit>>> = + OnceInit::new(); + + #[cfg(feature = "threaded")] + static RECORD_REGISTRY: OnceLock>>> = + OnceLock::new(); + + #[cfg(not(feature = "threaded"))] + fn registry_insert(tid: TypeId, info: LrcPtr) { + RECORD_REGISTRY + .get_or_init(|| MutCell::new(collections::HashMap::new())) + .get_mut() + .insert(tid, info); } - #[cfg(feature = "no_std")] - fn registry_insert(_tid: TypeId, _info: RecordTypeInfo) { - // no registry when no_std + #[cfg(feature = "threaded")] + fn registry_insert(tid: TypeId, info: LrcPtr) { + RECORD_REGISTRY + .get_or_init(|| RwLock::new(collections::HashMap::new())) + .write() + .unwrap() + .insert(tid, info); } - #[cfg(not(feature = "no_std"))] - fn registry_get(tid: &TypeId) -> Option { - RECORD_REGISTRY.with(|r| r.borrow().get(tid).cloned()) + #[cfg(not(feature = "threaded"))] + fn registry_get(tid: &TypeId) -> Option> { + RECORD_REGISTRY + .get_or_init(|| MutCell::new(collections::HashMap::new())) + .get() + .get(tid) + .cloned() } - #[cfg(feature = "no_std")] - fn registry_get(_tid: &TypeId) -> Option { - None // no registry when no_std + #[cfg(feature = "threaded")] + fn registry_get(tid: &TypeId) -> Option> { + RECORD_REGISTRY + .get_or_init(|| RwLock::new(collections::HashMap::new())) + .read() + .unwrap() + .get(tid) + .cloned() } - // Emitted by generated `typeof`. Builds the field metadata, registers + // Emitted by the generated record declaration's reflection method. Builds the field metadata, registers // the type by its concrete TypeId, and returns the boxed RecordTypeInfo // (which is what a `System.Type` value holds on the Rust target). pub fn recordType( @@ -92,6 +193,10 @@ pub mod Reflection_ { make: Func1, obj>, getters: Array>, ) -> obj { + if let Some(typ) = registry_get(&tid) { + return box_lrc(typ); + } + let names: Vec = field_names.get().iter().cloned().collect(); let gets: Vec> = getters.get().iter().cloned().collect(); let fields_vec: Vec> = names @@ -99,26 +204,20 @@ pub mod Reflection_ { .zip(gets.into_iter()) .map(|(n, g)| LrcPtr::new(RecordFieldInfo { name: n, get: g })) .collect(); - let info = RecordTypeInfo { + let info = LrcPtr::new(RecordTypeInfo { tid, name, fields: array_from(fields_vec), make, - }; + }); + register_type_name_value(tid, info.name.clone()); registry_insert(tid, info.clone()); - box_(info) - } - - fn type_info_of(typ: &obj) -> RecordTypeInfo { - (**typ) - .downcast_ref::() - .expect("Type does not carry record reflection info") - .clone() + box_lrc(info) } // FSharpValue.MakeRecord(typ, values, ?bindingFlags) -> obj pub fn makeRecord(typ: obj, values: Array, _flags: Option) -> obj { - let info = type_info_of(&typ); + let info = unbox_lrc::(typ); (info.make)(values) } @@ -126,7 +225,7 @@ pub mod Reflection_ { // The PropertyInfo carrier holds only a name (a quotation knows nothing more), so the // getter is looked up here, mirroring JS/TS where getValue(pi, v) reads v[pi.Name]. fn getterOf(record: &obj, name: &string) -> Func1 { - let tid = (**record).type_id(); + let tid = object_type_id(record); let info = registry_get(&tid).expect("Record type not registered; evaluate typeof first"); let field = info .fields @@ -140,7 +239,8 @@ pub mod Reflection_ { // FSharpType.GetRecordFields(typ, ?bindingFlags) -> PropertyInfo[] pub fn getRecordElements(typ: obj, _flags: Option) -> Array> { - let info = type_info_of(&typ); + let info = unbox_lrc::(typ); + // registry_insert(info.tid, info.clone()); let props: Vec> = info .fields .get() @@ -151,9 +251,9 @@ pub mod Reflection_ { } // FSharpValue.GetRecordFields(record, ?bindingFlags) -> obj[] - pub fn getRecordFields(record: obj, _flags: Option) -> Array { - let tid = (*record).type_id(); - let info = registry_get(&tid).expect("Record type not registered; evaluate typeof first"); + pub fn getRecordFields(record: obj, typ: obj, _flags: Option) -> Array { + let info = unbox_lrc::(typ); + registry_insert(info.tid, info.clone()); let vals: Vec = info .fields .get() @@ -193,6 +293,8 @@ pub mod Reflection_ { info.name.clone() } else if let Some(s) = (*typ).downcast_ref::() { s.clone() + } else if let Some(tid) = (*typ).downcast_ref::() { + registered_type_name(tid).unwrap_or_else(|| crate::String_::string("")) } else { crate::String_::string("") } @@ -201,25 +303,26 @@ pub mod Reflection_ { // obj.GetType() for a statically-erased value (static type = obj/Any). // Reads the boxed value's *runtime* type id and returns the concrete type's // registered reflection info, so `(box record : obj).GetType()` resolves to - // the real record type (not typeof) whenever that type was registered - // via a prior `typeof`. Falls back to returning the object itself as an - // opaque placeholder for unregistered types. + // the real record type (not typeof). Unregistered values receive a + // TypeId carrier, which still supports type identity and primitive names. pub fn getTypeFromObj(o: obj) -> obj { - let tid = (*o).type_id(); + let tid = object_type_id(&o); match registry_get(&tid) { - Some(info) => box_(info), - None => o, + Some(typ) => box_lrc(typ), + None => box_(tid), } } // System.Type equality (typeof = typeof). Compares the carried concrete - // TypeId when both operands are record type infos; otherwise falls back to - // comparing the boxed values' runtime type ids. + // TypeId when both operands are record type infos; otherwise compares the + // TypeId carried by each non-record type value. pub fn typeEquals(a: obj, b: obj) -> bool { let ta = (*a).downcast_ref::(); let tb = (*b).downcast_ref::(); match (ta, tb) { (Some(x), Some(y)) => x.tid == y.tid, + (Some(x), None) => (*b).downcast_ref::().map_or(false, |y| x.tid == *y), + (None, Some(y)) => (*a).downcast_ref::().map_or(false, |x| *x == y.tid), (None, None) => { // Non-record `System.Type` values carry a boxed concrete `TypeId`. // Downcast both and compare the *carried* ids; comparing the boxed @@ -227,11 +330,9 @@ pub mod Reflection_ { // and therefore make every non-record type compare equal. match ((*a).downcast_ref::(), (*b).downcast_ref::()) { (Some(x), Some(y)) => x == y, - _ => (*a).type_id() == (*b).type_id(), + _ => object_type_id(&a) == object_type_id(&b), } } - // One record, one non-record: definitely different types. - _ => false, } } } diff --git a/src/fable-library-rust/src/RegExp.rs b/src/fable-library-rust/src/RegExp.rs index 57071f7e9b..366728c947 100644 --- a/src/fable-library-rust/src/RegExp.rs +++ b/src/fable-library-rust/src/RegExp.rs @@ -193,7 +193,8 @@ pub mod RegExp_ { // * Singleline: 0x0010 // * IgnorePatternWhitespace: 0x0020 // * ECMAScript: 0x0100 (ignored) - if ((options & !(1 | 2 | 8 | 16 | 32 | 256)) != 0) { + // * NonBacktracking: 0x0400 (ignored) + if ((options & !(1 | 2 | 8 | 16 | 32 | 256 | 1024)) != 0) { panic!("Unsupported Regex Option"); } let regex_ = regex::RegexBuilder::new(pat.as_str()) diff --git a/src/fable-library-rust/src/Rune.rs b/src/fable-library-rust/src/Rune.rs new file mode 100644 index 0000000000..9cfaf78106 --- /dev/null +++ b/src/fable-library-rust/src/Rune.rs @@ -0,0 +1,283 @@ +pub mod Rune_ { + use crate::Char_::{ + GetNumericValue, GetUnicodeCategory, IsControl, IsDigit, IsLetter, IsLetterOrDigit, + IsLower, IsNumber, IsPunctuation, IsSeparator, IsSymbol, IsUpper, IsWhiteSpace, + ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant, + }; + use crate::Native_::MutCell; + use crate::String_::{ofChar, string}; + + pub fn zero() -> char { + '\u{0000}' + } + + pub fn replacementChar() -> char { + '\u{FFFD}' + } + + fn isValidScalar(value: u32) -> bool { + value <= 0x10FFFF && !(0xD800..=0xDFFF).contains(&value) + } + + fn newScalar(value: u32) -> char { + if isValidScalar(value) { + char::from_u32(value).unwrap() + } else { + panic!("Value is not a valid Unicode scalar value") + } + } + + pub fn newInt(value: i32) -> char { + if value < 0 { + panic!("Value is not a valid Unicode scalar value") + } + newScalar(value as u32) + } + + pub fn newUInt(value: u32) -> char { + newScalar(value) + } + + pub fn newPair(highSurrogate: char, lowSurrogate: char) -> char { + let high = highSurrogate as u32; + let low = lowSurrogate as u32; + if (0xD800..=0xDBFF).contains(&high) && (0xDC00..=0xDFFF).contains(&low) { + let value = 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00); + newScalar(value) + } else { + panic!("The values do not form a valid UTF-16 surrogate pair") + } + } + + pub fn tryCreateChar(value: char, result: &MutCell) -> bool { + result.set(value); + true + } + + pub fn tryCreatePair(highSurrogate: char, lowSurrogate: char, result: &MutCell) -> bool { + let high = highSurrogate as u32; + let low = lowSurrogate as u32; + if (0xD800..=0xDBFF).contains(&high) && (0xDC00..=0xDFFF).contains(&low) { + result.set(newScalar(0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00))); + true + } else { + result.set(zero()); + false + } + } + + pub fn tryCreateInt(value: i32, result: &MutCell) -> bool { + if value >= 0 && isValidScalar(value as u32) { + result.set(char::from_u32(value as u32).unwrap()); + true + } else { + result.set(zero()); + false + } + } + + pub fn tryCreateUInt(value: u32, result: &MutCell) -> bool { + if isValidScalar(value) { + result.set(char::from_u32(value).unwrap()); + true + } else { + result.set(zero()); + false + } + } + + pub fn value(value: char) -> i32 { + value as i32 + } + + pub fn utf8SequenceLength(value: char) -> i32 { + value.len_utf8() as i32 + } + + pub fn utf16SequenceLength(value: char) -> i32 { + value.len_utf16() as i32 + } + + pub fn isAscii(value: char) -> bool { + value.is_ascii() + } + + pub fn isBmp(value: char) -> bool { + value as u32 <= 0xFFFF + } + + pub fn plane(value: char) -> i32 { + (value as u32 >> 16) as i32 + } + + pub fn getNumericValue(value: char) -> f64 { + GetNumericValue(value) + } + + pub fn getUnicodeCategory(value: char) -> i32 { + GetUnicodeCategory(value) + } + + pub fn isControl(value: char) -> bool { + IsControl(value) + } + + pub fn isDigit(value: char) -> bool { + IsDigit(value) + } + + pub fn isLetter(value: char) -> bool { + IsLetter(value) + } + + pub fn isLetterOrDigit(value: char) -> bool { + IsLetterOrDigit(value) + } + + pub fn isLower(value: char) -> bool { + IsLower(value) + } + + pub fn isNumber(value: char) -> bool { + IsNumber(value) + } + + pub fn isPunctuation(value: char) -> bool { + IsPunctuation(value) + } + + pub fn isSeparator(value: char) -> bool { + IsSeparator(value) + } + + pub fn isSymbol(value: char) -> bool { + IsSymbol(value) + } + + pub fn isUpper(value: char) -> bool { + IsUpper(value) + } + + pub fn isWhiteSpace(value: char) -> bool { + IsWhiteSpace(value) + } + + pub fn isValidInt(value: i32) -> bool { + value >= 0 && isValidScalar(value as u32) + } + + pub fn isValidUInt(value: u32) -> bool { + isValidScalar(value) + } + + pub fn toLower(value: char) -> char { + ToLower(value) + } + + pub fn toLowerInvariant(value: char) -> char { + ToLowerInvariant(value) + } + + pub fn toUpper(value: char) -> char { + ToUpper(value) + } + + pub fn toUpperInvariant(value: char) -> char { + ToUpperInvariant(value) + } + + fn tryGetRune(input: &string, index: i32) -> Option { + if index < 0 { + return None; + } + + let target = index as usize; + let mut offset = 0; + for value in input.as_str().chars() { + if target == offset { + return Some(value); + } + + offset += value.len_utf16(); + if target < offset { + return None; + } + } + + None + } + + pub fn getRuneAt(input: string, index: i32) -> char { + match tryGetRune(&input, index) { + Some(value) => value, + None => panic!("Cannot extract a Unicode scalar value from the specified index") + } + } + + pub fn tryGetRuneAt(input: string, index: i32, result: &MutCell) -> bool { + if index < 0 { + panic!("Index was out of range") + } + + let target = index as usize; + let mut offset = 0; + for value in input.as_str().chars() { + if target == offset { + result.set(value); + return true; + } + + offset += value.len_utf16(); + if target < offset { + result.set(zero()); + return false; + } + } + + panic!("Index was out of range") + } + + pub fn parse(input: string) -> char { + let mut chars = input.as_str().chars(); + + match (chars.next(), chars.next()) { + (Some(value), None) => value, + _ => panic!("Input must contain exactly one Unicode scalar value"), + } + } + + pub fn tryParse(input: string, result: &MutCell) -> bool { + let mut chars = input.as_str().chars(); + + match (chars.next(), chars.next()) { + (Some(value), None) => { + result.set(value); + true + } + _ => { + result.set(zero()); + false + } + } + } + + pub fn toString(value: char) -> string { + ofChar(value) + } + + pub fn compareTo(left: char, right: char) -> i32 { + match left.cmp(&right) { + core::cmp::Ordering::Less => -1, + core::cmp::Ordering::Equal => 0, + core::cmp::Ordering::Greater => 1, + } + } + + pub fn equals(left: char, right: char) -> bool { + left == right + } + + pub fn getHashCode(value: char) -> i32 { + value as i32 + } +} diff --git a/src/fable-library-rust/src/Seq.fs b/src/fable-library-rust/src/Seq.fs index 76c9fbddce..eb818d6c63 100644 --- a/src/fable-library-rust/src/Seq.fs +++ b/src/fable-library-rust/src/Seq.fs @@ -1328,20 +1328,36 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T seq) : 'T seq = ofArray arr let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = toArray xs + Array.randomShuffleInPlaceWith random arr + ofArray arr let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceBy randomizer + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceWith random + randomChoiceSource 1 xs |> Array.randomChoiceWith random -let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice +// Draws on enumeration, like FSharp.Core let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = - xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = randomChoicesBy (fun () -> random.NextDouble()) count xs @@ -1353,7 +1369,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T se xs |> toArray |> Array.randomSampleBy randomizer count |> ofArray let randomSampleWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = - randomSampleBy (fun () -> random.NextDouble()) count xs + xs |> toArray |> Array.randomSampleWith random count |> ofArray let randomSample (count: int) (xs: 'T seq) : 'T seq = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-rust/src/Set.fs b/src/fable-library-rust/src/Set.fs index da6fd60563..e7fc3fdbba 100644 --- a/src/fable-library-rust/src/Set.fs +++ b/src/fable-library-rust/src/Set.fs @@ -638,6 +638,8 @@ let toSeq (s: Set<'T>) = ) ) +let toEnumerable (s: Set<'T>) = toSeq s + let compareTo (s1: Set<'T>) (s2: Set<'T>) = // LanguagePrimitives.GenericComparison s1 s2 Seq.compareWith compare (toSeq s1) (toSeq s2) diff --git a/src/fable-library-rust/src/String.rs b/src/fable-library-rust/src/String.rs index 86dcab77c2..ce682d23d8 100644 --- a/src/fable-library-rust/src/String.rs +++ b/src/fable-library-rust/src/String.rs @@ -5,7 +5,7 @@ pub mod String_ { // Strings // ----------------------------------------------------------- - use crate::Native_::{compare, getHashCode, Func1, Func2, Hashable, NullableRef, String, ToString, Vec}; + use crate::Native_::{compare, iter_to_seq, hash_code, seq_to_iter, Func1, Func2, Hashable, NullableRef, Seq, String, ToString, Vec}; use crate::NativeArray_::{array_from, Array}; use core::cmp::Ordering; @@ -54,8 +54,11 @@ pub mod String_ { LrcStr(Some(Lrc::from(s))) } - pub fn fromIter(iter: impl Iterator + Clone) -> string { - let s = iter.collect::(); + pub fn fromIter(it: I) -> string + where + I: Iterator + { + let s = it.collect::(); LrcStr(Some(Lrc::from(s))) } } @@ -130,26 +133,33 @@ pub mod String_ { fromSlice(s.as_str()) } - pub fn fromIter(iter: impl Iterator + Clone) -> string { - let len: usize = iter - .clone() - .take(INLINE_MAX + 1) - .map(|c| c.len_utf8()) - .sum(); - if len <= INLINE_MAX { - let mut buf = [0u8; INLINE_MAX]; - let mut pos: usize = 0; - for c in iter { + pub fn fromIter(it: I) -> string + where + I: Iterator + { + let mut len: usize = 0; + let mut pos: usize = 0; + let mut buf = [0u8; INLINE_MAX]; + let mut str: String = String::new(); + for c in it { + len += c.len_utf8(); + if len <= INLINE_MAX { let s = c.encode_utf8(&mut buf[pos..]); pos = pos + s.len(); + } else { + if str.len() == 0 { + str.push_str(core::str::from_utf8(&buf[0..pos]).unwrap()); + } + str.push(c); } + } + if len <= INLINE_MAX { LrcStr::Inline { len: len as u8, buf: buf, } } else { - let s = iter.collect::(); - LrcStr::Shared(Lrc::from(s)) + LrcStr::Shared(Lrc::from(str)) } } } @@ -275,7 +285,7 @@ pub mod String_ { impl Hashable for string { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } @@ -318,6 +328,43 @@ pub mod String_ { } } + // ----------------------------------------------------------- + // IEnumerable + // ----------------------------------------------------------- + + struct StringIterator { + str: string, + pos: usize, + } + + impl Iterator for StringIterator { + type Item = char; + + fn next(&mut self) -> Option { + match self.str.as_str()[self.pos..].chars().next() { + Some(c) => { + self.pos += c.len_utf8(); + Some(c) + }, + None => { None } + } + } + } + + pub fn toSeq(s: string) -> Seq { + let it = StringIterator { str: s, pos: 0 }; + iter_to_seq(it) + } + + pub fn ofSeq(seq: Seq) -> string { + let it = seq_to_iter(seq); + fromIter(it) + } + + pub fn enumerateRunes(s: string) -> Seq { + toSeq(s) + } + // ----------------------------------------------------------- // string implementation // ----------------------------------------------------------- diff --git a/src/fable-library-rust/src/System.Collections.Generic.fs b/src/fable-library-rust/src/System.Collections.Generic.fs index ac2461e630..e4e5f79f17 100644 --- a/src/fable-library-rust/src/System.Collections.Generic.fs +++ b/src/fable-library-rust/src/System.Collections.Generic.fs @@ -48,7 +48,7 @@ type Stack<'T when 'T: equality> private (initialContents: 'T[], initialCount) = new(initialCapacity: int) = - let arr = Array.zeroCreate<'T> (initialCapacity) + let arr = Array.zeroCreate<'T>(initialCapacity) Stack<'T>(arr, 0) new() = Stack<'T>(4) @@ -177,7 +177,7 @@ type Queue<'T when 'T: equality> private (initialContents, initialCount) = if initialCapacity < 0 then failwith "capacity is less than 0" - Queue<'T>(Array.zeroCreate<'T> (initialCapacity), 0) + Queue<'T>(Array.zeroCreate<'T>(initialCapacity), 0) new() = Queue<'T>(4) diff --git a/src/fable-library-rust/src/System.Globalization.fs b/src/fable-library-rust/src/System.Globalization.fs new file mode 100644 index 0000000000..c92538679c --- /dev/null +++ b/src/fable-library-rust/src/System.Globalization.fs @@ -0,0 +1,6 @@ +namespace System.Globalization + +open Global_ + +type CultureInfo() = + static member InvariantCulture = CultureInfo() diff --git a/src/fable-library-rust/src/System.Text.fs b/src/fable-library-rust/src/System.Text.fs index 0735dde387..3a57ee61a7 100644 --- a/src/fable-library-rust/src/System.Text.fs +++ b/src/fable-library-rust/src/System.Text.fs @@ -13,8 +13,12 @@ type StringBuilder(value: string, capacity: int) = new(value: string) = StringBuilder(value, 16) new() = StringBuilder("", 16) - member x.Append(s: string) = - buf.Add(s) + member x.Append(s: string | null) = + buf.Add(string s) + x + + member x.Append(s: string | null, startIndex: int, count: int) = + buf.Add((string s).Substring(startIndex, count)) x member x.Append(o: bool) = x.Append(string o) @@ -36,8 +40,6 @@ type StringBuilder(value: string, capacity: int) = member x.Append(o: float32) = x.Append(string o) member x.Append(o: float) = x.Append(string o) - member x.Append(s: string, index: int, count: int) = x.Append(s.Substring(index, count)) - member x.Append(cs: char[]) = x.Append(System.String(cs)) member x.Append(sb: StringBuilder) = x.Append(sb.ToString()) // member x.Append(o: obj) = x.Append(string o) @@ -106,4 +108,6 @@ type StringBuilder(value: string, capacity: int) = override _.ToString() = System.String.Concat(buf |> asArray) - member x.ToString(index: int, count: int) = x.ToString().Substring(index, count) + member x.ToString(startIndex: int, length: int) = + let str = x.ToString() + str.Substring(startIndex, length) diff --git a/src/fable-library-rust/src/TimeOnly.rs b/src/fable-library-rust/src/TimeOnly.rs index 2cc57dcc1c..925e1c7e88 100644 --- a/src/fable-library-rust/src/TimeOnly.rs +++ b/src/fable-library-rust/src/TimeOnly.rs @@ -3,7 +3,7 @@ pub mod TimeOnly_ { use crate::{ DateTime_::{DateTime, duration_to_ticks, ticks_to_duration}, Format::DateTime::{format_dotnet_custom, split_meridiem_suffix, try_parse_time_str}, - Native_::{Hashable, MutCell, ToString, compare, format, getHashCode}, + Native_::{Hashable, MutCell, ToString, compare, format, hash_code}, String_::{fromString, string}, TimeSpan_::{TimeSpan, nanoseconds_per_tick, ticks_per_day}, }; @@ -23,7 +23,7 @@ pub mod TimeOnly_ { impl Hashable for TimeOnly { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } diff --git a/src/fable-library-rust/src/TimeSpan.rs b/src/fable-library-rust/src/TimeSpan.rs index b387b13c61..cb58cf0102 100644 --- a/src/fable-library-rust/src/TimeSpan.rs +++ b/src/fable-library-rust/src/TimeSpan.rs @@ -1,7 +1,7 @@ pub mod TimeSpan_ { use crate::{ Format::TimeSpan::{format_time_span, try_parse_time_span_str}, - Native_::{Hashable, MutCell, ToString, compare, getHashCode}, + Native_::{Hashable, MutCell, ToString, compare, hash_code}, String_::{fromString, string}, }; use core::ops::{Add, Div, Mul, Sub}; @@ -24,6 +24,11 @@ pub mod TimeSpan_ { pub const min_value: TimeSpan = TimeSpan { ticks: i64::MIN }; pub const max_value: TimeSpan = TimeSpan { ticks: i64::MAX }; + #[inline] + fn truncate_f64(value: f64) -> f64 { + value as i64 as f64 + } + impl core::fmt::Debug for TimeSpan { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "{}", self.toString(string(""))) @@ -39,7 +44,7 @@ pub mod TimeSpan_ { impl Hashable for TimeSpan { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(&self.ticks()) + hash_code(&self.ticks()) } } @@ -231,27 +236,27 @@ pub mod TimeSpan_ { } pub fn hours(&self) -> i32 { - (self.totalHours() - self.totalDays().trunc() * 24.0) as i32 + (self.totalHours() - truncate_f64(self.totalDays()) * 24.0) as i32 } pub fn minutes(&self) -> i32 { - (self.totalMinutes() - self.totalHours().trunc() * 60.0) as i32 + (self.totalMinutes() - truncate_f64(self.totalHours()) * 60.0) as i32 } pub fn seconds(&self) -> i32 { - (self.totalSeconds() - self.totalMinutes().trunc() * 60.0) as i32 + (self.totalSeconds() - truncate_f64(self.totalMinutes()) * 60.0) as i32 } pub fn milliseconds(&self) -> i32 { - (self.totalMilliseconds() - self.totalSeconds().trunc() * 1000.0) as i32 + (self.totalMilliseconds() - truncate_f64(self.totalSeconds()) * 1000.0) as i32 } pub fn microseconds(&self) -> i32 { - (self.totalMicroseconds() - self.totalMilliseconds().trunc() * 1000.0) as i32 + (self.totalMicroseconds() - truncate_f64(self.totalMilliseconds()) * 1000.0) as i32 } pub fn nanoseconds(&self) -> i32 { - (self.totalNanoseconds() - self.totalMicroseconds().trunc() * 1000.0) as i32 + (self.totalNanoseconds() - truncate_f64(self.totalMicroseconds()) * 1000.0) as i32 } pub fn negate(&self) -> TimeSpan { diff --git a/src/fable-library-rust/src/Uri.rs b/src/fable-library-rust/src/Uri.rs new file mode 100644 index 0000000000..069ae57beb --- /dev/null +++ b/src/fable-library-rust/src/Uri.rs @@ -0,0 +1,591 @@ +#[cfg_attr(rustfmt, rustfmt::skip)] +pub mod Uri_ { + use crate::Native_::{format, LrcPtr, MutCell, String, ToString, Vec}; + use crate::String_::{fromSlice, fromString, string}; + + // UriKind: 0 = RelativeOrAbsolute, 1 = Absolute, 2 = Relative + + #[derive(Clone, Default, PartialEq)] + pub struct Uri { + isAbsolute: bool, + hasAuthority: bool, // true for scheme://authority/... , false for scheme:path (mailto:, tel:, urn:) + original: string, + relative: string, // raw string for relative URIs + schemePart: string, + userInfoPart: string, + hostPart: string, + port: i32, // -1 when no explicit port + pathPart: string, + queryPart: string, // "" or "?..." + fragmentPart: string, // "" or "#..." + } + + // ------------------------------------------------------------------- + // helpers + // ------------------------------------------------------------------- + + // Returns the byte index of the ':' that terminates a valid URI scheme + // prefix (scheme = ALPHA *(ALPHA / DIGIT / "+" / "-" / ".")), or None when + // `s` has no scheme (i.e. it is a relative reference). This recognizes both + // `scheme://authority/...` and authority-less forms like `mailto:a@b.com`. + fn scheme_end(s: &str) -> Option { + let bytes = s.as_bytes(); + match bytes.first() { + Some(&c) if (c as char).is_ascii_alphabetic() => {} + _ => return None, + } + let mut i = 1; + while i < bytes.len() { + let c = bytes[i] as char; + if c == ':' { + return Some(i); + } else if c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.' { + i += 1; + } else { + return None; + } + } + None + } + + fn is_absolute_str(s: &str) -> bool { + scheme_end(s).is_some() + } + + // RFC 3986 §5.2.4 "remove dot segments": collapses `.` and `..` in a path. + fn remove_dot_segments(path: &str) -> String { + let mut input = path.to_string(); + let mut output = String::new(); + while !input.is_empty() { + if input.starts_with("../") { + input.replace_range(0..3, ""); + } else if input.starts_with("./") { + input.replace_range(0..2, ""); + } else if input.starts_with("/./") { + input.replace_range(0..3, "/"); + } else if input == "/." { + input = "/".to_string(); + } else if input.starts_with("/../") { + input.replace_range(0..4, "/"); + remove_last_segment(&mut output); + } else if input == "/.." { + input = "/".to_string(); + remove_last_segment(&mut output); + } else if input == "." || input == ".." { + input.clear(); + } else { + let start = if input.starts_with('/') { 1 } else { 0 }; + let end = match input[start..].find('/') { + Some(i) => start + i, + None => input.len(), + }; + output.push_str(&input[..end]); + input.replace_range(0..end, ""); + } + } + output + } + + fn remove_last_segment(output: &mut String) { + match output.rfind('/') { + Some(i) => output.truncate(i), + None => output.clear(), + } + } + + fn is_default_port(scheme: &str, port: i32) -> bool { + port == -1 + || (scheme == "http" && port == 80) + || (scheme == "https" && port == 443) + || (scheme == "ftp" && port == 21) + || (scheme == "ws" && port == 80) + || (scheme == "wss" && port == 443) + } + + fn authority_str(scheme: &str, user_info: &str, host: &str, port: i32) -> String { + let host_port = if is_default_port(scheme, port) { + host.to_string() + } else { + format!("{}:{}", host, port) + }; + if user_info.is_empty() { + host_port + } else { + format!("{}@{}", user_info, host_port) + } + } + + fn hex_val(b: u8) -> Option { + match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'f' => Some(b - b'a' + 10), + b'A'..=b'F' => Some(b - b'A' + 10), + _ => None, + } + } + + fn percent_decode(s: &str) -> String { + let bytes = s.as_bytes(); + let mut out: Vec = Vec::new(); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'%' && i + 2 < bytes.len() { + if let (Some(h), Some(l)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) { + out.push(h * 16 + l); + i += 3; + continue; + } + } + out.push(bytes[i]); + i += 1; + } + String::from_utf8_lossy(&out).into_owned() + } + + // parse scheme://host:port/path?query#fragment , or the authority-less + // form scheme:path?query#fragment (mailto:, tel:, urn:, ...). + // Returns (scheme, hasAuthority, userInfo, host, port, path, query, fragment). + fn parse_absolute(s: &str) -> (String, bool, String, String, i32, String, String, String) { + let idx = scheme_end(s).unwrap(); + let scheme = s[..idx].to_ascii_lowercase(); + let after = &s[idx + 1..]; // everything after "scheme:" + + match after.strip_prefix("//") { + Some(rest) => { + // scheme://authority/path?query#fragment + let auth_end = rest.find(|c| c == '/' || c == '?' || c == '#').unwrap_or(rest.len()); + let authority = &rest[..auth_end]; + let remainder = &rest[auth_end..]; + + let (user_info, host_port) = match authority.rfind('@') { + Some(index) => (authority[..index].to_string(), &authority[index + 1..]), + None => (String::new(), authority), + }; + let (host, port) = if host_port.starts_with('[') { + match host_port.find(']') { + Some(end) => { + let host = host_port[..=end].to_ascii_lowercase(); + let port = host_port[end + 1..] + .strip_prefix(':') + .and_then(|value| value.parse::().ok()) + .unwrap_or(-1); + (host, port) + } + None => (host_port.to_ascii_lowercase(), -1), + } + } else { + match host_port.rfind(':') { + Some(ci) => { + let h = &host_port[..ci]; + let p = &host_port[ci + 1..]; + match p.parse::() { + Ok(n) => (h.to_ascii_lowercase(), n), + Err(_) => (host_port.to_ascii_lowercase(), -1), + } + } + None => (host_port.to_ascii_lowercase(), -1), + } + }; + + let (before_frag, fragment) = match remainder.find('#') { + Some(fi) => (&remainder[..fi], remainder[fi..].to_string()), + None => (remainder, String::new()), + }; + let (path, query) = match before_frag.find('?') { + Some(qi) => (before_frag[..qi].to_string(), before_frag[qi..].to_string()), + None => (before_frag.to_string(), String::new()), + }; + let path = if path.is_empty() { "/".to_string() } else { path }; + (scheme, true, user_info, host, port, path, query, fragment) + } + None => { + // authority-less: scheme:path?query#fragment (no host/port) + let (before_frag, fragment) = match after.find('#') { + Some(fi) => (&after[..fi], after[fi..].to_string()), + None => (after, String::new()), + }; + let (path, query) = match before_frag.find('?') { + Some(qi) => (before_frag[..qi].to_string(), before_frag[qi..].to_string()), + None => (before_frag.to_string(), String::new()), + }; + (scheme, false, String::new(), String::new(), -1, path, query, fragment) + } + } + } + + fn parse_abs_uri(orig: &str) -> Uri { + let (scheme, has_authority, user_info, host, port, path, query, fragment) = parse_absolute(orig); + Uri { + isAbsolute: true, + hasAuthority: has_authority, + original: fromSlice(orig), + relative: string(""), + schemePart: fromString(scheme), + userInfoPart: fromString(user_info), + hostPart: fromString(host), + port, + pathPart: fromString(path), + queryPart: fromString(query), + fragmentPart: fromString(fragment), + } + } + + fn make_relative(orig: &str) -> Uri { + Uri { + isAbsolute: false, + hasAuthority: false, + original: fromSlice(orig), + relative: fromSlice(orig), + schemePart: string(""), + userInfoPart: string(""), + hostPart: string(""), + port: -1, + pathPart: string(""), + queryPart: string(""), + fragmentPart: string(""), + } + } + + // resolve a relative reference against an absolute base URI + fn resolve(base: &Uri, rel: &str) -> Uri { + if is_absolute_str(rel) { + return parse_abs_uri(rel); + } + let (before_frag, fragment) = match rel.find('#') { + Some(fi) => (&rel[..fi], rel[fi..].to_string()), + None => (rel, String::new()), + }; + let (rel_path, query) = match before_frag.find('?') { + Some(qi) => (before_frag[..qi].to_string(), Some(before_frag[qi..].to_string())), + None => (before_frag.to_string(), None), + }; + let rel_path_is_empty = rel_path.is_empty(); + let base_path = base.pathPart.as_str(); + let merged_path = if rel_path.is_empty() { + base_path.to_string() + } else if rel_path.starts_with('/') { + rel_path + } else { + let cut = base_path.rfind('/').map(|i| i + 1).unwrap_or(0); + format!("{}{}", &base_path[..cut], rel_path) + }; + // RFC 3986 §5.2.4: collapse `.`/`..` segments in the merged path. + let new_path = remove_dot_segments(&merged_path); + let query = match query { + Some(query) => query, + None if rel_path_is_empty => base.queryPart.as_str().to_string(), + None => String::new(), + }; + let fragment = match rel.find('#') { + Some(_) => fragment, + None if rel.is_empty() => base.fragmentPart.as_str().to_string(), + None => String::new(), + }; + let scheme = base.schemePart.as_str(); + let user_info = base.userInfoPart.as_str(); + let host = base.hostPart.as_str(); + let orig = if base.hasAuthority { + let auth = authority_str(scheme, user_info, host, base.port); + format!("{}://{}{}{}{}", scheme, auth, new_path, query, fragment) + } else { + format!("{}:{}{}{}", scheme, new_path, query, fragment) + }; + Uri { + isAbsolute: true, + hasAuthority: base.hasAuthority, + original: fromString(orig), + relative: string(""), + schemePart: base.schemePart.clone(), + userInfoPart: base.userInfoPart.clone(), + hostPart: base.hostPart.clone(), + port: base.port, + pathPart: fromString(new_path), + queryPart: fromString(query), + fragmentPart: fromString(fragment), + } + } + + fn build_from_string(orig: &str, kind: i32) -> Result { + let is_abs = is_absolute_str(orig); + match kind { + 1 => { + // Absolute + if is_abs { + Ok(parse_abs_uri(orig)) + } else { + Err("Invalid URI: The format of the URI could not be determined.") + } + } + 2 => { + // Relative + if is_abs { + Err("URI is not a relative path.") + } else { + Ok(make_relative(orig)) + } + } + _ => { + // RelativeOrAbsolute + if is_abs { + Ok(parse_abs_uri(orig)) + } else { + Ok(make_relative(orig)) + } + } + } + } + + // ------------------------------------------------------------------- + // instance surface + // ------------------------------------------------------------------- + + impl Uri { + fn ensure_absolute(&self) { + if !self.isAbsolute { + panic!("This operation is not supported for a relative URI."); + } + } + + fn build_absolute(&self) -> String { + let scheme = self.schemePart.as_str(); + if self.hasAuthority { + let auth = authority_str( + scheme, + self.userInfoPart.as_str(), + self.hostPart.as_str(), + self.port, + ); + format!( + "{}://{}{}{}{}", + scheme, + auth, + self.pathPart.as_str(), + self.queryPart.as_str(), + self.fragmentPart.as_str() + ) + } else { + // authority-less scheme (mailto:, tel:, urn:): no `//host`. + format!( + "{}:{}{}{}", + scheme, + self.pathPart.as_str(), + self.queryPart.as_str(), + self.fragmentPart.as_str() + ) + } + } + + pub fn isAbsoluteUri(&self) -> bool { + self.isAbsolute + } + + pub fn scheme(&self) -> string { + self.ensure_absolute(); + self.schemePart.clone() + } + + pub fn host(&self) -> string { + self.ensure_absolute(); + self.hostPart.clone() + } + + pub fn port(&self) -> i32 { + self.ensure_absolute(); + if self.port != -1 { + self.port + } else { + match self.schemePart.as_str() { + "http" => 80, + "https" => 443, + "ftp" => 21, + "ws" => 80, + "wss" => 443, + _ => -1, + } + } + } + + pub fn isDefaultPort(&self) -> bool { + self.ensure_absolute(); + is_default_port(self.schemePart.as_str(), self.port) + } + + pub fn absolutePath(&self) -> string { + self.ensure_absolute(); + self.pathPart.clone() + } + + pub fn query(&self) -> string { + self.ensure_absolute(); + self.queryPart.clone() + } + + pub fn fragment(&self) -> string { + self.ensure_absolute(); + self.fragmentPart.clone() + } + + pub fn pathAndQuery(&self) -> string { + self.ensure_absolute(); + fromString(format!("{}{}", self.pathPart.as_str(), self.queryPart.as_str())) + } + + pub fn absoluteUri(&self) -> string { + self.ensure_absolute(); + fromString(self.build_absolute()) + } + + pub fn originalString(&self) -> string { + self.original.clone() + } + } + + impl core::fmt::Display for Uri { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + if self.isAbsolute { + write!(f, "{}", percent_decode(&self.build_absolute())) + } else { + write!(f, "{}", self.relative.as_str()) + } + } + } + + impl core::fmt::Debug for Uri { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::Display::fmt(self, f) + } + } + + // ------------------------------------------------------------------- + // constructors (free functions, mirror System.Uri overloads) + // ------------------------------------------------------------------- + + pub fn create(uriString: string) -> LrcPtr { + createWithKind(uriString, 1) + } + + pub fn createWithKind(uriString: string, kind: i32) -> LrcPtr { + match build_from_string(uriString.as_str(), kind) { + Ok(u) => LrcPtr::new(u), + Err(msg) => panic!("{}", msg), + } + } + + pub fn createFromString(baseUri: LrcPtr, relativeUri: string) -> LrcPtr { + if !baseUri.isAbsolute { + panic!("Invalid URI: A base URI cannot be created because the 'baseUri' parameter represents a relative URI."); + } + LrcPtr::new(resolve(&baseUri, relativeUri.as_str())) + } + + pub fn createFromUri(baseUri: LrcPtr, relativeUri: LrcPtr) -> LrcPtr { + if !baseUri.isAbsolute { + panic!("Invalid URI: A base URI cannot be created because the 'baseUri' parameter represents a relative URI."); + } + if relativeUri.isAbsolute { + // .NET: when the second URI is absolute it wins, and the base is ignored. + relativeUri.clone() + } else { + LrcPtr::new(resolve(&baseUri, relativeUri.relative.as_str())) + } + } + + pub fn tryCreateWithKind(uriString: string, kind: i32, res: &MutCell>) -> bool { + match build_from_string(uriString.as_str(), kind) { + Ok(u) => { + res.set(LrcPtr::new(u)); + true + } + Err(_) => false, + } + } + + pub fn tryCreateFromString( + baseUri: LrcPtr, + relativeUri: string, + res: &MutCell>, + ) -> bool { + if !baseUri.isAbsolute { + return false; + } + res.set(LrcPtr::new(resolve(&baseUri, relativeUri.as_str()))); + true + } + + pub fn tryCreateFromUri( + baseUri: LrcPtr, + relativeUri: LrcPtr, + res: &MutCell>, + ) -> bool { + if !baseUri.isAbsolute { + return false; + } + if relativeUri.isAbsolute { + // .NET: when the second URI is absolute it wins, and the base is ignored. + res.set(relativeUri.clone()); + } else { + res.set(LrcPtr::new(resolve(&baseUri, relativeUri.relative.as_str()))); + } + true + } + + // --- Percent-encoding (RFC 3986). Encoding operates on UTF-8 bytes. --- + + fn is_unreserved(b: u8) -> bool { + b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') + } + + // gen-delims and sub-delims, kept intact by EscapeUriString. + fn is_reserved(b: u8) -> bool { + matches!( + b, + b':' | b'/' | b'?' | b'#' | b'[' | b']' | b'@' + | b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'=' + ) + } + + fn escape_with(s: &str, keep: fn(u8) -> bool) -> string { + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + let mut out = String::with_capacity(s.len()); + for b in s.bytes() { + if keep(b) { + out.push(b as char); + } else { + out.push('%'); + out.push(HEX[(b >> 4) as usize] as char); + out.push(HEX[(b & 0x0F) as usize] as char); + } + } + fromString(out) + } + + pub fn escapeDataString(s: string) -> string { + escape_with(s.as_str(), is_unreserved) + } + + pub fn escapeUriString(s: string) -> string { + fn keep(b: u8) -> bool { + is_unreserved(b) || is_reserved(b) + } + escape_with(s.as_str(), keep) + } + + pub fn unescapeDataString(s: string) -> string { + let bytes = s.as_str().as_bytes(); + let mut out: Vec = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + // An invalid or truncated %XX sequence is left untouched, as in .NET. + if bytes[i] == b'%' && i + 2 < bytes.len() { + if let (Some(hi), Some(lo)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) { + out.push((hi << 4) | lo); + i += 3; + continue; + } + } + out.push(bytes[i]); + i += 1; + } + fromString(String::from_utf8_lossy(&out).into_owned()) + } +} diff --git a/src/fable-library-rust/src/lib.fs b/src/fable-library-rust/src/lib.fs index 464b245a8a..e3145b1911 100644 --- a/src/fable-library-rust/src/lib.fs +++ b/src/fable-library-rust/src/lib.fs @@ -30,7 +30,9 @@ let _imports () = importAll "./Random.rs" importAll "./Reflection.rs" importAll "./RegExp.rs" + importAll "./Rune.rs" importAll "./String.rs" importAll "./TimeOnly.rs" importAll "./TimeSpan.rs" + importAll "./Uri.rs" () diff --git a/src/fable-library-rust/src/print_no_std.rs b/src/fable-library-rust/src/print_no_std.rs index 63aa6356b2..f5f83facb6 100644 --- a/src/fable-library-rust/src/print_no_std.rs +++ b/src/fable-library-rust/src/print_no_std.rs @@ -4,7 +4,7 @@ const STDERR: i32 = 2; #[link(name = "c")] unsafe extern "C" { - fn write(filedes: i32, buf: *const core::ffi::c_void, nbyte: usize); + fn write(filedes: core::ffi::c_int, buf: *const core::ffi::c_void, nbyte: usize) -> isize; } pub fn write_to(filedes: i32, s: &str) { diff --git a/src/fable-library-rust/vendored/hashable/src/lib.rs b/src/fable-library-rust/vendored/hashable/src/lib.rs index 413d88d47b..b0c2143b8f 100644 --- a/src/fable-library-rust/vendored/hashable/src/lib.rs +++ b/src/fable-library-rust/vendored/hashable/src/lib.rs @@ -22,6 +22,7 @@ fn expand(input: DeriveInput) -> syn::Result { Ok(quote! { impl #impl_generics ::core::hash::Hash for #ident #ty_generics #where_clause { + #[inline] fn hash(&self, state: &mut H) { #hash_body } @@ -30,7 +31,7 @@ fn expand(input: DeriveInput) -> syn::Result { impl #impl_generics #runtime::Native_::Hashable for #ident #ty_generics #where_clause { #[inline] fn getHashCode(&self) -> i32 { - #runtime::Native_::getHashCode(self) + #runtime::Native_::hash_code(self) } } }) diff --git a/src/fable-library-ts/Array.fs b/src/fable-library-ts/Array.fs index 9632b11662..4b1b36c79d 100644 --- a/src/fable-library-ts/Array.fs +++ b/src/fable-library-ts/Array.fs @@ -11,9 +11,10 @@ open Native open Native.Helpers let private indexNotFound () = - failwith "An index satisfying the predicate was not found in the collection." + raise (KeyNotFoundException(SR.keyNotFoundAlt)) -let private differentLengths () = failwith "Arrays had different lengths" +let private differentLengths (paramName: string) = + invalidArg paramName "The arrays have different lengths." // Pay attention when benchmarking to append and filter functions below // if implementing via native JS array .concat() and .filter() do not fall behind due to js-native transitions. @@ -35,9 +36,30 @@ let append (array1: 'T[]) (array2: 'T[]) ([] cons: Con let filter (predicate: 'T -> bool) (array: 'T[]) = filterImpl predicate array // intentionally returns target instead of unit -let fill (target: 'T[]) (targetIndex: int) (count: int) (value: 'T) : 'T[] = fillImpl target value targetIndex count +let fill (target: 'T[]) (targetIndex: int) (count: int) (value: 'T) : 'T[] = + if targetIndex < 0 then + invalidArg "targetIndex" SR.inputMustBeNonNegative -let getSubArray (array: 'T[]) (start: int) (count: int) : 'T[] = subArrayImpl array start count + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + // .NET fills up to the end of the array before raising, this checks first + if count > 0 && targetIndex + count > target.Length then + raise (System.IndexOutOfRangeException()) + + fillImpl target value targetIndex count + +let getSubArray (array: 'T[]) (start: int) (count: int) : 'T[] = + if start < 0 then + invalidArg "startIndex" SR.inputMustBeNonNegative + + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + if start + count > array.Length then + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) + + subArrayImpl array start count let last (array: 'T[]) = if array.Length = 0 then @@ -77,7 +99,7 @@ let mapIndexed2 : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" let result = allocateArrayFromCons cons source1.Length @@ -88,7 +110,7 @@ let mapIndexed2 let map2 (f: 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) ([] cons: Cons<'U>) : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" let result = allocateArrayFromCons cons source1.Length @@ -106,7 +128,7 @@ let mapIndexed3 : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" let result = allocateArrayFromCons cons source1.Length @@ -124,7 +146,7 @@ let map3 : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" let result = allocateArrayFromCons cons source1.Length @@ -242,6 +264,50 @@ let indexOf<'T> let contains<'T> (value: 'T) (array: 'T[]) ([] eq: IEqualityComparer<'T>) = indexOf array value None None eq >= 0 +// Follows .NET's probe order, so duplicates and comparers with side effects give the same results +let private binarySearchFrom (array: 'T[]) (index: int) (length: int) (value: 'T) (comparer: IComparer<'T>) = + let rec loop lo hi = + if lo > hi then + ~~~lo + else + let i = lo + ((hi - lo) >>> 1) + let c = comparer.Compare(array.[i], value) + + if c = 0 then + i + elif c < 0 then + loop (i + 1) hi + else + loop lo (i - 1) + + loop index (index + length - 1) + +let private checkBinarySearchRange (lengthName: string) (array: 'T[]) (index: int) (length: int) = + if index < 0 then + raise (System.ArgumentOutOfRangeException("index", "Non-negative number required.")) + + if length < 0 then + raise (System.ArgumentOutOfRangeException(lengthName, "Non-negative number required.")) + + if array.Length - index < length then + raise ( + System.ArgumentException( + "Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection." + ) + ) + +let binarySearch (array: 'T[]) (value: 'T) (comparer: IComparer<'T>) : int = + binarySearchFrom array 0 array.Length value comparer + +let binarySearchRange (array: 'T[]) (index: int) (length: int) (value: 'T) (comparer: IComparer<'T>) : int = + checkBinarySearchRange "length" array index length + binarySearchFrom array index length value comparer + +// ResizeArray's range overload, which names its length "count" +let binarySearchRangeCount (array: 'T[]) (index: int) (count: int) (value: 'T) (comparer: IComparer<'T>) : int = + checkBinarySearchRange "count" array index count + binarySearchFrom array index count value comparer + let empty cons = allocateArrayFromCons cons 0 let singleton value ([] cons: Cons<'T>) = @@ -293,9 +359,44 @@ let copy (array: 'T[]) = // else copyImpl array +// Not inline: TypeScript accepts the typed array methods only on the obj (any) parameters +let private copyTypedArray (source: obj) (sourceIndex: int) (target: obj) (targetIndex: int) (count: int) : unit = + target?set (source?subarray (sourceIndex, sourceIndex + count), targetIndex) + +let private argumentOutOfRange (paramName: string) (value: int) (requirement: string) = + raise ( + System.ArgumentOutOfRangeException(paramName, paramName + " ('" + string value + "') must be " + requirement) + ) + +// System.Array.Copy compiles to this function, so it cannot call System.Array.Copy itself let copyTo (source: 'T[]) (sourceIndex: int) (target: 'T[]) (targetIndex: int) (count: int) = - // TODO: Check array lengths - System.Array.Copy(source, sourceIndex, target, targetIndex, count) + if count < 0 then + argumentOutOfRange "length" count "a non-negative value." + + if sourceIndex < 0 then + argumentOutOfRange "sourceIndex" sourceIndex "greater than or equal to '0'." + + if sourceIndex + count > source.Length then + invalidArg + "sourceArray" + "Source array was not long enough. Check the source index, length, and the array's lower bounds." + + if targetIndex < 0 then + argumentOutOfRange "destinationIndex" targetIndex "greater than or equal to '0'." + + if targetIndex + count > target.Length then + invalidArg + "destinationArray" + "Destination array was not long enough. Check the destination index, length, and the array's lower bounds." + + if isTypedArrayImpl source && isTypedArrayImpl target then + copyTypedArray (source :> obj) sourceIndex (target :> obj) targetIndex count + elif sourceIndex < targetIndex then + for i = count - 1 downto 0 do + arraySetItemImpl target (targetIndex + i) (arrayAccessImpl (sourceIndex + i) source) + else + for i = 0 to count - 1 do + arraySetItemImpl target (targetIndex + i) (arrayAccessImpl (sourceIndex + i) source) let reverse (array: 'T[]) = // if isTypedArrayImpl array then @@ -328,7 +429,7 @@ let scanBack<'T, 'State> folder (array: 'T[]) (state: 'State) ([] cons: Cons<'T>) = if count > array.Length then - invalidArg "count" "count is greater than array length" + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) if count = array.Length then allocateArrayFromCons cons 0 @@ -357,7 +458,7 @@ let take count (array: 'T[]) ([] cons: Cons<'T>) = invalidArg "count" LanguagePrimitives.ErrorStrings.InputMustBeNonNegativeString if count > array.Length then - invalidArg "count" "count is greater than array length" + invalidOp SR.notEnoughElements if count = 0 then allocateArrayFromCons cons 0 @@ -571,14 +672,14 @@ let iterateIndexed action (array: 'T[]) = let iterate2 action (array1: 'T1[]) (array2: 'T2[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action array1.[i] array2.[i] let iterateIndexed2 action (array1: 'T1[]) (array2: 'T2[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action i array1.[i] array2.[i] @@ -599,14 +700,15 @@ let forAll predicate (array: 'T[]) = let permute f (array: 'T[]) = let size = array.Length let res = copyImpl array - let checkFlags = allocateArray size + // Native `every` skips the empty slots of `new Array(size)` + let checkFlags = fillImpl (allocateArray size) 0 0 size iterateIndexed (fun i x -> let j = f i if j < 0 || j >= size then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res.[j] <- x checkFlags.[j] <- 1 @@ -616,7 +718,7 @@ let permute f (array: 'T[]) = let isValid = checkFlags |> forAllImpl ((=) 1) if not isValid then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res @@ -719,7 +821,7 @@ let unzip3 (array: _[]) = let zip (array1: 'T[]) (array2: 'U[]) = // Shorthand version: map2 (fun x y -> x, y) array1 array2 if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" let result = allocateArray array1.Length @@ -731,7 +833,7 @@ let zip (array1: 'T[]) (array2: 'U[]) = let zip3 (array1: 'T[]) (array2: 'U[]) (array3: 'V[]) = // Shorthand version: map3 (fun x y z -> x, y, z) array1 array2 array3 if array1.Length <> array2.Length || array2.Length <> array3.Length then - differentLengths () + differentLengths "array1, array2, array3" let result = allocateArray array1.Length @@ -742,7 +844,7 @@ let zip3 (array1: 'T[]) (array2: 'U[]) (array3: 'V[]) = let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = if chunkSize < 1 then - invalidArg "size" "The input must be positive." + invalidArg "chunkSize" "The input must be positive." let result = ResizeArray<'T[]>() @@ -758,9 +860,12 @@ let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = result |> asArray let splitAt (index: int) (array: 'T[]) : 'T[] * 'T[] = - if index < 0 || index > array.Length then + if index < 0 then invalidArg "index" SR.indexOutOfBounds + if index > array.Length then + invalidOp SR.notEnoughElements + subArrayImpl array 0 index, skipImpl array index // Note that, though it's not consistent with `compare` operator, @@ -883,10 +988,13 @@ let tail (array: 'T[]) = skipImpl array 1 let item (index: int) (array: 'T[]) : 'T = - if index < 0 || index >= array.Length then + let value = arrayAccessImpl index array + + // Only an out-of-range index reads undefined, so the bounds are checked on that path alone + if emitJsExpr value "$0 === undefined" && (index < 0 || index >= array.Length) then invalidArg "index" "Index was outside the bounds of the array." else - arrayAccessImpl index array + value // Using `set` seems to cause issues with the TS let setItem (array: 'T[]) (index: int) (value: 'T) = @@ -921,7 +1029,7 @@ let foldIndexed2 folder state (array1: _[]) (array2: _[]) = let mutable acc = state if array1.Length <> array2.Length then - failwith "Arrays have different lengths" + differentLengths "array1" for i = 0 to array1.Length - 1 do acc <- folder i acc array1.[i] array2.[i] @@ -935,7 +1043,7 @@ let foldBackIndexed2<'T1, 'T2, 'State> folder (array1: 'T1[]) (array2: 'T2[]) (s let mutable acc = state if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" let size = array1.Length @@ -949,7 +1057,7 @@ let foldBack2<'T1, 'T2, 'State> f (array1: 'T1[]) (array2: 'T2[]) (state: 'State let reduce reduction (array: 'T[]) = if array.Length = 0 then - invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString // if isTypedArrayImpl array then // foldIndexed (fun i acc x -> if i = 0 then x else reduction acc x) Unchecked.defaultof<_> array // else @@ -957,7 +1065,7 @@ let reduce reduction (array: 'T[]) = let reduceBack reduction (array: 'T[]) = if array.Length = 0 then - invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString // if isTypedArrayImpl array then // foldBackIndexed (fun i x acc -> if i = 0 then x else reduction acc x) array Unchecked.defaultof<_> // else @@ -983,7 +1091,7 @@ let rec existsOffset2 predicate (array1: _[]) (array2: _[]) index = let rec exists2 predicate (array1: _[]) (array2: _[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" existsOffset2 predicate array1 array2 0 @@ -1069,7 +1177,7 @@ let averageBy (projection: 'T -> 'T2) (array: 'T[]) ([] averager: IGener let windowed (windowSize: int) (source: 'T[]) : 'T[][] = if windowSize <= 0 then - failwith "windowSize must be positive" + invalidArg "windowSize" "The input must be positive." let res = FSharp.Core.Operators.max 0 (source.Length - windowSize + 1) |> allocateArray @@ -1081,7 +1189,7 @@ let windowed (windowSize: int) (source: 'T[]) : 'T[][] = let splitInto (chunks: int) (array: 'T[]) : 'T[][] = if chunks < 1 then - invalidArg "chunks" "The input must be positive." + invalidArg "count" "The input must be positive." let result = ResizeArray<'T[]>() @@ -1120,7 +1228,7 @@ let transpose (arrays: 'T[] seq) ([] cons: Cons<'T>) : let lenInner = firstArray.Length if arrays |> forAll (fun a -> a.Length = lenInner) |> not then - differentLengths () + differentLengths "array.[0]" let result: 'T[][] = allocateArray lenInner @@ -1171,22 +1279,40 @@ let insertManyAt (index: int) (ys: seq<'T>) (xs: 'T[]) ([ float) (xs: 'T[]) : unit = +// Ports FSharp.Core's Random helpers so results and draw counts match .NET for the same seed +let private randomNext (randomizer: unit -> float) (minValue: int) (maxValue: int) : int = + let r = randomizer () + + if not (r >= 0.0 && r < 1.0) then + let msg = + "The index is outside the legal range.\nrandomizer returned " + + string r + + ", should be in range [0.0, 1.0)." + + raise (System.ArgumentOutOfRangeException("randomizer", msg)) + + int (r * float (maxValue - minValue)) + minValue + +let private checkRandomCount (count: int) = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + +let private shuffleInPlace (next: int -> int -> int) (xs: 'T[]) : unit = let len = xs.Length - for i = len - 1 downto 1 do - let r = randomizer () + for i = 0 to len - 2 do + let j = next i len - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + if j <> i then + let tmp = xs[i] + xs[i] <- xs[j] + xs[j] <- tmp - let j = int (r * float (i + 1)) - let tmp = xs[i] - xs[i] <- xs[j] - xs[j] <- tmp +let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = + shuffleInPlace (randomNext randomizer) xs let randomShuffleInPlaceWith (random: System.Random) (xs: 'T[]) : unit = - randomShuffleInPlaceBy (fun () -> random.NextDouble()) xs + shuffleInPlace (fun i n -> random.Next(i, n)) xs let randomShuffleInPlace (xs: 'T[]) : unit = randomShuffleInPlaceWith (System.Random()) xs @@ -1197,21 +1323,17 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T[]) : 'T[] = arr let randomShuffleWith (random: System.Random) (xs: 'T[]) : 'T[] = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = copy xs + randomShuffleInPlaceWith random arr + arr let randomShuffle (xs: 'T[]) : 'T[] = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = if isEmpty xs then - invalidArg "source" SR.inputSequenceEmpty - - let len = xs.Length - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + invalidArg "source" "The input array was empty." - xs[int (r * float len)] + xs[randomNext randomizer 0 xs.Length] let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = randomChoiceBy (fun () -> random.NextDouble()) xs @@ -1225,25 +1347,13 @@ let randomChoicesBy ([] cons: Cons<'T>) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative + checkRandomCount count if count > 0 && isEmpty xs then invalidArg "source" SR.inputSequenceEmpty let len = xs.Length - - initialize - count - (fun _ -> - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException - - xs[int (r * float len)] - ) - cons + initialize count (fun _ -> xs[randomNext randomizer 0 len]) cons let randomChoicesWith (random: System.Random) @@ -1257,12 +1367,16 @@ let randomChoicesWith let randomChoices (count: int) (xs: 'T[]) ([] cons: Cons<'T>) : 'T[] = randomChoicesWith (System.Random()) count xs cons -let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative +let private getMaxSetSizeForSampling (count: int) = + if count > 5 then + 21 + int (4.0 ** ceil (System.Math.Log(float (count * 3), 4.0))) + else + 21 - let arr = copy xs - let len = arr.Length +// Picks from a shrinking pool for small inputs, otherwise draws indices until unseen (FSharp.Core, after CPython) +let private randomSampleWithNext (next: int -> int -> int) (count: int) (xs: 'T[]) : 'T[] = + checkRandomCount count + let len = xs.Length if len = 0 && count > 0 then invalidArg "source" SR.inputSequenceEmpty @@ -1270,21 +1384,33 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = if count > len then invalidArg "count" SR.notEnoughElements - for i = 0 to count - 1 do - let r = randomizer () + let result = getSubArray xs 0 count + + if len <= getMaxSetSizeForSampling count then + let pool = copy xs + + for i = 0 to count - 1 do + let j = next 0 (len - i) + result[i] <- pool[j] + pool[j] <- pool[len - i - 1] + else + let selected = HashSet() - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + for i = 0 to count - 1 do + let mutable j = next 0 len - let j = i + int (r * float (len - i)) - let tmp = arr[i] - arr[i] <- arr[j] - arr[j] <- tmp + while not (selected.Add j) do + j <- next 0 len - getSubArray arr 0 count + result[i] <- xs[j] + + result + +let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = + randomSampleWithNext (randomNext randomizer) count xs let randomSampleWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = - randomSampleBy (fun () -> random.NextDouble()) count xs + randomSampleWithNext (fun a b -> random.Next(a, b)) count xs let randomSample (count: int) (xs: 'T[]) : 'T[] = randomSampleWith (System.Random()) count xs @@ -1302,45 +1428,17 @@ let removeAt (index: int) (xs: 'T[]) : 'T[] = ) let removeManyAt (index: int) (count: int) (xs: 'T[]) : 'T[] = - let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 - - let ys = - xs - |> filter (fun _ -> - i <- i + 1 + // Unlike FSharp.Core, a negative count removes nothing instead of duplicating items + if index < 0 || index > xs.Length - count || index > xs.Length then + invalidArg "index" "index must be within bounds of the array" - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true - ) - - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status - - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" - else - "count" - - invalidArg arg SR.indexOutOfBounds + let mutable i = -1 - ys + xs + |> filter (fun _ -> + i <- i + 1 + i < index || i >= index + count + ) let updateAt (index: int) (y: 'T) (xs: 'T[]) ([] cons: Cons<'T>) : 'T[] = let len = xs.Length diff --git a/src/fable-library-ts/Async.ts b/src/fable-library-ts/Async.ts index eb0fb63a6b..c668bd85c0 100644 --- a/src/fable-library-ts/Async.ts +++ b/src/fable-library-ts/Async.ts @@ -4,8 +4,9 @@ import { Async, IAsyncContext, CancellationToken } from "./AsyncBuilder.ts"; import { protectedCont, protectedBind, protectedReturn } from "./AsyncBuilder.ts"; import type { IEvent$2 } from "./Event.ts"; import { FSharpChoice$2_$union, Choice_makeChoice1Of2, Choice_makeChoice2Of2 } from "./Choice.ts"; -import { TimeoutException_$ctor } from "./System.ts"; -import { Exception } from "./Util.ts"; +import { ArgumentException_$ctor_Z384F8060, InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.ts"; +import { Exception, ensureErrorOrException, IDisposable } from "./Util.ts"; +import type { Option } from "./Option.ts"; function emptyContinuation(_x: T) { // NOP @@ -54,33 +55,45 @@ export function throwIfCancellationRequested(token: CancellationToken) { } } +// The parent keeps the listener until unlink is called +function createLinkedToken(parent: CancellationToken): [token: CancellationToken, unlink: () => void] { + const token = new CancellationToken(); + const id = parent.addListener(() => token.cancel()); + return [token, () => { parent.removeListener(id); }]; +} + export function startChild(computation: Async, ms?: number): Async> { return protectedCont((ctx) => { - // Share the parent's cancellation token so cancelling the parent - // cancels the child, like .NET Async.StartChild - const promise = startAsPromise(computation, ctx.cancelToken); - let promiseToRun = promise; - - if (ms) { - // Race the computation against a timeout: whichever settles first wins. - promiseToRun = new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => reject(TimeoutException_$ctor()), ms); + const timeout = ms == null || ms === -1 ? undefined : ms; + const [childToken, unlink] = timeout === undefined + ? [ctx.cancelToken, () => { /* NOP */ }] + : createLinkedToken(ctx.cancelToken); + const promise = startAsPromise(computation, childToken); + let settled = false; + const onSettled = () => { settled = true; unlink(); }; + + // Without a rejection handler an unawaited child's failure is an unhandled rejection + promise.then(onSettled, onSettled); + + // Like .NET, the timeout only runs while the result is awaited, not from the start of the child + const awaitResult = (): Promise => + timeout === undefined || settled ? promise : new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + // .NET doesn't cancel the child when the timeout is 0 + if (timeout !== 0) { + try { childToken.cancel(); } catch (error) { reject(error); return; } + } + reject(TimeoutException_$ctor()); + }, timeout); promise.then( value => { clearTimeout(timeoutId); resolve(value); }, error => { clearTimeout(timeoutId); reject(error); } ); }); - } - - // Prevent an unhandled rejection when the child is cancelled or fails - // while nobody awaits it (in F# an unobserved child's cancellation or - // failure is simply not propagated). awaitPromise attaches its own - // handlers to the same promise, so awaiting still observes the result. - promiseToRun.catch(emptyContinuation); // JS Promises are hot, computation has already started // but we delay returning the result - protectedReturn(awaitPromise(promiseToRun))(ctx); + protectedReturn(protectedCont((awaitCtx) => awaitPromise(awaitResult())(awaitCtx)))(ctx); }); } @@ -104,6 +117,7 @@ export function awaitEvent(event: IEvent$2, can if (cancelAction != null) { cancelAction(); } ctx.onCancel(new OperationCanceledException()); }); + if (ctx.cancelToken.isCancelled) { return; } event.AddHandler(handler); }); } @@ -126,19 +140,159 @@ export function catchAsync(work: Async) { }); } +export function tryCancelled(computation: Async, compensation: (error: OperationCanceledException) => void): Async { + return (ctx) => { + // invariant: install the handler without adding a cancellation checkpoint, including for nested handlers + computation({ + ...ctx, + onCancel: (error) => { + // decision: preserve the original cancellation even when compensation throws, matching .NET + try { compensation(error); } catch { /* Cancellation takes precedence. */ } + if (ctx.trampoline.incrementAndCheck()) { + ctx.trampoline.hijack(() => ctx.onCancel(error)); + } else { + ctx.onCancel(error); + } + }, + }); + }; +} + export function fromContinuations(f: (conts: Continuations) => void) { - return protectedCont((ctx: IAsyncContext) => - f([ctx.onSuccess, ctx.onError, ctx.onCancel])); + return (ctx: IAsyncContext) => { + let settled = false; + const claim = () => { + if (settled) { throw InvalidOperationException_$ctor_Z721C83C5("A continuation was invoked more than once"); } + settled = true; + }; + const success = (x: T) => { claim(); ctx.onSuccess(x); }; + const error = (x: Exception) => { claim(); ctx.onError(x); }; + const cancelled = (x: OperationCanceledException) => { claim(); ctx.onCancel(x); }; + protectedCont(() => f([success, error, cancelled]))({ + onSuccess: success, + onError: (x: Exception) => { if (settled) { throw x; } error(x); }, + onCancel: cancelled, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + }); + }; } export function ignore(computation: Async) { return protectedBind(computation, (_x) => protectedReturn(void 0)); } -export function parallel(computations: Iterable>) { - // Children share the parent's cancellation token, like .NET Async.Parallel +export function parallel(computations: Iterable>, maxDegreeOfParallelism?: number) { + if (maxDegreeOfParallelism != null && maxDegreeOfParallelism < 1) { + throw ArgumentException_$ctor_Z384F8060( + `maxDegreeOfParallelism must be positive, was ${maxDegreeOfParallelism}`, "maxDegreeOfParallelism"); + } return protectedCont((ctx: IAsyncContext) => - awaitPromise(Promise.all(Array.from(computations, (w) => startAsPromise(w, ctx.cancelToken))))(ctx)); + awaitPromise(runParallel(Array.from(computations), maxDegreeOfParallelism, ctx.cancelToken))(ctx)); +} + +function runParallel(computations: Async[], limit: number | undefined, parentToken: CancellationToken) { + return new Promise((resolve, reject) => { + const count = computations.length; + const results = new Array(count); + const [token, unlink] = createLinkedToken(parentToken); + let next = 0; + let running = 0; + let failed = false; + let failure: unknown; + + const startNext = () => { + const i = next++; + running++; + startAsPromise(computations[i], token) + .then( + (x) => { results[i] = x; }, + (err) => { + if (!failed) { + failed = true; + failure = err; + try { token.cancel(); } catch (error) { failure = error; } + } + }) + .then(() => { + running--; + if (!failed && next < count) { + startNext(); + } else if (running === 0) { + unlink(); + if (failed) { reject(failure); } else { resolve(results); } + } + }); + }; + + if (count === 0) { + unlink(); + resolve(results); + } + const initial = Math.min(limit ?? count, count); + while (next < initial) { + startNext(); + } + }); +} + +export function choice(computations: Iterable>>): Async> { + return protectedCont((ctx: IAsyncContext>) => { + const works = Array.from(computations); + let remaining = works.length; + if (remaining === 0) { + ctx.onSuccess(undefined); + return; + } + const [token, unlink] = createLinkedToken(ctx.cancelToken); + let done = false; + const finish = (report: () => void) => { + if (done) { return; } + done = true; + unlink(); + try { token.cancel(); } catch (error) { ctx.onError(ensureErrorOrException(error)); return; } + report(); + }; + for (const work of works) { + startWithContinuations( + work, + (x) => { + if (x != null) { + finish(() => ctx.onSuccess(x)); + } else if (--remaining === 0) { + finish(() => ctx.onSuccess(undefined)); + } + }, + (err) => finish(() => ctx.onError(err)), + (err) => finish(() => ctx.onCancel(err)), + token); + if (done) { break; } + } + }); +} + +export function onCancel(interruption: () => void): Async { + return protectedCont((ctx: IAsyncContext) => { + const token = ctx.cancelToken; + let interrupted = false; + const interrupt = () => { + if (!interrupted) { + interrupted = true; + try { interruption(); } catch { /* Ignored, like .NET Async.OnCancel */ } + } + }; + const id = token.addListener(interrupt); + let disposed = false; + ctx.onSuccess({ + Dispose() { + if (disposed) { return; } + disposed = true; + token.removeListener(id); + // With use! Dispose runs inside cancel() before this listener gets its turn + if (token.isCancelled) { interrupt(); } + }, + }); + }); } export function sequential(computations: Iterable>) { @@ -158,15 +312,18 @@ export function sequential(computations: Iterable>) { export function sleep(millisecondsDueTime: number) { return protectedCont((ctx: IAsyncContext) => { - let tokenId: number; - const timeoutId = setTimeout(() => { - ctx.cancelToken.removeListener(tokenId); - ctx.onSuccess(void 0); - }, millisecondsDueTime); - tokenId = ctx.cancelToken.addListener(() => { + let completed = false; + let tokenId: number | undefined; + const settle = (cancelled: boolean) => { + if (completed) { return; } + completed = true; clearTimeout(timeoutId); - ctx.onCancel(new OperationCanceledException()); - }); + if (tokenId !== undefined) { ctx.cancelToken.removeListener(tokenId); } + if (cancelled) { ctx.onCancel(new OperationCanceledException()); } + else { ctx.onSuccess(void 0); } + }; + const timeoutId = setTimeout(() => settle(false), millisecondsDueTime); + tokenId = ctx.cancelToken.addListener(() => settle(true)); }); } @@ -211,3 +368,11 @@ export function startAsPromise(computation: Async, cancellationToken?: Can startWithContinuations(computation, resolve, reject, reject, cancellationToken ? cancellationToken : defaultCancellationToken)); } + +export function startAsTask(computation: Async, _taskCreationOptions?: number, cancellationToken?: CancellationToken) { + return startAsPromise(computation, cancellationToken); +} + +export function startImmediateAsTask(computation: Async, cancellationToken?: CancellationToken) { + return startAsPromise(computation, cancellationToken); +} diff --git a/src/fable-library-ts/AsyncBuilder.ts b/src/fable-library-ts/AsyncBuilder.ts index 86d59e4ce4..0a89c59e18 100644 --- a/src/fable-library-ts/AsyncBuilder.ts +++ b/src/fable-library-ts/AsyncBuilder.ts @@ -12,6 +12,16 @@ export type Continuations = [ Continuation ]; +// AggregateError requires ES2021; the runtime also supports ES2020 hosts. +export class CancellationCallbackError extends Exception { + public readonly errors: unknown[]; + constructor(errors: unknown[]) { + // invariant: F# InnerException exposes the first failure without discarding the remaining errors + super("Cancellation callbacks failed", errors[0] as Exception); + this.errors = errors; + } +} + export class CancellationToken implements IDisposable { private _id: number; private _cancelled: boolean; @@ -27,14 +37,23 @@ export class CancellationToken implements IDisposable { public cancel() { if (!this._cancelled) { this._cancelled = true; - for (const [, listener] of this._listeners) { - listener(); + // decision: visit newest registrations first, matching .NET; disposal can skip pending callbacks + const pending = Array.from(this._listeners.keys()).reverse(); + const errors: unknown[] = []; + for (const id of pending) { + const listener = this._listeners.get(id); + this._listeners.delete(id); + if (listener) { + try { listener(); } catch (error) { errors.push(error); } + } } + if (errors.length > 0) { throw new CancellationCallbackError(errors); } } } public addListener(f: () => void) { const id = this._id; - this._listeners.set(this._id++, f); + this._id++; + if (this._cancelled) { f(); } else { this._listeners.set(id, f); } return id; } public removeListener(id: number) { @@ -181,24 +200,39 @@ export class AsyncBuilder { } public TryFinally(computation: Async, compensation: () => void) { - return protectedCont((ctx: IAsyncContext) => { - computation({ + // The cancellation check in protectedCont must see the cleanup continuations + return (ctx: IAsyncContext) => { + let settled = false; + protectedCont(computation)({ onSuccess: (x: T) => { - compensation(); + if (settled) { return; } + settled = true; + try { compensation(); } catch (error) { + ctx.onError(ensureErrorOrException(error)); + return; + } ctx.onSuccess(x); }, onError: (x: any) => { - compensation(); + if (settled) { throw x; } + settled = true; + try { compensation(); } catch (error) { + ctx.onError(ensureErrorOrException(error)); + return; + } ctx.onError(x); }, onCancel: (x: any) => { - compensation(); + if (settled) { return; } + settled = true; + // decision: cancellation remains terminal when compensation throws, matching .NET Async.TryFinally + try { compensation(); } catch { /* Cancellation takes precedence. */ } ctx.onCancel(x); }, cancelToken: ctx.cancelToken, trampoline: ctx.trampoline, }); - }); + }; } public TryWith(computation: Async, catchHandler: (e: any) => Async) { @@ -224,7 +258,8 @@ export class AsyncBuilder { } public Using(resource: T, binder: (x: T) => Async) { - return this.TryFinally(binder(resource), () => resource.Dispose()); + // Without Delay the binder runs before TryFinally installs Dispose + return this.TryFinally(this.Delay(() => binder(resource)), () => resource.Dispose()); } public While(guard: () => boolean, computation: Async): Async { diff --git a/src/fable-library-ts/BigInt.ts b/src/fable-library-ts/BigInt.ts index 18539c8231..9593f1328f 100644 --- a/src/fable-library-ts/BigInt.ts +++ b/src/fable-library-ts/BigInt.ts @@ -2,6 +2,7 @@ import { FSharpRef } from "./Types.ts"; import { int8, uint8, int16, uint16, int32, uint32, float16, float32, float64 } from "./Int32.ts"; import { decimal, fromParts, truncate } from "./Decimal.ts"; import { Exception, bigintHash } from "./Util.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts"; const isBigEndian = false; @@ -195,18 +196,27 @@ export function toChar(x: bigint): string { export function toString(x: bigint): string { return x.toString(); } +// .NET's Integer style: white space, a sign and decimal digits, then NULs +const integerRegex = /^[\t-\r ]*([+-]?\d+)[\t-\r ]*\0*$/; + export function tryParse(s: string, res: FSharpRef): boolean { - try { - res.contents = BigInt(s); + const m = s == null ? null : integerRegex.exec(s); + if (m != null) { + res.contents = BigInt(m[1]); return true; } - catch (err: any) { - return false; - } + return false; } export function parse(s: string): bigint { - return BigInt(s); + const res = new FSharpRef(zero); + if (tryParse(s, res)) { + return res.contents; + } else if (s == null) { + throw ArgumentNullException_$ctor_Z721C83C5("value"); + } else { + throw FormatException_$ctor_Z721C83C5("The value could not be parsed."); + } } export function pow(x: bigint, n: int32): bigint { diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index 8d7be77bf0..712308a38a 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 619d7afddbaf25108e819abaaa9d8a6503c457d6 +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 updaters: - package.json: file: package.json @@ -15,6 +15,158 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.13.0 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Unescape doubled braces in String.Format (#5231) ([24e01fd3](https://github.com/fable-compiler/Fable/commit/24e01fd30e3eec7571816b4d6cc9f6281cce0de4)) +* *(js/ts)* Treat null as empty in String.Join and Concat (#5230) ([a7dcab8d](https://github.com/fable-compiler/Fable/commit/a7dcab8d770ac56f825e15361df7b2e05f618e5e)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Round exact midpoints to even in numeric formats (#5245) ([350a3900](https://github.com/fable-compiler/Fable/commit/350a390071473bb4e93fb055a3876d7b3215ada1)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + +## 2.12.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + +## 2.11.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(js/ts)* Format DateTimeOffset in its own offset (#5171) ([04ddbd20](https://github.com/fable-compiler/Fable/commit/04ddbd2027e03f4cf7b0b22fe886fdabadf26d93)) +* *(js/ts)* Support %o in sprintf and interpolated strings (#5175) ([e13e7b0f](https://github.com/fable-compiler/Fable/commit/e13e7b0fdc16d26793833c77a11d242ccc61109b)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) + +### ⚡ Performance Improvements + +* *(js/ts)* Standard library combine numeric hash from snapshot of object instead of mapping (#5151) ([44495104](https://github.com/fable-compiler/Fable/commit/44495104b5c7ac65bfcea184d1305fd29f1db9b4)) +* *(js/ts)* Standard library remove .map result array from record hash using snapshot (#5150) ([0ceb5e9e](https://github.com/fable-compiler/Fable/commit/0ceb5e9ec5b470d00295a5cea9ead1c621c7858e)) +* *(js/ts)* Standard library one pass List.append builder (#5148) ([2ed9d6dd](https://github.com/fable-compiler/Fable/commit/2ed9d6dd0a372e5bcf53dfe24d786bf9b9256fd3)) +* *(js/ts)* List node as a class with a shared empty node (#5185) ([187f8985](https://github.com/fable-compiler/Fable/commit/187f89852413186b52d4ecdde55e1be7cf736785)) +* *(js/ts)* Check array bounds only on undefined reads (#5188) ([435fc0bb](https://github.com/fable-compiler/Fable/commit/435fc0bb008ff2ab7c14a08c7218be94f4735029)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Keep the curried original on its adapter (#5191) ([bfb4a2a4](https://github.com/fable-compiler/Fable/commit/bfb4a2a4e3bb1b5bd4491955a1e1cb90c14050b6)) +* *(js/ts)* Walk arrays by index when enumerated as seq (#5192) ([d88621c3](https://github.com/fable-compiler/Fable/commit/d88621c3b9d6ce48a72dc09b86a6805f612ac185)) +* *(js/ts)* Hash unions without allocating (#5195) ([2b66a5d2](https://github.com/fable-compiler/Fable/commit/2b66a5d2184f3a35f0d73fb397ab902e300ac4bd)) +* *(js/ts)* Probe dictionary buckets without allocating (#5196) ([9517eafd](https://github.com/fable-compiler/Fable/commit/9517eafd93aa7b84061662a421b7a39a7269318b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + +## 2.10.0 - 2026-10-22 + +### 🚀 Features + +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) + +### 🐞 Bug Fixes + +* *(js/ts)* Round decimals below 0.1 correctly ([fca4c408](https://github.com/fable-compiler/Fable/commit/fca4c408a64e33c58293f12869975e5b61bcb96a)) +* *(js/ts)* Keep decimal zero scale without leading zeros ([3076b45a](https://github.com/fable-compiler/Fable/commit/3076b45acacf39b8c99ec57025560ad355ab74db)) +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Keep .NET decimal scale through arithmetic (#5126) ([bdf0a6c8](https://github.com/fable-compiler/Fable/commit/bdf0a6c8cb56151a5431b5e3703f7ee165b26dba)) +* *(js/ts)* Accept decimal integers up to 2^96 ([c9feb339](https://github.com/fable-compiler/Fable/commit/c9feb339fb8a4e98bebf17b3facdc42ecd22d628)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Respect maxDegreeOfParallelism and cancel on failure in Async.Parallel (#5098) ([5de3271d](https://github.com/fable-compiler/Fable/commit/5de3271d8b47f1752c4e74c010e4314cf40bbcbe)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..2098f64a25e96385306ef19ed8c289f3b6c84db9) + +## 2.9.0 - 2026-10-16 + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) + +### 🐞 Bug Fixes + +* *(js/ts)* Avoid full array copy in Encoding.GetString (#5069) ([e6b72ec7](https://github.com/fable-compiler/Fable/commit/e6b72ec73be454bd3b3233167542a65fc6553c8a)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..e33c57870454fea189a3eb33519f583a5003f8c3) + +## 2.8.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + +## 2.8.0 - 2026-09-25 + +### 🐞 Bug Fixes + +* *(js/ts)* Fix Dictionary.Remove and HashSet.Remove to remove empty buckets (#4985) ([aea61b56](https://github.com/fable-compiler/Fable/commit/aea61b562c461a9b383e805f06f68a3b21ed45c2)) +* *(js/ts)* Clear custom System.Collections.IList implementations ([2369fbd2](https://github.com/fable-compiler/Fable/commit/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36)) + +### ⚡ Performance Improvements + +* *(js/ts)* Faster `resizeArray.Clear()` in JS/TS ([32d85add](https://github.com/fable-compiler/Fable/commit/32d85add01422547dba03b15e0a0787d8afb031b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + +## 2.7.1 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(js/ts)* Keep the sign of negative decimals below 0.1 ([91d80f18](https://github.com/fable-compiler/Fable/commit/91d80f185fb38e4d23fd9e906037a14daab3e3b9)) +* *(js/ts)* Respect the precision when formatting decimals below 0.1 ([1c02557e](https://github.com/fable-compiler/Fable/commit/1c02557efd113d8d30a922c74c3fc9d1e7168cfd)) +* *(js/ts)* Omit the decimal sign when the rounded result is zero ([ef63087d](https://github.com/fable-compiler/Fable/commit/ef63087d97b61927763023c4b2e5bb16bcd64af2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + +## 2.7.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Match .NET for G, E and custom numeric formats ([6fb03090](https://github.com/fable-compiler/Fable/commit/6fb03090ce328b8c5d6a36422f120dd641aefe3a)) +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + +## 2.6.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) + +### 🐞 Bug Fixes + +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/619d7afddbaf25108e819abaaa9d8a6503c457d6..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 2.5.1 - 2026-07-24 ### 🐞 Bug Fixes diff --git a/src/fable-library-ts/CollectionUtil.ts b/src/fable-library-ts/CollectionUtil.ts index b3df1fbd5f..7fda114eb7 100644 --- a/src/fable-library-ts/CollectionUtil.ts +++ b/src/fable-library-ts/CollectionUtil.ts @@ -143,19 +143,13 @@ export function remove(col: Iterable, item: T): boolean { export function clear(col: Iterable): void { if (typeof (col as any)["System.Collections.Generic.ICollection`1.Clear"] === "function") { return (col as any)["System.Collections.Generic.ICollection`1.Clear"](); // collection + } else if (Array.isArray(col)) { + col.length = 0; + } else if (ArrayBuffer.isView(col)) { + // TODO: throw for typed arrays? + } else if (typeof (col as any).clear === "function") { + (col as any).clear(); // map, set } else { - if (isArrayLike(col)) { - if (ArrayBuffer.isView(col)) { - // TODO: throw for typed arrays? - } else { - col.splice(0); // array, resize array - } - } else { - if (typeof (col as any).clear === "function") { - (col as any).clear(); // map, set - } else { - // TODO: throw for other collections? - } - } + // TODO: throw for other collections? } -} \ No newline at end of file +} diff --git a/src/fable-library-ts/Date.ts b/src/fable-library-ts/Date.ts index c585623b14..5fa3bb9d0e 100644 --- a/src/fable-library-ts/Date.ts +++ b/src/fable-library-ts/Date.ts @@ -403,7 +403,7 @@ function dateToISOStringWithOffset(dateWithOffset: Date, offset: number) { } function dateToStringWithOffset(date: IDateTimeOffset, format?: string) { - const d = new Date(date.getTime() + (date.offset ?? 0)); + const d = DateTime(date.getTime() + (date.offset ?? 0), DateTimeKind.Utc); if (typeof format !== "string") { return d.toISOString().replace(/\.\d+/, "").replace(/[A-Z]|\.\d+/g, " ") + dateOffsetToString((date.offset ?? 0)); } else if (format.length === 1) { diff --git a/src/fable-library-ts/DateOnlyTemporal.ts b/src/fable-library-ts/DateOnlyTemporal.ts new file mode 100644 index 0000000000..30a476bb99 --- /dev/null +++ b/src/fable-library-ts/DateOnlyTemporal.ts @@ -0,0 +1,174 @@ +import { FSharpRef } from "./Types.ts"; +import { TimeOnly } from "./TimeOnlyTemporal.ts"; +import { TimeSpan } from "./TimeSpanTemporal.ts"; +import { DateTime, dateTime as DateTime_stamp } from "./DateTimeTemporal.ts"; +import { DateTimeOffset, fromDate as DateTimeOffset_fromDate } from "./DateTimeOffsetTemporal.ts"; +import { Exception, DateTimeKind, padWithZeros } from "./Util.ts"; + +export type DateOnly = Temporal.PlainDate; + +export const PlainDate = Temporal.PlainDate; +export type PlainDate = Temporal.PlainDate; + +// The generic equality/comparison/hashing helpers in Util.ts dispatch at runtime +// on .NET-style Equals/CompareTo/GetHashCode methods (with a special case for JS +// Date, which Temporal.PlainDate cannot benefit from). Attach them so DateOnly +// also works in generic contexts: records, tuples, erased generics, etc. +const proto = Temporal.PlainDate.prototype as any; +proto.Equals = function (this: DateOnly, other: DateOnly): boolean { return this.equals(other); }; +proto.CompareTo = function (this: DateOnly, other: DateOnly): number { return Temporal.PlainDate.compare(this, other); }; +proto.GetHashCode = function (this: DateOnly): number { return dayNumber(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: DateOnly, format?: string): string { return toString(this, format); }; + +export function create(year: number, month: number, day: number): DateOnly { + return new Temporal.PlainDate(year, month, day); +} + +export function maxValue(): DateOnly { + return new Temporal.PlainDate(9999, 12, 31); +} + +export function minValue(): DateOnly { + return new Temporal.PlainDate(1, 1, 1); +} + +export function dayNumber(d: DateOnly): number { + return minValue().until(d).days; +} + +export function fromDayNumber(dayNumber: number): DateOnly { + return minValue().add({ days: dayNumber }); +} + +export function fromDateTime(d: Temporal.PlainDateTime): DateOnly { + // Under the Temporal representation a DateTime is a PlainDateTime (kind-agnostic wall-clock) + return new Temporal.PlainDate(d.year, d.month, d.day); +} + +export function dayOfWeek(d: DateOnly): number { + // Temporal: Monday = 1 ... Sunday = 7, .NET: Sunday = 0 ... Saturday = 6 + return d.dayOfWeek % 7; +} + +export function toDateTime(d: DateOnly, time: TimeOnly, kind: DateTimeKind = DateTimeKind.Unspecified): DateTime { + return DateTime_stamp(d.toPlainDateTime(time), kind); +} + +export function toDateTimeOffset(d: DateOnly, time: TimeOnly, offset: TimeSpan): DateTimeOffset { + return DateTimeOffset_fromDate(DateTime_stamp(d.toPlainDateTime(time), DateTimeKind.Unspecified), offset); +} + +export function addDays(d: DateOnly, v: number): DateOnly { + return d.add({ days: v }); +} + +export function addMonths(d: DateOnly, v: number): DateOnly { + return d.add({ months: v }); +} + +export function addYears(d: DateOnly, v: number): DateOnly { + return d.add({ years: v }); +} + +export function equals(x: DateOnly, y: DateOnly): boolean { + return x.equals(y); +} + +export function compare(x: DateOnly, y: DateOnly): number { + return Temporal.PlainDate.compare(x, y); +} + +export function hash(d: DateOnly): number { + return dayNumber(d); +} + +export function toString(d: DateOnly, format = "d", _provider?: any): string { + switch (format) { + case "d": + return `${padWithZeros(d.month, 2)}/${padWithZeros(d.day, 2)}/${padWithZeros(d.year, 4)}`; + case "o": + case "O": + // PlainDate.toString() is the ISO yyyy-MM-dd round-trip format + return d.toString(); + default: + throw new Exception("Custom formats are not supported"); + } +} + +export function parse(str: string): DateOnly { + function fail(): DateOnly { + throw new Exception(`String '${str}' was not recognized as a valid DateOnly.`); + } + + // Allowed separators: . , / - + // TODO whitespace alone as the separator + // + // Whitespace around separators + // + // Allowed format types: + // yyyy/mm/dd + // mm/dd/yyyy + // mm/dd + // mm/yyyy + // yyyy/mm + const r = /^\s*(\d{1,4})(?:\s*[.,-\/]\s*(\d{1,2}))?\s*[.,-\/]\s*(\d{1,4})\s*$/.exec(str); + if (r != null) { + let y = 0; + let m = 0; + let d = 1; + + if (r[2] == null) { + if (r[1].length < 3) { + if (r[3].length < 3) { + // 12/30 = December 30, {CurrentYear} + y = Temporal.Now.plainDateISO().year; + m = +r[1]; + d = +r[3]; + } else { + // 12/2000 = December 1, 2000 + m = +r[1]; + y = +r[3]; + } + } else { + if (r[3].length > 2) + fail(); + + // 2000/12 = December 1, 2000 + y = +r[1]; + m = +r[3]; + } + } else { + // 2000/1/30 or 1/30/2000 + const yearFirst = r[1].length > 2; + const yTmp = r[yearFirst ? 1 : 3]; + y = +yTmp; + + // year 0-29 is 2000-2029, 30-99 is 1930-1999 + if (yTmp.length < 3) + y += y >= 30 ? 1900 : 2000; + + m = +r[yearFirst ? 2 : 1]; + d = +r[yearFirst ? 3 : 2]; + } + + if (y > 0) { + try { + return new Temporal.PlainDate(y, m, d); + } catch { + return fail(); + } + } + } + + return fail(); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/DateTimeFormat.ts b/src/fable-library-ts/DateTimeFormat.ts new file mode 100644 index 0000000000..87548cfa6a --- /dev/null +++ b/src/fable-library-ts/DateTimeFormat.ts @@ -0,0 +1,591 @@ +/** + * Field-based .NET date/time formatting and lenient parsing, shared by the + * Temporal DateTime / DateTimeOffset representations. + * + * The formatters operate on a plain `DateInfo` (wall-clock fields), which the + * Temporal types expose directly (`d.year`, `d.hour`, `d.dayOfWeek % 7`, ...), + * so no JS-Date intermediate is needed there. + * + * Parsing is the exception. .NET accepts far more than ISO 8601 ("12/30/2009", + * "June 15, 2009 1:45 PM"), and the JS `Date` built-in is the only lenient date + * parser the platform offers — Temporal's `from` is strict ISO and rejects all of + * it. So `parseRaw` uses `Date` as a parsing engine and returns the instant it + * resolved, which callers convert into the Temporal value they need. That instant + * is only millisecond-precise, hence `subMillisecondTicks`: the finer digits are + * read straight off the input, because `Date` has already discarded them. + * + * This is a port of the equivalent logic in Date.ts / DateOffset.ts, kept + * separate so the Temporal modules do not depend on the JS-Date representation. + */ + +import { Exception, DateTimeKind, padWithZeros } from "./Util.ts"; + +// Wall-clock fields. dayOfWeek is .NET-style: 0 = Sunday ... 6 = Saturday. +// +// `tick` is the sub-second component in .NET ticks (100ns units), 0..9_999_999 — +// not milliseconds. .NET's fractional-second format specifiers ("f"/"F", and the +// round-trip "O") go all the way down to a single tick, so carrying only +// milliseconds here would cap the output at 3 of the 7 digits .NET prints. +export interface DateInfo { + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + tick: number; + dayOfWeek: number; +} + +// The 7 fractional-second digits .NET works with, most significant first. +function fractionDigits(info: DateInfo): string { + return padWithZeros(info.tick, 7); +} + +type OffsetInMinutes = number; +export type Offset = "Z" | OffsetInMinutes | null; + +const shortDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +const longDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; +const shortMonths = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +const longMonths = + ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + +function parseRepeatToken(format: string, pos: number, patternChar: string) { + let tokenLength = 0; + let internalPos = pos; + while (internalPos < format.length && format[internalPos] === patternChar) { + internalPos++; + tokenLength++; + } + + return tokenLength; +} + +function parseNextChar(format: string, pos: number) { + if (pos >= format.length - 1) { + return -1; + } + + return format.charCodeAt(pos + 1); +} + +function parseQuotedString(format: string, pos: number): [string, number] { + let beginPos = pos; + // Get the character used to quote the string + const quoteChar = format[pos]; + + let result = ""; + let foundQuote = false; + + while (pos < format.length) { + pos++; + const currentChar = format[pos]; + if (currentChar === quoteChar) { + foundQuote = true; + break; + } else if (currentChar === "\\") { + if (pos < format.length) { + pos++; + result += format[pos]; + } else { + // This means that '\' is the last character in the string. + throw new Exception("Invalid string format"); + } + } else { + result += currentChar; + } + } + + if (!foundQuote) { + // We could not find the matching quote + throw new Exception(`Invalid string format could not find matching quote for ${quoteChar}`); + } + + return [result, pos - beginPos + 1]; +} + +// Custom format string. `kind` decides the K/z tokens; `hostOffsetString` +// (e.g. "+01:00") is the host-zone offset at this wall-clock, used for the +// Local / Unspecified cases. +export function dateToStringWithCustomFormat( + info: DateInfo, format: string, kind: DateTimeKind, hostOffsetString: string): string { + let cursorPos = 0; + let tokenLength = 0; + let result = ""; + + while (cursorPos < format.length) { + const token = format[cursorPos]; + + switch (token) { + case "d": + tokenLength = parseRepeatToken(format, cursorPos, "d"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.day; + break; + case 2: + result += padWithZeros(info.day, 2); + break; + case 3: + result += shortDays[info.dayOfWeek]; + break; + case 4: + default: + result += longDays[info.dayOfWeek]; + break; + } + break; + case "f": + // The first `tokenLength` fractional-second digits, always padded. + tokenLength = parseRepeatToken(format, cursorPos, "f"); + cursorPos += tokenLength; + if (tokenLength <= 7) { + result += fractionDigits(info).substring(0, tokenLength); + } else { + throw "Input string was not in a correct format."; + } + break; + case "F": + // As "f", but with trailing zeros dropped — and nothing at all when the + // requested digits are all zero. + tokenLength = parseRepeatToken(format, cursorPos, "F"); + cursorPos += tokenLength; + if (tokenLength <= 7) { + result += fractionDigits(info).substring(0, tokenLength).replace(/0+$/, ""); + } else { + throw "Input string was not in a correct format."; + } + break; + case "g": + tokenLength = parseRepeatToken(format, cursorPos, "g"); + cursorPos += tokenLength; + result += "A.D."; + break; + case "h": + tokenLength = parseRepeatToken(format, cursorPos, "h"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + const h1Value = info.hour % 12; + result += h1Value ? h1Value : 12; + break; + case 2: + default: + const h2Value = info.hour % 12; + result += padWithZeros(h2Value ? h2Value : 12, 2); + break; + } + break; + case "H": + tokenLength = parseRepeatToken(format, cursorPos, "H"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.hour; + break; + case 2: + default: + result += padWithZeros(info.hour, 2); + break; + } + break; + case "K": + tokenLength = parseRepeatToken(format, cursorPos, "K"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + switch (kind) { + case DateTimeKind.Utc: + result += "Z"; + break; + case DateTimeKind.Local: + result += hostOffsetString; + break; + case DateTimeKind.Unspecified: + break; + } + break; + default: + break; + } + break; + case "m": + tokenLength = parseRepeatToken(format, cursorPos, "m"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.minute; + break; + case 2: + default: + result += padWithZeros(info.minute, 2); + break; + } + break; + case "M": + tokenLength = parseRepeatToken(format, cursorPos, "M"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.month; + break; + case 2: + result += padWithZeros(info.month, 2); + break; + case 3: + result += shortMonths[info.month - 1]; + break; + case 4: + default: + result += longMonths[info.month - 1]; + break; + } + break; + case "s": + tokenLength = parseRepeatToken(format, cursorPos, "s"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.second; + break; + case 2: + default: + result += padWithZeros(info.second, 2); + break; + } + break; + case "t": + tokenLength = parseRepeatToken(format, cursorPos, "t"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.hour < 12 ? "A" : "P"; + break; + case 2: + default: + result += info.hour < 12 ? "AM" : "PM"; + break; + } + break; + case "y": + tokenLength = parseRepeatToken(format, cursorPos, "y"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.year % 100; + break; + case 2: + result += padWithZeros(info.year % 100, 2); + break; + default: + result += padWithZeros(info.year, tokenLength); + break; + } + break; + case "z": + tokenLength = parseRepeatToken(format, cursorPos, "z"); + cursorPos += tokenLength; + const utcOffsetText = kind === DateTimeKind.Utc ? "+00:00" : hostOffsetString; + + const sign = utcOffsetText[0] === "-" ? "-" : "+"; + const hours = parseInt(utcOffsetText.substring(1, 3), 10); + const minutes = parseInt(utcOffsetText.substring(4, 6), 10); + + switch (tokenLength) { + case 1: + result += `${sign}${hours}`; + break; + case 2: + result += `${sign}${padWithZeros(hours, 2)}`; + break; + default: + result += `${sign}${padWithZeros(hours, 2)}:${padWithZeros(minutes, 2)}`; + break; + } + break; + case ":": + result += ":"; + cursorPos++; + break; + case "/": + result += "/"; + cursorPos++; + break; + case "'": + case '"': + const [quotedString, quotedStringLenght] = parseQuotedString(format, cursorPos); + result += quotedString; + cursorPos += quotedStringLenght; + break; + case "%": + const nextChar = parseNextChar(format, cursorPos); + if (nextChar >= 0 && nextChar !== "%".charCodeAt(0)) { + cursorPos += 2; + result += dateToStringWithCustomFormat(info, String.fromCharCode(nextChar), kind, hostOffsetString); + } else { + throw new Exception("Invalid format string"); + } + break; + case "\\": + const nextChar2 = parseNextChar(format, cursorPos); + if (nextChar2 >= 0) { + cursorPos += 2; + result += String.fromCharCode(nextChar2); + } else { + throw new Exception("Invalid format string"); + } + break; + default: + cursorPos++; + result += token; + break; + } + } + + return result; +} + +// --- Standard single-letter format building blocks (InvariantCulture) --- + +export function dateToString_D(info: DateInfo): string { + return longDays[info.dayOfWeek] + + ", " + padWithZeros(info.day, 2) + + " " + longMonths[info.month - 1] + + " " + info.year; +} + +export function dateToString_d(info: DateInfo): string { + return padWithZeros(info.month, 2) + + "/" + padWithZeros(info.day, 2) + + "/" + info.year; +} + +export function dateToString_T(info: DateInfo): string { + return padWithZeros(info.hour, 2) + + ":" + padWithZeros(info.minute, 2) + + ":" + padWithZeros(info.second, 2); +} + +export function dateToString_t(info: DateInfo): string { + return padWithZeros(info.hour, 2) + + ":" + padWithZeros(info.minute, 2); +} + +// RFC 1123: "Thu, 01 Jan 2009 00:00:00 GMT". `info` must already be UTC. +export function dateToString_R(info: DateInfo): string { + return shortDays[info.dayOfWeek] + ", " + + padWithZeros(info.day, 2) + " " + + shortMonths[info.month - 1] + " " + + info.year + " " + + padWithZeros(info.hour, 2) + ":" + + padWithZeros(info.minute, 2) + ":" + + padWithZeros(info.second, 2) + " GMT"; +} + +// Universal sortable: "2009-06-15 13:45:30Z". `info` must already be UTC. +export function dateToString_u(info: DateInfo): string { + return padWithZeros(info.year, 4) + "-" + + padWithZeros(info.month, 2) + "-" + + padWithZeros(info.day, 2) + " " + + padWithZeros(info.hour, 2) + ":" + + padWithZeros(info.minute, 2) + ":" + + padWithZeros(info.second, 2) + "Z"; +} + +// Month/day (InvariantCulture "MMMM dd"): "June 15" +export function dateToString_M(info: DateInfo): string { + return longMonths[info.month - 1] + " " + padWithZeros(info.day, 2); +} + +// Year/month (InvariantCulture "yyyy MMMM"): "2009 June" +export function dateToString_Y(info: DateInfo): string { + return info.year + " " + longMonths[info.month - 1]; +} + +// Everything a standard format specifier needs beyond the wall-clock fields. +// DateTime and DateTimeOffset differ only in these, so they share the table below. +// Everything past `info` is a thunk: most specifiers need none of them, and each +// costs a time-zone conversion. +export interface FormatContext { + info: DateInfo; + utcInfo: () => DateInfo; + // "O"/"o" and "s" are ISO 8601, which Temporal renders itself — and renders + // tick-exactly — so the owning module supplies them rather than this file + // reassembling the fields by hand. + roundTrip: () => string; + sortable: () => string; + // Trails the no-format rendering. A DateTimeOffset shows its offset there; a + // DateTime has none to show. + defaultSuffix: string; + // Drive the K and z specifiers of a custom format. + kind: DateTimeKind; + hostOffsetString: () => string; +} + +// .NET's standard date/time format specifiers, InvariantCulture. Anything that is +// not a single recognised letter is treated as a custom format string. +export function dateToString(ctx: FormatContext, format?: string): string { + const info = ctx.info; + + if (typeof format !== "string") { + return dateToString_d(info) + " " + dateToString_T(info) + ctx.defaultSuffix; + } + + if (format.length === 1) { + switch (format) { + case "D": return dateToString_D(info); + case "d": return dateToString_d(info); + case "F": return dateToString_D(info) + " " + dateToString_T(info); + case "f": return dateToString_D(info) + " " + dateToString_t(info); + case "G": return dateToString_d(info) + " " + dateToString_T(info); + case "g": return dateToString_d(info) + " " + dateToString_t(info); + case "M": case "m": return dateToString_M(info); + case "O": case "o": return ctx.roundTrip(); + case "R": case "r": return dateToString_R(ctx.utcInfo()); + case "s": return ctx.sortable(); + case "T": return dateToString_T(info); + case "t": return dateToString_t(info); + case "u": return dateToString_u(ctx.utcInfo()); + case "U": { + const utc = ctx.utcInfo(); + return dateToString_D(utc) + " " + dateToString_T(utc); + } + case "Y": case "y": return dateToString_Y(info); + default: throw new Exception("Unrecognized Date print format"); + } + } + + return dateToStringWithCustomFormat(info, format, ctx.kind, ctx.hostOffsetString()); +} + +// The only date words .NET's invariant parser recognises: month names, weekday names, +// meridiem designators and zone markers (plus the ISO "T" separator). Anything else is +// rejected. Used to reject JS-permissive inputs (see `parseRaw`). +const recognizedDateWords = new Set([ + "january", "february", "march", "april", "may", "june", + "july", "august", "september", "october", "november", "december", + "jan", "feb", "mar", "apr", "jun", "jul", "aug", "sep", "sept", "oct", "nov", "dec", + "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", + "mon", "tue", "wed", "thu", "fri", "sat", "sun", + "am", "pm", "gmt", "utc", "ut", "t", "z", +]); + +// The ticks below the millisecond (0..9999) written in the seconds fraction of +// `input`, e.g. "…:30.6175425" -> 5425. +// +// `parseRaw` resolves the instant through the JS `Date` built-in, which is +// millisecond-precise, so those digits would otherwise be lost. Anchoring on +// ":ss." keeps date separators (as in "15.06.2009") from matching. Digits past +// the 7th are truncated, matching how TimeOnly/TimeSpan parsing treats them. +export function subMillisecondTicks(input: string): number { + const m = /:\d{1,2}\.(\d+)/.exec(input); + return m == null ? 0 : +m[1].padEnd(7, "0").substring(3, 7); +} + +// A trailing UTC designator, if the input carries one. The JS `Date` built-in +// applies the offset but does not report whether there was one, and callers need +// that: .NET gives a parsed value Kind=Local when an offset is present and +// Kind=Unspecified when it is not. +// +// The offset is only recognised after a time, so that the trailing "-11" of a +// date like "2014-09-11" is not mistaken for one. +function detectOffset(input: string): Offset { + const m = /\d\d:\d\d(?::\d\d)?(?:\.\d+)?\s*(Z|[+-]\d\d:?(?:\d\d)?)\s*$/i.exec(input); + if (m == null) { + return null; + } + + const designator = m[1]; + if (designator.toUpperCase() === "Z") { + return "Z"; + } + + const digits = designator.substring(1).replace(":", ""); + const minutes = +digits.substring(0, 2) * 60 + (digits.length > 2 ? +digits.substring(2) : 0); + return designator[0] === "-" ? -minutes : minutes; +} + +// Lenient .NET-compatible parse. Returns a JS Date (the parsed instant) and the +// detected offset, if any. Callers read the wall-clock fields and build the +// appropriate Temporal value. +export function parseRaw(input: string): [Date, Offset] { + function fail() { + throw new Exception(`The string is not a valid Date: ${input}`); + } + + if (input == null || input.trim() === "") { + fail(); + } + + if ((input.match(/[a-z]+/gi) ?? []).some(word => !recognizedDateWords.has(word.toLowerCase()))) { + fail(); + } + + // ISO dates without TZ are parsed as UTC. Adding time without TZ keeps them local. + if (input.length === 10 && input[4] === "-" && input[7] === "-") { + input += "T00:00:00"; + } + let date = new Date(input); + let offset: Offset = detectOffset(input); + + if (isNaN(date.getTime())) { + // Try to check strings JS Date cannot parse (see #1045, #1422) + const m = /^\s*(\d+[^\w\s:]\d+[^\w\s:]\d+)?\s*(\d+:\d+(?::\d+(?:\.\d+)?)?)?\s*([AaPp][Mm])?\s*(Z|[+-]([01]?\d):?([0-5]?\d)?)?\s*$/.exec(input); + if (m != null) { + let baseDate: Date; + let timeInSeconds = 0; + if (m[2] != null) { + const timeParts = m[2].split(":"); + const hourPart = parseInt(timeParts[0], 10); + timeInSeconds = + hourPart * 3600 + + parseInt(timeParts[1] || "0", 10) * 60 + + parseFloat(timeParts[2] || "0"); + if (m[3] != null && m[3].toUpperCase() === "PM" && hourPart < 12) { + timeInSeconds += 12 * 3600; + } else if (m[3] != null && m[3].toUpperCase() === "AM" && hourPart === 12) { + timeInSeconds -= 12 * 3600; + } + } + if (m[4] != null) { // There's an offset, parse as UTC + if (m[1] != null) { + baseDate = new Date(m[1] + " UTC"); + } else { + const d = new Date(); + baseDate = new Date(d.getUTCFullYear() + "/" + (d.getUTCMonth() + 1) + "/" + d.getUTCDate()); + } + if (m[4] === "Z") { + offset = "Z"; + } else { + let offsetInMinutes = parseInt(m[5], 10) * 60 + parseInt(m[6] || "0", 10); + if (m[4][0] === "-") { + offsetInMinutes *= -1; + } + offset = offsetInMinutes; + timeInSeconds -= offsetInMinutes * 60; + } + } else { + if (m[1] != null) { + baseDate = new Date(m[1]); + } else { + const d = new Date(); + baseDate = new Date(d.getFullYear() + "/" + (d.getMonth() + 1) + "/" + d.getDate()); + } + } + date = new Date(baseDate.getTime() + timeInSeconds * 1000); + // correct for daylight savings time + date = new Date(date.getTime() + (date.getTimezoneOffset() - baseDate.getTimezoneOffset()) * 60_000); + } else { + fail(); + } + + // Check again the date is valid after transformations, see #2229 + if (isNaN(date.getTime())) { + fail(); + } + } + + return [date, offset]; +} diff --git a/src/fable-library-ts/DateTimeOffsetTemporal.ts b/src/fable-library-ts/DateTimeOffsetTemporal.ts new file mode 100644 index 0000000000..230b7de4cd --- /dev/null +++ b/src/fable-library-ts/DateTimeOffsetTemporal.ts @@ -0,0 +1,355 @@ +/** + * DateTimeOffset as an offset-only Temporal.ZonedDateTime. + * + * .NET DateTimeOffset is a wall-clock date+time plus an explicit UTC offset (a + * TimeSpan of whole minutes). A ZonedDateTime whose time zone is a fixed offset + * (e.g. "-08:00") carries the wall-clock, the offset, and the absolute instant + * natively — so there is no extra metadata to stamp and no re-stamping after + * operations (arithmetic returns a ZonedDateTime that keeps the offset). + * + * The offset is exchanged with the rest of the runtime as a TimeSpan (Temporal + * .Duration), matching how .NET types it. Equality/comparison are by instant + * (offset-independent), matching .NET '=='; EqualsExact also compares the offset. + * + * Formatting and parsing run on the Temporal value's own fields (see + * DateTimeFormat.ts) rather than through a JS Date, which would truncate every + * value to milliseconds. + */ + +import { int64, fromFloat64 } from "./BigInt.ts"; +import { FSharpRef } from "./Types.ts"; +import { Exception, DateTimeKind, padWithZeros } from "./Util.ts"; +import { + TimeSpan, fromTicks as TimeSpan_fromTicks, fromUnits as TimeSpan_fromUnits, + totalNanoseconds as TimeSpan_totalNanoseconds, +} from "./TimeSpanTemporal.ts"; +import { DateTime, dateTime, create as createDateTime, getTicks as DateTime_getTicks } from "./DateTimeTemporal.ts"; +import * as Format from "./DateTimeFormat.ts"; + +export type DateTimeOffset = Temporal.ZonedDateTime; + +export const ZonedDateTime = Temporal.ZonedDateTime; +export type ZonedDateTime = Temporal.ZonedDateTime; + +// Generic equality/comparison/hashing dispatch (Util.ts) uses .NET-style methods. +const proto = Temporal.ZonedDateTime.prototype as any; +proto.Equals = function (this: DateTimeOffset, other: DateTimeOffset): boolean { return equals(this, other); }; +proto.CompareTo = function (this: DateTimeOffset, other: DateTimeOffset): number { return compare(this, other); }; +proto.GetHashCode = function (this: DateTimeOffset): number { return hash(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: DateTimeOffset, format?: string): string { return toString(this, format); }; + +const nsPerMinute = 60_000_000_000n; + +// Nanoseconds between the Unix epoch (1970-01-01) and DateTime.MinValue (0001-01-01) +const epochToMinValueNs = 62_135_596_800_000n * 1_000_000n; + +function hostTimeZone(): string { + return Temporal.Now.timeZoneId(); +} + +// .NET DateTimeOffset has 100ns (tick) precision; Temporal instants are +// nanosecond-precise. Floors, so the tick count from year 1 is floored too. +function truncateToTicks(epochNs: bigint): bigint { + const remainder = epochNs % 100n; + return remainder >= 0n ? epochNs - remainder : epochNs - remainder - 100n; +} + +function checkOffsetInRange(offsetNs: bigint) { + if (offsetNs % nsPerMinute !== 0n) { + throw new Exception("Offset must be specified in whole minutes."); + } + if (offsetNs > 14n * 60n * nsPerMinute || offsetNs < -14n * 60n * nsPerMinute) { + throw new Exception("Offset must be within plus or minus 14 hours."); + } +} + +function offsetNanosecondsToZoneString(offsetNs: bigint): string { + const sign = offsetNs < 0n ? "-" : "+"; + const abs = offsetNs < 0n ? -offsetNs : offsetNs; + const totalMinutes = Number(abs / nsPerMinute); + return `${sign}${padWithZeros(Math.trunc(totalMinutes / 60), 2)}:${padWithZeros(totalMinutes % 60, 2)}`; +} + +function offsetToNanoseconds(offset: TimeSpan): bigint { + return TimeSpan_totalNanoseconds(offset); +} + +function fromEpoch(epochNs: bigint, offsetNs: bigint = 0n): DateTimeOffset { + if (offsetNs === 0n) + return new Temporal.ZonedDateTime(epochNs, "UTC"); + + checkOffsetInRange(offsetNs); + return new Temporal.ZonedDateTime(epochNs, offsetNanosecondsToZoneString(offsetNs)); +} + +export function offset(d: DateTimeOffset): TimeSpan { + return TimeSpan_fromTicks(BigInt(d.offsetNanoseconds) / 100n); +} + +export function create( + year: number, month: number, day: number, + h: number, m: number, s: number, + ms: number | TimeSpan, offset?: TimeSpan): DateTimeOffset { + // Overload without milliseconds: the 7th argument is the offset + if (offset == null) { + offset = ms as TimeSpan; + ms = 0; + } + const offsetNs = offsetToNanoseconds(offset); + checkOffsetInRange(offsetNs); + return Temporal.ZonedDateTime.from({ + year, month, day, hour: h, minute: m, second: s, millisecond: ms as number, + timeZone: offsetNanosecondsToZoneString(offsetNs), + }); +} + +// Offset (ns) of the host time zone for a given wall-clock, DST-aware. Resolved +// through Temporal rather than `new Date(year, ...)`, which maps years 0-99 into +// 1900-1999 and would pick that era's rules. +function hostOffsetNanoseconds(d: DateTime): bigint { + return BigInt(d.toZonedDateTime(hostTimeZone()).offsetNanoseconds); +} + +export function fromDate(date: DateTime, offset?: TimeSpan): DateTimeOffset { + const kind = date.kind ?? DateTimeKind.Unspecified; + let offsetNs: bigint; + switch (kind) { + case DateTimeKind.Utc: + if (offset != null && offsetToNanoseconds(offset) !== 0n) { + throw new Exception("The UTC Offset for Utc DateTime instances must be 0."); + } + offsetNs = 0n; + break; + case DateTimeKind.Local: + offsetNs = hostOffsetNanoseconds(date); + if (offset != null && offsetToNanoseconds(offset) !== offsetNs) { + throw new Exception("The UTC Offset of the local dateTime parameter does not match the offset argument."); + } + break; + default: + offsetNs = offset != null ? offsetToNanoseconds(offset) : hostOffsetNanoseconds(date); + break; + } + checkOffsetInRange(offsetNs); + return Temporal.ZonedDateTime.from({ + year: date.year, month: date.month, day: date.day, + hour: date.hour, minute: date.minute, second: date.second, + millisecond: date.millisecond, microsecond: date.microsecond, nanosecond: date.nanosecond, + timeZone: offsetNanosecondsToZoneString(offsetNs), + }); +} + +export function fromTicks(ticks: int64, offset: TimeSpan): DateTimeOffset { + const offsetNs = offsetToNanoseconds(offset); + // ticks are the local wall-clock ticks; the instant is that minus the offset + const localNs = BigInt(ticks) * 100n - epochToMinValueNs; + return fromEpoch(localNs - offsetNs, offsetNs); +} + +export function fromUnixTimeMilliseconds(ms: int64): DateTimeOffset { + return fromEpoch(BigInt(ms) * 1_000_000n); +} + +export function fromUnixTimeSeconds(seconds: int64): DateTimeOffset { + return fromEpoch(BigInt(seconds) * 1_000_000_000n); +} + +export function minValue(): DateTimeOffset { + return fromEpoch(-epochToMinValueNs); +} + +export function maxValue(): DateTimeOffset { + // 9999-12-31T23:59:59.9999999 UTC + return fromEpoch(253_402_300_799_999_999_900n, 0n); +} + +export function dayOfWeek(d: DateTimeOffset): number { + return d.dayOfWeek % 7; +} + +export function timeOfDay(d: DateTimeOffset): TimeSpan { + // Elapsed since midnight (local wall-clock). DateTimeOffset values are tick-aligned, so this is too. + return d.startOfDay().until(d); +} + +export function date(d: DateTimeOffset): DateTime { + return createDateTime(d.year, d.month, d.day, 0, 0, 0, 0, DateTimeKind.Unspecified); +} + +export function dateTimeProp(d: DateTimeOffset): DateTime { + return dateTime(d.toPlainDateTime(), DateTimeKind.Unspecified); +} + +export function toUniversalTime(d: DateTimeOffset): DateTimeOffset { + return fromEpoch(d.epochNanoseconds); +} + +export function toLocalTime(d: DateTimeOffset): DateTimeOffset { + const local = d.withTimeZone(hostTimeZone()); + return fromEpoch(local.epochNanoseconds, BigInt(local.offsetNanoseconds)); +} + +export function utcDateTime(d: DateTimeOffset): DateTime { + return dateTime(d.withTimeZone("UTC").toPlainDateTime(), DateTimeKind.Utc); +} + +export function localDateTime(d: DateTimeOffset): DateTime { + return dateTime(d.withTimeZone(hostTimeZone()).toPlainDateTime(), DateTimeKind.Local); +} + +export function add(d: DateTimeOffset, ts: TimeSpan): DateTimeOffset { + return d.add(ts); +} + +export function addYears(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add({ years: v }); +} + +export function addMonths(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add({ months: v }); +} + +export function addDays(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "days")); +} + +export function addHours(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "hours")); +} + +export function addMinutes(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "minutes")); +} + +export function addSeconds(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "seconds")); +} + +export function addMilliseconds(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "milliseconds")); +} + +export function addTicks(d: DateTimeOffset, v: int64): DateTimeOffset { + return d.add(TimeSpan_fromTicks(v)); +} + +// Split rather than returning a union — see the note in DateTimeTemporal.ts. +export function subtractDate(d: DateTimeOffset, that: DateTimeOffset): TimeSpan { + return TimeSpan_fromTicks((d.epochNanoseconds - that.epochNanoseconds) / 100n); // instant difference +} + +export function subtractTimeSpan(d: DateTimeOffset, ts: TimeSpan): DateTimeOffset { + return d.subtract(ts); +} + +export function equals(d1: DateTimeOffset, d2: DateTimeOffset): boolean { + return d1.epochNanoseconds === d2.epochNanoseconds; +} + +export function equalsExact(d1: DateTimeOffset, d2: DateTimeOffset): boolean { + return d1.epochNanoseconds === d2.epochNanoseconds && d1.offsetNanoseconds === d2.offsetNanoseconds; +} + +export function compare(d1: DateTimeOffset, d2: DateTimeOffset): number { + const a = d1.epochNanoseconds, b = d2.epochNanoseconds; + return a < b ? -1 : a > b ? 1 : 0; +} + +export const compareTo = compare; + +export function op_Addition(x: DateTimeOffset, y: TimeSpan): DateTimeOffset { + return add(x, y); +} + +export function hash(d: DateTimeOffset): number { + return Number(d.epochNanoseconds % 2147483647n); +} + +export function toOffset(d: DateTimeOffset, offset: TimeSpan): DateTimeOffset { + return fromEpoch(d.epochNanoseconds, offsetToNanoseconds(offset)); +} + +export function getUtcTicks(d: DateTimeOffset): int64 { + return DateTime_getTicks(utcDateTime(d)); +} + +export function getTicks(d: DateTimeOffset): int64 { + return DateTime_getTicks(dateTimeProp(d)); +} + +export function toUnixTimeMilliseconds(d: DateTimeOffset): int64 { + return fromFloat64(d.epochMilliseconds); +} + +export function toUnixTimeSeconds(d: DateTimeOffset): int64 { + return fromFloat64(Math.floor(d.epochMilliseconds / 1000)); +} + +export function now(): DateTimeOffset { + const z = Temporal.Now.zonedDateTimeISO(hostTimeZone()); + return fromEpoch(truncateToTicks(z.epochNanoseconds), BigInt(z.offsetNanoseconds)); +} + +export function utcNow(): DateTimeOffset { + return fromEpoch(truncateToTicks(Temporal.Now.instant().epochNanoseconds)); +} + +// --- Formatting and parsing --- + +// Wall-clock fields for the formatter, with the sub-second component in .NET +// ticks so "O" and the f/F specifiers can print all 7 digits. +function toDateInfo(z: DateTimeOffset): Format.DateInfo { + return { + year: z.year, + month: z.month, + day: z.day, + hour: z.hour, + minute: z.minute, + second: z.second, + tick: z.millisecond * 10_000 + z.microsecond * 10 + Math.trunc(z.nanosecond / 100), + dayOfWeek: z.dayOfWeek % 7, + }; +} + +export function toString(d: DateTimeOffset, format?: string, _provider?: any): string { + const offsetString = () => offsetNanosecondsToZoneString(BigInt(d.offsetNanoseconds)); + + return Format.dateToString( + { + info: toDateInfo(d), + utcInfo: () => toDateInfo(d.withTimeZone("UTC")), + // A fixed-offset zone renders as .NET's "O" exactly: ISO wall-clock plus + // the numeric offset, with no [zone] annotation. + roundTrip: () => d.toString({ fractionalSecondDigits: 7, timeZoneName: "never" }), + sortable: () => d.toPlainDateTime().toString({ smallestUnit: "second" }), + defaultSuffix: " " + offsetString(), + // K/z print the value's own offset, never the host zone's, so Local is + // passed as the kind and that offset stands in for the host one. + kind: DateTimeKind.Local, + hostOffsetString: offsetString, + }, + format); +} + +export function parse(str: string): DateTimeOffset { + const [parsed, offsetMatch] = Format.parseRaw(str); + // parseRaw resolves the instant through JS Date, which truncates at the + // millisecond, so the digits below that are recovered from the input separately. + const epochNs = BigInt(parsed.getTime()) * 1_000_000n + BigInt(Format.subMillisecondTicks(str)) * 100n; + + const offsetNs = offsetMatch == null + ? BigInt(new Temporal.Instant(epochNs).toZonedDateTimeISO(hostTimeZone()).offsetNanoseconds) + : (offsetMatch === "Z" ? 0n : BigInt(offsetMatch) * nsPerMinute); + + return fromEpoch(epochNs, offsetNs); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/DateTimeTemporal.ts b/src/fable-library-ts/DateTimeTemporal.ts new file mode 100644 index 0000000000..0faab16ffa --- /dev/null +++ b/src/fable-library-ts/DateTimeTemporal.ts @@ -0,0 +1,313 @@ +/** + * DateTime as an extended Temporal.PlainDateTime. + * + * .NET DateTime is a wall-clock date+time (no offset) plus a Kind (Utc | Local | + * Unspecified) that is metadata only. PlainDateTime models the wall-clock exactly + * and, unlike the JS Date representation, is tick (100ns) precise and DST-agnostic + * for arithmetic — which matches .NET, where DateTime math operates on the raw + * wall-clock ticks. + * + * Kind has no Temporal home, so it is attached to the instance as a `kind` property + * (Temporal objects are extensible). Because Temporal operations return fresh, + * un-stamped instances, every operation must funnel through `dateTime(...)` to + * re-stamp the kind. + * + * Formatting, parsing and time-zone conversion are done on the Temporal value + * itself (see DateTimeFormat.ts), never by round-tripping through a JS Date — + * that would silently truncate every value to milliseconds. The one exception is + * the four ToLong/ShortDate/TimeString members, which are host-locale display + * strings with no .NET-defined layout and no sub-second component. + */ + +import { int64, toInt64 } from "./BigInt.ts"; +import { FSharpRef } from "./Types.ts"; +import { DateTimeKind } from "./Util.ts"; +import { + TimeSpan, fromTicks as TimeSpan_fromTicks, fromUnits as TimeSpan_fromUnits, + totalNanoseconds as TimeSpan_totalNanoseconds, +} from "./TimeSpanTemporal.ts"; +import * as Format from "./DateTimeFormat.ts"; + +export type DateTime = Temporal.PlainDateTime & { kind?: DateTimeKind }; + +export const PlainDateTime = Temporal.PlainDateTime; +export type PlainDateTime = Temporal.PlainDateTime; + +// Generic equality/comparison/hashing dispatch (Util.ts) uses .NET-style methods. +const proto = Temporal.PlainDateTime.prototype as any; +proto.Equals = function (this: DateTime, other: DateTime): boolean { return Temporal.PlainDateTime.compare(this, other) === 0; }; +proto.CompareTo = function (this: DateTime, other: DateTime): number { return Temporal.PlainDateTime.compare(this, other); }; +proto.GetHashCode = function (this: DateTime): number { return hash(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: DateTime, format?: string): string { return toString(this, format); }; + +const minDateTime = new Temporal.PlainDateTime(1, 1, 1); + +export function getKind(value: DateTime): DateTimeKind { + return value.kind ?? DateTimeKind.Unspecified; +} + +export function dateTime(value: Temporal.PlainDateTime, kind: DateTimeKind = DateTimeKind.Unspecified): DateTime { + const d = value as DateTime; + d.kind = kind; + return d; +} + +// `dateTime` stamps in place, so anything handing out a value derived from a +// shared instance (a module constant, or the argument of SpecifyKind) has to +// copy first, or it would mutate the caller's value. +function copy(d: Temporal.PlainDateTime): Temporal.PlainDateTime { + return new Temporal.PlainDateTime( + d.year, d.month, d.day, d.hour, d.minute, d.second, d.millisecond, d.microsecond, d.nanosecond); +} + +function hostTimeZone(): string { + return Temporal.Now.timeZoneId(); +} + +// Host-zone offset at this wall-clock, e.g. "+01:00". Unlike building a JS Date +// from the fields, this is correct for years 0-99 (which JS maps to 1900-1999). +function hostOffsetString(d: Temporal.PlainDateTime): string { + return d.toZonedDateTime(hostTimeZone()).offset; +} + +// Wall-clock fields for the formatter. The sub-second component is carried in +// .NET ticks so that "O" and the f/F specifiers can print all 7 digits. +function toDateInfo(d: DateTime): Format.DateInfo { + return { + year: d.year, + month: d.month, + day: d.day, + hour: d.hour, + minute: d.minute, + second: d.second, + tick: d.millisecond * 10_000 + d.microsecond * 10 + Math.trunc(d.nanosecond / 100), + dayOfWeek: dayOfWeek(d), + }; +} + +// Display-only bridge for the host-locale strings below. Sub-second precision is +// irrelevant to all four, but years 0-99 still need correcting. +function toJsDate(d: DateTime): Date { + const jsDate = new Date(d.year, d.month - 1, d.day, d.hour, d.minute, d.second, d.millisecond); + jsDate.setFullYear(d.year, d.month - 1, d.day); + return jsDate; +} + +export function create( + year: number, month: number, day: number, + h: number = 0, m: number = 0, s: number = 0, + ms: number = 0, kind?: DateTimeKind): DateTime { + return dateTime(new Temporal.PlainDateTime(year, month, day, h, m, s, ms), kind); +} + +export function fromTicks(ticks: number | bigint, kind?: DateTimeKind): DateTime { + return dateTime(minDateTime.add(TimeSpan_fromTicks(ticks)), kind); +} + +export function getTicks(date: DateTime): int64 { + return toInt64(TimeSpan_totalNanoseconds(minDateTime.until(date, { largestUnit: "day" })) / 100n); +} + +export function minValue(): DateTime { + return dateTime(copy(minDateTime), DateTimeKind.Unspecified); +} + +export function maxValue(): DateTime { + return dateTime(new Temporal.PlainDateTime(9999, 12, 31, 23, 59, 59, 999, 999, 900), DateTimeKind.Unspecified); +} + +// .NET DateTime has 100ns (tick) precision; Temporal.Now is nanosecond-precise. +function truncateToTicks(d: Temporal.PlainDateTime): Temporal.PlainDateTime { + return d.round({ smallestUnit: "nanosecond", roundingIncrement: 100, roundingMode: "trunc" }); +} + +export function now(): DateTime { + return dateTime(truncateToTicks(Temporal.Now.plainDateTimeISO()), DateTimeKind.Local); +} + +export function utcNow(): DateTime { + return dateTime(truncateToTicks(Temporal.Now.plainDateTimeISO("UTC")), DateTimeKind.Utc); +} + +export function today(): DateTime { + return date(now()); +} + +export function specifyKind(d: DateTime, kind: DateTimeKind): DateTime { + // Kind is metadata: every tick of the wall-clock is preserved. + return dateTime(copy(d), kind); +} + +export function dayOfWeek(d: DateTime): number { + // Temporal: Monday = 1 ... Sunday = 7, .NET: Sunday = 0 ... Saturday = 6 + return d.dayOfWeek % 7; +} + +export function date(d: DateTime): DateTime { + return dateTime(d.with({ hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 }), getKind(d)); +} + +export function timeOfDay(d: DateTime): TimeSpan { + // Elapsed since midnight. DateTime values are tick-aligned, so this is too. + return d.with({ hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 }).until(d); +} + +export function add(d: DateTime, ts: TimeSpan): DateTime { + return dateTime(d.add(ts), getKind(d)); +} + +export function addYears(d: DateTime, v: number): DateTime { + return dateTime(d.add({ years: v }), getKind(d)); +} + +export function addMonths(d: DateTime, v: number): DateTime { + return dateTime(d.add({ months: v }), getKind(d)); +} + +export function addDays(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "days")), getKind(d)); +} + +export function addHours(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "hours")), getKind(d)); +} + +export function addMinutes(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "minutes")), getKind(d)); +} + +export function addSeconds(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "seconds")), getKind(d)); +} + +export function addMilliseconds(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "milliseconds")), getKind(d)); +} + +export function addTicks(d: DateTime, v: int64): DateTime { + return dateTime(d.add(TimeSpan_fromTicks(v)), getKind(d)); +} + +export function subtractDate(d: DateTime, that: DateTime): TimeSpan { + return that.until(d, { largestUnit: "day" }); +} + +export function subtractTimeSpan(d: DateTime, ts: TimeSpan): DateTime { + return dateTime(d.subtract(ts), getKind(d)); +} + +export function equals(d1: DateTime, d2: DateTime): boolean { + return Temporal.PlainDateTime.compare(d1, d2) === 0; +} + +export function compare(d1: DateTime, d2: DateTime): number { + return Temporal.PlainDateTime.compare(d1, d2); +} + +export const compareTo = compare; + +export function op_Addition(x: DateTime, y: TimeSpan): DateTime { + return add(x, y); +} + +export function hash(d: DateTime): number { + return Number(getTicks(d) % 2147483647n); +} + +export function isLeapYear(year: number): boolean { + return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; +} + +export function daysInMonth(year: number, month: number): number { + return month === 2 + ? (isLeapYear(year) ? 29 : 28) + : (month >= 8 ? (month % 2 === 0 ? 31 : 30) : (month % 2 === 0 ? 30 : 31)); +} + +// --- Host time zone --- + +export function toUniversalTime(d: DateTime): DateTime { + // .NET reads an Unspecified value as local time here. + return getKind(d) === DateTimeKind.Utc + ? d + : dateTime(d.toZonedDateTime(hostTimeZone()).withTimeZone("UTC").toPlainDateTime(), DateTimeKind.Utc); +} + +export function toLocalTime(d: DateTime): DateTime { + // .NET reads an Unspecified value as UTC here — the mirror image of the above. + return getKind(d) === DateTimeKind.Local + ? d + : dateTime(d.toZonedDateTime("UTC").withTimeZone(hostTimeZone()).toPlainDateTime(), DateTimeKind.Local); +} + +export function isDaylightSavingTime(d: DateTime): boolean { + const tz = hostTimeZone(); + const offsetAt = (month: number) => new Temporal.PlainDateTime(d.year, month, 1).toZonedDateTime(tz).offsetNanoseconds; + // The larger of the two mid-season offsets is the daylight-saving one. + return Math.max(offsetAt(1), offsetAt(7)) === d.toZonedDateTime(tz).offsetNanoseconds; +} + +// --- Host-locale display strings (no .NET-defined layout) --- + +export function toLongDateString(d: DateTime): string { + return toJsDate(d).toDateString(); +} + +export function toShortDateString(d: DateTime): string { + return toJsDate(d).toLocaleDateString(); +} + +export function toLongTimeString(d: DateTime): string { + return toJsDate(d).toLocaleTimeString(); +} + +export function toShortTimeString(d: DateTime): string { + return toJsDate(d).toLocaleTimeString().replace(/:\d\d(?!:)/, ""); +} + +// --- Formatting and parsing --- + +export function toString(d: DateTime, format?: string, _provider?: any): string { + const kind = getKind(d); + + return Format.dateToString( + { + info: toDateInfo(d), + utcInfo: () => toDateInfo(toUniversalTime(d)), + // An Unspecified DateTime prints no zone at all, which is what makes "O" + // round-trip it back to Unspecified. + roundTrip: () => d.toString({ fractionalSecondDigits: 7 }) + + (kind === DateTimeKind.Utc ? "Z" : kind === DateTimeKind.Local ? hostOffsetString(d) : ""), + sortable: () => d.toString({ smallestUnit: "second" }), + defaultSuffix: "", + kind, + hostOffsetString: () => hostOffsetString(d), + }, + format); +} + +export function parse(str: string, detectUTC = false): DateTime { + const [parsed, offset] = Format.parseRaw(str); + // .NET always parses DateTime as Local if there's offset info (even "Z") + // Newtonsoft.Json uses UTC if the offset is "Z" + const kind = offset != null + ? (detectUTC && offset === "Z" ? DateTimeKind.Utc : DateTimeKind.Local) + : DateTimeKind.Unspecified; + + // parseRaw resolves the instant through JS Date, which truncates at the + // millisecond, so the digits below that are recovered from the input separately. + const epochNs = BigInt(parsed.getTime()) * 1_000_000n + BigInt(Format.subMillisecondTicks(str)) * 100n; + const zone = kind === DateTimeKind.Utc ? "UTC" : hostTimeZone(); + + return dateTime(new Temporal.Instant(epochNs).toZonedDateTimeISO(zone).toPlainDateTime(), kind); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/Decimal.ts b/src/fable-library-ts/Decimal.ts index c564838b28..f12cda7161 100644 --- a/src/fable-library-ts/Decimal.ts +++ b/src/fable-library-ts/Decimal.ts @@ -1,13 +1,23 @@ import Decimal, { BigSource } from "./lib/big.js"; import { Numeric, symbol } from "./Numeric.ts"; import { FSharpRef } from "./Types.ts"; -import { Exception, combineHashCodes } from "./Util.ts"; +import { Exception, MidpointRounding, combineHashCodes } from "./Util.ts"; +import { NumberStyles, splitNumber } from "./NumberStyles.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5, OverflowException_$ctor_Z721C83C5 } from "./System.ts"; import { int8, uint8, int16, uint16, int32, uint32, float16, float32, float64 } from "./Int32.ts"; import { fromDecimal, int64, uint64, int128, uint128, nativeint, unativeint } from "./BigInt.ts"; import * as bigInt from "./BigInt.ts"; Decimal.prototype.GetHashCode = function () { - return combineHashCodes([this.s, this.e].concat(this.c)) + const digits: number[] = this.c; + if (digits[0] === 0) { + return combineHashCodes([1, 0, 0]); + } + let end = digits.length; + while (digits[end - 1] === 0) { + end--; + } + return combineHashCodes([this.s, this.e].concat(digits.slice(0, end))); } Decimal.prototype.Equals = function (x: Decimal) { @@ -49,7 +59,7 @@ export function equals(x: Decimal, y: Decimal) { export function abs(x: Decimal) { return x.abs(); } export function sign(x: Decimal): number { return x.lt(get_Zero) ? -1 : x.gt(get_Zero) ? 1 : 0; } -export function max(x: Decimal, y: Decimal): Decimal { return x.gt(y) ? x : y; } +export function max(x: Decimal, y: Decimal): Decimal { return x.lt(y) ? y : x; } export function min(x: Decimal, y: Decimal): Decimal { return x.lt(y) ? x : y; } export function maxMagnitude(x: Decimal, y: Decimal): Decimal { return abs(x).gt(abs(y)) ? x : y; } @@ -63,6 +73,24 @@ export function round(x: Decimal, digits: number = 0) { return x.round(digits, 2 /* ROUND_HALF_EVEN */); } +export function roundToMode(x: Decimal, digits: number, mode: number) { + if (!(digits >= 0 && digits <= 28)) { + throw new Exception("Decimal can only round to between 0 and 28 digits of precision. (Parameter 'decimals')"); + } + switch (mode) { + case MidpointRounding.ToEven: return x.round(digits, 2 /* ROUND_HALF_EVEN */); + case MidpointRounding.AwayFromZero: return x.round(digits, 1 /* ROUND_HALF_UP */); + case MidpointRounding.ToZero: return x.round(digits, 0 /* ROUND_DOWN */); + case MidpointRounding.ToNegativeInfinity: return x.round(digits, x.cmp(0) >= 0 ? 0 /* ROUND_DOWN */ : 3 /* ROUND_UP */); + case MidpointRounding.ToPositiveInfinity: return x.round(digits, x.cmp(0) >= 0 ? 3 /* ROUND_UP */ : 0 /* ROUND_DOWN */); + default: throw new Exception(`The value '${mode}' is not valid for this usage of the type MidpointRounding. (Parameter 'mode')`); + } +} + +export function roundMode(x: Decimal, mode: number) { + return roundToMode(x, 0, mode); +} + export function truncate(x: Decimal) { return x.round(0, 0 /* ROUND_DOWN */); } @@ -124,22 +152,47 @@ export function toString(x: Decimal) { return x.toString(); } -export function tryParse(str: string, defValue: FSharpRef): boolean { +// Returns undefined when the string does not have the format allowed by the style +function parseWithStyle(str: string, style: number): Decimal | undefined { + const parts = splitNumber(str, style); + if (parts === undefined) { + return undefined; + } + let d: Decimal; + try { + d = new Decimal(parts.digits); + } catch { + // Fable's big.js fork throws for values outside the decimal range + throw OverflowException_$ctor_Z721C83C5(OVERFLOW); + } + if (parts.negative) { + d.s = -1; + } + return d; +} + +export function tryParse(str: string, style: number, defValue: FSharpRef): boolean { try { - defValue.contents = new Decimal(str.trim()); + const d = parseWithStyle(str, style); + if (d === undefined) { + return false; + } + defValue.contents = d; return true; } catch { return false; } } -export function parse(str: string): Decimal { - const defValue = new FSharpRef(get_Zero); - if (tryParse(str, defValue)) { - return defValue.contents; - } else { - throw new Exception(`The input string ${str} was not in a correct format.`); +export function parse(str: string, style: number = NumberStyles.Number): Decimal { + if (str == null) { + throw ArgumentNullException_$ctor_Z721C83C5("s"); + } + const d = parseWithStyle(str, style); + if (d === undefined) { + throw FormatException_$ctor_Z721C83C5(`The input string '${str}' was not in a correct format.`); } + return d; } export function toNumber(x: Decimal): number { @@ -220,6 +273,37 @@ function getInt32Bits(hexDigits: Uint8Array, offset: number) { return bits; } +const OVERFLOW = "Value was either too large or too small for a Decimal."; + +// The first double outside the decimal range +const MAX_DOUBLE = 2 ** 96; + +function fromFloat(n: number, significantDigits: number): Decimal { + if (!isFinite(n) || Math.abs(n) >= MAX_DOUBLE) { + throw new Exception(OVERFLOW); + } + const exact = Math.abs(n) >= 1e21 ? BigInt(n).toString() : n.toFixed(100); + const d = new Decimal(exact).prec(significantDigits, 2); + const digits: number[] = d.c; + let end = digits.length; + while (end > 1 && digits[end - 1] === 0) { + end--; + } + d.c = digits.slice(0, end); + if (d.c[0] === 0) { + d.e = 0; + } + return d; +} + +export function fromFloat64(n: number): Decimal { + return fromFloat(n, 15); +} + +export function fromFloat32(n: number): Decimal { + return fromFloat(n, 7); +} + export function fromIntArray(bits: ArrayLike) { return fromInts(bits[0], bits[1], bits[2], bits[3]); } diff --git a/src/fable-library-ts/Double.ts b/src/fable-library-ts/Double.ts index 5c73a6b537..fab6826826 100644 --- a/src/fable-library-ts/Double.ts +++ b/src/fable-library-ts/Double.ts @@ -1,27 +1,56 @@ import { FSharpRef } from "./Types.ts"; -import { Exception } from "./Util.ts"; - -export function tryParse(str: string, defValue: FSharpRef): boolean { - // TODO: test if value is valid and in range - if (str != null && /\S/.test(str)) { - const v = +str.replace("_", ""); - if (!Number.isNaN(v)) { - defValue.contents = v; +import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts"; +import { NumberStyles, splitNumber } from "./NumberStyles.ts"; + +// .NET's NaN and infinity symbols, ignoring case, allowing a sign and trimmed like String.Trim +const symbolRegex = /^[\t-\r \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]*(?:[+-]?nan|([+-]?)infinity)[\t-\r \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]*$/i; + +const defaultStyle = NumberStyles.Float | NumberStyles.AllowThousands; + +export function tryParse(str: string, style: number, defValue: FSharpRef): boolean { + if (str != null) { + const parts = splitNumber(str, style); + if (parts !== undefined) { + const value = Number.parseFloat(parts.digits); + defValue.contents = parts.negative ? -value : value; + return true; + } + const symbol = symbolRegex.exec(str); + if (symbol != null) { + defValue.contents = symbol[1] == null ? NaN : symbol[1] === "-" ? -Infinity : Infinity; return true; } } return false; } -export function parse(str: string): number { +export function parse(str: string, style: number = defaultStyle): number { const defValue = new FSharpRef(0); - if (tryParse(str, defValue)) { + if (tryParse(str, style, defValue)) { return defValue.contents; + } else if (str == null) { + throw ArgumentNullException_$ctor_Z721C83C5("s"); } else { - throw new Exception(`The input string ${str} was not in a correct format.`); + throw FormatException_$ctor_Z721C83C5(`The input string '${str}' was not in a correct format.`); } } +export function tryParseSingle(str: string, style: number, defValue: FSharpRef): boolean { + if (tryParse(str, style, defValue)) { + defValue.contents = Math.fround(defValue.contents); + return true; + } + return false; +} + +export function parseSingle(str: string, style?: number): number { + return Math.fround(parse(str, style)); +} + +export function parseIgnoringUnderscores(str: string): number { + return parse(str == null ? str : str.replace(/_/g, ""), NumberStyles.Float); +} + // JS Number.isFinite function evals false for NaN export function isPositiveInfinity(x: number) { return x === Number.POSITIVE_INFINITY; diff --git a/src/fable-library-ts/Encoding.ts b/src/fable-library-ts/Encoding.ts index 781a920d2c..37495c8f3a 100644 --- a/src/fable-library-ts/Encoding.ts +++ b/src/fable-library-ts/Encoding.ts @@ -89,6 +89,25 @@ function utf8_decode(bytes: ArrayLike) { return chars.join(""); } +function toUint8Array(bytes: ArrayLike, index?: number, count?: number) { + if (ArrayBuffer.isView(bytes)) { + const buffer = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength); + if (index != null && count != null) { + return buffer.subarray(index, index + count); + } else if (index != null) { + return buffer.subarray(index); + } + return buffer; + } else { + if (index != null && count != null) { + return Uint8Array.from(Array.prototype.slice.call(bytes, index, index + count)); + } else if (index != null) { + return Uint8Array.from(Array.prototype.slice.call(bytes, index)); + } + return Uint8Array.from(bytes); + } +} + class UTF16LE { public getBytes(str: string | string[], index?: number, count?: number) { @@ -110,13 +129,7 @@ class UTF16LE { } public getString(bytes: ArrayLike, index?: number, count?: number) { - const array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes); - let buffer = new Uint8Array(array.buffer, array.byteOffset, array.byteLength); - if (index != null && count != null) { - buffer = buffer.subarray(index, index + count); - } else if (index != null) { - buffer = buffer.subarray(index); - } + const buffer = toUint8Array(bytes, index, count); // Allow to compile for Browser environment without @types/node being installed // See https://github.com/fable-compiler/Fable/issues/4368 const g = globalThis as any; @@ -154,13 +167,7 @@ class UTF8 { } public getString(bytes: ArrayLike, index?: number, count?: number) { - const array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes); - let buffer = new Uint8Array(array.buffer, array.byteOffset, array.byteLength); - if (index != null && count != null) { - buffer = buffer.subarray(index, index + count); - } else if (index != null) { - buffer = buffer.subarray(index); - } + const buffer = toUint8Array(bytes, index, count); // Allow to compile for Browser environment without @types/node being installed // See https://github.com/fable-compiler/Fable/issues/4368 const g = globalThis as any; diff --git a/src/fable-library-ts/FSharp.Core.fs b/src/fable-library-ts/FSharp.Core.fs index 4257e6aed2..42a163060c 100644 --- a/src/fable-library-ts/FSharp.Core.fs +++ b/src/fable-library-ts/FSharp.Core.fs @@ -41,6 +41,14 @@ module Operators = let nullArg (argumentName: string) = raise (System.ArgumentNullException(argumentName)) + [] + let invalidArg (argumentName: string) (message: string) = + raise (System.ArgumentException(message, argumentName)) + + [] + let invalidOp (message: string) = + raise (System.InvalidOperationException(message)) + [] let using<'T, 'R when 'T :> System.IDisposable and 'T: null> (resource: 'T) (action: 'T -> 'R) = try diff --git a/src/fable-library-ts/Global.fs b/src/fable-library-ts/Global.fs index 8b132814d3..416e179656 100644 --- a/src/fable-library-ts/Global.fs +++ b/src/fable-library-ts/Global.fs @@ -22,7 +22,7 @@ module SR = let indexOutOfBounds = "The index was outside the range of elements in the collection." - let inputWasEmpty = "Collection was empty." + let inputWasEmpty = "The input list was empty." let inputMustBeNonNegative = "The input must be non-negative." let inputSequenceEmpty = "The input sequence was empty." diff --git a/src/fable-library-ts/Guid.ts b/src/fable-library-ts/Guid.ts index e74d2011a0..009c2350bf 100644 --- a/src/fable-library-ts/Guid.ts +++ b/src/fable-library-ts/Guid.ts @@ -73,8 +73,10 @@ export function newGuid() { } // RFC 9562 UUID v7 -export function createVersion7(timestamp?: Date): string { - const ms = timestamp != null ? timestamp.getTime() : Date.now(); +export function createVersion7(timestamp?: Date | number | bigint): string { + // A number/bigint timestamp is Unix milliseconds (used by the Temporal representation, + // whose DateTimeOffset is not a JS Date); a Date uses getTime(). + const ms = timestamp == null ? Date.now() : typeof timestamp === "object" ? timestamp.getTime() : Number(timestamp); // 48-bit timestamp as hex const msHex = Math.floor(ms).toString(16).padStart(12, "0"); diff --git a/src/fable-library-ts/Int32.ts b/src/fable-library-ts/Int32.ts index 743272624a..21220602bc 100644 --- a/src/fable-library-ts/Int32.ts +++ b/src/fable-library-ts/Int32.ts @@ -1,5 +1,6 @@ import { FSharpRef } from "./Types.ts"; import { Exception } from "./Util.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5, OverflowException_$ctor_Z721C83C5 } from "./System.ts"; export type int8 = number; export type uint8 = number; @@ -106,7 +107,15 @@ export function isValid(str: string, style: number, radix?: number) { return null; } +export function parseOverflow(unsigned: boolean, bitsize: number) { + const type = bitsize === 8 ? (unsigned ? "an unsigned byte" : "a signed byte") : (unsigned ? "a UInt" : "an Int") + bitsize; + return OverflowException_$ctor_Z721C83C5(`Value was either too large or too small for ${type}.`); +} + export function parse(str: string, style: number, unsigned: boolean, bitsize: number, radix?: number): number { + if (str == null) { + throw ArgumentNullException_$ctor_Z721C83C5("s"); + } const res = isValid(str, style, radix); if (res != null) { let v = Number.parseInt(res.sign + res.digits, res.radix); @@ -119,9 +128,10 @@ export function parse(str: string, style: number, unsigned: boolean, bitsize: nu if (v >= min && v <= max) { return v; } + throw parseOverflow(unsigned, bitsize); } } - throw new Exception(`The input string ${str} was not in a correct format.`); + throw FormatException_$ctor_Z721C83C5(`The input string '${str}' was not in a correct format.`); } export function tryParse(str: string, style: number, unsigned: boolean, bitsize: number, defValue: FSharpRef): boolean { diff --git a/src/fable-library-ts/List.fs b/src/fable-library-ts/List.fs index e41e9724c7..bb653c7c16 100644 --- a/src/fable-library-ts/List.fs +++ b/src/fable-library-ts/List.fs @@ -3,31 +3,21 @@ module ListModule open Fable.Core open Native -[] [] type LinkedList<'T> = - { - head: 'T - mutable tail: LinkedList<'T> option - } + val head: 'T + val mutable tail: LinkedList<'T> option - static member Empty: 'T list = + new(head: 'T, tail: LinkedList<'T> option) + = { - head = Unchecked.defaultof<'T> - tail = None + head = head + tail = tail } - static member Cons(x: 'T, xs: 'T list) = - { - head = x - tail = Some xs - } + static member inline Cons(x: 'T, xs: 'T list) = LinkedList<'T>(x, Some xs) - static member inline internal ConsNoTail(x: 'T) = - { - head = x - tail = None - } + static member inline internal ConsNoTail(x: 'T) = LinkedList<'T>(x, None) member inline internal xs.SetConsTail(t: 'T list) = xs.tail <- Some t @@ -48,12 +38,12 @@ type LinkedList<'T> = member xs.Head = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some _ -> xs.head member xs.Tail = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some t -> t member xs.Item @@ -169,6 +159,16 @@ and ListEnumerator<'T>(xs: 'T list) = and 'T list = LinkedList<'T> and List<'T> = LinkedList<'T> +// F# has no generic values, so the shared node is typed obj and cast per use +let private emptyNode: LinkedList = + LinkedList(Unchecked.defaultof, None) + +let empty<'T> () : 'T list = unbox<'T list> emptyNode + +// Builders mutate the tail of their root node, so it must not be the shared empty node +let inline private newRoot<'T> () : 'T list = + List.ConsNoTail(Unchecked.defaultof<'T>) + // [] // [] // module List = @@ -176,17 +176,21 @@ and List<'T> = LinkedList<'T> let indexNotFound () = raise (System.Collections.Generic.KeyNotFoundException(SR.keyNotFoundAlt)) -let empty () = List.Empty +let private differentLengths (paramName: string) = + invalidArg paramName "The lists had different lengths." let cons (x: 'T) (xs: 'T list) = List.Cons(x, xs) -let singleton x = List.Cons(x, List.Empty) +let singleton x = List.Cons(x, empty ()) let isEmpty (xs: 'T list) = xs.IsEmpty let length (xs: 'T list) = xs.Length -let head (xs: 'T list) = xs.Head +let head (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some _ -> xs.head let tryHead (xs: 'T list) = if xs.IsEmpty then @@ -194,7 +198,10 @@ let tryHead (xs: 'T list) = else Some xs.Head -let tail (xs: 'T list) = xs.Tail +let tail (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some t -> t let rec tryLast (xs: 'T list) = if xs.IsEmpty then @@ -210,7 +217,7 @@ let rec tryLast (xs: 'T list) = let last (xs: 'T list) = match tryLast xs with | Some x -> x - | None -> failwith SR.inputWasEmpty + | None -> invalidArg "list" SR.inputWasEmpty let compareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list) : int = let rec loop (xs: 'T list) (ys: 'T list) = @@ -255,7 +262,7 @@ let fold<'T, 'State> (folder: 'State -> 'T -> 'State) (state: 'State) (xs: 'T li acc let reverse (xs: 'T list) = - fold (fun acc x -> List.Cons(x, acc)) List.Empty xs + fold (fun acc x -> List.Cons(x, acc)) (empty ()) xs let foldBack<'T, 'State> (folder: 'T -> 'State -> 'State) (xs: 'T list) (state: 'State) = // fold (fun acc x -> folder x acc) state (reverse xs) @@ -284,11 +291,29 @@ let fold2<'T1, 'T2, 'State> (folder: 'State -> 'T1 -> 'T2 -> 'State) (state: 'St xs <- xs.Tail ys <- ys.Tail + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc let foldBack2<'T1, 'T2, 'State> (folder: 'T1 -> 'T2 -> 'State -> 'State) (xs: 'T1 list) (ys: 'T2 list) (state: 'State) = // fold2 (fun acc x y -> folder x y acc) state (reverse xs) (reverse ys) - Array.foldBack2 folder (toArray xs) (toArray ys) state + let xs = toArray xs + let ys = toArray ys + + if xs.Length <> ys.Length then + differentLengths ( + if xs.Length = 0 then + "list1" + elif ys.Length = 0 then + "list2" + else + "list1, list2" + ) + + Array.foldBack2 folder xs ys state let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) = let rec loop acc (node: 'T list) = @@ -296,9 +321,9 @@ let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) = | None -> node | Some(x, acc) -> loop acc (node.AppendConsNoTail x) - let root = List.Empty + let root = newRoot () let node = loop state root - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let iterate action xs = fold (fun () x -> action x) () xs @@ -338,24 +363,24 @@ let ofArrayWithTail (xs: 'T[]) (tail: 'T list) = res -let ofArray (xs: 'T[]) = ofArrayWithTail xs List.Empty +let ofArray (xs: 'T[]) = ofArrayWithTail xs (empty ()) let ofSeq (xs: seq<'T>) : 'T list = match xs with | :? array<'T> as xs -> ofArray xs | :? list<'T> as xs -> xs | _ -> - let root = List.Empty + let root = newRoot () let mutable node = root for x in xs do node <- node.AppendConsNoTail x - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let concat (lists: seq<'T list>) = - let root = List.Empty + let root = newRoot () let mutable node = root let action xs = @@ -368,11 +393,11 @@ let concat (lists: seq<'T list>) = for xs in lists do action xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let scan (folder: 'State -> 'T -> 'State) (state: 'State) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let mutable node = root.AppendConsNoTail state let mutable acc = state let mutable xs = xs @@ -382,17 +407,30 @@ let scan (folder: 'State -> 'T -> 'State) (state: 'State) (xs: 'T list) = node <- node.AppendConsNoTail acc xs <- xs.Tail - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let scanBack (folder: 'T -> 'State -> 'State) (xs: 'T list) (state: 'State) = Array.scanBack folder (toArray xs) state |> ofArray +// Valid on a non-empty list: a reference-type option is erased, so tail holds the node; the cast is for tsc. +[] +let inline private uncheckedTail (xs: 'T list) : 'T list = nativeOnly + let append (xs: 'T list) (ys: 'T list) = - fold (fun acc x -> List.Cons(x, acc)) ys (reverse xs) + let root = newRoot () + let mutable node = root + let mutable rest = xs + + while rest.tail.IsSome do + node <- node.AppendConsNoTail rest.head + rest <- uncheckedTail rest + + node.SetConsTail ys + root.Tail let collect (mapping: 'T -> 'U list) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let mutable node = root let mutable ys = xs @@ -405,56 +443,64 @@ let collect (mapping: 'T -> 'U list) (xs: 'T list) = ys <- ys.Tail - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let mapIndexed (mapping: int -> 'T -> 'U) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let folder i (acc: 'U list) x = acc.AppendConsNoTail(mapping i x) let node = foldIndexed folder root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let map (mapping: 'T -> 'U) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let folder (acc: 'U list) x = acc.AppendConsNoTail(mapping x) let node = fold folder root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let indexed xs = mapIndexed (fun i x -> (i, x)) xs let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = - let root = List.Empty + let root = newRoot () let folder (acc: 'U list) x y = acc.AppendConsNoTail(mapping x y) let node = fold2 folder root xs ys - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let rec loop i (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) = if xs.IsEmpty || ys.IsEmpty then + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc else let node = acc.AppendConsNoTail(mapping i xs.Head ys.Head) loop (i + 1) node xs.Tail ys.Tail - let root = List.Empty + let root = newRoot () let node = loop 0 root xs ys - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = let rec loop (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = if xs.IsEmpty || ys.IsEmpty || zs.IsEmpty then + if not (xs.IsEmpty && ys.IsEmpty && zs.IsEmpty) then + differentLengths "list1, list2, list3" + acc else let node = acc.AppendConsNoTail(mapping xs.Head ys.Head zs.Head) loop node xs.Tail ys.Tail zs.Tail - let root = List.Empty + let root = newRoot () let node = loop root xs ys zs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let mapFold (mapping: 'State -> 'T -> 'Result * 'State) (state: 'State) (xs: 'T list) = @@ -462,9 +508,9 @@ let mapFold (mapping: 'State -> 'T -> 'Result * 'State) (state: 'State) (xs: 'T let r, st = mapping st x node.AppendConsNoTail r, st - let root = List.Empty + let root = newRoot () let node, state = fold folder (root, state) xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail, state let mapFoldBack (mapping: 'T -> 'State -> 'Result * 'State) (xs: 'T list) (state: 'State) = @@ -550,7 +596,7 @@ let tryItem n (xs: 'T list) = let item n (xs: 'T list) = xs.Item(n) let filter f (xs: 'T list) = - let root = List.Empty + let root = newRoot () let folder (acc: 'T list) x = if f x then @@ -559,11 +605,11 @@ let filter f (xs: 'T list) = acc let node = fold folder root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let partition f (xs: 'T list) = - let root1, root2 = List.Empty, List.Empty + let root1, root2 = newRoot (), newRoot () let folder (lacc: 'T list, racc: 'T list) x = if f x then @@ -572,12 +618,12 @@ let partition f (xs: 'T list) = lacc, racc.AppendConsNoTail x let node1, node2 = fold folder (root1, root2) xs - node1.SetConsTail List.Empty - node2.SetConsTail List.Empty + node1.SetConsTail(empty ()) + node2.SetConsTail(empty ()) root1.Tail, root2.Tail let choose<'T, 'U> (f: 'T -> 'U option) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let folder (acc: 'U list) x = match f x with @@ -585,40 +631,47 @@ let choose<'T, 'U> (f: 'T -> 'U option) (xs: 'T list) = | None -> acc let node = fold folder root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let contains (value: 'T) (xs: 'T list) ([] eq: System.Collections.Generic.IEqualityComparer<'T>) = tryFindIndex (fun v -> eq.Equals(value, v)) xs |> Option.isSome let initialize n (f: int -> 'T) = - let root = List.Empty + if n < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + let root = newRoot () let mutable node = root for i = 0 to n - 1 do node <- node.AppendConsNoTail(f i) - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let replicate n x = initialize n (fun _ -> x) let reduce f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else fold f (head xs) (tail xs) let reduceBack f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else foldBack f (tail xs) (head xs) let forAll f xs = fold (fun acc x -> acc && f x) true xs -let forAll2 f xs ys = - fold2 (fun acc x y -> acc && f x y) true xs ys +let rec forAll2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = + match xs.IsEmpty, ys.IsEmpty with + | true, true -> true + | false, false -> f xs.Head ys.Head && forAll2 f xs.Tail ys.Tail + | true, false -> differentLengths "list1" + | false, true -> differentLengths "list2" let exists f xs = tryFindIndex f xs |> Option.isSome @@ -626,16 +679,16 @@ let rec exists2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = match xs.IsEmpty, ys.IsEmpty with | true, true -> false | false, false -> f xs.Head ys.Head || exists2 f xs.Tail ys.Tail - | _ -> invalidArg "list2" SR.differentLengths + | _ -> differentLengths "list2" let unzip xs = - foldBack (fun (x, y) (lacc, racc) -> List.Cons(x, lacc), List.Cons(y, racc)) xs (List.Empty, List.Empty) + foldBack (fun (x, y) (lacc, racc) -> List.Cons(x, lacc), List.Cons(y, racc)) xs (empty (), empty ()) let unzip3 xs = foldBack (fun (x, y, z) (lacc, macc, racc) -> List.Cons(x, lacc), List.Cons(y, macc), List.Cons(z, racc)) xs - (List.Empty, List.Empty, List.Empty) + (empty (), empty (), empty ()) let zip xs ys = map2 (fun x y -> x, y) xs ys @@ -709,6 +762,9 @@ let min (xs: 'T list) ([] comparer: System.Collections.Generic.IComparer xs let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let folder acc x = @@ -719,6 +775,9 @@ let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = averager.DivideByInt(total, count) let averageBy (f: 'T -> 'U) (xs: 'T list) ([] averager: IGenericAverager<'U>) : 'U = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let inline folder acc x = @@ -735,19 +794,19 @@ let chunkBySize (chunkSize: int) (xs: 'T list) : 'T list list = toArray xs |> Array.chunkBySize chunkSize |> Array.map ofArray |> ofArray let allPairs (xs: 'T1 list) (ys: 'T2 list) : ('T1 * 'T2) list = - let root = List.Empty + let root = newRoot () let mutable node = root iterate (fun x -> iterate (fun y -> node <- node.AppendConsNoTail(x, y)) ys) xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let rec skip count (xs: 'T list) = if count <= 0 then xs elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) else skip (count - 1) xs.Tail @@ -767,13 +826,13 @@ let take count (xs: 'T list) = if i <= 0 then acc elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + invalidOp SR.notEnoughElements else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail - let root = List.Empty + let root = newRoot () let node = loop count root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let takeWhile predicate (xs: 'T list) = @@ -785,9 +844,9 @@ let takeWhile predicate (xs: 'T list) = else loop (acc.AppendConsNoTail xs.Head) xs.Tail - let root = List.Empty + let root = newRoot () let node = loop root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let truncate count (xs: 'T list) = @@ -799,9 +858,9 @@ let truncate count (xs: 'T list) = else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail - let root = List.Empty + let root = newRoot () let node = loop count root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let getSlice (startIndex: int option) (endIndex: int option) (xs: 'T list) = @@ -824,7 +883,7 @@ let getSlice (startIndex: int option) (endIndex: int option) (xs: 'T list) = index if endIndex < startIndex then - List.Empty + (empty ()) else xs |> skip startIndex |> take (endIndex - startIndex + 1) @@ -833,7 +892,7 @@ let splitAt index (xs: 'T list) = invalidArg "index" SR.inputMustBeNonNegative if index > xs.Length then - invalidArg "index" SR.notEnoughElements + invalidOp SR.notEnoughElements take index xs, skip index xs @@ -885,7 +944,7 @@ let insertAt (index: int) (y: 'T) (xs: 'T list) : 'T list = let mutable isDone = false let result = - (List.Empty, xs) + (empty (), xs) ||> fold (fun acc x -> i <- i + 1 @@ -912,7 +971,7 @@ let insertManyAt (index: int) (ys: seq<'T>) (xs: 'T list) : 'T list = let ys = ofSeq ys let result = - (List.Empty, xs) + (empty (), xs) ||> fold (fun acc x -> i <- i + 1 @@ -955,43 +1014,21 @@ let removeAt (index: int) (xs: 'T list) : 'T list = ys let removeManyAt (index: int) (count: int) (xs: 'T list) : 'T list = + if index < 0 then + invalidArg "index" "index must be within bounds of the list" + + let endIndex = index + count let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 let ys = xs |> filter (fun _ -> i <- i + 1 - - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true + i < index || i >= endIndex ) - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status - - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" - else - "count" - - invalidArg arg SR.indexOutOfBounds + if endIndex > i + 1 then + invalidArg "index" "index must be within bounds of the list" ys @@ -1024,6 +1061,9 @@ let randomShuffleWith (random: System.Random) (xs: 'T list) : 'T list = let randomShuffle (xs: 'T list) : 'T list = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T list) : 'T = + if isEmpty xs then + invalidArg "source" SR.inputSequenceEmpty + toArray xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T list) : 'T = diff --git a/src/fable-library-ts/Long.ts b/src/fable-library-ts/Long.ts index ed81356dc6..9b35950809 100644 --- a/src/fable-library-ts/Long.ts +++ b/src/fable-library-ts/Long.ts @@ -1,7 +1,8 @@ -import { getPrefix, isValid } from "./Int32.ts"; +import { getPrefix, isValid, parseOverflow } from "./Int32.ts"; import { fromString } from "./BigInt.ts"; import { FSharpRef } from "./Types.ts"; import { Exception } from "./Util.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts"; function getRange(unsigned: boolean, bitsize: number): [bigint, bigint] { switch (bitsize) { @@ -13,6 +14,9 @@ function getRange(unsigned: boolean, bitsize: number): [bigint, bigint] { } export function parse(str: string, style: number, unsigned: boolean, bitsize: number, radix?: number) { + if (str == null) { + throw ArgumentNullException_$ctor_Z721C83C5("s"); + } const res = isValid(str, style, radix); if (res != null) { let v = fromString(getPrefix(res.radix) + res.digits); @@ -27,8 +31,9 @@ export function parse(str: string, style: number, unsigned: boolean, bitsize: nu if (v >= min && v <= max) { return v; } + throw parseOverflow(unsigned, bitsize); } - throw new Exception(`The input string ${str} was not in a correct format.`); + throw FormatException_$ctor_Z721C83C5(`The input string '${str}' was not in a correct format.`); } export function tryParse(str: string, style: number, unsigned: boolean, bitsize: number, defValue: FSharpRef) { diff --git a/src/fable-library-ts/MailboxProcessor.ts b/src/fable-library-ts/MailboxProcessor.ts index b78689d7c7..c4d08b3b24 100644 --- a/src/fable-library-ts/MailboxProcessor.ts +++ b/src/fable-library-ts/MailboxProcessor.ts @@ -1,7 +1,7 @@ import { defaultCancellationToken } from "./Async.ts"; import { fromContinuations } from "./Async.ts"; import { startImmediate } from "./Async.ts"; -import { Async } from "./AsyncBuilder.ts"; +import { Async, IAsyncContext, OperationCanceledException, protectedCont } from "./AsyncBuilder.ts"; import { Continuation, Continuations } from "./AsyncBuilder.ts"; import { CancellationToken } from "./AsyncBuilder.ts"; import { Exception } from "./Util.ts"; @@ -80,12 +80,25 @@ export function startInstance($this: MailboxProcessor) { } export function receive($this: MailboxProcessor) { - return fromContinuations((conts: Continuations) => { + return protectedCont((ctx: IAsyncContext) => { if ($this.continuation) { throw new Exception("Receive can only be called once!"); } - $this.continuation = conts[0]; - __processEvents($this); + let tokenId: number | undefined; + const continuation = (message: Msg) => { + if (tokenId !== undefined) { ctx.cancelToken.removeListener(tokenId); } + ctx.onSuccess(message); + }; + $this.continuation = continuation; + tokenId = ctx.cancelToken.addListener(() => { + if ($this.continuation === continuation) { + delete $this.continuation; + ctx.onCancel(new OperationCanceledException()); + } + }); + if ($this.continuation === continuation) { + __processEvents($this); + } }); } @@ -110,6 +123,8 @@ export function postAndAsyncReply( } const reply = { reply: (res: Reply) => { + // .NET AsyncReplyChannel.Reply ignores a second reply + if (replied) { return; } result = res; replied = true; checkCompletion(); diff --git a/src/fable-library-ts/Map.fs b/src/fable-library-ts/Map.fs index ee52a22643..97d8f177fc 100644 --- a/src/fable-library-ts/Map.fs +++ b/src/fable-library-ts/Map.fs @@ -592,9 +592,11 @@ module MapTree = started = false } - let notStarted () = failwith "enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "enumeration already finished" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then @@ -905,14 +907,14 @@ type Map<[] 'Key, [> with member m.Add x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Clear() = - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Remove x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Contains x = m.ContainsKey x.Key && Unchecked.equals m.[x.Key] x.Value @@ -935,11 +937,11 @@ type Map<[] 'Key, [ Seq.map (fun p -> p.Key, p.Value) @@ -948,7 +950,7 @@ type Map<[] 'Key, [ Seq.map (fun p -> p.Key) member m.set(k, v) = - failwith "Map cannot be mutated" + raise (System.NotSupportedException("Map values cannot be mutated.")) m :> JS.Map<'Key, 'Value> member m.values() = m |> Seq.map (fun p -> p.Value) diff --git a/src/fable-library-ts/MutableMap.fs b/src/fable-library-ts/MutableMap.fs index 12fb5aee85..9ed742434a 100644 --- a/src/fable-library-ts/MutableMap.fs +++ b/src/fable-library-ts/MutableMap.fs @@ -12,6 +12,7 @@ type MutableMap<'Key, 'Value when 'Key: equality> // Compiles to JS Map of key hashes pointing to dynamic arrays of KeyValuePair<'Key, 'Value>. let hashMap = Dictionary>>() + let mutable count = 0 do for pair in pairs do @@ -20,29 +21,43 @@ type MutableMap<'Key, 'Value when 'Key: equality> // new () = MutableMap (Seq.empty, EqualityComparer.Default) // new (comparer) = MutableMap (Seq.empty, comparer) - member private this.TryFindIndex(k) = - let h = comparer.GetHashCode(k) + // An erased option of a reference type, so a hit allocates nothing + member private this.TryFindBucket(h: int) : ResizeArray> option = + if hashMap.ContainsKey h then + Some hashMap.[h] + else + None - match hashMap.TryGetValue h with - | true, pairs -> true, h, pairs.FindIndex(fun pair -> comparer.Equals(k, pair.Key)) - | false, _ -> false, h, -1 + member private this.FindIndex(k, bucket: ResizeArray>) = + let mutable i = 0 + let mutable found = -1 - member this.TryFind(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> Some hashMap.[h].[i] - | _, _, _ -> None + while found < 0 && i < bucket.Count do + if comparer.Equals(k, bucket.[i].Key) then + found <- i - member this.Comparer = comparer + i <- i + 1 + + found - member this.Clear() = hashMap.Clear() + member this.TryFind(k: 'Key) = + match this.TryFindBucket(comparer.GetHashCode(k)) with + | Some bucket -> + let i = this.FindIndex(k, bucket) - member this.Count = - let mutable count = 0 + if i > -1 then + Some bucket.[i] + else + None + | None -> None + + member this.Comparer = comparer - for pairs in hashMap.Values do - count <- count + pairs.Count + member this.Clear() = + hashMap.Clear() + count <- 0 - count + member this.Count = count member this.Item with get (k: 'Key) = @@ -50,32 +65,61 @@ type MutableMap<'Key, 'Value when 'Key: equality> | Some pair -> pair.Value | _ -> raise (KeyNotFoundException("The item was not found in collection")) and set (k: 'Key) (v: 'Value) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> hashMap.[h].[i] <- KeyValuePair(k, v) // replace - | true, h, _ -> hashMap.[h].Add(KeyValuePair(k, v)) |> ignore // append - | false, h, _ -> hashMap.[h] <- ResizeArray([| KeyValuePair(k, v) |]) - - member this.Add(k, v) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> - let msg = - System.String.Format("An item with the same key has already been added. Key: {0}", k) - - raise (System.ArgumentException(msg)) - | true, h, _ -> hashMap.[h].Add(KeyValuePair(k, v)) |> ignore // append - | false, h, _ -> hashMap.[h] <- ResizeArray([| KeyValuePair(k, v) |]) // add new - - member this.ContainsKey(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> true - | _, _, _ -> false - - member this.Remove(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> - hashMap.[h].RemoveAt(i) - true - | _, _, _ -> false + let h = comparer.GetHashCode(k) + + match this.TryFindBucket h with + | Some bucket -> + let i = this.FindIndex(k, bucket) + + if i > -1 then + bucket.[i] <- KeyValuePair(k, v) + else + bucket.Add(KeyValuePair(k, v)) + count <- count + 1 + | None -> + hashMap.[h] <- ResizeArray([| KeyValuePair(k, v) |]) + count <- count + 1 + + member this.Add(k: 'Key, v: 'Value) = + let h = comparer.GetHashCode(k) + + match this.TryFindBucket h with + | Some bucket -> + if this.FindIndex(k, bucket) > -1 then + let msg = + System.String.Format("An item with the same key has already been added. Key: {0}", k) + + raise (System.ArgumentException(msg)) + + bucket.Add(KeyValuePair(k, v)) + count <- count + 1 + | None -> + hashMap.[h] <- ResizeArray([| KeyValuePair(k, v) |]) + count <- count + 1 + + member this.ContainsKey(k: 'Key) = + match this.TryFindBucket(comparer.GetHashCode(k)) with + | Some bucket -> this.FindIndex(k, bucket) > -1 + | None -> false + + member this.Remove(k: 'Key) = + let h = comparer.GetHashCode(k) + + match this.TryFindBucket h with + | Some bucket -> + let i = this.FindIndex(k, bucket) + + if i > -1 then + bucket.RemoveAt(i) + count <- count - 1 + + if bucket.Count = 0 then + hashMap.Remove(h) |> ignore + + true + else + false + | None -> false interface Fable.Core.Symbol_wellknown with member _.``Symbol.toStringTag`` = "Dictionary" diff --git a/src/fable-library-ts/MutableSet.fs b/src/fable-library-ts/MutableSet.fs index db04abb6a6..69f2bb780f 100644 --- a/src/fable-library-ts/MutableSet.fs +++ b/src/fable-library-ts/MutableSet.fs @@ -10,6 +10,7 @@ type MutableSet<'T when 'T: equality>(items: 'T seq, comparer: IEqualityComparer // Compiles to JS Map of key hashes pointing to dynamic arrays of 'T. let hashMap = Dictionary>() + let mutable count = 0 do for item in items do @@ -18,51 +19,83 @@ type MutableSet<'T when 'T: equality>(items: 'T seq, comparer: IEqualityComparer // new () = MutableSet (Seq.empty, EqualityComparer.Default) // new (comparer) = MutableSet (Seq.empty, comparer) - member private this.TryFindIndex(k) = - let h = comparer.GetHashCode(k) + // An erased option of a reference type, so a hit allocates nothing + member private this.TryFindBucket(h: int) : ResizeArray<'T> option = + if hashMap.ContainsKey h then + Some hashMap.[h] + else + None - match hashMap.TryGetValue h with - | true, values -> true, h, values.FindIndex(fun v -> comparer.Equals(k, v)) - | false, _ -> false, h, -1 + member private this.FindIndex(k, bucket: ResizeArray<'T>) = + let mutable i = 0 + let mutable found = -1 - member private this.TryFind(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> Some hashMap.[h].[i] - | _, _, _ -> None + while found < 0 && i < bucket.Count do + if comparer.Equals(k, bucket.[i]) then + found <- i - member this.Comparer = comparer + i <- i + 1 - member this.Clear() = hashMap.Clear() + found - member this.Count = - let mutable count = 0 + member private this.TryFind(k: 'T) = + match this.TryFindBucket(comparer.GetHashCode(k)) with + | Some bucket -> + let i = this.FindIndex(k, bucket) - for items in hashMap.Values do - count <- count + items.Count + if i > -1 then + Some bucket.[i] + else + None + | None -> None - count + member this.Comparer = comparer - member this.Add(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> false - | true, h, _ -> - hashMap.[h].Add(k) |> ignore - true - | false, h, _ -> + member this.Clear() = + hashMap.Clear() + count <- 0 + + member this.Count = count + + member this.Add(k: 'T) = + let h = comparer.GetHashCode(k) + + match this.TryFindBucket h with + | Some bucket -> + if this.FindIndex(k, bucket) > -1 then + false + else + bucket.Add(k) + count <- count + 1 + true + | None -> hashMap.[h] <- ResizeArray([| k |]) + count <- count + 1 true - member this.Contains(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> true - | _, _, _ -> false + member this.Contains(k: 'T) = + match this.TryFindBucket(comparer.GetHashCode(k)) with + | Some bucket -> this.FindIndex(k, bucket) > -1 + | None -> false - member this.Remove(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> - hashMap.[h].RemoveAt(i) - true - | _, _, _ -> false + member this.Remove(k: 'T) = + let h = comparer.GetHashCode(k) + + match this.TryFindBucket h with + | Some bucket -> + let i = this.FindIndex(k, bucket) + + if i > -1 then + bucket.RemoveAt(i) + count <- count - 1 + + if bucket.Count = 0 then + hashMap.Remove(h) |> ignore + + true + else + false + | None -> false interface Fable.Core.Symbol_wellknown with member _.``Symbol.toStringTag`` = "HashSet" diff --git a/src/fable-library-ts/NumberStyles.ts b/src/fable-library-ts/NumberStyles.ts new file mode 100644 index 0000000000..8e96aa5a8d --- /dev/null +++ b/src/fable-library-ts/NumberStyles.ts @@ -0,0 +1,63 @@ +import { Exception } from "./Util.ts"; + +export const NumberStyles = { + None: 0, + AllowLeadingWhite: 1, + AllowTrailingWhite: 2, + AllowLeadingSign: 4, + AllowTrailingSign: 8, + AllowParentheses: 16, + AllowDecimalPoint: 32, + AllowThousands: 64, + AllowExponent: 128, + AllowHexSpecifier: 512, + Integer: 7, + Number: 111, + Float: 167, +} as const; + +export interface NumberParts { + digits: string; + negative: boolean; +} + +// The digits of a number string once white space, sign and parentheses are stripped. +// .NET allows group separators anywhere after the first digit of the integral part, even repeated. +function digitsPattern(style: number): RegExp { + const integral = style & NumberStyles.AllowThousands ? "\\d+(?:,\\d*)*" : "\\d+"; + const point = style & NumberStyles.AllowDecimalPoint ? "(?:\\.\\d*)?" : ""; + const leadingPoint = style & NumberStyles.AllowDecimalPoint ? "|\\.\\d+" : ""; + const exponent = style & NumberStyles.AllowExponent ? "(?:[eE][+-]?\\d+)?" : ""; + return new RegExp(`^(?:${integral}${point}${leadingPoint})${exponent}$`); +} + +// Returns undefined when the string does not have the format allowed by the style +export function splitNumber(str: string, style: number): NumberParts | undefined { + if (style & NumberStyles.AllowHexSpecifier) { + throw new Exception("The number style AllowHexSpecifier is not supported on floating point data types."); + } + // .NET ignores trailing NULs and its white space is only [\t\n\v\f\r ] + let s = str.replace(/\0+$/, ""); + if (style & NumberStyles.AllowLeadingWhite) { + s = s.replace(/^[\t-\r ]+/, ""); + } + if (style & NumberStyles.AllowTrailingWhite) { + s = s.replace(/[\t-\r ]+$/, ""); + } + let negative = false; + if (style & NumberStyles.AllowParentheses && s.startsWith("(") && s.endsWith(")")) { + s = s.slice(1, -1); + negative = true; + } + if (style & NumberStyles.AllowLeadingSign && /^[+-]/.test(s)) { + negative = s[0] === "-" ? !negative : negative; + s = s.slice(1); + } else if (style & NumberStyles.AllowTrailingSign && /[+-]$/.test(s)) { + negative = s[s.length - 1] === "-" ? !negative : negative; + s = s.slice(0, -1); + } + if (!digitsPattern(style).test(s)) { + return undefined; + } + return { digits: s.replace(/,/g, ""), negative }; +} diff --git a/src/fable-library-ts/Numeric.ts b/src/fable-library-ts/Numeric.ts index 33bb460a72..19c982d22f 100644 --- a/src/fable-library-ts/Numeric.ts +++ b/src/fable-library-ts/Numeric.ts @@ -43,6 +43,17 @@ export function multiply(x: Numeric, y: number) { } } +export function divide(x: Numeric, y: number) { + if (typeof x === "number") { + return x / y; + } else if (typeof x === "bigint") { + // Keep the fractional part so the caller can round it like .NET does + return Number(x) / y; + } else { + return x[symbol]().multiply(1 / y); + } +} + export function toFixed(x: Numeric, dp?: number) { if (typeof x === "number") { return x.toFixed(dp); @@ -63,23 +74,119 @@ export function toPrecision(x: Numeric, sd?: number) { } } +// A bigint can hold more significant digits than a double, so the mantissa is rounded on the +// decimal digits themselves instead of going through Number +function bigintToExponential(x: bigint, dp?: number) { + const sign = x < 0n ? "-" : ""; + let digits = (x < 0n ? -x : x).toString(); + if (x === 0n) { + return sign + (dp ? "0." + "0".repeat(dp) : "0") + "e+0"; + } + dp = dp ?? digits.length - 1; + let exponent = digits.length - 1; + if (digits.length > dp + 1) { + // .NET rounds the discarded digits away from zero + const roundUp = digits.charCodeAt(dp + 1) >= 53; + digits = (BigInt(digits.slice(0, dp + 1)) + (roundUp ? 1n : 0n)).toString(); + if (digits.length > dp + 1) { + digits = digits.slice(0, dp + 1); + exponent += 1; + } + } else { + digits = digits.padEnd(dp + 1, "0"); + } + const mantissa = dp > 0 ? digits[0] + "." + digits.slice(1) : digits[0]; + return sign + mantissa + "e+" + exponent; +} + export function toExponential(x: Numeric, dp?: number) { if (typeof x === "number") { return x.toExponential(dp); } else if (typeof x === "bigint") { - return x; + return bigintToExponential(x, dp); } else { return x[symbol]().toExponential(dp); } } -export function toHex(x: Numeric) { +function isExactTie(x: number, k: number) { + if (k >= 0) { + const t = x * 2 ** (k + 1); + return Number.isInteger(t) && t % 2 !== 0; + } else if (Number.isInteger(x)) { + const p = 10n ** BigInt(-k); + return BigInt(Math.abs(x)) % p === p / 2n; + } else { + return false; + } +} + +function truncateLastDigit(digits: string) { + const s = digits.slice(0, -1); + return s.endsWith(".") ? s.slice(0, -1) : s; +} + +function isLastDigitEven(digits: string) { + return digits.charCodeAt(digits.length - 1) % 2 === 0; +} + +// .NET standard numeric formats give an exact midpoint of a double the even digit +export function toFixedHalfEven(x: Numeric, dp: number) { + if (typeof x === "number" && dp < 100 && isExactTie(x, dp)) { + const truncated = truncateLastDigit(x.toFixed(dp + 1)); + return isLastDigitEven(truncated) ? truncated : x.toFixed(dp); + } else { + return toFixed(x, dp); + } +} + +export function toExponentialHalfEven(x: Numeric, dp: number) { + if (typeof x === "number" && Number.isFinite(x) && dp < 100) { + const exact = x.toExponential(dp + 1); + const eIdx = exact.indexOf("e"); + const mantissa = exact.slice(0, eIdx); + if (mantissa.endsWith("5") && isExactTie(x, dp - Number(exact.slice(eIdx + 1)))) { + const truncated = truncateLastDigit(mantissa); + return isLastDigitEven(truncated) ? truncated + exact.slice(eIdx) : x.toExponential(dp); + } + } + return toExponential(x, dp); +} + +export function toPrecisionHalfEven(x: Numeric, sd: number) { + if (typeof x !== "number" || !(sd > 0)) { + return toPrecision(x, sd); + } + const s = toExponentialHalfEven(x, sd - 1); + if (s === x.toExponential(sd - 1)) { + return x.toPrecision(sd); + } + // Same notation as Number.prototype.toPrecision + const eIdx = s.indexOf("e"); + const e = Number(s.slice(eIdx + 1)); + if (e < -6 || e >= sd) { + return s; + } + const sign = s.startsWith("-") ? "-" : ""; + const digits = s.slice(sign.length, eIdx).replace(".", ""); + return e < 0 + ? sign + "0." + "0".repeat(-e - 1) + digits + : sign + digits.slice(0, e + 1) + (sd > e + 1 ? "." + digits.slice(e + 1) : ""); +} + +export function toRadix(x: Numeric, radix: number) { if (typeof x === "number") { - return (Number(x) >>> 0).toString(16); + return (Number(x) >>> 0).toString(radix); } else if (typeof x === "bigint") { // TODO: properly handle other bit sizes - return BigInt.asUintN(64, x).toString(16); - } else { + return BigInt.asUintN(64, x).toString(radix); + } else if (radix === 16) { return x[symbol]().toHex(); + } else { + return String(x); } } + +export function toHex(x: Numeric) { + return toRadix(x, 16); +} diff --git a/src/fable-library-ts/Random.fs b/src/fable-library-ts/Random.fs index 8b686e3e5a..a21d4b39fa 100644 --- a/src/fable-library-ts/Random.fs +++ b/src/fable-library-ts/Random.fs @@ -2,6 +2,16 @@ module Random open System +let private checkMaxValue (maxValue: int) = + if maxValue < 0 then + let msg = "maxValue ('" + string maxValue + "') must be a non-negative value." + raise <| ArgumentOutOfRangeException("maxValue", msg) + +let private checkMinMaxValue (minValue: int) (maxValue: int) = + if minValue > maxValue then + let msg = "'minValue' cannot be greater than maxValue." + raise <| ArgumentOutOfRangeException("minValue", msg) + module private Native = open Fable.Core open Fable.Core.JsInterop @@ -9,8 +19,7 @@ module private Native = let random () = JS.Math.random () let randomNext (minValue: int, maxValue: int) : int = - if (maxValue < minValue) then - raise <| ArgumentOutOfRangeException("minValue must be less than maxValue") + checkMinMaxValue minValue maxValue emitJsStatement (box () |> nonNull) @@ -48,7 +57,10 @@ type IRandom = type NonSeeded() = interface IRandom with member _.Next0() = Native.randomNext (0, Int32.MaxValue) - member _.Next1(maxValue) = Native.randomNext (0, maxValue) + + member _.Next1(maxValue) = + checkMaxValue maxValue + Native.randomNext (0, maxValue) member _.Next2(minValue, maxValue) = Native.randomNext (minValue, maxValue) @@ -145,23 +157,19 @@ type Seeded(seed: int) = let mutable d = result d <- d + float (Int32.MaxValue - 1) - d <- d / (2.0 * float (uint (Int32.MaxValue - 1))) + d <- d / (2.0 * float Int32.MaxValue - 1.0) d interface IRandom with member this.Next0() = this.InternalSample() member this.Next1(maxValue: int) = - if maxValue < 0 then - raise <| ArgumentOutOfRangeException("maxValue must be positive") - + checkMaxValue maxValue int (this.Sample() * float maxValue) member this.Next2(minValue: int, maxValue: int) = - if minValue > maxValue then - raise <| ArgumentOutOfRangeException("minValue must be less than maxValue") - - let range = int64 (maxValue - minValue) + checkMinMaxValue minValue maxValue + let range = int64 maxValue - int64 minValue if range <= int64 Int32.MaxValue then int (int (this.Sample() * float range) + minValue) @@ -177,5 +185,8 @@ type Seeded(seed: int) = for i = 0 to buffer.Length - 1 do buffer.[i] <- byte ((int (this.InternalSample())) % (int Byte.MaxValue + 1)) +let private sharedRandom = NonSeeded() + let nonSeeded () = NonSeeded() let seeded seed = Seeded(seed) +let shared () = sharedRandom diff --git a/src/fable-library-ts/Range.fs b/src/fable-library-ts/Range.fs index d6ce3d8933..eeaa35e429 100644 --- a/src/fable-library-ts/Range.fs +++ b/src/fable-library-ts/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T when 'T: comparison> (step: 'T) (stop: 'T) (zero: ' let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-ts/Reflection.ts b/src/fable-library-ts/Reflection.ts index 4ee30aceba..8cb9e588bd 100644 --- a/src/fable-library-ts/Reflection.ts +++ b/src/fable-library-ts/Reflection.ts @@ -50,7 +50,8 @@ export class TypeInfo implements IEquatable { public fullname: string; public generics?: TypeInfo[]; public construct?: Constructor; - public parent?: TypeInfo; + // A thunk when the base type's generic arguments can refer back to this type, see #3607 + private lazyParent?: TypeInfo | (() => TypeInfo); public fields?: () => FieldInfo[]; public cases?: () => CaseInfo[]; public enumCases?: EnumCase[]; @@ -59,7 +60,7 @@ export class TypeInfo implements IEquatable { fullname: string, generics?: TypeInfo[], construct?: Constructor, - parent?: TypeInfo, + parent?: TypeInfo | (() => TypeInfo), fields?: () => FieldInfo[], cases?: () => CaseInfo[], enumCases?: EnumCase[] @@ -67,11 +68,17 @@ export class TypeInfo implements IEquatable { this.fullname = fullname; this.generics = generics; this.construct = construct; - this.parent = parent; + this.lazyParent = parent; this.fields = fields; this.cases = cases; this.enumCases = enumCases; } + public get parent(): TypeInfo | undefined { + if (typeof this.lazyParent === "function") { + this.lazyParent = this.lazyParent(); + } + return this.lazyParent; + } public toString() { return fullName(this); } @@ -115,7 +122,7 @@ export function class_type( fullname: string, generics?: TypeInfo[], construct?: Constructor, - parent?: TypeInfo): TypeInfo { + parent?: TypeInfo | (() => TypeInfo)): TypeInfo { return new TypeInfo(fullname, generics, construct, parent); } diff --git a/src/fable-library-ts/RegExp.ts b/src/fable-library-ts/RegExp.ts index de54752f71..e49b56bf51 100644 --- a/src/fable-library-ts/RegExp.ts +++ b/src/fable-library-ts/RegExp.ts @@ -9,8 +9,9 @@ export function create(pattern: string, options = 0) { // * Compiled: 0x0008 (ignored) // * Singleline: 0x0010 // * ECMAScript: 0x0100 (ignored) - if ((options & ~(1 ^ 2 ^ 8 ^ 16 ^ 256)) !== 0) { - throw new Exception("RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline and ECMAScript"); + // * NonBacktracking: 0x0400 (ignored) + if ((options & ~(1 ^ 2 ^ 8 ^ 16 ^ 256 ^ 1024)) !== 0) { + throw new Exception("RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline, ECMAScript and NonBacktracking"); } // Set always global and unicode flags for compatibility with dotnet, see #2925 let flags = "gu"; diff --git a/src/fable-library-ts/Seq.fs b/src/fable-library-ts/Seq.fs index 20631e4a89..b822bd50d0 100644 --- a/src/fable-library-ts/Seq.fs +++ b/src/fable-library-ts/Seq.fs @@ -13,6 +13,7 @@ type IEnumerable<'T> = System.Collections.Generic.IEnumerable<'T> module SR = let enumerationAlreadyFinished = "Enumeration already finished." let enumerationNotStarted = "Enumeration has not started. Call MoveNext." + let inputMustBePositive = "The input must be positive." let inputSequenceEmpty = "The input sequence was empty." let inputSequenceTooLong = "The input sequence contains more than one element." @@ -311,9 +312,16 @@ let ofArray (arr: 'T[]) = arr :> seq<'T> let toArray (xs: seq<'T>) : 'T[] = match xs with - // | :? array<'T> as a -> Array.ofSeq a + | :? array<'T> as a -> Array.ofSeq a | :? list<'T> as a -> Array.ofList a - | _ -> Array.ofSeq xs + | _ -> + let res = ResizeArray<'T>() + use e = xs.GetEnumerator() + + while e.MoveNext() do + res.Add(e.Current) + + res.ToArray() let ofList (xs: list<'T>) = (xs :> seq<'T>) @@ -682,6 +690,9 @@ let head (xs: seq<'T>) = | None -> invalidArg "source" SR.inputSequenceEmpty let initialize count f = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + unfold (fun i -> if (i < count) then @@ -991,7 +1002,7 @@ let reduce folder (xs: seq<'T>) = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack folder (xs: seq<'T>) = let arr = toArray xs @@ -999,9 +1010,14 @@ let reduceBack folder (xs: seq<'T>) = if arr.Length > 0 then Array.reduceBack folder arr else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty + +let replicate n x = + // FSharp.Core uses Enumerable.Repeat, which raises ArgumentOutOfRangeException + if n < 0 then + raise (System.ArgumentOutOfRangeException("count")) -let replicate n x = initialize n (fun _ -> x) + initialize n (fun _ -> x) let reverse (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.rev |> ofArray) @@ -1034,7 +1050,7 @@ let skip count (source: seq<'T>) = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -1056,9 +1072,26 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -let tail (xs: seq<'T>) = skip 1 xs +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException +let tail (xs: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) let take count (xs: seq<'T>) = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + generateIndexed (fun () -> ofSeq xs) (fun i e -> @@ -1066,7 +1099,7 @@ let take count (xs: seq<'T>) = if e.MoveNext() then Some(e.Current) else - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements else None ) @@ -1107,19 +1140,32 @@ let pairwise (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.pairwise |> ofArray) let splitInto (chunks: int) (xs: seq<'T>) : 'T[] seq = + if chunks < 1 then + invalidArg "count" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.splitInto chunks |> ofArray) let windowed (windowSize: int) (xs: seq<'T>) : 'T[] seq = + if windowSize < 1 then + invalidArg "windowSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.windowed windowSize |> ofArray) let transpose (xss: seq<#seq<'T>>) : seq> = delay (fun () -> - xss - |> toArray - |> Array.map toArray - |> Array.transpose - |> Array.map ofArray - |> ofArray + let rows = ResizeArray>() + + for xs in xss do + let mutable i = 0 + + for x in xs do + if i = rows.Count then + rows.Add(ResizeArray<'T>()) + + rows.[i].Add(x) + i <- i + 1 + + rows |> map (fun row -> row :> seq<'T>) ) let sortWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) = @@ -1223,6 +1269,9 @@ let permute f (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.permute f |> ofArray) let chunkBySize (chunkSize: int) (xs: seq<'T>) : seq<'T[]> = + if chunkSize < 1 then + invalidArg "chunkSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.chunkBySize chunkSize |> ofArray) // let init = initialize @@ -1327,28 +1376,28 @@ let removeAt (index: int) (xs: seq<'T>) : seq<'T> = let removeManyAt (index: int) (count: int) (xs: seq<'T>) : seq<'T> = if index < 0 then - invalidArg "index" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - generateIndexed - (fun () -> ofSeq xs) - (fun i e -> + let endIndex = index + count + + delay (fun () -> + let mutable i = -1 + + let rest () = if i < index then - if e.MoveNext() then - Some e.Current - else - invalidArg "index" SR.indexOutOfBounds - else - if i = index then - for _ = 1 to count do - if not (e.MoveNext()) then - invalidArg "count" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - if e.MoveNext() then - Some e.Current - else - None - ) - (fun e -> e.Dispose()) + empty () + + let ys = + xs + |> filter (fun _ -> + i <- i + 1 + i < index || i >= endIndex + ) + + append ys (delay rest) + ) let updateAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = let mutable isDone = false @@ -1382,16 +1431,30 @@ let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceBy randomizer + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceWith random + randomChoiceSource 1 xs |> Array.randomChoiceWith random -let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice +// Draws on enumeration, like FSharp.Core let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = - xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = randomChoicesBy (fun () -> random.NextDouble()) count xs diff --git a/src/fable-library-ts/Set.fs b/src/fable-library-ts/Set.fs index 4a39504015..ac222fc327 100644 --- a/src/fable-library-ts/Set.fs +++ b/src/fable-library-ts/Set.fs @@ -482,12 +482,12 @@ module SetTree = let minimumElement s = match minimumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." let maximumElement s = match maximumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." // Imperative left-to-right iterators. [] @@ -517,9 +517,11 @@ module SetTree = started = false } - let notStarted () = failwith "Enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "Enumeration already started" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then diff --git a/src/fable-library-ts/Stopwatch.ts b/src/fable-library-ts/Stopwatch.ts new file mode 100644 index 0000000000..b48178f11e --- /dev/null +++ b/src/fable-library-ts/Stopwatch.ts @@ -0,0 +1,81 @@ +import { int64 } from "./BigInt.ts"; + +// Timestamps are in microseconds, the resolution performance.now() can offer +const ticksPerMillisecond = 1000; + +export class Stopwatch { + // Milliseconds measured before the last Start + public elapsed = 0; + // performance.now() at the last Start, while running + public startedAt: number | undefined = undefined; +} + +export function create(): Stopwatch { + return new Stopwatch(); +} + +export function startNew(): Stopwatch { + const sw = new Stopwatch(); + start(sw); + return sw; +} + +export function start(sw: Stopwatch): void { + if (sw.startedAt === undefined) { + sw.startedAt = performance.now(); + } +} + +export function stop(sw: Stopwatch): void { + if (sw.startedAt !== undefined) { + sw.elapsed += performance.now() - sw.startedAt; + sw.startedAt = undefined; + } +} + +export function reset(sw: Stopwatch): void { + sw.elapsed = 0; + sw.startedAt = undefined; +} + +export function restart(sw: Stopwatch): void { + sw.elapsed = 0; + sw.startedAt = performance.now(); +} + +export function isRunning(sw: Stopwatch): boolean { + return sw.startedAt !== undefined; +} + +function rawElapsedMilliseconds(sw: Stopwatch): number { + return sw.elapsed + (sw.startedAt === undefined ? 0 : performance.now() - sw.startedAt); +} + +// In units of Frequency, like .NET (not TimeSpan ticks) +export function elapsedTicks(sw: Stopwatch): int64 { + return BigInt(Math.trunc(rawElapsedMilliseconds(sw) * ticksPerMillisecond)); +} + +export function elapsedMilliseconds(sw: Stopwatch): int64 { + return elapsedTicks(sw) / BigInt(ticksPerMillisecond); +} + +export function elapsedTotalMilliseconds(sw: Stopwatch): number { + return Number(elapsedTicks(sw)) / ticksPerMillisecond; +} + +export function frequency(): int64 { + return BigInt(ticksPerMillisecond * 1000); +} + +export function isHighResolution(): boolean { + return true; +} + +export function getTimestamp(): int64 { + return BigInt(Math.trunc(performance.now() * ticksPerMillisecond)); +} + +export function getElapsedTotalMilliseconds(startingTimestamp: int64, endingTimestamp?: int64): number { + return Number((endingTimestamp ?? getTimestamp()) - startingTimestamp) / ticksPerMillisecond; +} diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 03d259c7bd..3fb987499a 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -1,12 +1,27 @@ import { toString as dateToString } from "./Date.ts"; -import { compare as numericCompare, isNumeric, isIntegral, multiply, Numeric, toExponential, toFixed, toHex, toPrecision } from "./Numeric.ts"; +import { compare as numericCompare, divide, isNumeric, isIntegral, multiply, Numeric, toExponential, toExponentialHalfEven, toFixed, toFixedHalfEven, toHex, toPrecision, toPrecisionHalfEven, toRadix } from "./Numeric.ts"; import { escape } from "./RegExp.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, ArgumentOutOfRangeException_$ctor_Z384F8060, ArgumentOutOfRangeException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts"; import { toString } from "./Types.ts"; import { Exception } from "./Util.ts"; +// Temporal date/time values (only present under --test:js-temporal) are not JS Dates. To avoid +// importing the side-effecting Temporal runtime modules into this core file — which would pull +// them into every bundle — they advertise a .NET-style formatter through this well-known symbol, +// attached to their prototype when (and only when) those modules are loaded. +const dateTimeFormattableSymbol = Symbol.for("Fable.DateTimeFormattable"); + +function isNetDateFormattable(rep: any): boolean { + return rep != null && typeof rep[dateTimeFormattableSymbol] === "function"; +} + +function formatDateLike(rep: any, format: string | undefined): string { + return rep instanceof Date ? dateToString(rep, format) : rep[dateTimeFormattableSymbol](format); +} + const fsFormatRegExp = /(^|[^%])%([0+\- ]*)(\*|\d+)?(?:\.(\d+))?(\w)/g; const interpolateRegExp = /(?:(^|[^%])%([0+\- ]*)(\d+)?(?:\.(\d+))?(\w))?%P\(\)/g; -const formatRegExp = /\{(\d+)(,-?\d+)?(?:\:([a-zA-Z])(\d{0,2})|\:(.+?))?\}/g; +const formatRegExp = /\{\{|\}\}|\{(\d+)(,-?\d+)?(?:\:([a-zA-Z])(\d{0,2})|\:(.+?))?\}/g; function isLessThan(x: Numeric, y: number) { return numericCompare(x, y) < 0; @@ -165,6 +180,13 @@ export function printf(input: string): IPrintfFormat { }; } +export function interpolatedPrintf(input: string, text: string): IPrintfFormat { + return { + input, + cont: (f: (x: string) => any) => f(text), + }; +} + export function interpolate(str: string, values: any[]): string { let valIdx = 0; let strIdx = 0; @@ -217,44 +239,47 @@ function formatReplacement(rep: any, flags: any, padLength: any, precision: any, flags = flags || ""; format = format || ""; if (isNumeric(rep)) { - if (format.toLowerCase() !== "x") { - if (isLessThan(rep, 0)) { - rep = multiply(rep, -1); - sign = "-"; - } else { - if (flags.indexOf(" ") >= 0) { - sign = " "; - } else if (flags.indexOf("+") >= 0) { - sign = "+"; - } - } + const radix = format === "x" || format === "X" ? 16 : format === "o" ? 8 : 10; + if (radix === 10 && isLessThan(rep, 0)) { + rep = multiply(rep, -1); + sign = "-"; + } else if (flags.indexOf(" ") >= 0) { + sign = " "; + } else if (flags.indexOf("+") >= 0) { + sign = "+"; } precision = precision == null ? null : parseInt(precision, 10); switch (format) { case "f": case "F": precision = precision != null ? precision : 6; - rep = toFixed(rep, precision); + rep = toFixedHalfEven(rep, precision); break; case "g": case "G": - rep = precision != null ? toPrecision(rep, precision) : toPrecision(rep); + rep = precision != null ? toPrecisionHalfEven(rep, precision) : toPrecision(rep); break; case "e": case "E": - rep = precision != null ? toExponential(rep, precision) : toExponential(rep); + rep = precision != null ? toExponentialHalfEven(rep, precision) : toExponential(rep); break; - case "x": - rep = toHex(rep); + case "x": case "o": + rep = toRadix(rep, radix); break; case "X": - rep = toHex(rep).toUpperCase(); + rep = toRadix(rep, radix).toUpperCase(); break; - default: // AOid + case "": + case "O": + rep = typeof rep === "number" ? formatGeneral(rep, "R", null) : String(rep); + break; + default: // Aid rep = String(rep); break; } - } else if (rep instanceof Date) { - rep = dateToString(rep); + } else if (rep instanceof Date || isNetDateFormattable(rep)) { + rep = formatDateLike(rep, undefined); } else if (format === "A" && typeof rep === "string") { rep = "\"" + rep + "\""; + } else if (format === "s" && rep == null) { + rep = ""; } else { rep = toString(rep); } @@ -348,10 +373,76 @@ function splitIntAndDecimalPart(value: string) { } } +// .NET rounds a percentage on the unscaled value, at precision + 2 decimals +function shiftPercent(value: string) { + const sign = value.startsWith("-") ? "-" : ""; + const [integral, decimal = ""] = value.slice(sign.length).split("."); + const shifted = (integral + decimal.slice(0, 2)).replace(/^0+(?=\d)/, ""); + return sign + shifted + (decimal.length > 2 ? "." + decimal.slice(2) : ""); +} + function thousandSeparate(value: string) { return value.replace(/\B(?=(\d{3})+(?!\d))/g, ","); } +function splitExponential(s: string): [string, number] { + const eIdx = s.indexOf("e"); + return [s.slice(0, eIdx).replace(".", ""), Number(s.slice(eIdx + 1))]; +} + +function shortestSingle(x: number): [string, number] { + // 100 digits tell where a float32 lies between two candidates, and 9 digits always round-trip + const [digits, exponent] = splitExponential(x.toExponential(99)); + for (let p = 1; ; p++) { + const down = Number(digits.slice(0, p)); + const rest = digits.slice(p); + const half = "5".padEnd(rest.length, "0"); + const roundUp = rest > half || rest === half && down % 2 === 1; + for (const n of roundUp ? [down + 1, down] : [down, down + 1]) { + if (Math.fround(Number(n + "e" + (exponent - p + 1))) === x) { + return [String(n), exponent + String(n).length - p]; + } + } + } +} + +// .NET "G" and "R": fixed notation unless the exponent is below -4 or reaches the precision (17 for the shortest digits, 9 for a float32) +function formatGeneral(x: number, format: string, precision: number | null, isSingle = false) { + if (!Number.isFinite(x)) { + return String(x); + } + const sign = x < 0 || Object.is(x, -0) ? "-" : ""; + x = Math.abs(x); + // R ignores the precision and G0 is G + const p = format === "r" || format === "R" ? 0 : precision ?? 0; + const [significand, exponent] = + p > 0 ? splitExponential(toExponentialHalfEven(x, p - 1)) : isSingle ? shortestSingle(x) : splitExponential(x.toExponential()); + const digits = trimEnd(significand, "0") || "0"; + if (exponent < -4 || exponent >= (p > 0 ? p : isSingle ? 9 : 17)) { + const mantissa = digits.length > 1 ? digits[0] + "." + digits.slice(1) : digits; + const eChar = format === "G" || format === "R" ? "E" : "e"; + return sign + mantissa + eChar + (exponent < 0 ? "-" : "+") + padLeft(String(Math.abs(exponent)), 2, "0"); + } else if (exponent < 0) { + return sign + "0." + "0".repeat(-exponent - 1) + digits; + } else { + const decimals = digits.slice(exponent + 1); + return sign + digits.slice(0, exponent + 1).padEnd(exponent + 1, "0") + (decimals ? "." + decimals : ""); + } +} + +export function formatSingle(str: string, x: number) { + const m = /^\{0:([GgRr])(\d{0,2})\}$/.exec(str); + return m != null ? formatGeneral(Math.fround(x), m[1], m[2] ? parseInt(m[2], 10) : null, true) : format(str, x); +} + +export function float64ToString(x: number) { + return formatGeneral(x, "R", null); +} + +export function float32ToString(x: number) { + return formatGeneral(Math.fround(x), "R", null, true); +} + export function format(str: string | object, ...args: any[]) { let str2: string; if (typeof str === "object") { @@ -362,7 +453,10 @@ export function format(str: string | object, ...args: any[]) { str2 = str; } - return str2.replace(formatRegExp, (_, idx: number, padLength, format, precision, pattern) => { + return str2.replace(formatRegExp, (match, idx: number, padLength, format, precision, pattern) => { + if (match === "{{" || match === "}}") { + return match[0]; + } if (idx < 0 || idx >= args.length) { throw new Exception("Index must be greater or equal to zero and less than the arguments' length.") } @@ -383,7 +477,7 @@ export function format(str: string | object, ...args: any[]) { rep = multiply(rep, -1); } precision = precision == null ? 2 : precision; - rep = toFixed(rep, precision); + rep = toFixedHalfEven(rep, precision); parts = splitIntAndDecimalPart(rep); if (precision > 0) { rep = "¤" + thousandSeparate(parts.integral) + "." + padRight(parts.decimal, precision, "0"); @@ -407,19 +501,33 @@ export function format(str: string | object, ...args: any[]) { } } break; - case "e": case "E": - rep = precision != null ? toExponential(rep, precision) : toExponential(rep); + case "e": case "E": { + precision = precision != null ? precision : 6; + rep = toExponentialHalfEven(rep, precision); + // .NET always signs the exponent and pads it to at least three digits + const eIdx = rep.indexOf("e"); + rep = rep.slice(0, eIdx) + format + rep[eIdx + 1] + padLeft(rep.slice(eIdx + 2), 3, "0"); break; + } case "f": case "F": precision = precision != null ? precision : 2; - rep = toFixed(rep, precision); + rep = toFixedHalfEven(rep, precision); if (precision > 0) { parts = splitIntAndDecimalPart(rep); rep = parts.integral + "." + padRight(parts.decimal, precision, "0"); } break; - case "g": case "G": { - rep = precision != null ? toPrecision(rep, precision) : toPrecision(rep); + case "g": case "G": + case "r": case "R": { + if (typeof rep === "number") { + rep = formatGeneral(rep, format, precision); + break; + } + if (precision == null) { + rep = String(rep); + break; + } + rep = toPrecision(rep, precision); // Handle exponential notation: only trim trailing zeros from mantissa, not from exponent. // .NET G format guarantees an exponent of at least 2 digits with an explicit sign (e.g. "E-07"). const eIdx = rep.indexOf("e"); @@ -428,16 +536,16 @@ export function format(str: string | object, ...args: any[]) { const expSign = rep[eIdx + 1]; // toPrecision always emits "+" or "-" const expDigits = rep.slice(eIdx + 2); const paddedExpDigits = expDigits.length < 2 ? "0" + expDigits : expDigits; - const eChar = format === "G" ? "E" : "e"; + const eChar = format === "G" || format === "R" ? "E" : "e"; rep = mantissa + eChar + expSign + paddedExpDigits; - } else { + } else if (rep.indexOf(".") >= 0) { rep = trimEnd(trimEnd(rep, "0"), "."); } break; } case "n": case "N": precision = precision != null ? precision : 2; - rep = toFixed(rep, precision); + rep = toFixedHalfEven(rep, precision); parts = splitIntAndDecimalPart(rep); if (precision > 0) { rep = thousandSeparate(parts.integral) + "." + padRight(parts.decimal, precision, "0"); @@ -447,7 +555,7 @@ export function format(str: string | object, ...args: any[]) { break; case "p": case "P": precision = precision != null ? precision : 2; - rep = toFixed(multiply(rep, 100), precision) + rep = typeof rep === "number" ? shiftPercent(toFixedHalfEven(rep, precision + 2)) : toFixed(multiply(rep, 100), precision); parts = splitIntAndDecimalPart(rep); if (precision > 0) { rep = thousandSeparate(parts.integral) + "." + padRight(parts.decimal, precision, "0") + " %"; @@ -455,8 +563,6 @@ export function format(str: string | object, ...args: any[]) { rep = thousandSeparate(parts.integral) + " %"; } break; - case "r": case "R": - throw new Exception("The round-trip format is not supported by Fable"); case "x": case "X": if (!isIntegral(rep)) { throw new Exception("Format specifier was invalid."); @@ -476,7 +582,23 @@ export function format(str: string | object, ...args: any[]) { if (pattern) { let sign = ""; - rep = (pattern as string).replace(/([0#,]+)(\.[0#]+)?/, (_, intPart: string, decimalPart: string) => { + const patternStr = pattern as string; + + // Each `%` scales the value by 100 and is kept as a literal in the output + const percents = (patternStr.match(/%/g) ?? []).length; + if (percents > 0) { + rep = multiply(rep, Math.pow(100, percents)); + } + + // .NET ignores commas placed after the decimal placeholders, hence the trailing group + rep = patternStr.replace(/([0#,]+)(\.[0#]+)?(,*)/, (_, intPart: string, decimalPart: string) => { + // Commas between the last integer placeholder and the decimal point scale the + // value down by 1000 each; commas anywhere else only turn on digit grouping + const scaleCommas = /,*$/.exec(intPart)![0].length; + if (scaleCommas > 0) { + intPart = intPart.substring(0, intPart.length - scaleCommas); + rep = divide(rep, Math.pow(1000, scaleCommas)); + } if (isLessThan(rep, 0)) { rep = multiply(rep, -1); sign = "-"; @@ -511,12 +633,14 @@ export function format(str: string | object, ...args: any[]) { return repDecimal.length > 0 ? repInt + "." + repDecimal : repInt; }); rep = sign + rep; + } else if (typeof rep === "number") { + rep = formatGeneral(rep, "R", null); } } - } else if (rep instanceof Date) { - rep = dateToString(rep, pattern || format); + } else if (rep instanceof Date || isNetDateFormattable(rep)) { + rep = formatDateLike(rep, pattern || format); } else { - rep = toString(rep) + rep = rep == null ? "" : toString(rep); } padLength = parseInt((padLength || " ").substring(1), 10); if (!isNaN(padLength)) { @@ -554,14 +678,14 @@ export function isNullOrWhiteSpace(str: string | any) { } export function concat(...xs: any[]): string { - return xs.map((x) => String(x)).join(""); + return xs.map((x) => (x == null ? "" : String(x))).join(""); } export function join(delimiter: string, xs: Iterable): string { if (Array.isArray(xs)) { - return xs.join(delimiter); + return xs.join(delimiter ?? ""); } else { - return Array.from(xs).join(delimiter); + return Array.from(xs).join(delimiter ?? ""); } } @@ -570,7 +694,7 @@ export function joinWithIndices(delimiter: string, xs: string[], startIndex: num if (endIndexPlusOne > xs.length) { throw new Exception("Index and count must refer to a location within the buffer."); } - return xs.slice(startIndex, endIndexPlusOne).join(delimiter); + return xs.slice(startIndex, endIndexPlusOne).join(delimiter ?? ""); } function notSupported(name: string): never { @@ -594,6 +718,49 @@ export function fromBase64String(b64Encoded: string): number[] { return bytes as any as number[]; } +export function toHexString(inArray: ArrayLike, offset?: number, length?: number): string { + if (inArray == null) { + throw ArgumentNullException_$ctor_Z721C83C5("inArray"); + } + const start = offset ?? 0; + const count = length ?? inArray.length - start; + if (count < 0) { + throw ArgumentOutOfRangeException_$ctor_Z384F8060("length", "Non-negative number required."); + } + if (start < 0) { + throw ArgumentOutOfRangeException_$ctor_Z384F8060("offset", "Non-negative number required."); + } + if (start > inArray.length - count) { + throw ArgumentOutOfRangeException_$ctor_Z721C83C5("offset"); + } + let str = ""; + for (let i = start; i < start + count; i++) { + str += inArray[i].toString(16).padStart(2, "0"); + } + return str.toUpperCase(); +} + +export function toHexStringLower(inArray: ArrayLike, offset?: number, length?: number): string { + return toHexString(inArray, offset, length).toLowerCase(); +} + +export function fromHexString(s: string): number[] { + if (s == null) { + throw ArgumentNullException_$ctor_Z721C83C5("s"); + } + if (s.length % 2 !== 0) { + throw FormatException_$ctor_Z721C83C5("The input is not a valid hex string as its length is not a multiple of 2."); + } + if (!/^[0-9A-Fa-f]*$/.test(s)) { + throw FormatException_$ctor_Z721C83C5("The input is not a valid hex string as it contains a non-hex character."); + } + const bytes = new Uint8Array(s.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(s.substring(2 * i, 2 * i + 2), 16); + } + return bytes as any as number[]; +} + function pad(str: string, len: number, ch?: string, isRight?: boolean) { ch = ch || " "; len = len - str.length; diff --git a/src/fable-library-ts/System.Collections.Generic.fs b/src/fable-library-ts/System.Collections.Generic.fs index 2623852e62..d282f67a14 100644 --- a/src/fable-library-ts/System.Collections.Generic.fs +++ b/src/fable-library-ts/System.Collections.Generic.fs @@ -46,7 +46,7 @@ type Stack<'T> private (initialContents, initialCount) = let mutable contents = initialContents let mutable count = initialCount - new(initialCapacity: int) = Stack<'T>(Array.zeroCreate<'T> (initialCapacity), 0) + new(initialCapacity: int) = Stack<'T>(Array.zeroCreate<'T>(initialCapacity), 0) new() = Stack<'T>(4) @@ -174,7 +174,7 @@ type Queue<'T> private (initialContents, initialCount) = if initialCapacity < 0 then raise (System.ArgumentOutOfRangeException("capacity is less than 0")) - Queue<'T>(Array.zeroCreate<'T> (initialCapacity), 0) + Queue<'T>(Array.zeroCreate<'T>(initialCapacity), 0) new() = Queue<'T>(4) diff --git a/src/fable-library-ts/System.Text.fs b/src/fable-library-ts/System.Text.fs index 685cc1df3d..e43977bb5e 100644 --- a/src/fable-library-ts/System.Text.fs +++ b/src/fable-library-ts/System.Text.fs @@ -159,6 +159,14 @@ type StringBuilder(value: string, capacity: int) = let pos = index - len buf[i] <- buf[i][0 .. (pos - 1)] + (string value) + buf[i][(pos + 1) ..] + member x.Length = + let mutable len = 0 + + for i = buf.Count - 1 downto 0 do + len <- len + buf[i].Length + + len + member x.Replace(oldValue: char, newValue: char) = for i = buf.Count - 1 downto 0 do buf[i] <- buf[i].Replace(oldValue, newValue) @@ -169,16 +177,8 @@ type StringBuilder(value: string, capacity: int) = let str = x.ToString().Replace(oldValue, newValue) x.Clear().Append(str) - member x.Length = - let mutable len = 0 - - for i = buf.Count - 1 downto 0 do - len <- len + buf[i].Length - - len - override _.ToString() = System.String.Concat(buf) - member x.ToString(firstIndex: int, length: int) = + member x.ToString(startIndex: int, length: int) = let str = x.ToString() - str.Substring(firstIndex, length) + str.Substring(startIndex, length) diff --git a/src/fable-library-ts/System.fs b/src/fable-library-ts/System.fs index 03cb86a83d..86182aa495 100644 --- a/src/fable-library-ts/System.fs +++ b/src/fable-library-ts/System.fs @@ -74,21 +74,25 @@ type ArgumentException(message: string, paramName: string, innerException: exn | innerException ) - // Use Unchecked.defaultof rather than a `null` literal for the absent inner - // exception: Fable erases nullable reference annotations, so a `null` literal + // Use Unchecked.defaultof rather than a `null` literal for the absent parameter name + // and inner exception: Fable erases nullable reference annotations, so a `null` literal // would be emitted against a non-nullable parameter type and fail type checking. - new() = ArgumentException(SR.Arg_ArgumentException, "", Unchecked.defaultof) - new(message) = ArgumentException(message, "", Unchecked.defaultof) + new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof, Unchecked.defaultof) + new(message) = ArgumentException(message, Unchecked.defaultof, Unchecked.defaultof) new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) - new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException) + + new(message: string, innerException: exn | null) + = + ArgumentException(message, Unchecked.defaultof, innerException) + member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) - new() = ArgumentNullException("") + new() = ArgumentNullException(Unchecked.defaultof) type ArgumentOutOfRangeException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) - new() = ArgumentOutOfRangeException("") + new() = ArgumentOutOfRangeException(Unchecked.defaultof) diff --git a/src/fable-library-ts/Task.ts b/src/fable-library-ts/Task.ts index 0bceaabaee..6e1cb1c369 100644 --- a/src/fable-library-ts/Task.ts +++ b/src/fable-library-ts/Task.ts @@ -1,4 +1,4 @@ -import { OperationCanceledException } from "./AsyncBuilder.ts"; +import { CancellationToken, OperationCanceledException } from "./AsyncBuilder.ts"; export class TaskCompletionSource { private _resolve!: (value: T) => void; @@ -26,6 +26,27 @@ export function zero(): Promise { return Promise.resolve(); } +// Like .NET, -1 waits until the token is cancelled +export function delay(millisecondsDelay: number, cancellationToken?: CancellationToken): Promise { + return new Promise((resolve, reject) => { + if (cancellationToken?.isCancelled) { + reject(new OperationCanceledException()); + return; + } + let tokenId: number | undefined; + const timeoutId = millisecondsDelay === -1 ? undefined : setTimeout(() => { + if (tokenId !== undefined) { cancellationToken!.removeListener(tokenId); } + resolve(); + }, millisecondsDelay); + if (cancellationToken != null) { + tokenId = cancellationToken.addListener(() => { + clearTimeout(timeoutId); + reject(new OperationCanceledException()); + }); + } + }); +} + // Task = Promise in JS/TS. GetAwaiter/GetResult return the Promise itself; // callers should use Async.AwaitTask to extract the value in an async context. export function getAwaiter(t: Promise): Promise { diff --git a/src/fable-library-ts/TimeOnly.ts b/src/fable-library-ts/TimeOnly.ts index 7a80ba21cd..f788c0e158 100644 --- a/src/fable-library-ts/TimeOnly.ts +++ b/src/fable-library-ts/TimeOnly.ts @@ -39,7 +39,7 @@ export function add(t: number, ts: number, wrappedDays?: FSharpRef) { return t2 < 0 ? millisecondsPerDay + t2 : t2; } - wrappedDays.contents = ts / millisecondsPerDay; + wrappedDays.contents = Math.trunc(ts / millisecondsPerDay); let newMs = t + ts % millisecondsPerDay; if (newMs < 0) { diff --git a/src/fable-library-ts/TimeOnlyTemporal.ts b/src/fable-library-ts/TimeOnlyTemporal.ts new file mode 100644 index 0000000000..0ec7a8005b --- /dev/null +++ b/src/fable-library-ts/TimeOnlyTemporal.ts @@ -0,0 +1,181 @@ +import { FSharpRef } from "./Types.ts"; +import { toInt64, fromFloat64, int64 } from "./BigInt.ts"; +import { TimeSpan, fromTicks as TimeSpan_fromTicks, totalNanoseconds as TimeSpan_totalNanoseconds } from "./TimeSpanTemporal.ts"; +import { Exception } from "./Util.ts"; + +export type TimeOnly = Temporal.PlainTime; +export const PlainTime = Temporal.PlainTime; +export type PlainTime = Temporal.PlainTime; + +// The generic equality/comparison/hashing helpers in Util.ts dispatch at runtime +// on .NET-style Equals/CompareTo/GetHashCode methods. Attach them so TimeOnly +// also works in generic contexts: records, tuples, erased generics, etc. +const proto = Temporal.PlainTime.prototype as any; +proto.Equals = function (this: TimeOnly, other: TimeOnly): boolean { return this.equals(other); }; +proto.CompareTo = function (this: TimeOnly, other: TimeOnly): number { return Temporal.PlainTime.compare(this, other); }; +proto.GetHashCode = function (this: TimeOnly): number { return hash(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: TimeOnly, format?: string): string { return toString(this, format); }; + +const nanosecondsPerDay = 86_400_000_000_000; +const nanosecondsPerDayBig = 86_400_000_000_000n; + +function totalNanoseconds(t: TimeOnly): number { + return ((t.hour * 60 + t.minute) * 60 + t.second) * 1_000_000_000 + + t.millisecond * 1_000_000 + t.microsecond * 1_000 + t.nanosecond; +} + +function fromNanoseconds(n: number): TimeOnly { + return new Temporal.PlainTime( + Math.floor(n / 3_600_000_000_000), + Math.floor(n / 60_000_000_000) % 60, + Math.floor(n / 1_000_000_000) % 60, + Math.floor(n / 1_000_000) % 1000, + Math.floor(n / 1_000) % 1000, + n % 1000); +} + +export function create(h: number = 0, m: number = 0, s: number = 0, ms: number = 0, us: number = 0): TimeOnly { + return new Temporal.PlainTime(h, m, s, ms, us); +} + +export function fromTicks(ticks: number | bigint): TimeOnly { + return fromNanoseconds(Number(BigInt(ticks) * 100n)); +} + +export function fromTimeSpan(timeSpan: TimeSpan): TimeOnly { + const ns = Number(TimeSpan_totalNanoseconds(timeSpan)); + if (ns < 0 || ns >= nanosecondsPerDay) + throw new Exception("The TimeSpan describes an unrepresentable TimeOnly."); + + return fromNanoseconds(ns); +} + +export function fromDateTime(d: Temporal.PlainDateTime): TimeOnly { + // Under the Temporal representation a DateTime is a PlainDateTime (kind-agnostic wall-clock) + return new Temporal.PlainTime(d.hour, d.minute, d.second, d.millisecond, d.microsecond, d.nanosecond); +} + +export function minValue(): TimeOnly { + return new Temporal.PlainTime(); +} + +export function maxValue(): TimeOnly { + // This is "23:59:59.9999999" (.NET tick precision) + return new Temporal.PlainTime(23, 59, 59, 999, 999, 900); +} + +export function ticks(t: TimeOnly): int64 { + return toInt64(fromFloat64(totalNanoseconds(t) / 100)); +} + +export function toTimeSpan(t: TimeOnly): TimeSpan { + // TimeOnly is tick-precise, so the nanosecond count is always a multiple of 100. + return TimeSpan_fromTicks(Math.round(totalNanoseconds(t) / 100)); +} + +// Kept in bigint: a TimeOnly itself always fits a float64 exactly (under 2^47 ns), +// but the amount added does not — a TimeSpan of more than ~104 days already +// exceeds Number.MAX_SAFE_INTEGER in nanoseconds, and .NET wraps it modulo a day +// rather than saturating, so the low ticks are the part that must survive. +function addNanoseconds(t: TimeOnly, deltaNs: bigint, wrappedDays?: FSharpRef): TimeOnly { + const totalNs = BigInt(totalNanoseconds(t)) + deltaNs; + let days = totalNs / nanosecondsPerDayBig; + let remainder = totalNs % nanosecondsPerDayBig; + + // BigInt division truncates toward zero; .NET wraps toward the previous day. + if (remainder < 0n) { + remainder += nanosecondsPerDayBig; + days -= 1n; + } + + if (wrappedDays !== undefined) { + wrappedDays.contents = Number(days); + } + + return fromNanoseconds(Number(remainder)); +} + +export function add(t: TimeOnly, ts: TimeSpan, wrappedDays?: FSharpRef): TimeOnly { + return addNanoseconds(t, TimeSpan_totalNanoseconds(ts), wrappedDays); +} + +export function addHours(t: TimeOnly, h: number): TimeOnly { + // Scale to ticks first, as .NET does, then widen — `h` hours in nanoseconds + // overflows the exact float64 range two orders of magnitude sooner. + return addNanoseconds(t, BigInt(Math.round(h * 36_000_000_000)) * 100n); +} + +export function addMinutes(t: TimeOnly, m: number): TimeOnly { + return addNanoseconds(t, BigInt(Math.round(m * 600_000_000)) * 100n); +} + +export function isBetween(t: TimeOnly, start: TimeOnly, end: TimeOnly): boolean { + return Temporal.PlainTime.compare(start, end) <= 0 + ? (Temporal.PlainTime.compare(start, t) <= 0 && Temporal.PlainTime.compare(end, t) > 0) + : (Temporal.PlainTime.compare(start, t) <= 0 || Temporal.PlainTime.compare(end, t) > 0); +} + +export function equals(x: TimeOnly, y: TimeOnly): boolean { + return x.equals(y); +} + +export function compare(x: TimeOnly, y: TimeOnly): number { + return Temporal.PlainTime.compare(x, y); +} + +export function hash(t: TimeOnly): number { + return totalNanoseconds(t) % 2147483647; +} + +export function op_Subtraction(left: TimeOnly, right: TimeOnly): TimeSpan { + // Returns the elapsed TimeSpan, wrapping around midnight + const ns = (totalNanoseconds(left) - totalNanoseconds(right) + nanosecondsPerDay) % nanosecondsPerDay; + return TimeSpan_fromTicks(Math.round(ns / 100)); +} + +export function toString(t: TimeOnly, format = "t", _provider?: any): string { + switch (format) { + case "t": + return t.toString({ smallestUnit: "minute" }); + case "r": + case "R": + case "T": + return t.toString({ smallestUnit: "second" }); + case "o": + case "O": + // .NET tick precision is 7 fractional digits + return t.toString({ fractionalSecondDigits: 7 }); + default: + throw new Exception("Custom formats are not supported"); + } +} + +export function parse(str: string): TimeOnly { + // Allowed format types: + // hh:mm + // hh:mm:ss + // hh:mm:ss.fffffff + const r = /^\s*([0-1]?\d|2[0-3])\s*:\s*([0-5]?\d)(\s*:\s*([0-5]?\d)(\.(\d+))?)?\s*$/.exec(str); + if (r != null && r[1] != null && r[2] != null) { + const h = +r[1]; + const m = +r[2]; + const s = r[4] != null ? +r[4] : 0; + // .NET resolves the fraction down to a single tick (7 digits) and truncates + // anything finer, so the digits are read as ticks rather than milliseconds. + const subSecondTicks = r[6] != null ? +r[6].padEnd(7, "0").substring(0, 7) : 0; + + return fromNanoseconds(((h * 60 + m) * 60 + s) * 1_000_000_000 + subSecondTicks * 100); + } + + throw new Exception(`String '${str}' was not recognized as a valid TimeOnly.`); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/TimeSpan.ts b/src/fable-library-ts/TimeSpan.ts index 904ebc558a..7b27de262f 100644 --- a/src/fable-library-ts/TimeSpan.ts +++ b/src/fable-library-ts/TimeSpan.ts @@ -76,7 +76,7 @@ export function milliseconds(ts: TimeSpan) { } export function ticks(ts: TimeSpan) { - return toInt64(fromFloat64(ts * 10000)); + return toInt64(Number.isInteger(ts) ? BigInt(ts) * 10000n : fromFloat64(ts * 10000)); } export function totalDays(ts: TimeSpan) { diff --git a/src/fable-library-ts/TimeSpanTemporal.ts b/src/fable-library-ts/TimeSpanTemporal.ts new file mode 100644 index 0000000000..70ad0b265a --- /dev/null +++ b/src/fable-library-ts/TimeSpanTemporal.ts @@ -0,0 +1,280 @@ +import { FSharpRef } from "./Types.ts"; +import { toInt64, int64 } from "./BigInt.ts"; +import { Exception, padWithZeros } from "./Util.ts"; + +export type TimeSpan = Temporal.Duration; +export const Duration = Temporal.Duration; +export type Duration = Temporal.Duration; + +// The generic equality/comparison/hashing helpers in Util.ts dispatch at runtime +// on .NET-style Equals/CompareTo/GetHashCode methods. Attach them so TimeSpan +// also works in generic contexts: records, tuples, erased generics, etc. +const proto = Temporal.Duration.prototype as any; +proto.Equals = function (this: TimeSpan, other: TimeSpan): boolean { return Temporal.Duration.compare(this, other) === 0; }; +proto.CompareTo = function (this: TimeSpan, other: TimeSpan): number { return Temporal.Duration.compare(this, other); }; +proto.GetHashCode = function (this: TimeSpan): number { return hash(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: TimeSpan, format?: string): string { return toString(this, format); }; + +const nsPerMillisecond = 1_000_000n; +const nsPerSecond = 1_000_000_000n; +const nsPerMinute = 60_000_000_000n; +const nsPerHour = 3_600_000_000_000n; +const nsPerDay = 86_400_000_000_000n; + +export function totalNanoseconds(ts: TimeSpan): bigint { + return BigInt(ts.days) * nsPerDay + + BigInt(ts.hours) * nsPerHour + + BigInt(ts.minutes) * nsPerMinute + + BigInt(ts.seconds) * nsPerSecond + + BigInt(ts.milliseconds) * nsPerMillisecond + + BigInt(ts.microseconds) * 1000n + + BigInt(ts.nanoseconds); +} + +function fromNanoseconds(totalNs: bigint): TimeSpan { + const negative = totalNs < 0n; + // A .NET TimeSpan is a whole number of 100ns ticks, so anything finer than a + // tick is dropped (toward zero, as .NET does) rather than carried in the + // Duration's nanosecond field — otherwise values that cannot exist in .NET + // would leak out of Parse/FromMilliseconds/arithmetic. + let n = (negative ? -totalNs : totalNs) / 100n * 100n; + const days = Number(n / nsPerDay); n %= nsPerDay; + const hours = Number(n / nsPerHour); n %= nsPerHour; + const minutes = Number(n / nsPerMinute); n %= nsPerMinute; + const seconds = Number(n / nsPerSecond); n %= nsPerSecond; + const milliseconds = Number(n / nsPerMillisecond); n %= nsPerMillisecond; + const microseconds = Number(n / 1000n); n %= 1000n; + const d = new Temporal.Duration(0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, Number(n)); + return negative ? d.negated() : d; +} + +// Converts a possibly fractional unit count into an exact nanosecond amount, +// keeping the integer part exact for the full Int64 tick range. +function unitToNanoseconds(value: number, nsPerUnit: bigint): bigint { + const whole = Math.trunc(value); + const frac = value - whole; + return BigInt(whole) * nsPerUnit + BigInt(Math.round(frac * Number(nsPerUnit))); +} + +export function create(d: number = 0, h: number = 0, m: number = 0, s: number = 0, ms: number = 0, us: number = 0): TimeSpan { + switch (arguments.length) { + case 1: + // ticks + return fromTicks(arguments[0]); + case 3: + // h,m,s + d = 0, h = arguments[0], m = arguments[1], s = arguments[2], ms = 0; + break; + default: + // d,h,m,s,ms,us + break; + } + return fromNanoseconds( + unitToNanoseconds(d, nsPerDay) + unitToNanoseconds(h, nsPerHour) + unitToNanoseconds(m, nsPerMinute) + + unitToNanoseconds(s, nsPerSecond) + unitToNanoseconds(ms, nsPerMillisecond) + unitToNanoseconds(us, 1000n)); +} + +export function fromTicks(ticks: number | bigint): TimeSpan { + return fromNanoseconds(BigInt(ticks) * 100n); +} + +export function zero(): TimeSpan { + return new Temporal.Duration(); +} + +const ticksPerUnit = { + days: 864_000_000_000, + hours: 36_000_000_000, + minutes: 600_000_000, + seconds: 10_000_000, + milliseconds: 10_000, +}; + +// The DateTime/DateTimeOffset/TimeOnly Add* members take a double and are +// tick-precise in .NET: the value is scaled to ticks and rounded, so +// AddSeconds(0.0000001) moves exactly one tick and AddDays(1.5) exactly 36 hours. +// Temporal's own `add({ days: 1.5 })` rejects non-integers, so the scaling has to +// happen here. +export function fromUnits(value: number, unit: keyof typeof ticksPerUnit): TimeSpan { + return fromTicks(BigInt(Math.round(value * ticksPerUnit[unit]))); +} + +export function fromDays(d: number, h: number = 0, m: bigint = 0n, s: bigint = 0n, ms: bigint = 0n): TimeSpan { + return create(d, h, Number(m), Number(s), Number(ms)); +} + +export function fromHours(h: number, m: bigint = 0n, s: bigint = 0n, ms: bigint = 0n): TimeSpan { + return create(0, h, Number(m), Number(s), Number(ms)); +} + +export function fromMinutes(m: number | bigint, s: bigint = 0n, ms: bigint = 0n): TimeSpan { + return create(0, 0, Number(m), Number(s), Number(ms)); +} + +export function fromSeconds(s: number | bigint, ms: bigint = 0n): TimeSpan { + return create(0, 0, 0, Number(s), Number(ms)); +} + +export function fromMilliseconds(ms: number | bigint): TimeSpan { + return fromTicks(typeof ms === "bigint" || Number.isInteger(ms) ? BigInt(ms) * 10000n : BigInt(Math.trunc(ms * 10000))); +} + +export function ticks(ts: TimeSpan): int64 { + return toInt64(totalNanoseconds(ts) / 100n); +} + +// .NET computes each total as (double)Ticks / TicksPerUnit; match that exactly +// (Temporal's own .total() balances differently and can be 1 ULP off). +function totalTicks(ts: TimeSpan): number { + return Number(totalNanoseconds(ts) / 100n); +} + +export function totalDays(ts: TimeSpan): number { + return totalTicks(ts) / 864_000_000_000; +} + +export function totalHours(ts: TimeSpan): number { + return totalTicks(ts) / 36_000_000_000; +} + +export function totalMinutes(ts: TimeSpan): number { + return totalTicks(ts) / 600_000_000; +} + +export function totalSeconds(ts: TimeSpan): number { + return totalTicks(ts) / 10_000_000; +} + +export function totalMilliseconds(ts: TimeSpan): number { + return totalTicks(ts) / 10_000; +} + +export function negate(ts: TimeSpan): TimeSpan { + return ts.negated(); +} + +export function add(ts1: TimeSpan, ts2: TimeSpan): TimeSpan { + return fromNanoseconds(totalNanoseconds(ts1) + totalNanoseconds(ts2)); +} + +export function subtract(ts1: TimeSpan, ts2: TimeSpan): TimeSpan { + return fromNanoseconds(totalNanoseconds(ts1) - totalNanoseconds(ts2)); +} + +// .NET scales the tick count as a double and rounds: `new TimeSpan((long)Math +// .Round(Ticks * factor))`. Scaling ticks rather than nanoseconds keeps the +// magnitude — and so the rounding — identical to .NET's. +export function multiply(ts: TimeSpan, factor: number): TimeSpan { + return fromTicks(BigInt(Math.round(Number(ticks(ts)) * factor))); +} + +export function divide(ts: TimeSpan, factor: number): TimeSpan { + return fromTicks(BigInt(Math.round(Number(ticks(ts)) / factor))); +} + +export function divideByTimeSpan(ts: TimeSpan, other: TimeSpan): number { + return Number(ticks(ts)) / Number(ticks(other)); +} + +export const op_Addition = add; +export const op_Subtraction = subtract; +export const op_Multiply = multiply; +export const op_UnaryNegation = negate; + +export function compare(x: TimeSpan, y: TimeSpan): number { + return Temporal.Duration.compare(x, y); +} + +export const compareTo = compare; + +export function equals(x: TimeSpan, y: TimeSpan): boolean { + return Temporal.Duration.compare(x, y) === 0; +} + +export function hash(ts: TimeSpan): number { + return Number(totalNanoseconds(ts) % 2147483647n); +} + +export function duration(ts: TimeSpan): TimeSpan { + return ts.abs(); +} + +export function toString(ts: TimeSpan, format = "c", _provider?: any): string { + if (["c", "g", "G"].indexOf(format) === -1) { + throw new Exception("Custom formats are not supported"); + } + const d = Math.abs(ts.days); + const h = Math.abs(ts.hours); + const m = Math.abs(ts.minutes); + const s = Math.abs(ts.seconds); + const ticks = Math.abs(ts.milliseconds) * 10_000 + + Math.abs(ts.microseconds) * 10 + + Math.trunc(Math.abs(ts.nanoseconds) / 100); + const sign = ts.sign < 0 ? "-" : ""; + const days = d === 0 && format !== "G" ? "" : format === "c" ? d + "." : d + ":"; + const time = `${format === "g" ? h : padWithZeros(h, 2)}:${padWithZeros(m, 2)}:${padWithZeros(s, 2)}`; + // .NET prints seven tick digits for "c" and "G", trims trailing zeros for "g", and omits a zero fraction for "c" and "g". + const fraction = + format === "g" + ? (ticks === 0 ? "" : "." + padWithZeros(ticks, 7).replace(/0+$/, "")) + : (ticks === 0 && format === "c" ? "" : "." + padWithZeros(ticks, 7)); + return `${sign}${days}${time}${fraction}`; +} + +export function parse(str: string): TimeSpan { + const firstDot = str.search("\\."); + const firstColon = str.search("\\:"); + if (firstDot === -1 && firstColon === -1) { // There is only a day ex: 4 + const d = parseInt(str, 0); + if (isNaN(d)) { + throw new Exception(`String '${str}' was not recognized as a valid TimeSpan.`); + } else { + return create(d, 0, 0, 0, 0); + } + } + if (firstColon > 0) { // process time part + // WIP: (-?)(((\d+)\.)?([0-9]|0[0-9]|1[0-9]|2[0-3]):(\d+)(:\d+(\.\d{1,7})?)?|\d+(?:(?!\.))) + const r = /^(-?)((\d+)\.)?(?:0*)([0-9]|0[0-9]|1[0-9]|2[0-3]):(?:0*)([0-5][0-9]|[0-9])(:(?:0*)([0-5][0-9]|[0-9]))?\.?(\d+)?$/.exec(str); + if (r != null && r[4] != null && r[5] != null) { + let d = 0; + let ms = 0; + let s = 0; + const sign = r[1] != null && r[1] === "-" ? -1 : 1; + const h = +r[4]; + const m = +r[5]; + if (r[3] != null) { + d = +r[3]; + } + if (r[7] != null) { + s = +r[7]; + } + if (r[8] != null) { + // Depending on the number of decimals passed, we need to adapt the numbers + switch (r[8].length) { + case 1: ms = +r[8] * 100; break; + case 2: ms = +r[8] * 10; break; + case 3: ms = +r[8]; break; + case 4: ms = +r[8] / 10; break; + case 5: ms = +r[8] / 100; break; + case 6: ms = +r[8] / 1000; break; + case 7: ms = +r[8] / 10000; break; + default: + throw new Exception(`String '${str}' was not recognized as a valid TimeSpan.`); + } + } + const ts = create(d, h, m, s, ms); + return sign < 0 ? negate(ts) : ts; + } + } + throw new Exception(`String '${str}' was not recognized as a valid TimeSpan.`); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/Types.ts b/src/fable-library-ts/Types.ts index 2707bf65b8..629b2fe8ef 100644 --- a/src/fable-library-ts/Types.ts +++ b/src/fable-library-ts/Types.ts @@ -1,4 +1,4 @@ -import { Exception, IComparable, IEquatable, IHashable, combineHashCodes, compare, compareArrays, equalArrays, equals, sameConstructor, numberHash, structuralHash } from "./Util.ts"; +import { Exception, IComparable, IEquatable, IHashable, combineHashCode, combineHashCodes, compare, compareArrays, equalArrays, equals, sameConstructor, numberHash, structuralHash } from "./Util.ts"; // This type is only used internally for .ts files in the library // F# Result type is in Choice.fs @@ -45,6 +45,10 @@ export function toString(x: any, callStack = 0): string { return String(x); } +export function toStringOrEmpty(x: any): string { + return x == null ? "" : toString(x); +} + export function unionToString(name: string, fields: any[]) { if (fields.length === 0) { return name; @@ -79,9 +83,11 @@ export abstract class Union implements } public GetHashCode() { - const hashes = this.fields.map((x: any) => structuralHash(x)); - hashes.splice(0, 0, numberHash(this.tag)); - return combineHashCodes(hashes); + let h = combineHashCode(0, numberHash(this.tag)); + for (let i = 0; i < this.fields.length; i++) { + h = combineHashCode(h, structuralHash(this.fields[i])); + } + return h; } public Equals(other: Union) { @@ -123,8 +129,13 @@ function recordToString(self: T) { } function recordGetHashCode(self: T) { - const hashes = Object.values(self as any).map((v) => structuralHash(v)); - return combineHashCodes(hashes); + // Snapshot all values before calling any user-defined hash functions. + const values = Object.values(self as any); + let hash = 0; + for (let i = 0; i < values.length; i++) { + hash = combineHashCode(hash, structuralHash(values[i])); + } + return hash; } function recordEquals(self: T, other: T) { @@ -169,24 +180,32 @@ export abstract class Record implements IEquatable, IComparable, } export class FSharpRef { - private readonly getter: () => T; - private readonly setter: (v: T) => void; + private readonly getter: (() => T) | undefined; + private readonly setter: ((v: T) => void) | undefined; + private value: T | undefined; get contents() { - return this.getter(); + return this.getter === undefined ? this.value as T : this.getter(); } set contents(v) { - this.setter(v) + if (this.setter === undefined) { + this.value = v; + } else { + this.setter(v); + } } + // Same field layout on both paths so every cell shares one hidden class constructor(contentsOrGetter: T | (() => T), setter?: (v: T) => void) { if (typeof setter === "function") { this.getter = contentsOrGetter as () => T; - this.setter = setter + this.setter = setter; + this.value = undefined; } else { - this.getter = () => contentsOrGetter as T; - this.setter = (v) => { contentsOrGetter = v }; + this.getter = undefined; + this.setter = undefined; + this.value = contentsOrGetter as T; } } } diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 9961578a69..382a5c1d25 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -215,8 +215,39 @@ export function toEnumerable(e: IEnumerable | Iterable): IEnumerable else { return new Enumerable(new Enumerator(e[Symbol.iterator]())); } } +// Walks an array by index: the iterator protocol allocates a result object per element +export class ArrayEnumerator implements IEnumerator { + private readonly array: ArrayLike; + private current: T = defaultOf(); + private index = -1; + constructor(array: ArrayLike) { this.array = array; } + public ["System.Collections.Generic.IEnumerator`1.get_Current"]() { + return this.current; + } + public ["System.Collections.IEnumerator.get_Current"]() { + return this.current; + } + public ["System.Collections.IEnumerator.MoveNext"]() { + const i = ++this.index; + if (i < this.array.length) { + this.current = this.array[i]; + return true; + } + this.current = defaultOf(); + return false; + } + public ["System.Collections.IEnumerator.Reset"]() { + this.index = -1; + this.current = defaultOf(); + } + public Dispose() { + return; + } +} + export function getEnumerator(e: IEnumerable | Iterable): IEnumerator { if (isEnumerable(e)) { return e.GetEnumerator(); } + else if (Array.isArray(e)) { return new ArrayEnumerator(e); } else { return new Enumerator(e[Symbol.iterator]()); } } @@ -400,12 +431,15 @@ export function bigintHash(x: bigint) { } // From https://stackoverflow.com/a/37449594 +export function combineHashCode(h1: number, h2: number): number { + return ((h1 << 5) + h1) ^ h2; +} + export function combineHashCodes(hashes: ArrayLike) { let h1 = 0; const len = hashes.length; for (let i = 0; i < len; i++) { - const h2 = hashes[i]; - h1 = ((h1 << 5) + h1) ^ h2; + h1 = combineHashCode(h1, hashes[i]); } return h1; } @@ -441,12 +475,11 @@ export function dateHash(x: Date): number { } export function arrayHash(x: ArrayLike): number { - const len = x.length; - const hashes: number[] = new Array(len); - for (let i = 0; i < len; i++) { - hashes[i] = structuralHash(x[i]); + let h = 0; + for (let i = 0; i < x.length; i++) { + h = combineHashCode(h, structuralHash(x[i])); } - return combineHashCodes(hashes); + return h; } export function structuralHash(x: T): number { @@ -471,8 +504,13 @@ export function structuralHash(x: T): number { return dateHash(x); } else if (Object.getPrototypeOf(x)?.constructor === Object) { // TODO: check call-stack to prevent cyclic objects? - const hashes = Object.values(x).map((v) => structuralHash(v)); - return combineHashCodes(hashes); + // Snapshot values before user hashing can mutate the object. + const values = Object.values(x); + let hash = 0; + for (let i = 0; i < values.length; i++) { + hash = combineHashCode(hash, structuralHash(values[i])); + } + return hash; } else { // Classes don't implement GetHashCode by default, but must use identity hashing return numberHash(ObjectRef.id(x)); @@ -667,13 +705,68 @@ export function jsOptions(mutator: (x: T) => void): T { } export function round(value: number, digits: number = 0) { + return roundToMode(value, digits, MidpointRounding.ToEven); +} + +export function roundFloat32(value: number, digits: number = 0) { + return roundToModeFloat32(value, digits, MidpointRounding.ToEven); +} + +export const MidpointRounding = { + ToEven: 0, + AwayFromZero: 1, + ToZero: 2, + ToNegativeInfinity: 3, + ToPositiveInfinity: 4, +} as const; + +function invalidMidpointRounding(mode: number) { + return new Exception(`The value '${mode}' is not valid for this usage of the type MidpointRounding. (Parameter 'mode')`); +} + +function roundToModeWithLimits(value: number, digits: number, mode: number, maxDigits: number, limit: number): number { + if (!(digits >= 0 && digits <= maxDigits)) { + throw new Exception(`Rounding digits must be between 0 and ${maxDigits}, inclusive. (Parameter 'digits')`); + } + // .NET returns values at or above the limit unchanged, without validating mode + if (!(Math.abs(value) < limit)) { + return value; + } const m = Math.pow(10, digits); - const n = +(digits ? value * m : value).toFixed(8); - const i = Math.floor(n); - const f = n - i; - const e = 1e-8; - const r = (f > 0.5 - e && f < 0.5 + e) ? ((i % 2 === 0) ? i : i + 1) : Math.round(n); - return digits ? r / m : r; + let v = value * m; + switch (mode) { + case MidpointRounding.ToEven: { + const r = Math.round(v); + v = r - v === 0.5 && r % 2 !== 0 ? r - 1 : r; + break; + } + case MidpointRounding.AwayFromZero: { + const i = Math.trunc(v); + const f = v - i; + v = Math.abs(f) >= 0.5 ? i + Math.sign(f) : i; + break; + } + case MidpointRounding.ToZero: v = Math.trunc(v); break; + case MidpointRounding.ToNegativeInfinity: v = Math.floor(v); break; + case MidpointRounding.ToPositiveInfinity: v = Math.ceil(v); break; + default: throw invalidMidpointRounding(mode); + } + return v / m; +} + +export function roundMode(value: number, mode: number): number { + if (!(mode >= MidpointRounding.ToEven && mode <= MidpointRounding.ToPositiveInfinity)) { + throw invalidMidpointRounding(mode); + } + return roundToModeWithLimits(value, 0, mode, 15, 1e16); +} + +export function roundToMode(value: number, digits: number, mode: number): number { + return roundToModeWithLimits(value, digits, mode, 15, 1e16); +} + +export function roundToModeFloat32(value: number, digits: number, mode: number): number { + return roundToModeWithLimits(value, digits, mode, 6, 1e8); } export function sign(x: number): number { @@ -709,36 +802,52 @@ export function count(col: Iterable): number { } } -export function clear(col: Iterable) { - if (isArrayLike(col)) { - (col as any as T[]).splice(0); +export function clear(col: Iterable): void { + if (Array.isArray(col)) { + col.length = 0; + } else if (ArrayBuffer.isView(col)) { + // TODO: throw for typed arrays? + } else if (typeof (col as any).clear === "function") { + (col as any).clear(); // map, set + } else if (typeof (col as any)["System.Collections.Generic.ICollection`1.Clear"] === "function") { + (col as any)["System.Collections.Generic.ICollection`1.Clear"](); // collection + } else if (typeof (col as any)["System.Collections.IList.Clear"] === "function") { + (col as any)["System.Collections.IList.Clear"](); // collection } else { - (col as any).clear(); + // TODO: throw for other collections? } } -const curried = new WeakMap(); +// The original curried function rides on its adapter so curryN can hand it back +const curriedKey = Symbol("curried"); +type CurriedAdapter = { [curriedKey]?: object }; + +function getCurried(f: object | null | undefined): object | undefined { + return f == null ? undefined : (f as CurriedAdapter)[curriedKey]; +} export function uncurry2(f: (a1: T1) => (a2: T2) => TResult) { if (f == null) { return null as unknown as (a1: T1, a2: T2) => TResult } const f2 = (a1: T1, a2: T2) => f(a1)(a2); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry2(f: (a1: T1, a2: T2) => TResult) { - return curried.get(f) as ((a1: T1) => (a2: T2) => TResult) ?? ((a1: T1) => (a2: T2) => f(a1, a2)); + if (f == null) { return null as never; } + return getCurried(f) as ((a1: T1) => (a2: T2) => TResult) ?? ((a1: T1) => (a2: T2) => f(a1, a2)); } export function uncurry3(f: (a1: T1) => (a2: T2) => (a3: T3) => TResult) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3) => TResult } const f2 = (a1: T1, a2: T2, a3: T3) => f(a1)(a2)(a3); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry3(f: (a1: T1, a2: T2, a3: T3) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => f(a1, a2, a3)); } @@ -747,12 +856,13 @@ export function uncurry4( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4) => f(a1)(a2)(a3)(a4); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry4(f: (a1: T1, a2: T2, a3: T3, a4: T4) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => f(a1, a2, a3, a4)); } @@ -761,12 +871,13 @@ export function uncurry5( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => f(a1)(a2)(a3)(a4)(a5); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry5(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => f(a1, a2, a3, a4, a5)); } @@ -775,12 +886,13 @@ export function uncurry6( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => f(a1)(a2)(a3)(a4)(a5)(a6); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry6(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => f(a1, a2, a3, a4, a5, a6)); } @@ -789,12 +901,13 @@ export function uncurry7( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry7(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => f(a1, a2, a3, a4, a5, a6, a7)); } @@ -803,12 +916,13 @@ export function uncurry8( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry8(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => f(a1, a2, a3, a4, a5, a6, a7, a8)); } @@ -817,12 +931,13 @@ export function uncurry9( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry9(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9)); } @@ -831,12 +946,13 @@ export function uncurry10( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry10(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)); } @@ -845,12 +961,13 @@ export function uncurry11 ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry11(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11)); } @@ -859,12 +976,13 @@ export function uncurry12 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry12(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12)); } @@ -873,12 +991,13 @@ export function uncurry13 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry13(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13)); } @@ -887,12 +1006,13 @@ export function uncurry14 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry14(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14)); } @@ -901,12 +1021,13 @@ export function uncurry15 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry15(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)); } @@ -915,12 +1036,13 @@ export function uncurry16 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry16(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16)); } @@ -929,12 +1051,13 @@ export function uncurry17 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16)(a17); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry17(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17)); } @@ -943,12 +1066,13 @@ export function uncurry18 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16)(a17)(a18); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry18(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18)); } @@ -957,12 +1081,13 @@ export function uncurry19 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16)(a17)(a18)(a19); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry19(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19)); } @@ -971,12 +1096,13 @@ export function uncurry20 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19, a20: T20) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16)(a17)(a18)(a19)(a20); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry20(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19, a20: T20) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => (a20: T20) => TResult + if (f == null) { return null as never; } + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => (a20: T20) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => (a20: T20) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)); } diff --git a/src/fable-library-ts/lib/big.d.ts b/src/fable-library-ts/lib/big.d.ts index 521dc72ece..50c783eff4 100644 --- a/src/fable-library-ts/lib/big.d.ts +++ b/src/fable-library-ts/lib/big.d.ts @@ -202,6 +202,7 @@ export interface Big extends CustomNumeric { * @throws `!Big.RM!` if rm is invalid. */ round(dp?: number, rm?: RoundingMode): Big; + prec(sd: number, rm?: RoundingMode): Big; /** * Returns a Big number whose value is the square root of this Big number. * diff --git a/src/fable-library-ts/lib/big.js b/src/fable-library-ts/lib/big.js index 22a6c5ba50..d2cbd9d6d2 100644 --- a/src/fable-library-ts/lib/big.js +++ b/src/fable-library-ts/lib/big.js @@ -95,7 +95,6 @@ function _Big_() { x.s = n.s; x.e = n.e; x.c = n.c.slice(); - normalize(x); } else { if (typeof n !== 'string') { if (Big.strict === true) { @@ -124,15 +123,6 @@ function _Big_() { return Big; } -function normalize(x) { - // x = round(x, DP, 0); - if (x.c.length > 1 && !x.c[0]) { - let i = x.c.findIndex(x => x); - x.c = x.c.slice(i); - x.e = x.e - i; - } -} - /* * Parse the number or string value passed to a Big constructor. * @@ -167,17 +157,14 @@ function parse(x, n) { nl = n.length; - // Determine leading zeros before decimal point. - for (i = 0; i < e && i < nl && n.charAt(i) == '0';) ++i; - - // original version (ignores decimal point). - // // Determine leading zeros. - // for (i = 0; i < nl && n.charAt(i) == '0';) ++i; + // Determine leading zeros. + for (i = 0; i < nl && n.charAt(i) == '0';) ++i; if (i == nl) { - // Zero. - x.c = [x.e = 0]; + // Zero, keeping the scale: 0.00 is c = [0], e = -2. + x.c = [0]; + x.e = nl > e ? e - nl : 0; } else { x.e = e - i - 1; @@ -193,8 +180,7 @@ function parse(x, n) { // for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++); } - x = round(x, Big.DP + 1, Big.RM); - return x; + return fit(x); } @@ -218,21 +204,14 @@ function round(x, sd, rm, more) { more = rm === 3 && (more || !!xc[0]) || sd === 0 && ( rm === 1 && xc[0] >= 5 || - rm === 2 && (xc[0] > 5 || xc[0] === 5 && (more || xc[1] !== UNDEFINED)) + rm === 2 && (xc[0] > 5 || xc[0] === 5 && (more || xc.some((d, i) => i > 0 && d))) ); xc.length = 1; - if (more) { - - // 1, 0.1, 0.01, 0.001, 0.0001 etc. - x.e = x.e - sd + 1; - xc[0] = 1; - } else { - - // Zero. - xc[0] = x.e = 0; - } + // 1, 0.1, 0.01, 0.001, 0.0001 etc., or zero at the same place. + x.e = x.e - sd + 1; + xc[0] = more ? 1 : 0; } else if (sd < xc.length) { // xc[sd] is the digit after the digit that may be rounded up. @@ -240,7 +219,7 @@ function round(x, sd, rm, more) { more = rm === 1 && xc[sd] >= 5 || rm === 2 && (xc[sd] > 5 || xc[sd] === 5 && - (more || xc[sd + 1] !== UNDEFINED || xc[sd - 1] & 1)) || + (more || xc.some((d, i) => i > sd && d) || xc[sd - 1] & 1)) || rm === 3 && (more || !isZero); // Remove any digits after the required precision. @@ -259,14 +238,70 @@ function round(x, sd, rm, more) { } } - // Remove trailing zeros. - for (sd = xc.length; !xc[--sd];) xc.pop(); } return x; } +// Largest System.Decimal mantissa, 2^96 - 1. +var MAX_MANTISSA = '79228162514264337593543950335'.split('').map(Number), + OVERFLOW = 'Value was either too large or too small for a Decimal.'; + + +/* + * Return the number of decimal places of Big x. + */ +function scaleOf(x) { + return x.c.length - x.e - 1; +} + + +/* + * Pad Big x with trailing zeros up to dp decimal places. + */ +function padScale(x, dp) { + if (!x.c[0]) { + x.e = Math.min(x.e, -dp); + } else { + for (; scaleOf(x) < dp;) x.c.push(0); + } + return x; +} + + +/* + * Return true if the digits c, padded with zeros to the length of MAX_MANTISSA, exceed it. + */ +function exceedsMax(c) { + for (var i = 0; i < MAX_MANTISSA.length; i++) { + var d = i < c.length ? c[i] : 0; + if (d !== MAX_MANTISSA[i]) return d > MAX_MANTISSA[i]; + } + return false; +} + + +/* + * Round Big x half to even so that it fits a System.Decimal: at most Big.DP decimal places + * and a mantissa no larger than MAX_MANTISSA. Throws when no decimal place is left to drop. + */ +function fit(x) { + var scale = scaleOf(x), + dp = Math.min(scale, Big.DP, Big.DP - x.e); + + if (x.e > Big.DP) throw Error(OVERFLOW); + if (dp < scale) round(x, x.e + 1 + dp, 2); + if (x.e > Big.DP) throw Error(OVERFLOW); + + // Both the value and the mantissa itself must not exceed MAX_MANTISSA. + if (x.e === Big.DP && exceedsMax(x.c)) throw Error(OVERFLOW); + if (x.c.length === MAX_MANTISSA.length && exceedsMax(x.c)) round(x, x.c.length - 1, 2); + + return x; +} + + /* * Return a string representing the value of Big x in normal or exponential notation. * Handles P.toExponential, P.toFixed, P.toJSON, P.toPrecision, P.toString and P.valueOf. @@ -278,6 +313,7 @@ function stringify(x, doExponential, isNonzero) { // Exponential notation? if (doExponential) { + if (!x.c[0]) e = 0; s = s.charAt(0) + (n > 1 ? '.' + s.slice(1) : '') + (e < 0 ? 'e' : 'e+') + e; // Normal notation. @@ -365,28 +401,40 @@ P.cmp = function (y) { * if necessary, to a maximum of Big.DP decimal places using rounding mode Big.RM. */ P.div = function (y) { - var - Big = this.constructor, - x = new Big(this), - y = new Big(y), - a = x.c, // dividend - b = y.c, // divisor - k = x.s == y.s ? 1 : -1, + var Big = this.constructor, dp = Big.DP; if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { throw Error(INVALID_DP); } + return divide(new Big(this), new Big(y), dp, 2); +}; + + +/* + * Return Big x divided by Big y to dp decimal places using rounding mode rm. + * Both are modified. + */ +function divide(x, y, dp, rm) { + var + a = x.c, // dividend + b = y.c, // divisor + k = x.s == y.s ? 1 : -1; + // Divisor is zero? if (!b[0]) { throw Error(DIV_BY_ZERO); } + // Scale of the exact quotient is at least the scale of x less the scale of y. + var minScale = Math.max(0, scaleOf(x) - scaleOf(y)); + // Dividend is 0? Return +-0. if (!a[0]) { y.s = k; - y.c = [y.e = 0]; + y.c = [0]; + y.e = -minScale; return y; } @@ -466,11 +514,20 @@ P.div = function (y) { p--; } + var exact = r[0] === UNDEFINED; + // Round? - if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED); + if (qi > p) round(q, p, rm, !exact); + fit(q); + + if (!q.c[0]) { + q.e = 0; + } else if (exact) { + for (; scaleOf(q) > minScale && !q.c[q.c.length - 1];) q.c.pop(); + } return q; -}; +} /* @@ -540,6 +597,7 @@ P.minus = P.sub = function (y) { // Either zero? if (!xc[0] || !yc[0]) { + var dp = Math.max(scaleOf(x), scaleOf(y)); if (yc[0]) { y.s = -b; } else if (xc[0]) { @@ -547,7 +605,7 @@ P.minus = P.sub = function (y) { } else { y.s = 1; } - return y; + return padScale(y, dp); } // Determine which is the bigger number. Prepend zeros to equalise exponents. @@ -602,8 +660,8 @@ P.minus = P.sub = function (y) { xc[j] -= yc[j]; } - // Remove trailing zeros. - for (; xc[--b] === 0;) xc.pop(); + // The result keeps the larger scale. + var dp = xc.length - ye - 1; // Remove leading zeros and adjust exponent accordingly. for (; xc[0] === 0;) { @@ -617,13 +675,14 @@ P.minus = P.sub = function (y) { y.s = 1; // Result must be zero. - xc = [ye = 0]; + xc = [0]; + ye = -dp; } y.c = xc; y.e = ye; - return y; + return fit(y); }; @@ -649,12 +708,7 @@ P.mod = function (y) { if (ygtx) return new Big(x); - a = Big.DP; - b = Big.RM; - Big.DP = Big.RM = 0; - x = x.div(y); - Big.DP = a; - Big.RM = b; + x = divide(x, new Big(y), 0, 0); return this.minus(x.times(y)); }; @@ -682,6 +736,7 @@ P.plus = P.add = function (y) { // Either zero? if (!xc[0] || !yc[0]) { + var dp = Math.max(scaleOf(x), scaleOf(y)); if (!yc[0]) { if (xc[0]) { y = new Big(x); @@ -689,7 +744,7 @@ P.plus = P.add = function (y) { y.s = x.s; } } - return y; + return padScale(y, dp); } xc = xc.slice(); @@ -729,13 +784,10 @@ P.plus = P.add = function (y) { ++ye; } - // Remove trailing zeros. - for (e = xc.length; xc[--e] === 0;) xc.pop(); - y.c = xc; y.e = ye; - return y; + return fit(y); }; @@ -871,7 +923,9 @@ P.times = P.mul = function (y) { // Return signed 0 if either 0. if (!xc[0] || !yc[0]) { - y.c = [y.e = 0]; + var dp = scaleOf(x) + scaleOf(y); + y.c = [0]; + y.e = -dp; return y; } @@ -915,11 +969,9 @@ P.times = P.mul = function (y) { if (b) ++y.e; else c.shift(); - // Remove trailing zeros. - for (i = c.length; !c[--i];) c.pop(); y.c = c; - return y; + return fit(y); }; @@ -953,12 +1005,11 @@ P.toExponential = function (dp, rm) { * dp? {number} Decimal places: integer, 0 to MAX_DP inclusive. * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). * - * (-0).toFixed(0) is '0', but (-0.1).toFixed(0) is '-0'. - * (-0).toFixed(1) is '0.0', but (-0.01).toFixed(1) is '-0.0'. + * Unlike JavaScript, the sign is omitted when the rounded result is zero: + * (-0.1).toFixed(0) is '0' and (-0.01).toFixed(1) is '0.0', matching .NET. */ P.toFixed = function (dp, rm) { - var x = this, - n = x.c[0]; + var x = this; if (dp !== UNDEFINED) { if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { @@ -970,7 +1021,7 @@ P.toFixed = function (dp, rm) { for (dp = dp + x.e + 1; x.c.length < dp;) x.c.push(0); } - return stringify(x, false, !!n); + return stringify(x, false, !!x.c[0]); }; @@ -1018,6 +1069,7 @@ P.toPrecision = function (sd, rm) { throw Error(INVALID + 'precision'); } x = round(new Big(x), sd, rm); + if (!x.c[0]) x.e = 0; for (; x.c.length < sd;) x.c.push(0); } diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index d37cea52f2..50624ea243 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.5.1", + "version": "2.13.0", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-library-ts/quotation.ts b/src/fable-library-ts/quotation.ts index bf934c8859..34affe1a8d 100644 --- a/src/fable-library-ts/quotation.ts +++ b/src/fable-library-ts/quotation.ts @@ -664,3 +664,51 @@ export function exprFromJSON(json: any): Expr { default: return new ExprValue(json, "unknown"); } } + +// =================================================================== +// DerivedPatterns +// F# defines these on top of Patterns. AndAlso and OrElse recover the +// shape `&&` and `||` desugar into; SpecificCall matches a call by the +// identity of a template quotation rather than by compiled name. +// =================================================================== + +export function isAndAlso(expr: Expr): [Expr, Expr] | undefined { + // a && b ==> if a then b else false + if (expr instanceof ExprIfThenElse + && expr.elseExpr instanceof ExprValue + && expr.elseExpr.value === false) { + return [expr.guard, expr.thenExpr]; + } + return undefined; +} + +export function isOrElse(expr: Expr): [Expr, Expr] | undefined { + // a || b ==> if a then true else b + if (expr instanceof ExprIfThenElse + && expr.thenExpr instanceof ExprValue + && expr.thenExpr.value === true) { + return [expr.guard, expr.elseExpr]; + } + return undefined; +} + +/// A template such as <@ (=) @> quotes as nested lambdas around the call, so +/// the identifying Call node has to be dug out before comparing. +function templateCall(expr: Expr): ExprCall | undefined { + let current: Expr = expr; + while (current instanceof ExprLambda) { current = current.body; } + return current instanceof ExprCall ? current : undefined; +} + +// Takes both arguments; the partial application FSharp.Core expresses by +// currying is built by the replacement instead. +export function isSpecificCall(template: Expr, expr: Expr): [Expr | null, FSharpList, FSharpList] | undefined { + const wanted = templateCall(template); + if (wanted === undefined || !(expr instanceof ExprCall)) return undefined; + if (expr.method !== wanted.method || expr.declaringType !== wanted.declaringType) return undefined; + const instance = (expr.instance instanceof ExprValue && expr.instance.type === "novalue") ? null : expr.instance; + // The middle slot is F#'s generic-argument list. The runtime models types as + // names rather than System.Type, so it is always empty here; callers written + // against Fable match it with a wildcard. + return [instance, ofArray([]), ofArray(expr.args)]; +} diff --git a/src/fable-library-ts/ts/tsconfig.json b/src/fable-library-ts/ts/tsconfig.json index 5ace72b110..8bce17fa58 100644 --- a/src/fable-library-ts/ts/tsconfig.json +++ b/src/fable-library-ts/ts/tsconfig.json @@ -13,7 +13,7 @@ /* Language and Environment */ "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2020", "dom", "esnext.temporal"], /* Specify a set of bundled library declaration files that describe the target runtime environment. Temporal types (used by the *Temporal.ts files) live only in lib.esnext.temporal; "dom" is needed for setTimeout/console/TextEncoder since "types": [] excludes @types/node. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "libReplacement": true, /* Enable lib replacement. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ diff --git a/src/fable-library-ts/tsconfig.json b/src/fable-library-ts/tsconfig.json index 5ace72b110..8bce17fa58 100644 --- a/src/fable-library-ts/tsconfig.json +++ b/src/fable-library-ts/tsconfig.json @@ -13,7 +13,7 @@ /* Language and Environment */ "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2020", "dom", "esnext.temporal"], /* Specify a set of bundled library declaration files that describe the target runtime environment. Temporal types (used by the *Temporal.ts files) live only in lib.esnext.temporal; "dom" is needed for setTimeout/console/TextEncoder since "types": [] excludes @types/node. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "libReplacement": true, /* Enable lib replacement. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ diff --git a/src/fable-metadata/CHANGELOG.md b/src/fable-metadata/CHANGELOG.md index 204c735579..3736b6bab7 100644 --- a/src/fable-metadata/CHANGELOG.md +++ b/src/fable-metadata/CHANGELOG.md @@ -1,11 +1,11 @@ --- -last_commit_released: c977d78b39225a51c7bd051a1fe363ed0ccbe201 +last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f updaters: - package.json: file: package.json - regex: file: ../fable-compiler-js/package.json - pattern: '(?<="@fable-org/fable-metadata": "\^)\d+\.\d+\.\d+' + pattern: '(?<="@fable-org/fable-metadata": "\^)[^"]+' --- # Changelog @@ -15,6 +15,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.2.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..5ee8bbb832d9677ad702104ded8e00c09619393f) + +## 2.1.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c977d78b39225a51c7bd051a1fe363ed0ccbe201..a8661874d60694f0faad235adb7f760fed6f72a5) + ## 2.1.0 - 2026-06-30 ### 🚀 Features diff --git a/src/fable-metadata/lib/Fable.Core.dll b/src/fable-metadata/lib/Fable.Core.dll index cabadd25bf..9900807324 100644 Binary files a/src/fable-metadata/lib/Fable.Core.dll and b/src/fable-metadata/lib/Fable.Core.dll differ diff --git a/src/fable-metadata/package.json b/src/fable-metadata/package.json index d7b04b7401..3e9c96c643 100644 --- a/src/fable-metadata/package.json +++ b/src/fable-metadata/package.json @@ -1,7 +1,7 @@ { "name": "@fable-org/fable-metadata", "private": false, - "version": "2.1.0", + "version": "2.2.0", "description": "Assemblies needed to compile F# projects with Fable", "main": "index.js", "type": "module", @@ -25,4 +25,4 @@ "dependencies": { "dirname-filename-esm": "^1.1.1" } -} +} \ No newline at end of file diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 094c23e52c..00ea269864 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,15 +1,17 @@ --- -last_commit_released: b517d52df145ee30d1d3bd7e20fded1a9003ac54 +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 +pre_release: beta include: - ../Fable.Transforms/ - ../fcs-fable/ - ../Fable.AST/ + - ../fable-library-ts/ updaters: - package.json: file: package.json - regex: file: ../fable-compiler-js/package.json - pattern: '(?<="@fable-org/fable-standalone": "\^)\d+\.\d+\.\d+' + pattern: '(?<="@fable-org/fable-standalone": "\^)[^"]+' --- # Changelog @@ -19,6 +21,315 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.0.0-beta.6 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(all)* Keep reads of imported mutable values in place (#5242) ([1190dfa5](https://github.com/fable-compiler/Fable/commit/1190dfa5e5f12793fc2b3b2d15a24afb8253bce3)) +* *(all)* Rename idents of a witness inlined twice (#5249) ([1e41dcc5](https://github.com/fable-compiler/Fable/commit/1e41dcc5ac5a990a81aa15d43921d824b5466740)) +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Unescape doubled braces in String.Format (#5231) ([24e01fd3](https://github.com/fable-compiler/Fable/commit/24e01fd30e3eec7571816b4d6cc9f6281cce0de4)) +* *(js/ts)* Treat null as empty in String.Join and Concat (#5230) ([a7dcab8d](https://github.com/fable-compiler/Fable/commit/a7dcab8d770ac56f825e15361df7b2e05f618e5e)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Match Dictionary/HashSet array keys by reference (#5239) ([1d77d628](https://github.com/fable-compiler/Fable/commit/1d77d6282b8dea284bca3cff9a6455e561575e3a)) +* *(js/ts)* Round exact midpoints to even in numeric formats (#5245) ([350a3900](https://github.com/fable-compiler/Fable/commit/350a390071473bb4e93fb055a3876d7b3215ada1)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Escape */ in JSDoc comments (#5255) ([1072231c](https://github.com/fable-compiler/Fable/commit/1072231c4c64467547d7f69ab741824f2e1a522c)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Write negated integer zero without a sign (#5256) ([ce7fc717](https://github.com/fable-compiler/Fable/commit/ce7fc717c7597b133dc6f999f88a15b2fb636d65)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python)* Call base ctor from explicit constructors (#5226) ([508026e7](https://github.com/fable-compiler/Fable/commit/508026e73511f2636245515b8ec9476176e34b5c)) +* *(js/ts/python)* Clamp negative slice bounds like .NET (#5232) ([7319a994](https://github.com/fable-compiler/Fable/commit/7319a994a9c3be7d31348d29d2c464482b5f21f9)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) +* *(rust)* Inline hashing, more Decimal tests (#5236) ([705b6b70](https://github.com/fable-compiler/Fable/commit/705b6b70bf0b732811bd6e69cfc8fc0614b831c7)) +* *(ts)* Declare Mangle(false) interface members unmangled (#5234) ([86c00004](https://github.com/fable-compiler/Fable/commit/86c000041f286668353615003692b07e9082ff01)) +* *(ts)* Extend library classes for BCL collection bases (#5240) ([d82ef893](https://github.com/fable-compiler/Fable/commit/d82ef8932c58ddc73ff5aa6ca669be86aedbe9db)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + +## 4.0.0-beta.5 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + +## 4.0.0-beta.4 - 2026-10-29 + +### 🚀 Features + +* *(all)* Error on unsupported RegexOptions at compile time (#5180) ([a8cbc5dc](https://github.com/fable-compiler/Fable/commit/a8cbc5dc0c3a96639028057e132b39e559f40ced)) +* *(js/ts/python/dart)* Support Nullable conversions (#5183) ([6efa86d3](https://github.com/fable-compiler/Fable/commit/6efa86d35c5943dc8bbb6da0f27cc37095276064)) +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Warn on duplicate names in erased unions (#5169) ([459166d2](https://github.com/fable-compiler/Fable/commit/459166d2500a963a3616d8b1a63e1d8585d61ec6)) +* *(all)* Recompile inline users of a file with a signature (#5179) ([b383673d](https://github.com/fable-compiler/Fable/commit/b383673da61cfe3ca06416a624aaafbd732f876c)) +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(beam)* Raise ArgumentNullException from nullArg ([0b8f77f9](https://github.com/fable-compiler/Fable/commit/0b8f77f9880bcbd9317272d9b0d89f8fe8ac0257)) +* *(beam)* Keep parameter name in ArgumentException ([9e5d34f7](https://github.com/fable-compiler/Fable/commit/9e5d34f7b32cfdfb8b1adc93378895b26a5d69b9)) +* *(js/ts)* Format DateTimeOffset in its own offset (#5171) ([04ddbd20](https://github.com/fable-compiler/Fable/commit/04ddbd2027e03f4cf7b0b22fe886fdabadf26d93)) +* *(js/ts)* Support %o in sprintf and interpolated strings (#5175) ([e13e7b0f](https://github.com/fable-compiler/Fable/commit/e13e7b0fdc16d26793833c77a11d242ccc61109b)) +* *(js/ts)* Zero-fill Array.zeroCreate for numeric types without a typed array (#5211) ([d96b6ab8](https://github.com/fable-compiler/Fable/commit/d96b6ab82f0181be4a379c806abcedde23f1359a)) +* *(js/ts)* Throw on out-of-range string indexer (#5214) ([d5f1f8fd](https://github.com/fable-compiler/Fable/commit/d5f1f8fdfab535eaff5c492661d0178be1c4c76a)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/dart)* Float to small integer and char conversions (#5182) ([bb0faa79](https://github.com/fable-compiler/Fable/commit/bb0faa794080a04e7791d419b2fa4d2ad107c186)) +* *(js/ts/python)* Compare ReferenceEquality types by ref (#5174) ([9c351626](https://github.com/fable-compiler/Fable/commit/9c3516268674b54aec2efae381cad920d4f15922)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) +* *(python/beam/rust)* Implicit DateTime to DateTimeOffset (#5173) ([401c881f](https://github.com/fable-compiler/Fable/commit/401c881f729494d06d20da9687e78f7315f5efb2)) +* *(ts)* Annotate Fable.Core typed arrays with native TS types ([e35d06c3](https://github.com/fable-compiler/Fable/commit/e35d06c36ba8ce4ab6cce75d6b332b9dcd50d711)) +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +### ⚡ Performance Improvements + +* *(js/ts)* Standard library combine numeric hash from snapshot of object instead of mapping (#5151) ([44495104](https://github.com/fable-compiler/Fable/commit/44495104b5c7ac65bfcea184d1305fd29f1db9b4)) +* *(js/ts)* Standard library remove .map result array from record hash using snapshot (#5150) ([0ceb5e9e](https://github.com/fable-compiler/Fable/commit/0ceb5e9ec5b470d00295a5cea9ead1c621c7858e)) +* *(js/ts)* Standard library one pass List.append builder (#5148) ([2ed9d6dd](https://github.com/fable-compiler/Fable/commit/2ed9d6dd0a372e5bcf53dfe24d786bf9b9256fd3)) +* *(js/ts)* Cast erased option unwraps in TypeScript output (#5181) ([65cd419d](https://github.com/fable-compiler/Fable/commit/65cd419d78e4dbc2008f8a707d5298e390defbee)) +* *(js/ts)* List node as a class with a shared empty node (#5185) ([187f8985](https://github.com/fable-compiler/Fable/commit/187f89852413186b52d4ecdde55e1be7cf736785)) +* *(js/ts)* Compile isNull to a plain null check (#5186) ([71a0ad92](https://github.com/fable-compiler/Fable/commit/71a0ad92237586e9569fd1483f35c88f8477f478)) +* *(js/ts)* Check array bounds only on undefined reads (#5188) ([435fc0bb](https://github.com/fable-compiler/Fable/commit/435fc0bb008ff2ab7c14a08c7218be94f4735029)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Keep the curried original on its adapter (#5191) ([bfb4a2a4](https://github.com/fable-compiler/Fable/commit/bfb4a2a4e3bb1b5bd4491955a1e1cb90c14050b6)) +* *(js/ts)* Walk arrays by index when enumerated as seq (#5192) ([d88621c3](https://github.com/fable-compiler/Fable/commit/d88621c3b9d6ce48a72dc09b86a6805f612ac185)) +* *(js/ts)* Throw invalidArg and invalidOp at the call site (#5194) ([e6a742b1](https://github.com/fable-compiler/Fable/commit/e6a742b1ebda71342d4445705ea9198c2059ae90)) +* *(js/ts)* Hash unions without allocating (#5195) ([2b66a5d2](https://github.com/fable-compiler/Fable/commit/2b66a5d2184f3a35f0d73fb397ab902e300ac4bd)) +* *(js/ts)* Probe dictionary buckets without allocating (#5196) ([9517eafd](https://github.com/fable-compiler/Fable/commit/9517eafd93aa7b84061662a421b7a39a7269318b)) +* *(js/ts)* Pass array literals to typed array constructors as plain arrays (#5209) ([cb7d6d6e](https://github.com/fable-compiler/Fable/commit/cb7d6d6e49bb43b83afe80477ea2d6d667be3f5b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + +## 4.0.0-beta.3 - 2026-10-22 + +### 🚀 Features + +* *(all)* Support suppressing warnings via `// fable-disable` comments (#4808) ([f4a1a8cd](https://github.com/fable-compiler/Fable/commit/f4a1a8cdae2e8ac81edbdc6eba36cc140234170b)) +* *(all)* Warn on duplicate CompiledName on union cases ([2098f64a](https://github.com/fable-compiler/Fable/commit/2098f64a25e96385306ef19ed8c289f3b6c84db9)) +* *(js/ts)* Map static Decimal conversion methods (#5129) ([cd63e57e](https://github.com/fable-compiler/Fable/commit/cd63e57e909393ab493a1d8bfedbea9a50a377c9)) +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +### 🐞 Bug Fixes + +* *(js/ts)* Round decimals below 0.1 correctly ([fca4c408](https://github.com/fable-compiler/Fable/commit/fca4c408a64e33c58293f12869975e5b61bcb96a)) +* *(js/ts)* Keep decimal zero scale without leading zeros ([3076b45a](https://github.com/fable-compiler/Fable/commit/3076b45acacf39b8c99ec57025560ad355ab74db)) +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Keep .NET decimal scale through arithmetic (#5126) ([bdf0a6c8](https://github.com/fable-compiler/Fable/commit/bdf0a6c8cb56151a5431b5e3703f7ee165b26dba)) +* *(js/ts)* Warn on type test against Pojo class (#5128) ([798df101](https://github.com/fable-compiler/Fable/commit/798df10179576a28f602fad58e204881792a0aef)) +* *(js/ts)* Accept decimal integers up to 2^96 ([c9feb339](https://github.com/fable-compiler/Fable/commit/c9feb339fb8a4e98bebf17b3facdc42ecd22d628)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Respect maxDegreeOfParallelism and cancel on failure in Async.Parallel (#5098) ([5de3271d](https://github.com/fable-compiler/Fable/commit/5de3271d8b47f1752c4e74c010e4314cf40bbcbe)) +* *(js/ts)* Inline interface cases of TypeScriptTaggedUnion (#5146) ([2be66611](https://github.com/fable-compiler/Fable/commit/2be66611d884cd564fa3f7b06fd4a015add1cefd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + +## 4.0.0-beta.2 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +### 🐞 Bug Fixes + +* *(standalone)* Warn and skip plugins instead of throwing (#5114) ([021ab65c](https://github.com/fable-compiler/Fable/commit/021ab65c6d3adfe57cffb9f5ba6f595b034ac8bd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..5ee8bbb832d9677ad702104ded8e00c09619393f) + +## 4.0.0-beta.1 - 2026-10-16 + +### 🏗️ Breaking changes + +* *(standalone)* Return tagged tooltips instead of text (#5088) ([6b995913](https://github.com/fable-compiler/Fable/commit/6b995913acbe98434f252b19b07182cc831a8a0f)) +* *(standalone)* Keep file name in declaration locations (#5089) ([b210461c](https://github.com/fable-compiler/Fable/commit/b210461c1651eaaa47e1027597c7545b6f2097fa)) +* *(standalone)* Return full completions with lazy docs (#5102) ([53849cc5](https://github.com/fable-compiler/Fable/commit/53849cc5a65f5de57dc65c76158b840e7012bc9c)) +* *(standalone)* Report error codes and severities (#5103) ([4ec83ff4](https://github.com/fable-compiler/Fable/commit/4ec83ff4e280db3075230e72b02b699a16f2bc76)) +* *(standalone)* Drop GetErrors, use Errors on results (#5111) ([ec72286d](https://github.com/fable-compiler/Fable/commit/ec72286d3528d1bbc1bede72df11deb3432040b4)) +* *(standalone)* Take 0-based columns in positional APIs (#5112) ([e33c5787](https://github.com/fable-compiler/Fable/commit/e33c57870454fea189a3eb33519f583a5003f8c3)) + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) +* *(standalone)* Expose symbol lookup and uses in file (#5101) ([9eff071c](https://github.com/fable-compiler/Fable/commit/9eff071c8e5cf418e4bf9af63c051eae121d4512)) +* *(standalone)* Expose lexer tokens and classification (#5110) ([e65d42c0](https://github.com/fable-compiler/Fable/commit/e65d42c03b8262b94e058354904f1126560443b4)) + +### 🐞 Bug Fixes + +* *(all)* Recompile dependents on signature change in watch (#5084) ([87fc89f7](https://github.com/fable-compiler/Fable/commit/87fc89f75b6ad22fafec72b4a57415682b4059df)) +* *(beam)* Make lazy sequence wrappers process-portable (#5063) ([9e78a3b3](https://github.com/fable-compiler/Fable/commit/9e78a3b327ca78d01863bf19b18c543673633af7)) +* *(js/ts)* Avoid full array copy in Encoding.GetString (#5069) ([e6b72ec7](https://github.com/fable-compiler/Fable/commit/e6b72ec73be454bd3b3233167542a65fc6553c8a)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) +* *(python)* Settle cancelled tasks awaited by async workflows (#5064) ([52186937](https://github.com/fable-compiler/Fable/commit/5218693738defc8a4502d28c206e391b241f9fd8)) +* *(python)* Preserve reusable replies and unit continuations (#5082) ([f19e2c56](https://github.com/fable-compiler/Fable/commit/f19e2c56566488dfeb48e71508bdb218aaa58582)) +* *(python)* Emit widening integer casts (#5087) ([07989b16](https://github.com/fable-compiler/Fable/commit/07989b16214c4fcf8e33318fba1dd3f77df77cbb)) +* *(rust)* Support inref struct unions (#5060) ([a3f27e0e](https://github.com/fable-compiler/Fable/commit/a3f27e0e8c149d6bfeb347ca4f4146ccfd8fb063)) + +### ⚡ Performance Improvements + +* Update inline expressions incrementally in watch mode (#5080) ([594d17b4](https://github.com/fable-compiler/Fable/commit/594d17b4b551ae33b5f2df5e4049611b9dd24bc2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..e33c57870454fea189a3eb33519f583a5003f8c3) + +## 3.7.2 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) +* *(beam)* Preserve async cancellation and finalize resources once (#5057) ([617f7111](https://github.com/fable-compiler/Fable/commit/617f7111bf02f8536b321b4373bdffa7a0f7d633)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..a8661874d60694f0faad235adb7f760fed6f72a5) + +## 3.7.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(beam)* Disambiguate property accessors from ordinary methods (#5049) ([b30dc89d](https://github.com/fable-compiler/Fable/commit/b30dc89d0091c2bd1d9fd7a20042b7fbb4e5a3fa)) +* *(beam)* Normalize exception messages and string split separators (#5053) ([f13a9e14](https://github.com/fable-compiler/Fable/commit/f13a9e1494d8bbde8e74bfbf9dfbc8bd051a157e)) +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Preserve JS/TS generic unit arguments and Python unit continuations (#5037) ([e05bc752](https://github.com/fable-compiler/Fable/commit/e05bc752f1d8f16f08e2409a191d9c33463a1f04)) +* *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159)) +* *(rust)* Fix tailcall elimination for members (#5051) ([4eb21772](https://github.com/fable-compiler/Fable/commit/4eb2177263f544ec354076da07d8a097d56c273e)) +* *(rust)* Fix temporary struct locals type (#5052) ([17b36588](https://github.com/fable-compiler/Fable/commit/17b3658876a3bd776c4a0129ec3a4cddc9d7e05d)) +* *(rust)* Add main function for no_std (#5054) ([10fdccc0](https://github.com/fable-compiler/Fable/commit/10fdccc04bb0e421643426d21dfcbd940e3e5a16)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/23bc5c54db51469431e6fdab713dfe1b6a3df019..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + +## 3.7.0 - 2026-10-07 + +### 🚀 Features + +* *(python)* Give System exceptions native Python bases (#5040) ([4ee31586](https://github.com/fable-compiler/Fable/commit/4ee31586fd88ed0708e218e7f170432baa33551e)) + +### 🐞 Bug Fixes + +* *(beam)* Recognize boxed discriminated unions in type tests (#5033) ([dcfaa3d6](https://github.com/fable-compiler/Fable/commit/dcfaa3d6181f6789d0868e0bf9132ecdcdc76287)) +* *(beam)* Prevent identifier collisions and atom overflow (#5017) ([b9ce2fdb](https://github.com/fable-compiler/Fable/commit/b9ce2fdb53fc86a615970f4828005ee3a490385f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/aaf35f200be5bbc2698abe242f77b8de745e4337..23bc5c54db51469431e6fdab713dfe1b6a3df019) + +## 3.6.0 - 2026-10-03 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(all)* Minor type const strings cleanup (#5029) ([9b69e5be](https://github.com/fable-compiler/Fable/commit/9b69e5be8a2cffc3cbcb3920bf9924b72ed385d6)) +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) +* *(rust)* Remove trailing comma from macro args (#5031) ([e6e9f54c](https://github.com/fable-compiler/Fable/commit/e6e9f54c905750a1bf78ec454bca7d83cdffb61e)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..aaf35f200be5bbc2698abe242f77b8de745e4337) + +## 3.5.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +### 🐞 Bug Fixes + +* *(beam)* Preserve local spawn calls (#4992) ([70278ec6](https://github.com/fable-compiler/Fable/commit/70278ec636b7f948a6ba3d215b36a3974de89edc)) +* *(dart)* Prefix file scope names shadowed by class members (#4982) ([ece7b071](https://github.com/fable-compiler/Fable/commit/ece7b0714eae18650124cad017b0bda4da74ebc8)) +* *(js/ts)* Fix Dictionary.Remove and HashSet.Remove to remove empty buckets (#4985) ([aea61b56](https://github.com/fable-compiler/Fable/commit/aea61b562c461a9b383e805f06f68a3b21ed45c2)) +* *(js/ts)* Clear custom System.Collections.IList implementations ([2369fbd2](https://github.com/fable-compiler/Fable/commit/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36)) +* *(python)* Keep the payload type when reading an erased union field (#4981) ([e0536ecd](https://github.com/fable-compiler/Fable/commit/e0536ecd9b9aeb12cc96c7a2ef2edde7089e2dd0)) +* *(rust)* Less allocations when enumerating strings (#4970) ([5d62380c](https://github.com/fable-compiler/Fable/commit/5d62380cb30dc1080c6943445a256e27db6246f8)) +* *(rust)* Remove reflection registration on boxing (#4983) ([50a37f78](https://github.com/fable-compiler/Fable/commit/50a37f78f31394d61349b09a025c1713529d8ec0)) + +### ⚡ Performance Improvements + +* *(js/ts)* Faster `resizeArray.Clear()` in JS/TS ([32d85add](https://github.com/fable-compiler/Fable/commit/32d85add01422547dba03b15e0a0787d8afb031b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + +## 3.4.0 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(python)* Preserve record field names in structured formatting (#4952) ([fbc162d6](https://github.com/fable-compiler/Fable/commit/fbc162d6912d62b9a75e73f8264814f6519b6194)) +* *(python)* Reflect an erased union as the type it erases to ([0f564c74](https://github.com/fable-compiler/Fable/commit/0f564c744e86096c779d0acea6d677bce6d56ee1)) +* *(ts)* Declare the type parameter of a flexible-type overload ([6f1f1b8a](https://github.com/fable-compiler/Fable/commit/6f1f1b8afd9158743978720507ced5c4a8b2144f)) + +### ⚡ Performance Improvements + +* *(standalone)* Accumulate compile errors in a ResizeArray ([f282ab48](https://github.com/fable-compiler/Fable/commit/f282ab48203fc5544c74b809f4396bb924724a2a)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + +## 3.3.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) +* *(python)* Build typed arrays from lists and sequences (#4944) ([5af37779](https://github.com/fable-compiler/Fable/commit/5af37779ce33c1e9036df7a9548830c4f914bdd5)) +* *(rust)* Fix IEnumerable casting, interop and type testing (#4946) ([93c2df5b](https://github.com/fable-compiler/Fable/commit/93c2df5ba2b3316893dc85dc4fd746a09a07bf39)) +* *(rust)* Restore missing bindings (#4947) ([2fb46a26](https://github.com/fable-compiler/Fable/commit/2fb46a269bc4827494cab878f6e3f0e1b4052035)) +* *(ts)* Declare local function type params only reachable via constraints (#4943) ([8aa6d3b9](https://github.com/fable-compiler/Fable/commit/8aa6d3b9e9f3c1dc013d67406878ea93637ad74c)) +* *(ts/python)* Erase byref from struct copy-and-update locals (#4945) ([19a6522e](https://github.com/fable-compiler/Fable/commit/19a6522ea517db53dabd5e221206849d382e1025)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + +## 3.2.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) +* *(rust)* Observables, Events and IEnumerable (PR 6 of 7 Rust improvements) (#4789) ([20175eea](https://github.com/fable-compiler/Fable/commit/20175eea841adf14ed57c0068458fad591c8d5f4)) +* *(rust)* System.Uri (PR 7 of 7 Rust improvements) (#4790) ([8a551bc9](https://github.com/fable-compiler/Fable/commit/8a551bc9b2433035fc704dffc639e5f031ca7593)) + +### 🐞 Bug Fixes + +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) +* *(all)* Allow private inline functions to use private values (#4915) ([b0740ba9](https://github.com/fable-compiler/Fable/commit/b0740ba95859a80e7add07f21e2fc81a31c00853)) +* *(python)* Emit Python names for float32 specials (#4938) ([b4c45a93](https://github.com/fable-compiler/Fable/commit/b4c45a93385f249a8586789037b0af18005786e9)) +* *(rust)* Observables and Events cleanup (#4908) ([ca165045](https://github.com/fable-compiler/Fable/commit/ca165045703797f0558da4bc6e06cbea7af231de)) +* *(rust)* Support `string` on a tuple (#4916) ([510c8a43](https://github.com/fable-compiler/Fable/commit/510c8a43c159ffb9d7d7bdc4e2ee16b84b80b28e)) +* *(rust)* Decimal culture overloads, and mutables assigned in try/finally (#4918) ([58402471](https://github.com/fable-compiler/Fable/commit/584024712450d333193baddc239db977af05bd54)) +* *(rust)* Let a string be enumerated (#4921) ([395c152b](https://github.com/fable-compiler/Fable/commit/395c152bd4ef0c26af7a4ff48ed44c728aa73d25)) +* *(rust)* F# exceptions, non-exhaustive matches and obj type tests (#4917) ([28da1d76](https://github.com/fable-compiler/Fable/commit/28da1d764e63c39b96ca20e94d9a08786b3c9052)) +* *(rust)* Cleanup AsyncBuilder and TaskBuilder (#4936) ([3e6bc9bd](https://github.com/fable-compiler/Fable/commit/3e6bc9bd4d4337e901cc748f35df2a5933f0ba55)) +* *(rust)* Fix interface casting, move reflection into declarations (#4939) ([04299157](https://github.com/fable-compiler/Fable/commit/042991572969e1864a160b0a0145b1d59bc77236)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b517d52df145ee30d1d3bd7e20fded1a9003ac54..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 3.1.0 - 2026-08-25 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 250004fe9f..80d3800b8b 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.1.0", - "fableVersion": "5.15.0", + "version": "4.0.0-beta.6", + "fableVersion": "5.27.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ diff --git a/src/fable-standalone/src/Fable.Standalone.fsproj b/src/fable-standalone/src/Fable.Standalone.fsproj index ec13e56eab..48622595c6 100644 --- a/src/fable-standalone/src/Fable.Standalone.fsproj +++ b/src/fable-standalone/src/Fable.Standalone.fsproj @@ -10,8 +10,8 @@ - - + + @@ -25,6 +25,8 @@ + + diff --git a/src/fable-standalone/src/Interfaces.fs b/src/fable-standalone/src/Interfaces.fs index 311e9ac28b..2e5a1760de 100644 --- a/src/fable-standalone/src/Interfaces.fs +++ b/src/fable-standalone/src/Interfaces.fs @@ -18,6 +18,13 @@ type Glyph = | Event | TypeParameter +[] +type DiagnosticSeverity = + | Hidden + | Info + | Warning + | Error + type Error = { FileName: string @@ -26,27 +33,199 @@ type Error = EndLine: int EndColumn: int Message: string - IsWarning: bool + Severity: DiagnosticSeverity + /// FCS error number such as "FS0039", or the Fable log tag such as "FABLE" + Code: string } type Range = { + FileName: string StartLine: int StartColumn: int EndLine: int EndColumn: int } +[] +type DeclarationLocation = + /// The file is the one FCS recorded for the symbol, so it can be a source outside the project + | Found of Range + /// A .NET symbol from a referenced assembly, e.g. `System.Math.Abs` + | External of assembly: string * symbol: string + | NotFound of reason: string + +[] +type CompletionKind = + | SuggestedName + | Field + | Property + | Method + | ExtensionMethod + | Event + | Argument + | CustomOperation + | Other + type Completion = { - Name: string + NameInList: string + /// Text to insert, e.g. with backticks for names that need them + NameInCode: string Glyph: Glyph + Kind: CompletionKind + FullName: string + IsOwnMember: bool + MinorPriority: int + } + +/// Mirrors FCS `FSharp.Compiler.Text.TextTag` +[] +type TextTag = + | ActivePatternCase + | ActivePatternResult + | Alias + | Class + | Union + | UnionCase + | Delegate + | Enum + | Event + | Field + | Interface + | Keyword + | LineBreak + | Local + | Record + | RecordField + | Method + | Member + | ModuleBinding + | Function + | Module + | Namespace + | NumericLiteral + | Operator + | Parameter + | Property + | Space + | StringLiteral + | Struct + | TypeParameter + | Text + | Punctuation + | UnknownType + | UnknownEntity + +type TaggedText = + { + Tag: TextTag + Text: string + } + +/// One signature with its documentation. Overloads come as separate tooltips. +type ToolTip = + { + ParamName: string option + Signature: TaggedText[] + /// XML doc lines, with the implicit `` already inserted + XmlDoc: string[] + TypeMapping: TaggedText[][] + Remarks: TaggedText[] + } + +type CompletionItem = + { + Completion: Completion + /// Formats the tooltips on every call, so only call it for the highlighted row + GetDescription: unit -> ToolTip[] + } + +[] +type TokenColor = + | Default + | Keyword + | Comment + | Identifier + | String + | UpperIdentifier + | InactiveCode + | PreprocessorKeyword + | Number + | Operator + | Punctuation + +/// A lexical token of one line. Columns are 0-based and the end is exclusive. +type Token = + { + StartColumn: int + EndColumn: int + Color: TokenColor + /// FCS token name such as "IDENT" or "STRING_TEXT" + Name: string + } + +/// Mirrors FCS `SemanticClassificationType` +[] +type SemanticClass = + | ReferenceType + | ValueType + | UnionCase + | UnionCaseField + | Function + | Property + | MutableVar + | Module + | Namespace + | Printf + | ComputationExpression + | IntrinsicFunction + | Enumeration + | Interface + | TypeArgument + | Operator + | DisposableType + | DisposableTopLevelValue + | DisposableLocalValue + | Method + | ExtensionMethod + | ConstructorForReferenceType + | ConstructorForValueType + | Literal + | RecordField + | MutableRecordField + | RecordFieldAsFunction + | Exception + | Field + | Event + | Delegate + | NamedArgument + | Value + | LocalValue + | Type + | TypeDef + | Plaintext + +type SemanticClassification = + { + Range: Range + Type: SemanticClass } type SourceMapping = int * int * int * int * string option type IChecker = interface end +/// Opaque FCS symbol, valid for the checker it came from +type ISymbol = + abstract DisplayName: string + +type SymbolUse = + { + Range: Range + IsDefinition: bool + } + /// Root modules and inline member bodies from a `fable precompile` output. type IPrecompiledInfo = /// Must be the name the precompiled assembly is referenced under, plus ".dll" @@ -75,6 +254,10 @@ type IWriter = abstract MakeImportPath: string -> string abstract Write: string -> Async +/// Positional members take a 1-based `line` and a 0-based `col`, the same convention as the +/// ranges they return. Any column from the first character of an identifier up to the position +/// just after its last character resolves that identifier. For completions, `col` is the cursor +/// position and the text before it is the prefix to complete. type IFableManager = abstract Version: string @@ -101,15 +284,23 @@ type IFableManager = ?precompiledInfo: IPrecompiledInfo -> IParseAndCheckResults - abstract GetErrors: parseResults: IParseAndCheckResults -> Error[] + abstract GetSymbolAtLocation: + parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> ISymbol option + + abstract GetUsesOfSymbolInFile: parseResults: IParseAndCheckResults * symbol: ISymbol -> SymbolUse[] abstract GetDeclarationLocation: - parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> Range option + parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> DeclarationLocation + + abstract GetToolTips: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> ToolTip[] + + /// Lexical tokens of every line of the source, with `#if` blocks resolved against the defines + abstract Tokenize: source: string * defines: string[] -> Token[][] - abstract GetToolTipText: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> string[] + abstract GetSemanticClassification: parseResults: IParseAndCheckResults -> SemanticClassification[] abstract GetCompletionsAtLocation: - parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> Completion[] + parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> CompletionItem[] abstract CompileToTargetAst: fableLibrary: string * diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index b020d37741..53a5d0229a 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -11,6 +11,7 @@ open FSharp.Compiler.Diagnostics open FSharp.Compiler.EditorServices open FSharp.Compiler.SourceCodeServices open FSharp.Compiler.Symbols +open FSharp.Compiler.Tokenization type CheckerImpl(checker: InteractiveChecker) = member _.Checker = checker @@ -24,17 +25,19 @@ let mapError (error: FSharpDiagnostic) = EndLine = error.EndLine EndColumn = error.EndColumn Message = error.Message - IsWarning = + Severity = match error.Severity with - | FSharpDiagnosticSeverity.Info - | FSharpDiagnosticSeverity.Hidden - | FSharpDiagnosticSeverity.Warning -> true - | FSharpDiagnosticSeverity.Error -> false + | FSharpDiagnosticSeverity.Hidden -> DiagnosticSeverity.Hidden + | FSharpDiagnosticSeverity.Info -> DiagnosticSeverity.Info + | FSharpDiagnosticSeverity.Warning -> DiagnosticSeverity.Warning + | FSharpDiagnosticSeverity.Error -> DiagnosticSeverity.Error + Code = error.ErrorNumberText } type ParseAndCheckResults ( project: Lazy, + projectLogs: ResizeArray, parseFileResultsOpt: FSharpParseFileResults option, checkFileResultsOpt: FSharpCheckFileResults option, checkProjectResults: FSharpCheckProjectResults, @@ -43,6 +46,12 @@ type ParseAndCheckResults = member _.GetProject() = project.Force() + + member _.TakeProjectLogs() = + let logs = projectLogs.ToArray() + projectLogs.Clear() + logs + member _.ParseFileResultsOpt = parseFileResultsOpt member _.CheckFileResultsOpt = checkFileResultsOpt member _.CheckProjectResults = checkProjectResults @@ -152,6 +161,7 @@ let makeCompiler fableLibrary typedArrays language fsharpOptions project fileNam ?typedArrays = typedArrays ) + // No `warningSuppression`: the REPL has no `SourceReader` to scan, so `// fable-disable` comments are a no-op here. CompilerImpl(fileName, project, options, fableLibrary) /// `fable precompile` sorts inline expressions by member name, splits them into chunks and records @@ -207,6 +217,7 @@ let makeProject (projectOptions: FSharpProjectOptions) (checkResults: FSharpCheckProjectResults) (precompiledInfo: IPrecompiledInfo option) + (logs: ResizeArray) = // let errors = com.GetFormattedLogs() |> Map.tryFind "error" // if errors.IsSome then failwith (errors.Value |> String.concat "\n") @@ -218,7 +229,7 @@ let makeProject else checkResults.AssemblyContents.ImplementationFiles - let addLog (severity: Fable.Severity) (msg: string) = () //TODO: log loading errors + let addLog (severity: Fable.Severity) (msg: string) = logs.Add(LogEntry.Make(severity, msg)) Project.From( projectOptions.ProjectFileName, @@ -241,8 +252,12 @@ let parseAndCheckProject let projectOptions = makeProjOptions projectFileName fileNames otherFSharpOptions - let project = lazy (makeProject projectOptions checkResults precompiledInfo) - ParseAndCheckResults(project, None, None, checkResults, otherFSharpOptions) + let projectLogs = ResizeArray() + + let project = + lazy (makeProject projectOptions checkResults precompiledInfo projectLogs) + + ParseAndCheckResults(project, projectLogs, None, None, checkResults, otherFSharpOptions) let parseAndCheckFileInProject (checker: InteractiveChecker) @@ -258,65 +273,255 @@ let parseAndCheckFileInProject let projectOptions = makeProjOptions projectFileName fileNames otherFSharpOptions - let project = lazy (makeProject projectOptions projectResults precompiledInfo) + let projectLogs = ResizeArray() + + let project = + lazy (makeProject projectOptions projectResults precompiledInfo projectLogs) + + ParseAndCheckResults(project, projectLogs, Some results, Some checkResults, projectResults, otherFSharpOptions) + +type private FcsTextTag = FSharp.Compiler.Text.TextTag + +let private toTextTag (tag: FcsTextTag) = + match tag with + | FcsTextTag.ActivePatternCase -> TextTag.ActivePatternCase + | FcsTextTag.ActivePatternResult -> TextTag.ActivePatternResult + | FcsTextTag.Alias -> TextTag.Alias + | FcsTextTag.Class -> TextTag.Class + | FcsTextTag.Union -> TextTag.Union + | FcsTextTag.UnionCase -> TextTag.UnionCase + | FcsTextTag.Delegate -> TextTag.Delegate + | FcsTextTag.Enum -> TextTag.Enum + | FcsTextTag.Event -> TextTag.Event + | FcsTextTag.Field -> TextTag.Field + | FcsTextTag.Interface -> TextTag.Interface + | FcsTextTag.Keyword -> TextTag.Keyword + | FcsTextTag.LineBreak -> TextTag.LineBreak + | FcsTextTag.Local -> TextTag.Local + | FcsTextTag.Record -> TextTag.Record + | FcsTextTag.RecordField -> TextTag.RecordField + | FcsTextTag.Method -> TextTag.Method + | FcsTextTag.Member -> TextTag.Member + | FcsTextTag.ModuleBinding -> TextTag.ModuleBinding + | FcsTextTag.Function -> TextTag.Function + | FcsTextTag.Module -> TextTag.Module + | FcsTextTag.Namespace -> TextTag.Namespace + | FcsTextTag.NumericLiteral -> TextTag.NumericLiteral + | FcsTextTag.Operator -> TextTag.Operator + | FcsTextTag.Parameter -> TextTag.Parameter + | FcsTextTag.Property -> TextTag.Property + | FcsTextTag.Space -> TextTag.Space + | FcsTextTag.StringLiteral -> TextTag.StringLiteral + | FcsTextTag.Struct -> TextTag.Struct + | FcsTextTag.TypeParameter -> TextTag.TypeParameter + | FcsTextTag.Text -> TextTag.Text + | FcsTextTag.Punctuation -> TextTag.Punctuation + | FcsTextTag.UnknownType -> TextTag.UnknownType + | FcsTextTag.UnknownEntity -> TextTag.UnknownEntity + +let private toTaggedText (tts: FSharp.Compiler.Text.TaggedText[]) : TaggedText[] = + tts + |> Array.map (fun x -> + { + Tag = toTextTag x.Tag + Text = x.Text + } + ) - ParseAndCheckResults(project, Some results, Some checkResults, projectResults, otherFSharpOptions) +let private xmlDocLines (xmlDoc: FSharpXmlDoc) = + match xmlDoc with + | FSharpXmlDoc.FromXmlText xmlDoc -> xmlDoc.GetElaboratedXmlLines() + | _ -> [||] -let tooltipToString (el: ToolTipElement) : string[] = - let dataToString (data: ToolTipElementData) = - let toString (tts: FSharp.Compiler.Text.TaggedText[]) = - tts |> Array.map (fun x -> x.Text) |> String.concat " " +let toToolTips (el: ToolTipElement) : Fable.Standalone.ToolTip[] = + let toToolTip (data: ToolTipElementData) : Fable.Standalone.ToolTip = + { + ParamName = data.ParamName + Signature = toTaggedText data.MainDescription + XmlDoc = xmlDocLines data.XmlDoc + TypeMapping = data.TypeMapping |> List.map toTaggedText |> List.toArray + Remarks = data.Remarks |> Option.map toTaggedText |> Option.defaultValue [||] + } + match el with + | ToolTipElement.None -> [||] + | ToolTipElement.Group(els) -> els |> List.map toToolTip |> List.toArray + | ToolTipElement.CompositionError err -> [| - match data.ParamName with - | Some x -> yield x + ": " - | None -> () - yield data.MainDescription |> toString - match data.XmlDoc with - | FSharp.Compiler.Symbols.FSharpXmlDoc.FromXmlText xmlDoc -> - yield! xmlDoc.UnprocessedLines - yield! xmlDoc.GetElaboratedXmlLines() - | _ -> () - yield! data.TypeMapping |> List.map toString - match data.Remarks with - | Some x -> yield x |> toString - | None -> () + { + ParamName = None + Signature = + [| + { + Tag = TextTag.Text + Text = err + } + |] + XmlDoc = [||] + TypeMapping = [||] + Remarks = [||] + } |] - match el with - | ToolTipElement.None -> [||] - | ToolTipElement.Group(els) -> Seq.map dataToString els |> Array.concat - | ToolTipElement.CompositionError err -> [| err |] +let private declarationNotFoundReason (reason: FindDeclFailureReason) = + match reason with + | FindDeclFailureReason.Unknown message -> message + | FindDeclFailureReason.NoSourceCode -> "no source code" + | FindDeclFailureReason.ProvidedType typeName -> "provided type " + typeName + | FindDeclFailureReason.ProvidedMember memberName -> "provided member " + memberName -/// Get tool tip at the specified location -let getDeclarationLocation (results: ParseAndCheckResults) line col lineText = +let private toRange (range: FSharp.Compiler.Text.range) : Range = + { + FileName = range.FileName + StartLine = range.StartLine + StartColumn = range.StartColumn + EndLine = range.EndLine + EndColumn = range.EndColumn + } + +type private SymbolImpl(symbol: FSharpSymbol) = + member _.Symbol = symbol + + interface ISymbol with + member _.DisplayName = symbol.DisplayName + +let getSymbolAtLocation (results: ParseAndCheckResults) line col lineText = match results.CheckFileResultsOpt with | Some checkFile -> - match findLongIdents (col - 1, lineText) with + match findLongIdents (col, lineText) with | None -> None + | Some(col, identIsland) -> + checkFile.GetSymbolUseAtLocation(line, col, lineText, identIsland) + |> Option.map (fun symbolUse -> SymbolImpl(symbolUse.Symbol) :> ISymbol) + | None -> None + +let getUsesOfSymbolInFile (results: ParseAndCheckResults) (symbol: ISymbol) = + match results.CheckFileResultsOpt, symbol with + | Some checkFile, (:? SymbolImpl as symbol) -> + checkFile.GetUsesOfSymbolInFile(symbol.Symbol) + |> Array.map (fun symbolUse -> + { + Range = toRange symbolUse.Range + IsDefinition = symbolUse.IsFromDefinition + } + ) + | _ -> [||] + +/// Get the declaration location of the symbol at the specified location +let getDeclarationLocation (results: ParseAndCheckResults) line col lineText = + match results.CheckFileResultsOpt with + | Some checkFile -> + match findLongIdents (col, lineText) with + | None -> DeclarationLocation.NotFound "no identifier at location" | Some(col, identIsland) -> let (declarations: FindDeclResult) = checkFile.GetDeclarationLocation(line, col, lineText, identIsland) match declarations with - | FindDeclResult.DeclNotFound _ - | FindDeclResult.ExternalDecl _ -> None - | FindDeclResult.DeclFound range -> - Some + | FindDeclResult.DeclNotFound reason -> DeclarationLocation.NotFound(declarationNotFoundReason reason) + | FindDeclResult.ExternalDecl(assembly, externalSym) -> + DeclarationLocation.External(assembly, string externalSym) + | FindDeclResult.DeclFound range -> DeclarationLocation.Found(toRange range) + | None -> DeclarationLocation.NotFound "file not checked" + +let private toTokenColor (color: FSharpTokenColorKind) = + match color with + | FSharpTokenColorKind.Keyword -> TokenColor.Keyword + | FSharpTokenColorKind.Comment -> TokenColor.Comment + | FSharpTokenColorKind.Identifier -> TokenColor.Identifier + | FSharpTokenColorKind.String -> TokenColor.String + | FSharpTokenColorKind.UpperIdentifier -> TokenColor.UpperIdentifier + | FSharpTokenColorKind.InactiveCode -> TokenColor.InactiveCode + | FSharpTokenColorKind.PreprocessorKeyword -> TokenColor.PreprocessorKeyword + | FSharpTokenColorKind.Number -> TokenColor.Number + | FSharpTokenColorKind.Operator -> TokenColor.Operator + | FSharpTokenColorKind.Punctuation -> TokenColor.Punctuation + | _ -> TokenColor.Default + +let tokenize (source: string) (defines: string[]) : Token[][] = + let tokenizer = FSharpSourceTokenizer(List.ofArray defines, None, None, None) + + let tokenizeLine (state: FSharpTokenizerLexState) (line: string) = + let lineTokenizer = tokenizer.CreateLineTokenizer(line.TrimEnd('\r')) + + let rec loop state acc = + match lineTokenizer.ScanToken state with + | Some token, state -> + let token: Token = { - StartLine = range.StartLine - StartColumn = range.StartColumn - EndLine = range.EndLine - EndColumn = range.EndColumn + StartColumn = token.LeftColumn + EndColumn = token.RightColumn + 1 + Color = toTokenColor token.ColorClass + Name = token.TokenName } - | None -> None + + loop state (token :: acc) + | None, state -> List.rev acc |> List.toArray, state + + loop state [] + + source.Split('\n') + |> Array.mapFold tokenizeLine FSharpTokenizerLexState.Initial + |> fst + +let private toSemanticClass (classification: SemanticClassificationType) = + match classification with + | SemanticClassificationType.ReferenceType -> SemanticClass.ReferenceType + | SemanticClassificationType.ValueType -> SemanticClass.ValueType + | SemanticClassificationType.UnionCase -> SemanticClass.UnionCase + | SemanticClassificationType.UnionCaseField -> SemanticClass.UnionCaseField + | SemanticClassificationType.Function -> SemanticClass.Function + | SemanticClassificationType.Property -> SemanticClass.Property + | SemanticClassificationType.MutableVar -> SemanticClass.MutableVar + | SemanticClassificationType.Module -> SemanticClass.Module + | SemanticClassificationType.Namespace -> SemanticClass.Namespace + | SemanticClassificationType.Printf -> SemanticClass.Printf + | SemanticClassificationType.ComputationExpression -> SemanticClass.ComputationExpression + | SemanticClassificationType.IntrinsicFunction -> SemanticClass.IntrinsicFunction + | SemanticClassificationType.Enumeration -> SemanticClass.Enumeration + | SemanticClassificationType.Interface -> SemanticClass.Interface + | SemanticClassificationType.TypeArgument -> SemanticClass.TypeArgument + | SemanticClassificationType.Operator -> SemanticClass.Operator + | SemanticClassificationType.DisposableType -> SemanticClass.DisposableType + | SemanticClassificationType.DisposableTopLevelValue -> SemanticClass.DisposableTopLevelValue + | SemanticClassificationType.DisposableLocalValue -> SemanticClass.DisposableLocalValue + | SemanticClassificationType.Method -> SemanticClass.Method + | SemanticClassificationType.ExtensionMethod -> SemanticClass.ExtensionMethod + | SemanticClassificationType.ConstructorForReferenceType -> SemanticClass.ConstructorForReferenceType + | SemanticClassificationType.ConstructorForValueType -> SemanticClass.ConstructorForValueType + | SemanticClassificationType.Literal -> SemanticClass.Literal + | SemanticClassificationType.RecordField -> SemanticClass.RecordField + | SemanticClassificationType.MutableRecordField -> SemanticClass.MutableRecordField + | SemanticClassificationType.RecordFieldAsFunction -> SemanticClass.RecordFieldAsFunction + | SemanticClassificationType.Exception -> SemanticClass.Exception + | SemanticClassificationType.Field -> SemanticClass.Field + | SemanticClassificationType.Event -> SemanticClass.Event + | SemanticClassificationType.Delegate -> SemanticClass.Delegate + | SemanticClassificationType.NamedArgument -> SemanticClass.NamedArgument + | SemanticClassificationType.Value -> SemanticClass.Value + | SemanticClassificationType.LocalValue -> SemanticClass.LocalValue + | SemanticClassificationType.Type -> SemanticClass.Type + | SemanticClassificationType.TypeDef -> SemanticClass.TypeDef + | _ -> SemanticClass.Plaintext + +let getSemanticClassification (results: ParseAndCheckResults) : SemanticClassification[] = + match results.CheckFileResultsOpt with + | Some checkFile -> + checkFile.GetSemanticClassification(None) + |> Array.map (fun item -> + { + Range = toRange item.Range + Type = toSemanticClass item.Type + } + ) + | None -> [||] /// Get tool tip at the specified location let getToolTipAtLocation (results: ParseAndCheckResults) line col lineText = match results.CheckFileResultsOpt with | Some checkFile -> - match findLongIdents (col - 1, lineText) with - | None -> [| "Cannot find ident for tooltip" |] + match findLongIdents (col, lineText) with + | None -> [||] | Some(col, identIsland) -> let (ToolTipText els) = checkFile.GetToolTip( @@ -327,14 +532,44 @@ let getToolTipAtLocation (results: ParseAndCheckResults) line col lineText = FSharp.Compiler.Tokenization.FSharpTokenTag.IDENT ) - Seq.map tooltipToString els |> Array.concat + List.map toToolTips els |> Array.concat | None -> [||] +let private toCompletionKind (kind: CompletionItemKind) = + match kind with + | CompletionItemKind.SuggestedName -> CompletionKind.SuggestedName + | CompletionItemKind.Field -> CompletionKind.Field + | CompletionItemKind.Property -> CompletionKind.Property + | CompletionItemKind.Method false -> CompletionKind.Method + | CompletionItemKind.Method true -> CompletionKind.ExtensionMethod + | CompletionItemKind.Event -> CompletionKind.Event + | CompletionItemKind.Argument -> CompletionKind.Argument + | CompletionItemKind.CustomOperation -> CompletionKind.CustomOperation + | CompletionItemKind.Other -> CompletionKind.Other + +let private toCompletionItem (decl: DeclarationListItem) : Fable.Standalone.CompletionItem = + { + Completion = + { + NameInList = decl.NameInList + NameInCode = decl.NameInCode + Glyph = convertGlyph decl.Glyph + Kind = toCompletionKind decl.Kind + FullName = decl.FullName + IsOwnMember = decl.IsOwnMember + MinorPriority = decl.MinorPriority + } + GetDescription = + fun () -> + let (ToolTipText els) = decl.Description + List.map toToolTips els |> Array.concat + } + let getCompletionsAtLocation (results: ParseAndCheckResults) (line: int) (col: int) lineText = match results.CheckFileResultsOpt with | Some checkFile -> - let ln, residue = findLongIdentsAndResidue (col - 1, lineText) - let longName = QuickParse.GetPartialLongNameEx(lineText, col - 1) + let ln, residue = findLongIdentsAndResidue (col, lineText) + let longName = QuickParse.GetPartialLongNameEx(lineText, col) let longName = { longName with @@ -345,13 +580,7 @@ let getCompletionsAtLocation (results: ParseAndCheckResults) (line: int) (col: i let decls = checkFile.GetDeclarationListInfo(results.ParseFileResultsOpt, line, lineText, longName, (fun () -> [])) - decls.Items - |> Array.map (fun decl -> - { - Name = decl.NameInList - Glyph = convertGlyph decl.Glyph - } - ) + decls.Items |> Array.map toCompletionItem | None -> [||] let mapFableError (com: Compiler) (log: LogEntry) = @@ -363,16 +592,13 @@ let mapFableError (com: Compiler) (log: LogEntry) = StartColumn = r.start.column EndLine = r.``end``.line EndColumn = r.``end``.column - Message = - if log.Tag = "FABLE" then - "FABLE: " + log.Message - else - log.Message - IsWarning = + Message = log.Message + Severity = match log.Severity with - | Fable.Severity.Error -> false - | Fable.Severity.Warning - | Fable.Severity.Info -> true + | Fable.Severity.Info -> DiagnosticSeverity.Info + | Fable.Severity.Warning -> DiagnosticSeverity.Warning + | Fable.Severity.Error -> DiagnosticSeverity.Error + Code = log.Tag } type BabelResult(ast: Babel.Program, errors) = @@ -449,6 +675,8 @@ let transformToTargetAst (com: CompilerImpl) (fableAst: Fable.File) : IFableResu upcast RustResult(ast, errors) | Beam -> let ast = Fable.Transforms.Beam.Compiler.transformFile com fableAst + // Naming diagnostics are raised while constructing the target AST, after the earlier snapshot. + let errors = com.Logs |> Array.map (mapFableError com) upcast BeamResult(ast, errors) let compileToTargetAst (results: IParseAndCheckResults) fileName fableLibrary typedArrays language : IFableResult = @@ -458,6 +686,9 @@ let compileToTargetAst (results: IParseAndCheckResults) fileName fableLibrary ty let com = makeCompiler fableLibrary typedArrays language results.OtherFSharpOptions project fileName + for log in res.TakeProjectLogs() do + (com :> Compiler).AddLog(log.Message, log.Severity, tag = log.Tag) + let fableAst = transformToFableAst com fableAst |> transformToTargetAst com @@ -527,16 +758,28 @@ let init () = precompiledInfo :> IParseAndCheckResults - member _.GetErrors(results: IParseAndCheckResults) = results.Errors + member _.GetSymbolAtLocation(results: IParseAndCheckResults, line: int, col: int, lineText: string) = + let res = results :?> ParseAndCheckResults + getSymbolAtLocation res line col lineText + + member _.GetUsesOfSymbolInFile(results: IParseAndCheckResults, symbol: ISymbol) = + let res = results :?> ParseAndCheckResults + getUsesOfSymbolInFile res symbol member _.GetDeclarationLocation(results: IParseAndCheckResults, line: int, col: int, lineText: string) = let res = results :?> ParseAndCheckResults getDeclarationLocation res line col lineText - member _.GetToolTipText(results: IParseAndCheckResults, line: int, col: int, lineText: string) = + member _.GetToolTips(results: IParseAndCheckResults, line: int, col: int, lineText: string) = let res = results :?> ParseAndCheckResults getToolTipAtLocation res line col lineText + member _.Tokenize(source: string, defines: string[]) = tokenize source defines + + member _.GetSemanticClassification(results: IParseAndCheckResults) = + let res = results :?> ParseAndCheckResults + getSemanticClassification res + member _.GetCompletionsAtLocation(results: IParseAndCheckResults, line: int, col: int, lineText: string) = let res = results :?> ParseAndCheckResults getCompletionsAtLocation res line col lineText diff --git a/src/fable-standalone/src/Worker/Shared.fs b/src/fable-standalone/src/Worker/Shared.fs index b0b2e3a164..4ba682e75e 100644 --- a/src/fable-standalone/src/Worker/Shared.fs +++ b/src/fable-standalone/src/Worker/Shared.fs @@ -49,14 +49,21 @@ type WorkerRequest = filesToEmit: string[] * language: string * otherFSharpOptions: string[] + | Tokenize of id: Guid * source: string * defines: string[] + | GetSemanticClassification of id: Guid + | GetSemanticClassificationForFile of id: Guid * file: string | GetTooltip of id: Guid * line: int * column: int * lineText: string | GetCompletions of id: Guid * line: int * column: int * lineText: string + /// Tooltips of an item of the last FoundCompletions answer + | GetCompletionDescription of id: Guid * index: int | GetDeclarationLocation of id: Guid * line: int * column: int * lineText: string + | GetSymbolUses of id: Guid * line: int * column: int * lineText: string | GetTooltipForFile of id: Guid * file: string * line: int * column: int * lineText: string | GetCompletionsForFile of id: Guid * file: string * line: int * column: int * lineText: string | GetDeclarationLocationForFile of id: Guid * file: string * line: int * column: int * lineText: string + | GetSymbolUsesForFile of id: Guid * file: string * line: int * column: int * lineText: string - static member Decoder = Decode.Auto.generateDecoder () + static member Decoder = Decode.Auto.generateDecoder() type CompileStats = { @@ -72,11 +79,15 @@ type WorkerAnswer = | CompilationFinished of code: string * language: string * errors: Fable.Standalone.Error[] * stats: CompileStats | CompilationsFinished of code: string[] * language: string * errors: Fable.Standalone.Error[] * stats: CompileStats | CompilerCrashed of message: string - | FoundTooltip of id: Guid * lines: string[] + | Tokenized of id: Guid * lines: Fable.Standalone.Token[][] + | FoundSemanticClassification of id: Guid * items: Fable.Standalone.SemanticClassification[] + | FoundTooltip of id: Guid * tooltips: Fable.Standalone.ToolTip[] | FoundCompletions of id: Guid * Fable.Standalone.Completion[] - | FoundDeclarationLocation of id: Guid (* line1, col1, line2, col2 *) * (int * int * int * int) option + | FoundCompletionDescription of id: Guid * tooltips: Fable.Standalone.ToolTip[] + | FoundDeclarationLocation of id: Guid * Fable.Standalone.DeclarationLocation + | FoundSymbolUses of id: Guid * uses: Fable.Standalone.SymbolUse[] - static member Decoder = Decode.Auto.generateDecoder () + static member Decoder = Decode.Auto.generateDecoder() type ObservableWorker<'InMsg>(worker: obj, decoder: Decoder<'InMsg>, ?name: string) = let name = defaultArg name "FABLE WORKER" diff --git a/src/fable-standalone/src/Worker/Worker.fs b/src/fable-standalone/src/Worker/Worker.fs index 56451a9d8c..99d4336f81 100644 --- a/src/fable-standalone/src/Worker/Worker.fs +++ b/src/fable-standalone/src/Worker/Worker.fs @@ -29,9 +29,9 @@ let getAssemblyReader (_getBlobUrl: string -> string, _refs: string[]) : JS.Prom let escapeJsStringLiteral (str: string) : string = importMember "./util.js" let measureTime f arg = - let before: float = self?performance?now () + let before: float = self?performance?now() let res = f arg - let after: float = self?performance?now () + let after: float = self?performance?now() res, after - before type PrecompiledState = @@ -65,6 +65,7 @@ type State = Fable: FableState option Worker: ObservableWorker CurrentResults: Map + CurrentCompletions: CompletionItem[] } /// Map a REPL language string to its fable-library directory name (used as the @@ -246,7 +247,10 @@ let private compileCode fable fileName fsharpNames fsharpCodes language otherFSh let! compiledCode, errors, fableTransformTime = async { - if parseResults.Errors |> Array.exists (fun e -> not e.IsWarning) then + if + parseResults.Errors + |> Array.exists (fun e -> e.Severity = DiagnosticSeverity.Error) + then return "", parseResults.Errors, 0. else let! code, fableErrors, fableTransformTime = @@ -285,16 +289,22 @@ let private compileFiles fable fsharpNames fsharpCodes (filesToEmit: string[]) l ) () - let mutable errors = parseResults.Errors + let errors = ResizeArray parseResults.Errors let mutable fableTransformTime = 0. let compiledCode = ResizeArray() - if not (parseResults.Errors |> Array.exists (fun e -> not e.IsWarning)) then + if + not ( + parseResults.Errors + |> Array.exists (fun e -> e.Severity = DiagnosticSeverity.Error) + ) + then for fileName in filesToEmit do - let! code, fableErrors, transformTime = emitFile fable parseResults fileName language fableOptions + let! code, fableErrors, transformTime = + emitFile fable parseResults fileName language fableOptions - compiledCode.Add(code) - errors <- Array.append errors fableErrors + compiledCode.Add code + errors.AddRange fableErrors fableTransformTime <- fableTransformTime + transformTime let stats: CompileStats = @@ -304,7 +314,7 @@ let private compileFiles fable fsharpNames fsharpCodes (filesToEmit: string[]) l Fable_transform = fableTransformTime } - return (compiledCode.ToArray(), errors, stats) + return (compiledCode.ToArray(), errors.ToArray(), stats) } let private describeError (er: exn) = @@ -318,6 +328,14 @@ let private truncate (s: string) = else s +let private symbolUsesAt (fable: FableState) (results: IParseAndCheckResults option) line col lineText = + match results with + | None -> [||] + | Some res -> + match fable.Manager.GetSymbolAtLocation(res, line, col, lineText) with + | None -> [||] + | Some symbol -> fable.Manager.GetUsesOfSymbolInFile(res, symbol) + let rec loop (box: MailboxProcessor) (state: State) = async { @@ -419,7 +437,8 @@ let rec loop (box: MailboxProcessor) (state: State) = else filesToEmit - let! (code, errors, stats) = compileFiles fable names codes filesToEmit language otherFSharpOptions + let! (code, errors, stats) = + compileFiles fable names codes filesToEmit language otherFSharpOptions CompilationsFinished(code, language, errors, stats) |> state.Worker.Post with er -> @@ -428,65 +447,107 @@ let rec loop (box: MailboxProcessor) (state: State) = return! loop box state + | Some fable, Tokenize(id, source, defines) -> + Tokenized(id, fable.Manager.Tokenize(source, defines)) |> state.Worker.Post + return! loop box state + + | Some fable, GetSemanticClassification id -> + let items = + match FILE_NAME |> state.CurrentResults.TryFind with + | None -> [||] + | Some res -> fable.Manager.GetSemanticClassification(res) + + FoundSemanticClassification(id, items) |> state.Worker.Post + return! loop box state + + | Some fable, GetSemanticClassificationForFile(id, file) -> + let items = + match file |> state.CurrentResults.TryFind with + | None -> [||] + | Some res -> fable.Manager.GetSemanticClassification(res) + + FoundSemanticClassification(id, items) |> state.Worker.Post + return! loop box state + | Some fable, GetTooltip(id, line, col, lineText) -> - let tooltipLines = + let tooltips = match FILE_NAME |> state.CurrentResults.TryFind with | None -> [||] - | Some res -> fable.Manager.GetToolTipText(res, int line, int col, lineText) + | Some res -> fable.Manager.GetToolTips(res, int line, int col, lineText) - FoundTooltip(id, tooltipLines) |> state.Worker.Post + FoundTooltip(id, tooltips) |> state.Worker.Post return! loop box state | Some fable, GetCompletions(id, line, col, lineText) -> - let completions = + let items = match FILE_NAME |> state.CurrentResults.TryFind with | None -> [||] | Some res -> fable.Manager.GetCompletionsAtLocation(res, int line, int col, lineText) - FoundCompletions(id, completions) |> state.Worker.Post + FoundCompletions(id, items |> Array.map _.Completion) |> state.Worker.Post + + return! loop box { state with CurrentCompletions = items } + + | Some fable, GetSymbolUses(id, line, col, lineText) -> + let uses = + symbolUsesAt fable (state.CurrentResults.TryFind FILE_NAME) (int line) (int col) lineText + + FoundSymbolUses(id, uses) |> state.Worker.Post + return! loop box state + + | Some _, GetCompletionDescription(id, index) -> + let tooltips = + match Array.tryItem index state.CurrentCompletions with + | None -> [||] + | Some item -> item.GetDescription() + + FoundCompletionDescription(id, tooltips) |> state.Worker.Post return! loop box state | Some fable, GetDeclarationLocation(id, line, col, lineText) -> - let result = + let location = match FILE_NAME |> state.CurrentResults.TryFind with - | None -> None + | None -> DeclarationLocation.NotFound "file not parsed" | Some res -> fable.Manager.GetDeclarationLocation(res, int line, int col, lineText) - match result with - | Some x -> FoundDeclarationLocation(id, Some(x.StartLine, x.StartColumn, x.EndLine, x.EndColumn)) - | None -> FoundDeclarationLocation(id, None) - |> state.Worker.Post + FoundDeclarationLocation(id, location) |> state.Worker.Post return! loop box state | Some fable, GetTooltipForFile(id, file, line, col, lineText) -> - let tooltipLines = + let tooltips = match file |> state.CurrentResults.TryFind with | None -> [||] - | Some res -> fable.Manager.GetToolTipText(res, int line, int col, lineText) + | Some res -> fable.Manager.GetToolTips(res, int line, int col, lineText) - FoundTooltip(id, tooltipLines) |> state.Worker.Post + FoundTooltip(id, tooltips) |> state.Worker.Post return! loop box state | Some fable, GetCompletionsForFile(id, file, line, col, lineText) -> - let completions = + let items = match file |> state.CurrentResults.TryFind with | None -> [||] | Some res -> fable.Manager.GetCompletionsAtLocation(res, int line, int col, lineText) - FoundCompletions(id, completions) |> state.Worker.Post + FoundCompletions(id, items |> Array.map (fun item -> item.Completion)) + |> state.Worker.Post + + return! loop box { state with CurrentCompletions = items } + + | Some fable, GetSymbolUsesForFile(id, file, line, col, lineText) -> + let uses = + symbolUsesAt fable (state.CurrentResults.TryFind file) (int line) (int col) lineText + + FoundSymbolUses(id, uses) |> state.Worker.Post return! loop box state | Some fable, GetDeclarationLocationForFile(id, file, line, col, lineText) -> - let result = + let location = match file |> state.CurrentResults.TryFind with - | None -> None + | None -> DeclarationLocation.NotFound "file not parsed" | Some res -> fable.Manager.GetDeclarationLocation(res, int line, int col, lineText) - match result with - | Some x -> FoundDeclarationLocation(id, Some(x.StartLine, x.StartColumn, x.EndLine, x.EndColumn)) - | None -> FoundDeclarationLocation(id, None) - |> state.Worker.Post + FoundDeclarationLocation(id, location) |> state.Worker.Post return! loop box state } @@ -499,6 +560,7 @@ let box = Fable = None Worker = worker CurrentResults = Map.empty + CurrentCompletions = [||] } |> loop box ) diff --git a/src/fable-standalone/src/Worker/Worker.fsproj b/src/fable-standalone/src/Worker/Worker.fsproj index 6db8372780..be425edf44 100644 --- a/src/fable-standalone/src/Worker/Worker.fsproj +++ b/src/fable-standalone/src/Worker/Worker.fsproj @@ -11,7 +11,7 @@ - - + + diff --git a/src/fable-standalone/test/bench-compiler/app.fs b/src/fable-standalone/test/bench-compiler/app.fs index f5997ee673..9e95c18c0f 100644 --- a/src/fable-standalone/test/bench-compiler/app.fs +++ b/src/fable-standalone/test/bench-compiler/app.fs @@ -147,7 +147,7 @@ type SourceWriter(sourcePath, targetPath, projDir, options: CmdLineOptions, file member _.Dispose() = () - member _.SourceMap = mapGenerator.Force().toJSON () + member _.SourceMap = mapGenerator.Force().toJSON() member _.Result = sb.ToString() let printErrors showWarnings (errors: Fable.Standalone.Error[]) = diff --git a/src/fable-standalone/test/worker/README.md b/src/fable-standalone/test/worker/README.md index 97fdb5d269..532d1cf849 100644 --- a/src/fable-standalone/test/worker/README.md +++ b/src/fable-standalone/test/worker/README.md @@ -24,6 +24,15 @@ The fixture is generated, so nothing is downloaded. ## What it checks +It hovers a library function and a documented user function, and checks that the tooltip signature +has no stray spaces and keeps the FCS tags and the XML doc. It then asks for the declaration of a +project symbol, an FSharp.Core symbol and a .NET symbol, and checks that the three stay apart. +Finally it asks for every use of a project symbol and checks that the definition is marked. +It also lists completions and fetches the description of one of them by index. +The parsed fixture carries one warning and one error, checked for their FCS code and severity. +Finally it tokenizes a snippet with a multi-line string and an inactive `#if` block, and asks for the +semantic classification of the parsed fixture. + Beyond "the compile finished and returned a module per file", it measures the **stack headroom the compiler is handed for each file** and fails if it drifts between files. diff --git a/src/fable-standalone/test/worker/run.mjs b/src/fable-standalone/test/worker/run.mjs index 1696641081..ce2416cadb 100644 --- a/src/fable-standalone/test/worker/run.mjs +++ b/src/fable-standalone/test/worker/run.mjs @@ -114,7 +114,7 @@ console.log(` ${modules.length} modules in ${((Date.now() - started) / 1000).to const failures = [] if (modules.length !== fixture.length) failures.push(`expected ${fixture.length} modules, got ${modules.length}`) -const hard = (errors ?? []).filter((e) => !e.IsWarning) +const hard = (errors ?? []).filter((e) => e.Severity === "error") if (hard.length) failures.push(`${hard.length} compile error(s), first: ${hard[0].Message}`) // V8 caps the stack in bytes, so a frame count is only comparable against another count from this @@ -137,11 +137,153 @@ console.log(`re-emit ${last} only: ${subset.length} module ` + if (subset.length !== 1) failures.push(`expected 1 module for a single-file emit, got ${subset.length}`) if (subset[0] !== modules[modules.length - 1]) failures.push(`re-emitting ${last} produced different output`) -if ((subsetErrors ?? []).filter((e) => !e.IsWarning).length) failures.push("subset emit reported errors") +if ((subsetErrors ?? []).filter((e) => e.Severity === "error").length) failures.push("subset emit reported errors") if (subsetStats.Fable_transform >= stats.Fable_transform) { failures.push(`emitting 1 of ${fixture.length} files took as long as emitting all of them`) } +// BEAM names in the web compiler use its existing hash, with no cryptography metadata. +const longFunction = "long" + "x".repeat(230) +post(["CompileFiles", [{ Name: "Program.fs", Content: `module Demo\nlet ${longFunction} () = 42` }], [], "beam", []]) +const [, beamModules, , beamErrors] = await answer("CompilationsFinished") +const beamHard = (beamErrors ?? []).filter((e) => e.Severity === "error") +if (beamHard.length) failures.push(`BEAM example failed: ${beamHard[0].Message}`) +const beamCode = beamModules.join("\n") +if (!/longx+_[0-9a-f]{8}\(/.test(beamCode)) { + failures.push("standalone BEAM example did not use the existing 32-bit hash") +} + +// A collision in a state key must be diagnosed even if the unprefixed names remain distinct. +const mutablePrefix = "a".repeat(220) +post(["CompileFiles", [{ Name: "Program.fs", Content: [ + "module Demo", + `let mutable ${mutablePrefix}a2 = 1`, + `let mutable ${mutablePrefix}cp = 2`, + `let read () = ${mutablePrefix}a2, ${mutablePrefix}cp`, +].join("\n") }], [], "beam", []]) +const [, , , collisionErrors] = await answer("CompilationsFinished") +if (!(collisionErrors ?? []).some((e) => e.Severity === "error" && e.Message.includes("share Erlang state key"))) { + failures.push("standalone BEAM did not diagnose colliding mutable state keys") +} +console.log(" BEAM example hashing and state-key diagnostics checked") + +// Tooltips: FCS tagged text already carries its own spaces and punctuation, so the joined text must +// not gain any, and the tags and the XML doc must come through separately. +const greetLine = `let greet (name: string) = sprintf "Hello %s" name` +const runLine = `let run () = greet "world"` +const absLine = `let abs = System.Math.Abs(-1)` +const greLine = `let g = gre` +const genericLine = `let addOne (x: 'a) = x + 1` +const undefinedLine = `let bad = undefinedThing` +post(["ParseFile", "Program.fs", [{ Name: "Program.fs", Content: ["module Demo", "/// Says hello.", greetLine, runLine, absLine, greLine, genericLine, undefinedLine].join("\n") }], []]) +const [, parsedErrors] = await answer("ParsedCode") + +// Diagnostics: the FCS error number and the real severity must come through. +const diagnostics = parsedErrors.map((e) => `${e.Code}:${e.Severity}@${e.StartLine}`) +const fixtureLines = 6 +if (!parsedErrors.some((e) => e.Code === "FS0064" && e.Severity === "warning" && e.StartLine === fixtureLines + 1)) { + failures.push(`expected the FS0064 warning: ${diagnostics.join(", ")}`) +} +if (!parsedErrors.some((e) => e.Code === "FS0039" && e.Severity === "error" && e.StartLine === fixtureLines + 2)) { + failures.push(`expected the FS0039 error: ${diagnostics.join(", ")}`) +} +console.log(` diagnostics: ${diagnostics.join(", ")}`) + +const signatureOf = (tooltips) => (tooltips[0]?.Signature ?? []).map((t) => t.Text).join("") + +// Columns are 0-based: the first character of an identifier and the position just after its last +// character must both resolve it. +post(["GetTooltipForFile", "00000000-0000-0000-0000-000000000001", "Program.fs", 3, 34, greetLine]) +const [, , sprintfTooltip] = await answer("FoundTooltip") +const sprintfSignature = signatureOf(sprintfTooltip) +if (!sprintfSignature.includes("Printf.StringFormat<")) failures.push(`tooltip has stray spaces: ${sprintfSignature}`) +console.log(` tooltip: ${sprintfSignature}`) + +post(["GetTooltipForFile", "00000000-0000-0000-0000-000000000002", "Program.fs", 4, 13, runLine]) +const [, , greetTooltip] = await answer("FoundTooltip") +const signature = greetTooltip[0]?.Signature ?? [] +const greetSignature = signatureOf(greetTooltip) +if (!greetSignature.startsWith("val greet:")) failures.push(`unexpected tooltip signature: ${greetSignature}`) +if (!signature.some((t) => t.Tag === "keyword") || !signature.some((t) => t.Tag === "punctuation")) { + failures.push(`tooltip lost its tags: ${JSON.stringify(signature.slice(0, 4))}`) +} +if (!greetTooltip[0]?.XmlDoc?.includes("")) failures.push(`tooltip lost the XML doc: ${JSON.stringify(greetTooltip[0]?.XmlDoc)}`) +console.log(` tooltip: ${greetSignature} (${signature.length} tagged parts, ${greetTooltip[0]?.XmlDoc?.length ?? 0} doc lines)`) + +// Declaration locations: a project symbol, an FSharp.Core symbol (whose recorded file is not in the +// project) and a .NET symbol must stay distinguishable. +const declarationOf = async (id, line, col, lineText) => { + post(["GetDeclarationLocationForFile", id, "Program.fs", line, col, lineText]) + const [, , location] = await answer("FoundDeclarationLocation") + return location +} +const greetDecl = await declarationOf("00000000-0000-0000-0000-000000000003", 4, 13, runLine) +const sprintfDecl = await declarationOf("00000000-0000-0000-0000-000000000004", 3, 34, greetLine) +const absDecl = await declarationOf("00000000-0000-0000-0000-000000000005", 5, 22, absLine) +if (greetDecl[0] !== "Found" || greetDecl[1].FileName !== "Program.fs" || greetDecl[1].StartLine !== 3) { + failures.push(`greet should be found in Program.fs: ${JSON.stringify(greetDecl)}`) +} +if (sprintfDecl[0] !== "Found" || sprintfDecl[1].FileName === "Program.fs") { + failures.push(`sprintf should be found in an FSharp.Core source: ${JSON.stringify(sprintfDecl)}`) +} +if (absDecl[0] !== "External" || absDecl[2] !== "System.Math.Abs(System.Int32)") { + failures.push(`Math.Abs should be an external .NET symbol: ${JSON.stringify(absDecl)}`) +} +console.log(` declarations: greet in ${greetDecl[1]?.FileName}, sprintf in ${sprintfDecl[1]?.FileName}, Math.Abs in ${absDecl[1]}`) + +// Symbol uses: the definition of greet and its use must both come back, and nothing else. +post(["GetSymbolUsesForFile", "00000000-0000-0000-0000-000000000006", "Program.fs", 4, 13, runLine]) +const [, , greetUses] = await answer("FoundSymbolUses") +const usesText = greetUses.map((u) => `${u.Range.FileName}:${u.Range.StartLine}:${u.Range.StartColumn}-${u.Range.EndColumn}${u.IsDefinition ? " (definition)" : ""}`) +const definitions = greetUses.filter((u) => u.IsDefinition) +if (greetUses.length !== 2 || definitions.length !== 1 || definitions[0].Range.StartLine !== 3 || greetUses.some((u) => u.Range.FileName !== "Program.fs")) { + failures.push(`expected the definition and one use of greet: ${JSON.stringify(greetUses)}`) +} +console.log(` symbol uses of greet: ${usesText.join(", ")}`) + +// Completions: the full item must come back, and the description of an item must be fetchable +// afterwards by index. +const completionsStarted = Date.now() +post(["GetCompletionsForFile", "00000000-0000-0000-0000-000000000007", "Program.fs", 6, 11, greLine]) +const [, , greCompletions] = await answer("FoundCompletions") +const completionsMs = Date.now() - completionsStarted +const greetIndex = greCompletions.findIndex((c) => c.NameInList === "greet") +const greet = greCompletions[greetIndex] +if (!greet || greet.NameInCode !== "greet" || greet.FullName !== "Demo.greet") { + failures.push(`greet should be a completion of the project: ${JSON.stringify(greet)}`) +} +console.log(` completions for "${greLine}": ${greCompletions.length} items in ${completionsMs}ms`) + +post(["GetCompletionDescription", "00000000-0000-0000-0000-000000000008", greetIndex]) +const [, , greetDescription] = await answer("FoundCompletionDescription") +if (!greetDescription[0]?.XmlDoc?.includes(" Says hello.")) { + failures.push(`completion description lost the XML doc: ${JSON.stringify(greetDescription)}`) +} +console.log(` completion description of greet: ${signatureOf(greetDescription)} (${greetDescription[0]?.XmlDoc?.length ?? 0} doc lines)`) + +// Tokens and semantic classification: the lexer must carry state across lines, and the checker +// must classify identifiers by what they are. +post(["Tokenize", "00000000-0000-0000-0000-000000000010", ["let s = \"\"\"multi", "line\"\"\" // done", "#if FOO", "let hidden = 1", "#endif"].join("\n"), []]) +const [, , tokenLines] = await answer("Tokenized") +const tokenText = tokenLines.map((line) => line.map((t) => `${t.Color}:${t.StartColumn}-${t.EndColumn}`).join(" ")) +if (tokenLines[0]?.[0]?.Color !== "keyword" || tokenLines[0][0].EndColumn !== 3) failures.push(`first token should be the keyword let: ${tokenText[0]}`) +if (!tokenLines[1]?.some((t) => t.Color === "string") || !tokenLines[1]?.some((t) => t.Color === "comment")) { + failures.push(`second line should continue the string and end with a comment: ${tokenText[1]}`) +} +if (!tokenLines[3]?.every((t) => t.Color === "inactiveCode")) failures.push(`code under #if FOO should be inactive: ${tokenText[3]}`) +console.log(` tokens: ${tokenText.join(" | ")}`) + +post(["GetSemanticClassificationForFile", "00000000-0000-0000-0000-000000000011", "Program.fs"]) +const [, , classified] = await answer("FoundSemanticClassification") +const classifiedText = classified.map((c) => `${c.Type}@${c.Range.StartLine}:${c.Range.StartColumn}-${c.Range.EndColumn}`) +if (!classified.some((c) => c.Type === "function" && c.Range.StartLine === 3 && c.Range.StartColumn === 4 && c.Range.EndColumn === 9)) { + failures.push(`greet should be classified as a function: ${classifiedText.join(", ")}`) +} +if (!classified.some((c) => c.Type === "referenceType" && c.Range.StartLine === 3 && c.Range.StartColumn === 17 && c.Range.EndColumn === 23)) { + failures.push(`the string annotation should be classified as a reference type: ${classifiedText.join(", ")}`) +} +console.log(` classification: ${classifiedText.join(", ")}`) + if (failures.length) { console.error("\nFAILED") for (const f of failures) console.error(` - ${f}`) diff --git a/src/quicktest-rust/Cargo.toml b/src/quicktest-rust/Cargo.toml index 8b9d3740d0..792c20cd9c 100644 --- a/src/quicktest-rust/Cargo.toml +++ b/src/quicktest-rust/Cargo.toml @@ -3,5 +3,18 @@ name = "fable_quicktest_rust" version = "0.1.0" edition = "2024" +[[bin]] +name = "fable_quicktest_rust" +path = "src/main.rs" + +[features] +lrc_ptr = ["fable_library_rust/lrc_ptr"] +no_std = ["fable_library_rust/no_std"] +reflection = ["fable_library_rust/reflection"] +static_do_bindings = ["fable_library_rust/static_do_bindings"] +threaded = ["fable_library_rust/threaded"] +# default = ["reflection"] # Uncomment to default to "reflection" mode +# default = ["threaded"] # Uncomment to default to "threaded" mode + [dependencies] -fable_library_rust = { path = "../../temp/fable-library-rust" } +fable_library_rust = { path = "./fable_modules/fable-library-rust" } diff --git a/src/quicktest-rust/src/main.fs b/src/quicktest-rust/src/main.fs index deef3d37b3..555ece187c 100644 --- a/src/quicktest-rust/src/main.fs +++ b/src/quicktest-rust/src/main.fs @@ -1,39 +1,21 @@ open System +open System.Collections.Generic -type RefC(v: int) = - member _.V = v +let genericHash<'T when 'T: equality> (x: 'T) : int = + let cmp = EqualityComparer<'T>.Default + cmp.GetHashCode(x) -let twoSwitchRef (x: RefC option) (b: bool) = - match x with - | Some c when b -> c.V + 1 - | Some c -> c.V - | None -> -1 - -type RefU = - | RA of RefC - | RB of RefC - -let orPatRef (u: RefU) = - match u with - | RA c - | RB c -> c.V +[] +let main argv = + Console.WriteLine("Hello {0}!", "World") -type ObjU = - | OA of obj - | OB of obj + let o1 = [| 1; 2 |] + let o2 = [| 1; 2 |] + let o3 = [| 2; 1 |] + let h1 = genericHash o1 + let h2 = genericHash o2 + let h3 = genericHash o3 -let orPatObj (u: ObjU) : obj = - match u with - | OA o - | OB o -> o + Console.WriteLine(" h1 = {0}\n h2 = {1}\n h3 = {2}", h1, h2, h3) -[] -let main argv = - Console.WriteLine(twoSwitchRef (Some(RefC 5)) true) - Console.WriteLine(twoSwitchRef (Some(RefC 5)) false) - Console.WriteLine(twoSwitchRef None false) - Console.WriteLine(orPatRef (RA(RefC 5))) - Console.WriteLine(orPatRef (RB(RefC 7))) - Console.WriteLine((orPatObj (OA(box 9))) :? int) - Console.WriteLine((orPatObj (OB(box 11))) :? string) 0 diff --git a/tests/Beam/ApplicativeTests.fs b/tests/Beam/ApplicativeTests.fs index 6f7f0f8146..430132c091 100644 --- a/tests/Beam/ApplicativeTests.fs +++ b/tests/Beam/ApplicativeTests.fs @@ -379,10 +379,6 @@ let ``test No errors because references to missing unit args`` () = let f1 = foofy "bar" f1 () |> equal "foobar" -// Skipped: Delegate generates fun(X, X) -> which requires args to be equal in Erlang -// [] -// let ``test Arity is checked also when constructing records`` () = ... - [] let ``test Aether with generics works`` () = // See #750 let a = { RecordB = {A= "foo"; B=true} } diff --git a/tests/Beam/ArithmeticTests.fs b/tests/Beam/ArithmeticTests.fs index 647354b1b3..c88e9737b3 100644 --- a/tests/Beam/ArithmeticTests.fs +++ b/tests/Beam/ArithmeticTests.fs @@ -836,30 +836,26 @@ let ``test Math.DivRem works with ints`` () = Math.DivRem(5, 2) |> equal (2, 1) Math.DivRem(4, 2) |> equal (2, 0) -// TODO: DivRem with ref requires byref/address-of support which uses different -// ref patterns in Beam (process dictionary refs vs map-based refs). -// [] -// let ``test Math.DivRem works with ints and ref`` () = -// let rem = ref -1 -// Math.DivRem(5, 2, rem) |> equal 2 -// rem.Value |> equal 1 -// Math.DivRem(4, 2, rem) |> equal 2 -// rem.Value |> equal 0 +[] +let ``test Math.DivRem works with ints and ref`` () = + let rem = ref -1 + Math.DivRem(5, 2, rem) |> equal 2 + rem.Value |> equal 1 + Math.DivRem(4, 2, rem) |> equal 2 + rem.Value |> equal 0 [] let ``test Math.DivRem works with longs`` () = Math.DivRem(5L, 2L) |> equal (2L, 1L) Math.DivRem(4L, 2L) |> equal (2L, 0L) -// TODO: DivRem with ref requires byref/address-of support which uses different -// ref patterns in Beam (process dictionary refs vs map-based refs). -// [] -// let ``test Math.DivRem works with longs and ref`` () = -// let rem = ref -1L -// Math.DivRem(5L, 2L, rem) |> equal 2L -// rem.Value |> equal 1L -// Math.DivRem(4L, 2L, rem) |> equal 2L -// rem.Value |> equal 0L +[] +let ``test Math.DivRem works with longs and ref`` () = + let rem = ref -1L + Math.DivRem(5L, 2L, rem) |> equal 2L + rem.Value |> equal 1L + Math.DivRem(4L, 2L, rem) |> equal 2L + rem.Value |> equal 0L // --- Math.MinMagnitude / MaxMagnitude --- @@ -940,15 +936,28 @@ let ``test Numeric Clamp works`` () = Decimal.Clamp(5.0M, -4.0M, 3.0M) |> equal 3.0M bigint.Clamp(5I, -4I, 3I) |> equal 3I -// TODO: Erlang math:log2 returns float, but Numeric Log2 for integer types expects integer result. -// SByte.Log2, Int16.Log2, Byte.Log2, UInt16.Log2, UInt32.Log2, UInt64.Log2 would need trunc(). -// [] -// let ``test Numeric Log2 works`` () = -// Int32.Log2 8 |> equal 3 -// Int64.Log2 8L |> equal 3L -// Double.Log2 8.0 |> equal 3.0 -// Single.Log2 8.0f |> equal 3.0f -// bigint.Log2 8I |> equal 3I +[] +let ``test Numeric Log2 works`` () = + SByte.Log2 8y |> equal 3y + Int16.Log2 8s |> equal 3s + Int32.Log2 8 |> equal 3 + Int64.Log2 8L |> equal 3L + Byte.Log2 8uy |> equal 3uy + UInt16.Log2 8us |> equal 3us + UInt32.Log2 8u |> equal 3u + UInt64.Log2 8UL |> equal 3UL + Double.Log2 8.0 |> equal 3.0 + Single.Log2 8.0f |> equal 3.0f + bigint.Log2 8I |> equal 3I + + Int32.Log2 0 |> equal 0 + Int32.Log2 5 |> equal 2 + Int64.Log2 Int64.MaxValue |> equal 62L + UInt64.Log2 UInt64.MaxValue |> equal 63UL + bigint.Log2 ((1I <<< 256) + 1I) |> equal 256I + + throwsAnyError (fun () -> Int64.Log2 -1L |> ignore) + throwsAnyError (fun () -> bigint.Log2 -1I |> ignore) [] let ``test Math.Clamp expanded works`` () = @@ -1047,24 +1056,92 @@ let ``test Decimal floor works`` () = let ``test Decimal pown works`` () = pown 2.2M 3 |> equal 10.648M -// TODO: Decimal constructors from GetBits require low/mid/high int32 representation -// which doesn't map to the fixed-scale integer approach used in Beam. -// [] -// let ``test Decimal constructors work`` () = -// let d = 1.2493M -// let bits = Decimal.GetBits(d) -// let d2 = Decimal(bits) -// d2 |> equal d - -// TODO: BigInt byte array conversion requires two's complement encoding -// which isn't natively supported in Erlang. -// [] -// let ``test Big integer to byte array works`` () = -// 32767I.ToByteArray() |> equal [|255uy; 127uy|] +[] +let ``test Decimal constructors work`` () = + let d = 1.2493M + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits.[0], bits.[1], bits.[2], false, 4uy) + bits |> equal [| 12493; 0; 0; 262144 |] + d2 |> equal d + d3 |> equal d -// [] -// let ``test Big integer from byte array works`` () = -// Numerics.BigInteger([|255uy; 127uy|]) |> equal 32767I +[] +let ``test Decimal GetBits works`` () = + let d = Decimal([| -1; -1; -2; 0 |]) + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits.[0], bits.[1], bits.[2], true, 0uy) + bits |> equal [| -1; -1; -2; 0 |] + d2 |> equal d + d3 |> equal -d + +[] +let ``test Decimal bit constructors reject invalid layouts`` () = + throwsAnyError (fun () -> Decimal([| 0; 0; 0 |]) |> ignore) + throwsAnyError (fun () -> Decimal([| 0; 0; 0; 1 |]) |> ignore) + throwsAnyError (fun () -> Decimal(0, 0, 0, false, 29uy) |> ignore) + +[] +let ``test Decimal GetBits works with arithmetic operations`` () = + let check (d: decimal) = + let bits = Decimal.GetBits(d) + Decimal(bits) |> equal d + + check (10M + 10M) + check (100.5M - 0.5M) + check (5M * 200M) + check (-0.1M * 100M) + check (2000M / 10M) + check (1.5M + 0.5M) + +[] +let ``test Big integer to byte array works`` () = + 0I.ToByteArray() |> equal [| 0uy |] + 32767I.ToByteArray() |> equal [| 255uy; 127uy |] + 32768I.ToByteArray() |> equal [| 0uy; 128uy; 0uy |] + -32768I.ToByteArray() |> equal [| 0uy; 128uy |] + -32769I.ToByteArray() |> equal [| 255uy; 127uy; 255uy |] + + 111222333444555666777888999I.ToByteArray() + |> equal [| 231uy; 216uy; 2uy; 164uy; 86uy; 149uy; 8uy; 199uy; 62uy; 0uy; 92uy |] + + -111222333444555666777888999I.ToByteArray() + |> equal [| 25uy; 39uy; 253uy; 91uy; 169uy; 106uy; 247uy; 56uy; 193uy; 255uy; 163uy |] + +[] +let ``test Big integer from byte array works`` () = + Numerics.BigInteger([||]) |> equal 0I + Numerics.BigInteger([| 255uy; 127uy |]) |> equal 32767I + Numerics.BigInteger([| 0uy; 128uy; 0uy |]) |> equal 32768I + Numerics.BigInteger([| 0uy; 128uy |]) |> equal -32768I + Numerics.BigInteger([| 255uy; 127uy; 255uy |]) |> equal -32769I + + Numerics.BigInteger([| 231uy; 216uy; 2uy; 164uy; 86uy; 149uy; 8uy; 199uy; 62uy; 0uy; 92uy |]) + |> equal 111222333444555666777888999I + + Numerics.BigInteger([| 25uy; 39uy; 253uy; 91uy; 169uy; 106uy; 247uy; 56uy; 193uy; 255uy; 163uy |]) + |> equal -111222333444555666777888999I + +[] +let ``test Big integer byte arrays round-trip sign boundaries`` () = + [ -62837I; -256I; -129I; -128I; -1I; 0I; 127I; 128I; 255I; 256I ] + |> List.iter (fun value -> Numerics.BigInteger(value.ToByteArray()) |> equal value) + +[] +let ``test BigInteger Pow stays exact`` () = + bigint.Pow(0I, 0) |> equal 1I + bigint.Pow(-3I, 5) |> equal -243I + bigint.Pow(2I, 256) |> equal (1I <<< 256) + bigint.Pow(1I <<< 4_000_000, 1) |> equal (1I <<< 4_000_000) + throwsAnyError (fun () -> bigint.Pow(2I, -1) |> ignore) + +[] +let ``test BigInteger GreatestCommonDivisor works with negatives`` () = + bigint.GreatestCommonDivisor(-4I, 6I) |> equal 2I + bigint.GreatestCommonDivisor(4I, -6I) |> equal 2I + bigint.GreatestCommonDivisor(-4I, -6I) |> equal 2I + bigint.GreatestCommonDivisor(0I, -5I) |> equal 5I // --- Fixed-width integer semantics --- // .NET integers wrap on overflow; Erlang integers are arbitrary precision and never do. diff --git a/tests/Beam/ArrayTests.fs b/tests/Beam/ArrayTests.fs index 5369af0da2..261a507a95 100644 --- a/tests/Beam/ArrayTests.fs +++ b/tests/Beam/ArrayTests.fs @@ -963,6 +963,41 @@ let ``test Array.randomSampleBy works`` () = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) +[] +let ``test Array.randomShuffleBy matches FSharp.Core`` () = + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + float ((calls * 37) % 100) / 100.0 + Array.randomShuffleBy next [|0 .. 9|] |> equal [|3; 7; 2; 6; 9; 0; 8; 4; 5; 1|] + calls |> equal 9 + +[] +let ``test Array.randomSampleBy matches FSharp.Core`` () = + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + Array.randomSampleBy (fun () -> 0.0) 0 ([||]: int[]) |> equal [||] + // Above the max set size it draws again on a repeated index + let values = [|0.0; 0.0; 0.5|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + values[calls - 1] + Array.randomSampleBy next 2 [|0 .. 21|] |> equal [|0; 11|] + calls |> equal 3 + +[] +let ``test Array random functions give FSharp.Core's errors`` () = + throwsAnyError (fun () -> Array.randomShuffleBy (fun () -> nan) [|1; 2|]) + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomChoicesBy (fun () -> 0.0) -1 [|1|]) + // --- CompareWith --- [] @@ -981,6 +1016,140 @@ let ``test System.Array.IndexOf works with non-primitive types`` () = System.Array.IndexOf(myArray, Dog 5) |> equal -1 System.Array.IndexOf(myArray, Duck 5) |> equal 0 +[] +let ``test System.Array.BinarySearch works`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 0) |> equal -1 + System.Array.BinarySearch(xs, 1) |> equal 0 + System.Array.BinarySearch(xs, 2) |> equal -2 + System.Array.BinarySearch(xs, 7) |> equal 3 + System.Array.BinarySearch(xs, 8) |> equal -5 + System.Array.BinarySearch(xs, 10) |> equal -6 + System.Array.BinarySearch(([||]: int[]), 3) |> equal -1 + +[] +let ``test System.Array.BinarySearch works with duplicates`` () = + System.Array.BinarySearch([|2; 2; 2; 2; 2; 2; 2; 2|], 2) |> equal 3 + System.Array.BinarySearch([|1; 2; 2; 2; 2; 3; 4; 5; 6|], 2) |> equal 4 + +[] +let ``test System.Array.BinarySearch works with a range`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2) |> equal -2 + System.Array.BinarySearch(xs, 1, 3, 9) |> equal -5 + System.Array.BinarySearch(xs, 1, 2, 9) |> equal -4 + System.Array.BinarySearch(xs, 2, 0, 5) |> equal -3 + System.Array.BinarySearch(xs, 5, 0, 5) |> equal -6 + +[] +let ``test System.Array.BinarySearch works with a comparer`` () = + let desc = { new System.Collections.Generic.IComparer with member _.Compare(a, b) = compare b a } + let ys = [|9; 7; 5; 3; 1|] + System.Array.BinarySearch(ys, 3, desc) |> equal 3 + System.Array.BinarySearch(ys, 4, desc) |> equal -4 + System.Array.BinarySearch(ys, 1, 3, 3, desc) |> equal 3 + System.Array.BinarySearch([|1; 3; 5; 7; 9|], 7, (null: System.Collections.Generic.IComparer)) |> equal 3 + +[] +let ``test System.Array.BinarySearch passes the array element to the comparer first`` () = + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 9, logging) |> equal 4 + List.ofSeq calls |> equal [(5, 9); (7, 9); (9, 9)] + calls.Clear() + System.Array.BinarySearch(xs, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (1, 4); (3, 4)] + +type BinarySearchRecord = { A: int; B: string } + +[] +let ``test System.Array.BinarySearch works with other element types`` () = + let strings = [|"apple"; "banana"; "cherry"|] + System.Array.BinarySearch(strings, "banana") |> equal 1 + System.Array.BinarySearch(strings, "blue") |> equal -3 + let floats = [|1.5; 2.5; 3.5|] + System.Array.BinarySearch(floats, 2.5) |> equal 1 + System.Array.BinarySearch(floats, 3.0) |> equal -3 + let int64s = [|1L; 5L; 10L|] + System.Array.BinarySearch(int64s, 10L) |> equal 2 + System.Array.BinarySearch(int64s, 6L) |> equal -3 + let records = [|{ A = 1; B = "x" }; { A = 2; B = "a" }; { A = 2; B = "b" }|] + System.Array.BinarySearch(records, { A = 2; B = "b" }) |> equal 2 + System.Array.BinarySearch(records, { A = 1; B = "z" }) |> equal -2 + let unions = [|Duck 1; Duck 4; Dog 0|] + System.Array.BinarySearch(unions, Dog 0) |> equal 2 + System.Array.BinarySearch(unions, Duck 2) |> equal -2 + +[] +let ``test System.Array.BinarySearch works with a range and a null comparer`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7, null) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2, null) |> equal -2 + +[] +let ``test System.Array.BinarySearch works with a single element and a large array`` () = + System.Array.BinarySearch([|5|], 5) |> equal 0 + System.Array.BinarySearch([|5|], 3) |> equal -1 + System.Array.BinarySearch([|5|], 7) |> equal -2 + let big = Array.init 1000 (fun i -> i * 2) + System.Array.BinarySearch(big, 998) |> equal 499 + System.Array.BinarySearch(big, 999) |> equal -501 + +[] +let ``test System.Array.BinarySearch works with more element types`` () = + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 3uy) |> equal 1 + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 4uy) |> equal -3 + System.Array.BinarySearch([|-5s; 0s; 5s|], 0s) |> equal 1 + System.Array.BinarySearch([|-5s; 0s; 5s|], -6s) |> equal -1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'c') |> equal 1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'd') |> equal -3 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "b")) |> equal 1 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "c")) |> equal -3 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 2.5M) |> equal 1 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 3.0M) |> equal -3 + let dates = [|System.DateTime(2020, 1, 1); System.DateTime(2021, 1, 1)|] + System.Array.BinarySearch(dates, System.DateTime(2021, 1, 1)) |> equal 1 + System.Array.BinarySearch(dates, System.DateTime(2020, 6, 1)) |> equal -2 + System.Array.BinarySearch([|1I; 5I; 10I|], 5I) |> equal 1 + System.Array.BinarySearch([|1I; 5I; 10I|], 11I) |> equal -4 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 1) |> equal 1 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 2) |> equal -3 + +[] +let ``test System.Array.BinarySearch with a range passes the array element to the comparer first`` () = + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 1, 3, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (3, 4)] + +[] +let ``test System.Array.BinarySearch with a range checks its arguments`` () = + let xs = [|1; 3; 5; 7; 9|] + let raised index length = + try + System.Array.BinarySearch(xs, index, length, 5) |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised -1 2 |> equal "ArgumentOutOfRangeException" + raised -1 -1 |> equal "ArgumentOutOfRangeException" + raised 1 -1 |> equal "ArgumentOutOfRangeException" + raised 3 3 |> equal "ArgumentException" + raised 6 0 |> equal "ArgumentException" + // --- Additional tests ported from JS --- [] @@ -1078,6 +1247,35 @@ let ``test Array.sortInPlaceWith works`` () = Array.sortInPlaceWith (fun x y -> int(x - y)) xs xs.[0] + xs.[1] |> equal 3. +[] +let ``test In-place array functions evaluate computed arrays once`` () = + let mutable evaluations = 0 + let computed xs () = + evaluations <- evaluations + 1 + xs + + let sorted = [| 3; 1; 2 |] + Array.sortInPlace (computed sorted ()) + sorted |> equal [| 1; 2; 3 |] + + let sortedBy = [| 3; 1; 2 |] + Array.sortInPlaceBy (fun x -> -x) (computed sortedBy ()) + sortedBy |> equal [| 3; 2; 1 |] + + let sortedWith = [| 3; 1; 2 |] + Array.sortInPlaceWith compare (computed sortedWith ()) + sortedWith |> equal [| 1; 2; 3 |] + + let filled = [| 1; 2; 3; 4 |] + Array.fill (computed filled ()) 1 2 9 + filled |> equal [| 1; 9; 9; 4 |] + + let source = [| 5; 6; 7 |] + let target = [| 0; 0; 0 |] + Array.blit (computed source ()) 1 (computed target ()) 0 2 + target |> equal [| 6; 7; 0 |] + evaluations |> equal 6 + [] let ``test Array.sortByDescending works`` () = let xs = [|3.; 4.; 1.; 2.|] @@ -1314,3 +1512,81 @@ let ``test byte array from literal works`` () = data.[1] |> equal 2uy data.[2] |> equal 3uy data.Length |> equal 3 + +[] +let ``test Array.take raises InvalidOperationException when the array is too short`` () = + raisedExceptionType (fun () -> Array.take 5 [|1|]) |> equal "InvalidOperationException" + +[] +let ``test Array.find and friends raise KeyNotFoundException when nothing matches`` () = + raisedExceptionType (fun () -> Array.find (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findBack (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndex (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndexBack (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.pick (fun _ -> None: int option) [|1|]) |> equal "KeyNotFoundException" + +[] +let ``test Array random functions raise ArgumentOutOfRangeException when the randomizer is out of range`` () = + raisedExceptionType (fun () -> Array.randomShuffleBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomShuffleInPlaceBy (fun () -> -0.1) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Array random functions raise ArgumentException for invalid inputs`` () = + raisedExceptionType (fun () -> Array.randomChoice [||]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomChoices -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomChoices 1 [||]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomSample -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomSample 3 [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomSample 1 [||]) |> equal "ArgumentException" + +[] +let ``test Array.sub raises when the range is outside the array`` () = + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] -1 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 1 -1) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 4 -1) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 2 5) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 3 1) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 4 0) |> equal "ArgumentOutOfRangeException" + Array.sub [|1; 2; 3|] 3 0 |> equal [||] + +[] +let ``test Array.exists2 and Array.forall2 check the lengths first`` () = + raisedExceptionType (fun () -> Array.exists2 (fun _ _ -> true) [|1; 2|] [|1; 2; 3|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (fun _ _ -> false) [|1; 2|] [|1; 2; 3|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (fun _ _ -> true) [|1; 2; 3|] [|1; 2|]) |> equal "ArgumentException" + +[] +let ``test Array.chunkBySize and Array.splitInto reject a size below 1`` () = + raisedExceptionType (fun () -> Array.chunkBySize 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.chunkBySize 0 [||]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.splitInto 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.splitInto -1 [||]) |> equal "ArgumentException" + +[] +let ``test Array.splitInto works with an empty array`` () = + Array.splitInto 3 [||] |> equal [||] + +[] +let ``test Array.transpose raises ArgumentException for arrays of different lengths`` () = + raisedExceptionType (fun () -> Array.transpose [|[|1|]; [|2; 3|]|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [|[|1; 2|]; [|3|]|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [|[||]; [|1|]|]) |> equal "ArgumentException" + +[] +let ``test Array.blit raises like System.Array.Copy and leaves the target untouched`` () = + let target = Array.zeroCreate 10 + let blit sourceIndex targetIndex count = + raisedExceptionType (fun () -> Array.blit [|1; 2; 3|] sourceIndex target targetIndex count) + blit 0 0 5 |> equal "ArgumentException" + blit 2 0 2 |> equal "ArgumentException" + blit 4 0 0 |> equal "ArgumentException" + blit 0 9 2 |> equal "ArgumentException" + blit 0 0 -1 |> equal "ArgumentOutOfRangeException" + blit -1 0 2 |> equal "ArgumentOutOfRangeException" + blit 0 -1 2 |> equal "ArgumentOutOfRangeException" + blit 3 0 0 |> equal "no exception" + blit 0 10 0 |> equal "no exception" + target |> equal (Array.zeroCreate 10) diff --git a/tests/Beam/AsyncTests.fs b/tests/Beam/AsyncTests.fs index 46ad509762..364f65d769 100644 --- a/tests/Beam/AsyncTests.fs +++ b/tests/Beam/AsyncTests.fs @@ -3,6 +3,377 @@ module Fable.Tests.AsyncTests open System open Fable.Tests.Util open Util.Testing +open System.Threading + +let private runOffThreadPool action = +#if FABLE_COMPILER + action () +#else + // .NET reschedules deferred FromContinuations callbacks invoked on a pool thread. + // Use a dedicated thread so the probe can observe completion and thrown callbacks. + let mutable failure = None + let thread = Thread(ThreadStart(fun () -> + try action () + with error -> failure <- Some error)) + thread.Start() + thread.Join() + failure |> Option.iter raise +#endif + +let private observeTryCancelled token (work: Async) = + let calls = ResizeArray() + Async.StartWithContinuations( + work, + (fun value -> calls.Add(string value)), + (fun error -> calls.Add("error: " + error.Message)), + (fun error -> calls.Add("cancel: " + error.Message)), + token) + calls + +let private raiseCaughtCancellation (error: exn) : unit = raise error + +[] +let ``test TryCancelled preserves success and ordinary errors`` () = + let mutable compensations = 0 + let wrap work = Async.TryCancelled(work, fun _ -> compensations <- compensations + 1) + equal [|"42"|] ((observeTryCancelled CancellationToken.None (wrap (async.Return 42))).ToArray()) + for error in [Exception "body"; OperationCanceledException "thrown"] do + let work: Async = async { return raise error } + equal [|"error: " + error.Message|] ((observeTryCancelled CancellationToken.None (wrap work)).ToArray()) + let mutable completed = false + Async.StartWithContinuations( + Async.TryCancelled(async.Return (), fun _ -> compensations <- compensations + 1), + (fun () -> completed <- true), raise, raise) + equal true completed + equal 0 compensations + +[] +let ``test TryCancelled preserves cancellation identity and supports reuse`` () = + for throws in [false; true] do + let original = OperationCanceledException "original" + let calls = ResizeArray() + let mutable handlerSawOriginal = true + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel original) + let work = Async.TryCancelled(body, fun error -> + handlerSawOriginal <- handlerSawOriginal && obj.ReferenceEquals(original, error) + calls.Add "compensation" + if throws then failwith "compensation defect") + for _ in 1 .. 2 do + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), + (fun _ -> failwith "unexpected error"), + (fun error -> + equal true (obj.ReferenceEquals(original, error)) + calls.Add "cancel")) + equal [|"compensation"; "cancel"; "compensation"; "cancel"|] (calls.ToArray()) + equal true handlerSawOriginal + +[] +let ``test TryCancelled installs nested handlers before cancellation`` () = + use source = new CancellationTokenSource() + source.Cancel() + let mutable entered = false + let calls = ResizeArray() + let body = async { entered <- true; return 42 } + let inner = Async.TryCancelled(body, fun _ -> calls.Add "inner") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer") + Async.StartWithContinuations(outer, + (fun _ -> failwith "unexpected success"), raise, + (fun _ -> calls.Add "cancel"), source.Token) + equal false entered + equal [|"inner"; "outer"; "cancel"|] (calls.ToArray()) + +[] +let ``test TryCancelled handles deferred cancellation`` () = + runOffThreadPool (fun () -> + let original = OperationCanceledException "deferred" + let mutable cancel = fun (_: OperationCanceledException) -> failwith "not started" + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, continuation) -> cancel <- continuation) + let work = Async.TryCancelled(body, fun _ -> calls.Add "compensation") + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), raise, + (fun error -> equal true (obj.ReferenceEquals(original, error)); calls.Add "cancel")) + equal 0 calls.Count + cancel original + equal [|"compensation"; "cancel"|] (calls.ToArray())) + +[] +let ``test TryCancelled preserves finalizer order when handlers throw`` () = + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel (OperationCanceledException "original")) + let finalized = async.TryFinally(body, fun () -> calls.Add "finally") + let inner = Async.TryCancelled(finalized, fun _ -> calls.Add "inner"; failwith "inner defect") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer"; failwith "outer defect") + equal [|"cancel: original"|] ((observeTryCancelled CancellationToken.None outer).ToArray()) + equal [|"finally"; "inner"; "outer"|] (calls.ToArray()) + +[] +let ``test TryCancelled propagates downstream errors once`` () = + runOffThreadPool (fun () -> + for channel in ["success"; "error"; "cancel"] do + let mutable compensations = 0 + let calls = ResizeArray() + let mutable complete = fun () -> failwith "not started" + let body: Async = Async.FromContinuations(fun (ok, error, cancel) -> + complete <- fun () -> + match channel with + | "success" -> ok 42 + | "error" -> error (Exception "body") + | _ -> cancel (OperationCanceledException "original")) + let work = Async.TryCancelled(body, fun _ -> compensations <- compensations + 1) + let terminal name = calls.Add name; failwith "downstream" + Async.StartWithContinuations(work, + (fun _ -> terminal "success"), + (fun _ -> terminal "error"), + (fun _ -> terminal "cancel")) + let caught = + try + complete () + "no exception" + with error -> error.Message + equal "downstream" caught + equal [|channel|] (calls.ToArray()) + equal (if channel = "cancel" then 1 else 0) compensations) + +[] +let ``test TryCancelled preserves bounded trampoline execution`` () = + async { + let original = OperationCanceledException "deep cancellation" + let mutable compensations = 0 + let rec loop remaining = + if remaining = 0 then + Async.FromContinuations(fun (_, _, cancel) -> cancel original) + else + Async.TryCancelled(async { + do! async.Return () + return! loop (remaining - 1) + }, fun _ -> compensations <- compensations + 1) + do! Async.FromContinuations(fun (ok, error, _) -> + Async.StartWithContinuations(loop 10000, + (fun () -> error (Exception "unexpected success")), error, + (fun cancelled -> + if obj.ReferenceEquals(original, cancelled) then ok () + else error (Exception "cancellation identity changed")))) + equal 10000 compensations + } + |> Async.RunSynchronously + +[] +let ``test RunSynchronously optional timeout and cancellation token`` () = + let work = async { return 42 } + Async.RunSynchronously work |> equal 42 + Async.RunSynchronously(work, timeout = -1) |> equal 42 + Async.RunSynchronously(work, timeout = 10000) |> equal 42 + let invalidTimeout = + try + Async.RunSynchronously(work, timeout = -2) |> ignore + false + with :? ArgumentException -> true + equal true invalidTimeout + use cts = new CancellationTokenSource() + Async.RunSynchronously(work, cancellationToken = cts.Token) |> equal 42 + Async.RunSynchronously(work, timeout = 10000, cancellationToken = cts.Token) |> equal 42 + cts.Cancel() + let mutable entered = false + let cancelled = + try + Async.RunSynchronously(async { entered <- true }, cancellationToken = cts.Token) + false + with :? OperationCanceledException -> true + equal true cancelled + equal false entered + +[] +let ``test cancellation identity survives ordinary catch and raise`` () = + let token = CancellationToken(true) + let mutable caught = false + let cancelled = + try + try token.ThrowIfCancellationRequested() + with error -> + caught <- true + raiseCaughtCancellation error + false + with :? OperationCanceledException -> true + equal true caught + equal true cancelled + let ordinary = + try raise (Exception("operation_cancelled")) + with + | :? OperationCanceledException -> false + | error -> error.Message = "operation_cancelled" + equal true ordinary + +[] +let ``test Async TryWith catches explicitly thrown cancellation exceptions`` () = + async { + try + CancellationToken(true).ThrowIfCancellationRequested() + return false + with :? OperationCanceledException -> return true + } + |> Async.RunSynchronously + |> equal true + +[] +let ``test a real Async defect racing cancellation remains a failure`` () = + use cts = new CancellationTokenSource() + let work = async { + cts.Cancel() + return failwith "real defect" + } + let outcome = + try + Async.RunSynchronously(work, cancellationToken = cts.Token) |> ignore + "success" + with + | :? OperationCanceledException -> "cancelled" + | error -> error.Message + equal "real defect" outcome + +[] +let ``test Async finalizers run once and retain throwing compensation`` () = + for bodyFails in [ false; true ] do + let mutable count = 0 + let work = async { + try + if bodyFails then failwith "body defect" + return 42 + finally + count <- count + 1 + failwith "cleanup defect" + } + let failure = + try Async.RunSynchronously work |> ignore; "success" + with error -> error.Message + equal "cleanup defect" failure + equal 1 count + +[] +let ``test StartImmediate propagates errors after a finalizer containing a bind`` () = + let mutable count = 0 + let work = async { + try + let! value = async { return 42 } + equal 42 value + finally + count <- count + 1 + failwith "downstream defect" + } + let failure = + try Async.StartImmediate work; "success" + with error -> error.Message + equal "downstream defect" failure + equal 1 count + +[] +let ``test nested Async finalizers retain outer failure and run once`` () = + let mutable inner = 0 + let mutable outer = 0 + let work = async { + try + try return 42 + finally + inner <- inner + 1 + failwith "inner cleanup" + finally + outer <- outer + 1 + failwith "outer cleanup" + } + let failure = + try Async.RunSynchronously work |> ignore; "success" + with error -> error.Message + equal "outer cleanup" failure + equal 1 inner + equal 1 outer + +[] +let ``test Async Using disposes once and retains disposal defects`` () = + for bodyFails in [ false; true ] do + for disposalFails in [ false; true ] do + let mutable count = 0 + let resource = + { new IDisposable with + member _.Dispose() = + count <- count + 1 + if disposalFails then failwith "disposal defect" } + let work = async { + use owned = resource + if bodyFails then failwith "body defect" + return 42 + } + let outcome = + try Async.RunSynchronously work |> string + with error -> error.Message + equal (if disposalFails then "disposal defect" elif bodyFails then "body defect" else "42") outcome + equal 1 count + async { + use resource = Unchecked.defaultof + return 42 + } + |> Async.RunSynchronously + |> equal 42 + +let private cancelledComputation : Async = + Async.FromContinuations(fun (_, _, cancel) -> + cancel (OperationCanceledException("original cancellation"))) + +let private cancellationOutcome (work: Async) = + try + Async.RunSynchronously work + "success" + with + | :? OperationCanceledException as error -> error.Message + | error -> "failure: " + error.Message + +[] +let ``test Async cancellation remains terminal when compensation throws`` () = + for throws in [ false; true ] do + let mutable count = 0 + let work = async { + try do! cancelledComputation + finally + count <- count + 1 + if throws then failwith "cleanup defect" + } + equal "original cancellation" (cancellationOutcome work) + equal 1 count + +[] +let ``test nested throwing Async finalizers preserve cancellation`` () = + for outerThrows in [ false; true ] do + let mutable inner = 0 + let mutable outer = 0 + let work = async { + try + try do! cancelledComputation + finally + inner <- inner + 1 + failwith "inner cleanup" + finally + outer <- outer + 1 + if outerThrows then failwith "outer cleanup" + } + equal "original cancellation" (cancellationOutcome work) + equal 1 inner + equal 1 outer + +[] +let ``test Async Using preserves cancellation when Dispose throws`` () = + for throws in [ false; true ] do + let mutable count = 0 + let resource = + { new IDisposable with + member _.Dispose() = + count <- count + 1 + if throws then failwith "disposal defect" } + let work = async { + use owned = resource + do! cancelledComputation + } + equal "original cancellation" (cancellationOutcome work) + equal 1 count type MyException(value) = inherit Exception() @@ -428,6 +799,248 @@ let ``test Unit arguments are erased`` () = #if FABLE_COMPILER +open Fable.Core.BeamInterop + +type private CancellationProbeMessage = + | CancellationProbeReady of worker: obj + | CancellationProbeContinue + | CancellationCallbackInvoked + | CancellationCallbackStarted of worker: obj + | CancellationDisposeReturned + +// Run a thunk in a fresh BEAM process and return its result to the caller. +[ CancellationParent = self(), CancellationFun = $0, spawn(fun() -> CancellationParent ! {cancellation_result, case erlang:fun_info(CancellationFun, arity) of {arity, 0} -> CancellationFun(); _ -> CancellationFun(ok) end} end), receive {cancellation_result, CancellationResult} -> CancellationResult after 1000 -> timeout end end)()")>] +let private runInCancellationProcess (f: unit -> 'a) : 'a = Fable.Core.Util.nativeOnly + +[ CancellationFun = $0, spawn(fun() -> case erlang:fun_info(CancellationFun, arity) of {arity, 0} -> CancellationFun(); _ -> CancellationFun(ok) end end) end)()")>] +let private startCancellationProcess (f: unit -> unit) : obj = Fable.Core.Util.nativeOnly + +let private runSynchronouslyWithCancellation (workflow: Async<'a>) (token: System.Threading.CancellationToken) : 'a = + Async.RunSynchronously(workflow, cancellationToken = token) + +[] +let private pendingCancellation (_parent: obj) : Async = Fable.Core.Util.nativeOnly + +[] +let ``test legacy runtime cancellation remains typed through normalization and raise`` () = + let cancelled = + try + try emitErlExpr () "erlang:error(operation_cancelled)" + with error -> raiseCaughtCancellation error + false + with :? OperationCanceledException -> true + equal true cancelled + +[] +let ``test pending RunSynchronously cancellation bypasses Async TryWith and disposes once`` () = + use cts = new CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + let work = async { + let! child = Async.StartChild(async { + let mutable handled = false + let mutable finalized = 0 + let mutable disposed = 0 + let resource = + { new IDisposable with + member _.Dispose() = disposed <- disposed + 1 } + let cancelled = + try + Async.RunSynchronously( + async { + use owned = resource + try + try do! pendingCancellation parent + with _ -> handled <- true + finally finalized <- finalized + 1 + }, cancellationToken = cts.Token) + false + with :? OperationCanceledException -> true + return cancelled, handled, finalized, disposed + }) + let ready: bool = emitErlExpr () "receive {pending, _} -> true after 5000 -> false end" + equal true ready + cts.Cancel() + return! child + } + Async.RunSynchronously work |> equal (true, false, 1, 1) + +let private sendCancellationReady (parent: obj) = + let worker: obj = emitErlExpr () "erlang:self()" + emitErlExpr (parent, worker) "$0 ! {cancellation_probe_ready, $1}" + +let private waitForCancellationReady () = + match Erlang.receive 1000 with + | Some(CancellationProbeReady worker) -> Some worker + | _ -> None + +[] +let ``test cancellation token state crosses process boundaries`` () = + let cts = new System.Threading.CancellationTokenSource() + + runInCancellationProcess (fun () -> cts.Token.IsCancellationRequested) + |> equal false + +[] +let ``test running process observes cancellation from source process`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + + let comp = async { + let! child = + Async.StartChild(async { + sendCancellationReady parent + + match Erlang.receive 1000 with + | Some CancellationProbeContinue -> return cts.Token.IsCancellationRequested + | _ -> return false + }) + + match waitForCancellationReady () with + | Some worker -> + cts.Cancel() + emitErlExpr worker "$0 ! cancellation_probe_continue" + return! child + | None -> return false + } + + Async.RunSynchronously comp |> equal true + +[] +let ``test Async Sleep in another process is interrupted by live cancellation`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + + let comp = async { + let! child = + Async.StartChild(async { + let wasCancelledBeforeStart = cts.Token.IsCancellationRequested + sendCancellationReady parent + let mutable cancelled = false + + Async.StartWithContinuations( + async { do! Async.Sleep 1000 }, + ignore, + ignore, + (fun _ -> cancelled <- true), + cts.Token + ) + + return + not wasCancelledBeforeStart + && cancelled + && cts.Token.IsCancellationRequested + }) + + match waitForCancellationReady () with + | Some _ -> + cts.Cancel() + return! child + | None -> return false + } + + Async.RunSynchronously comp |> equal true + +[] +let ``test cross-process cancellation registration runs once`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + + let comp = async { + let! child = + Async.StartChild(async { + cts.Token.Register(fun () -> + emitErlExpr parent "$0 ! cancellation_callback_invoked" + ) + |> ignore + + sendCancellationReady parent + + match Erlang.receive 1000 with + | Some CancellationProbeContinue -> return true + | _ -> return false + }) + + match waitForCancellationReady () with + | Some worker -> + cts.Cancel() + cts.Cancel() + + let invokedOnce = + match Erlang.receive 1000 with + | Some CancellationCallbackInvoked -> + Erlang.receive 0 |> Option.isNone + | _ -> false + + emitErlExpr worker "$0 ! cancellation_probe_continue" + let! childCompleted = child + return invokedOnce && childCompleted + | None -> return false + } + + Async.RunSynchronously comp |> equal true + +[] +let ``test CancellationToken registration Dispose waits for a running callback`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + let registration = + cts.Token.Register(fun () -> + let worker: obj = emitErlExpr () "erlang:self()" + emitErlExpr (parent, worker) "$0 ! {cancellation_callback_started, $1}" + Erlang.receive 1000 |> ignore + ) + + startCancellationProcess (fun () -> cts.Cancel()) |> ignore + + match Erlang.receive 1000 with + | Some(CancellationCallbackStarted worker) -> + startCancellationProcess (fun () -> + registration.Dispose() + emitErlExpr parent "$0 ! cancellation_dispose_returned" + ) |> ignore + + Erlang.receive 0 |> Option.isNone |> equal true + emitErlExpr worker "$0 ! cancellation_probe_continue" + + match Erlang.receive 1000 with + | Some CancellationDisposeReturned -> () + | _ -> failwith "Dispose did not return after the callback completed" + | _ -> failwith "Cancellation callback did not start" + +[] +let ``test Async AwaitEvent cancellation runs in the event owner process`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + + let comp = async { + let! child = + Async.StartChild(async { + let ev = Event() + let mutable cancelCalled = false + sendCancellationReady parent + + let wasCancelled = + try + runSynchronouslyWithCancellation + (async { + let! _ = Async.AwaitEvent(ev.Publish, fun () -> cancelCalled <- true) + return false + }) + cts.Token + with _ -> true + + return wasCancelled && cancelCalled + }) + + match waitForCancellationReady () with + | Some _ -> + cts.Cancel() + return! child + | None -> return false + } + + Async.RunSynchronously comp |> equal true + [] let ``test CancellationTokenSource create and cancel works`` () = let tcs = new System.Threading.CancellationTokenSource() @@ -459,6 +1072,27 @@ let ``test CancellationTokenSource multiple registers work`` () = tcs.Cancel() equal 11 x +[] +let ``test CancellationToken registration after cancellation runs immediately`` () = + let mutable calls = 0 + let cts = new System.Threading.CancellationTokenSource() + cts.Cancel() + + cts.Token.Register(fun () -> calls <- calls + 1) |> ignore + + equal 1 calls + +[] +let ``test CancellationToken registration can be disposed`` () = + let mutable calls = 0 + let cts = new System.Threading.CancellationTokenSource() + let registration = cts.Token.Register(fun () -> calls <- calls + 1) + registration.Dispose() + + cts.Cancel() + + equal 0 calls + [] let ``test async cancellation with pre-cancelled token`` () = let mutable res = false @@ -513,11 +1147,18 @@ let ``test Can use custom exceptions in async workflows`` () = [] let ``test Async.AwaitEvent fires continuation when event is triggered`` () = let ev = Event() + let cts = new System.Threading.CancellationTokenSource() + let mutable cancelCalled = false + let mutable completed = false Async.StartImmediate(async { - let! v = Async.AwaitEvent ev.Publish + let! v = Async.AwaitEvent(ev.Publish, fun () -> cancelCalled <- true) equal 42 v - }) + completed <- true + }, cts.Token) ev.Trigger(42) + cts.Cancel() + equal true completed + equal false cancelCalled [] let ``test Async.AwaitEvent with cancelAction invokes it on cancellation`` () = @@ -532,3 +1173,25 @@ let ``test Async.AwaitEvent with cancelAction invokes it on cancellation`` () = equal true cancelCalled #endif +[] +let ``test FromContinuations unit and generic values`` () = + let fromValue value = Async.FromContinuations(fun (ok, _, _) -> ok value) + let mutable completed = 0 + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok ()) + Async.StartWithContinuations(unitWork, (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 3 completed + +[] +let ``test unit continuations preserve captured callbacks`` () = + let mutable calls = 0 + let rec genericDeadline callback value remaining = + if remaining > 0 then genericDeadline callback value (remaining - 1) + else Async.StartWithContinuations(async { return value }, (fun result -> callback result), raise, raise) + let rec unitDeadline (callback: unit -> unit) remaining = + if remaining > 0 then unitDeadline callback (remaining - 1) + else Async.StartWithContinuations(async { return () }, (fun () -> callback ()), raise, raise) + genericDeadline (fun () -> calls <- calls + 1) () 2 + unitDeadline (fun () -> calls <- calls + 1) 2 + equal 2 calls diff --git a/tests/Beam/CollidingMemberNames/CollidingMemberNames.fsproj b/tests/Beam/CollidingMemberNames/CollidingMemberNames.fsproj new file mode 100644 index 0000000000..705031e61e --- /dev/null +++ b/tests/Beam/CollidingMemberNames/CollidingMemberNames.fsproj @@ -0,0 +1,13 @@ + + + net10.0 + false + preview + + + + + + + + diff --git a/tests/Beam/CollidingMemberNames/Program.fs b/tests/Beam/CollidingMemberNames/Program.fs new file mode 100644 index 0000000000..7dd9454bac --- /dev/null +++ b/tests/Beam/CollidingMemberNames/Program.fs @@ -0,0 +1,51 @@ +module Fable.Tests.CollidingMemberNames + +type Foo() = class end + +let fooBar () = 1 +let foo_bar () = 2 +let foo_ctor () = 42 + +let values () = fooBar (), foo_bar (), foo_ctor () + +// Members differing only in case are legal F# but collapse to one Erlang atom. Unlike a captured +// `let`, another file can call them, so they must be reported rather than silently renamed. +type Renderer() = + member _.work() = 1 + member _.Work() = 2 + +let render () = + let r = Renderer() + r.work (), r.Work() + +// Class and `val` fields are keyed by `classFieldAtomName`, which collapses case and runs of +// underscores. Two fields that differ only there share one map key. +type CtorFields(fooBar: int, FooBar: int) = + member _.Lower = fooBar + member _.Upper = FooBar + +type ValFields() = + [] + val mutable fooBar: int + + [] + val mutable FooBar: int + +// Union tags and interface dispatch keys are atoms too. A shared tag makes `match` pick the wrong +// branch and two values compare equal; a shared key makes one member resolve to the other's body. +type CollidingUnion = + | FooBar + | Foo_Bar + +type CompiledNameUnion = + | [] Tagged + | Foo_Bar + +type ICollidingMembers = + abstract FooBar: unit -> int + abstract Foo_Bar: unit -> int + +// The prefixed setter key is bounded again, so it can collide with an explicitly named method. +type ILongSetterCollision = + abstract pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp: int with get, set + abstract set_pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp_eb2801e2b9e2ecf71459fa6e18bf811861522adcea87b798f928b62a2242245c: unit -> int diff --git a/tests/Beam/ComparisonTests.fs b/tests/Beam/ComparisonTests.fs index 1754c9f926..4f24be8278 100644 --- a/tests/Beam/ComparisonTests.fs +++ b/tests/Beam/ComparisonTests.fs @@ -23,6 +23,8 @@ let fableCompare (a: obj) (b: obj) : int = nativeOnly let fableHash (a: obj) : int = nativeOnly #endif +let private getObjectHashCode (value: obj) = value.GetHashCode() + type UTest = A of int | B of int type RTest = { a: int; b: int } @@ -671,12 +673,25 @@ let ``test GetHashCode with records works`` () = ({a=1; b=2}.GetHashCode(), {a=1; b=2}.GetHashCode()) ||> equal ({a=2; b=1}.GetHashCode(), {a=1; b=2}.GetHashCode()) ||> notEqual -// TODO: GetHashCode on arrays uses identity (ref) hashing on .NET, content hashing on Beam. -// Test fails on .NET because two [|1; 2|] literals are different instances with different hash codes. -// [] -// let ``test GetHashCode with arrays works`` () = -// ([|1; 2|].GetHashCode(), [|1; 2|].GetHashCode()) ||> equal -// ([|2; 1|].GetHashCode(), [|1; 2|].GetHashCode()) ||> notEqual +[] +let ``test GetHashCode with arrays uses identity`` () = + let xs1 = [| 1; 2 |] + let xs2 = [| 1; 2 |] + let hash1 = getObjectHashCode xs1 + (hash1, xs1.GetHashCode()) ||> equal + (hash1, getObjectHashCode xs2) ||> notEqual + xs1[0] <- 2 + (hash1, getObjectHashCode xs1) ||> equal + +[] +let ``test GetHashCode with byte arrays uses identity`` () = + let xs1 = [| 1uy; 2uy |] + let xs2 = [| 1uy; 2uy |] + let hash1 = getObjectHashCode xs1 + (hash1, xs1.GetHashCode()) ||> equal + (hash1, getObjectHashCode xs2) ||> notEqual + xs1[0] <- 2uy + (hash1, getObjectHashCode xs1) ||> equal [] let ``test Set option equality works`` () = diff --git a/tests/Beam/ConversionTests.fs b/tests/Beam/ConversionTests.fs index 896bbd54cf..cf08ef23e2 100644 --- a/tests/Beam/ConversionTests.fs +++ b/tests/Beam/ConversionTests.fs @@ -20,6 +20,14 @@ let parse f (a: string) = f(a, System.Globalization.CultureInfo("en-US")) #endif +let throwsOverflow (f: unit -> unit) = + try + f () + false + with + | :? OverflowException -> true + | _ -> false + // --- Basic type conversions --- [] @@ -34,6 +42,14 @@ let ``test negative int to string works`` () = let ``test string to int works`` () = int "42" |> equal 42 +[] +let ``test string to integer conversion checks the target range`` () = + int "2147483647" |> equal Int32.MaxValue + byte "255" |> equal Byte.MaxValue + (fun () -> int "2147483648" |> ignore) |> throwsOverflow |> equal true + (fun () -> byte "256" |> ignore) |> throwsOverflow |> equal true + (fun () -> uint32 "-1" |> ignore) |> throwsOverflow |> equal true + [] let ``test float to int works`` () = int 3.14 |> equal 3 @@ -283,6 +299,58 @@ let ``test System.Convert.ToUInt64 works`` () = Convert.ToUInt64(1.) |> equal x Convert.ToUInt64("1") |> equal x +[] +let ``test System.Convert integer conversions round midpoint values to even`` () = + Convert.ToInt32(1.4) |> equal 1 + Convert.ToInt32(1.5) |> equal 2 + Convert.ToInt32(2.5) |> equal 2 + Convert.ToInt32(3.5) |> equal 4 + Convert.ToInt32(-1.5) |> equal -2 + Convert.ToInt32(-2.5) |> equal -2 + Convert.ToInt32(-3.5) |> equal -4 + Convert.ToByte(255.4) |> equal Byte.MaxValue + (fun () -> Convert.ToByte(255.5) |> ignore) |> throwsOverflow |> equal true + +[] +let ``test System.Convert integer conversions check the target range`` () = + Convert.ToSByte(-128) |> equal SByte.MinValue + Convert.ToByte(255) |> equal Byte.MaxValue + Convert.ToInt16(-32768) |> equal Int16.MinValue + Convert.ToUInt16(65535) |> equal UInt16.MaxValue + Convert.ToInt32(2147483647L) |> equal Int32.MaxValue + Convert.ToUInt32(4294967295L) |> equal UInt32.MaxValue + Convert.ToInt64(Int64.MinValue) |> equal Int64.MinValue + Convert.ToUInt64(UInt64.MaxValue) |> equal UInt64.MaxValue + (fun () -> Convert.ToSByte(128) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToByte(-1) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToInt16(32768) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToUInt16(-1) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToInt32(2147483648L) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToUInt32(-1L) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToInt64(UInt64.MaxValue) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToUInt64(-1L) |> ignore) |> throwsOverflow |> equal true + +[] +let ``test System.Convert integer conversions handle decimal boolean and char sources`` () = + Convert.ToInt32(1.5M) |> equal 2 + Convert.ToInt32(2.5M) |> equal 2 + Convert.ToInt32(-1.5M) |> equal -2 + Convert.ToInt32(-2.5M) |> equal -2 + Convert.ToByte(255.4M) |> equal Byte.MaxValue + Convert.ToInt32(true) |> equal 1 + Convert.ToInt32(false) |> equal 0 + Convert.ToByte(char 255) |> equal Byte.MaxValue + Convert.ToChar(65535) |> equal Char.MaxValue + (fun () -> Convert.ToByte(255.5M) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToByte(char 256) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToChar(-1) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToChar(65536) |> ignore) |> throwsOverflow |> equal true + +[] +let ``test FSharp integer casts remain unchecked`` () = + byte 257 |> equal 1uy + sbyte 255 |> equal -1y + [] let ``test System.Convert.ToChar works`` () = let x = 'a' @@ -301,11 +369,19 @@ let ``test System.Convert.ToChar works`` () = let ``test System.Int32.Parse works`` () = Int32.Parse("5") |> equal 5 Int32.Parse("-5") |> equal -5 + Int32.Parse("-2147483648") |> equal Int32.MinValue + Int32.Parse("2147483647") |> equal Int32.MaxValue + (fun () -> Int32.Parse("2147483648") |> ignore) |> throwsOverflow |> equal true + (fun () -> Int32.Parse("-2147483649") |> ignore) |> throwsOverflow |> equal true [] let ``test System.Int64.Parse works`` () = Int64.Parse("5") |> equal 5L Int64.Parse("-5") |> equal -5L + Int64.Parse("-9223372036854775808") |> equal Int64.MinValue + Int64.Parse("9223372036854775807") |> equal Int64.MaxValue + (fun () -> Int64.Parse("9223372036854775808") |> ignore) |> throwsOverflow |> equal true + (fun () -> Int64.Parse("-9223372036854775809") |> ignore) |> throwsOverflow |> equal true [] let ``test System.Double.Parse works`` () = @@ -411,31 +487,46 @@ let ``test System.SByte.Parse works`` () = SByte.Parse("5") |> equal 5y SByte.Parse("-5") |> equal -5y SByte.Parse("-128") |> equal -128y + SByte.Parse("127") |> equal SByte.MaxValue + (fun () -> SByte.Parse("128") |> ignore) |> throwsOverflow |> equal true + (fun () -> SByte.Parse("-129") |> ignore) |> throwsOverflow |> equal true [] let ``test System.Int16.Parse works`` () = Int16.Parse("5") |> equal 5s Int16.Parse("-5") |> equal -5s Int16.Parse("-32768") |> equal -32768s + Int16.Parse("32767") |> equal Int16.MaxValue + (fun () -> Int16.Parse("32768") |> ignore) |> throwsOverflow |> equal true + (fun () -> Int16.Parse("-32769") |> ignore) |> throwsOverflow |> equal true [] let ``test System.Byte.Parse works`` () = Byte.Parse("5") |> equal 5uy Byte.Parse("255") |> equal 255uy + (fun () -> Byte.Parse("256") |> ignore) |> throwsOverflow |> equal true + (fun () -> Byte.Parse("-1") |> ignore) |> throwsOverflow |> equal true [] let ``test System.UInt16.Parse works`` () = UInt16.Parse("5") |> equal 5us UInt16.Parse("65535") |> equal 65535us + (fun () -> UInt16.Parse("65536") |> ignore) |> throwsOverflow |> equal true + (fun () -> UInt16.Parse("-1") |> ignore) |> throwsOverflow |> equal true [] let ``test System.UInt32.Parse works`` () = UInt32.Parse("5") |> equal 5u UInt32.Parse("4294967295") |> equal 4294967295u + (fun () -> UInt32.Parse("4294967296") |> ignore) |> throwsOverflow |> equal true + (fun () -> UInt32.Parse("-1") |> ignore) |> throwsOverflow |> equal true [] let ``test System.UInt64.Parse works`` () = UInt64.Parse("5") |> equal 5uL + UInt64.Parse("18446744073709551615") |> equal UInt64.MaxValue + (fun () -> UInt64.Parse("18446744073709551616") |> ignore) |> throwsOverflow |> equal true + (fun () -> UInt64.Parse("-1") |> ignore) |> throwsOverflow |> equal true // --- Convert with base parameter --- @@ -464,6 +555,7 @@ let ``test System.Convert.ToByte with base works`` () = Convert.ToByte(x, 2) |> equal 5uy Convert.ToByte(x, 8) |> equal 65uy Convert.ToByte(x, 10) |> equal 101uy + (fun () -> Convert.ToByte("100", 16) |> ignore) |> throwsOverflow |> equal true [] let ``test System.Convert.ToInt16 with base works`` () = @@ -473,6 +565,7 @@ let ``test System.Convert.ToInt16 with base works`` () = Convert.ToInt16(x, 8) |> equal 65s Convert.ToInt16(x, 10) |> equal 101s Convert.ToInt16(x, 16) |> equal 257s + Convert.ToInt16("FFFF", 16) |> equal -1s // --- Convert.ToString with base parameter --- @@ -556,9 +649,25 @@ let ``test System.Int32.TryParse works`` () = success2 |> equal false value2 |> equal 0 +[] +let ``test integer TryParse checks the target range`` () = + SByte.TryParse("128") |> equal (false, 0y) + Byte.TryParse("-1") |> equal (false, 0uy) + Int16.TryParse("32768") |> equal (false, 0s) + UInt16.TryParse("65536") |> equal (false, 0us) + Int32.TryParse("2147483648") |> equal (false, 0) + UInt32.TryParse("4294967296") |> equal (false, 0u) + Int64.TryParse("9223372036854775808") |> equal (false, 0L) + UInt64.TryParse("18446744073709551616") |> equal (false, 0uL) + [] let ``test System.Int32.Parse with hex works`` () = Int32.Parse("5f", System.Globalization.NumberStyles.HexNumber) |> equal 95 + Int32.Parse("FFFFFFFF", System.Globalization.NumberStyles.HexNumber) |> equal -1 + + (fun () -> Int32.Parse("1FFFFFFFF", System.Globalization.NumberStyles.HexNumber) |> ignore) + |> throwsOverflow + |> equal true [] let ``test BitConverter.GetBytes Int32 works`` () = @@ -572,23 +681,20 @@ let ``test Convert.ToBase64String works`` () = // --- TryParse methods --- -// TODO: TryParse via higher-order function — auto-generated lambda unwraps ref.contents -// instead of passing the ref directly to try_parse_float (which needs it for put/2). -// Fixing requires changes to how byref method references are compiled. -// [] -// let ``test System.Double.TryParse works`` () = -// tryParse Double.TryParse 0.0 "1" |> equal (true, 1.0) -// tryParse Double.TryParse 0.0 "1.5" |> equal (true, 1.5) -// tryParse Double.TryParse 0.0 "foo" |> equal (false, 0.0) -// tryParse Double.TryParse 0.0 "" |> equal (false, 0.0) -// tryParse Double.TryParse 0.0 "-1.5" |> equal (true, -1.5) -// -// [] -// let ``test System.Single.TryParse works`` () = -// tryParse Single.TryParse 0.0f "1" |> equal (true, 1.0f) -// tryParse Single.TryParse 0.0f "1.5" |> equal (true, 1.5f) -// tryParse Single.TryParse 0.0f "foo" |> equal (false, 0.0f) -// tryParse Single.TryParse 0.0f "-1.5" |> equal (true, -1.5f) +[] +let ``test System.Double.TryParse works`` () = + tryParse Double.TryParse 0.0 "1" |> equal (true, 1.0) + tryParse Double.TryParse 0.0 "1.5" |> equal (true, 1.5) + tryParse Double.TryParse 0.0 "foo" |> equal (false, 0.0) + tryParse Double.TryParse 0.0 "" |> equal (false, 0.0) + tryParse Double.TryParse 0.0 "-1.5" |> equal (true, -1.5) + +[] +let ``test System.Single.TryParse works`` () = + tryParse Single.TryParse 0.0f "1" |> equal (true, 1.0f) + tryParse Single.TryParse 0.0f "1.5" |> equal (true, 1.5f) + tryParse Single.TryParse 0.0f "foo" |> equal (false, 0.0f) + tryParse Single.TryParse 0.0f "-1.5" |> equal (true, -1.5f) [] let ``test System.Boolean.TryParse works`` () = @@ -599,21 +705,20 @@ let ``test System.Boolean.TryParse works`` () = Boolean.TryParse "tru" |> equal (false, false) Boolean.TryParse "falsee" |> equal (false, false) -// TODO: TryParse via higher-order function — same byref method reference issue as above -// [] -// let ``test System.Int64.TryParse works`` () = -// tryParse Int64.TryParse 0L "99" |> equal (true, 99L) -// tryParse Int64.TryParse 0L "foo" |> equal (false, 0L) -// -// [] -// let ``test System.UInt32.TryParse works`` () = -// tryParse UInt32.TryParse 0u "99" |> equal (true, 99u) -// tryParse UInt32.TryParse 0u "foo" |> equal (false, 0u) -// -// [] -// let ``test System.UInt64.TryParse works`` () = -// tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) -// tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) +[] +let ``test System.Int64.TryParse works`` () = + tryParse Int64.TryParse 0L "99" |> equal (true, 99L) + tryParse Int64.TryParse 0L "foo" |> equal (false, 0L) + +[] +let ``test System.UInt32.TryParse works`` () = + tryParse UInt32.TryParse 0u "99" |> equal (true, 99u) + tryParse UInt32.TryParse 0u "foo" |> equal (false, 0u) + +[] +let ``test System.UInt64.TryParse works`` () = + tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) + tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) // --- Single.Parse --- @@ -938,12 +1043,11 @@ let ``test BitConverter.ToUInt32 works`` () = let bytes = BitConverter.GetBytes(value) BitConverter.ToUInt32(bytes, 0) |> equal value -// TODO: UInt64 values > Int64.MaxValue — Fable compiles UInt64 as Int64, round-trip fails -// [] -// let ``test BitConverter.ToUInt64 works`` () = -// let value = 0xFF02030405060708UL -// let bytes = BitConverter.GetBytes(value) -// BitConverter.ToUInt64(bytes, 0) |> equal value +[] +let ``test BitConverter.ToUInt64 works above Int64.MaxValue`` () = + let value = 0xFF02030405060708UL + let bytes = BitConverter.GetBytes(value) + BitConverter.ToUInt64(bytes, 0) |> equal value [] let ``test BitConverter.ToSingle works`` () = diff --git a/tests/Beam/DateTimeOffsetTests.fs b/tests/Beam/DateTimeOffsetTests.fs index 64eb006b15..e791c81ca9 100644 --- a/tests/Beam/DateTimeOffsetTests.fs +++ b/tests/Beam/DateTimeOffsetTests.fs @@ -518,3 +518,16 @@ let ``test DateTimeOffset constructor from DateOnly and TimeOnly works`` () = dt.Minute |> equal dt_exp.Minute dt.Second |> equal dt_exp.Second dt.Millisecond |> equal dt_exp.Millisecond + +[] +let ``test Implicit conversion from DateTime preserves value`` () = + let d = DateTime(2024, 1, 15, 13, 23, 30, DateTimeKind.Utc) + let dto: DateTimeOffset = d + dto |> equal (DateTimeOffset(d)) + dto.Offset |> equal TimeSpan.Zero + dto.Year |> equal 2024 + dto.Month |> equal 1 + dto.Day |> equal 15 + dto.Hour |> equal 13 + dto.Minute |> equal 23 + dto.Second |> equal 30 diff --git a/tests/Beam/DictionaryTests.fs b/tests/Beam/DictionaryTests.fs index 0694e9ae13..d7cd601aa7 100644 --- a/tests/Beam/DictionaryTests.fs +++ b/tests/Beam/DictionaryTests.fs @@ -5,6 +5,8 @@ open System.Collections.Generic open Fable.Tests.Util open Util.Testing +type MyRecord = { a: int } + // --- Construction --- [] @@ -74,6 +76,28 @@ let ``test Dictionary.Remove works`` () = dic.Remove("C") |> equal false dic.Count |> equal 1 +[] +let ``test Dictionary.Remove with complex keys works`` () = + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 + +[] +let ``test Dictionary.Remove with records as keys works`` () = + let x1 = { a = 5 } + let x2 = { a = 5 } + let x3 = { a = 10 } + let x4 = { a = 15 } + let dict = Dictionary<_, _>() + dict.Add(x1, "Hello") + dict.Add(x3, "World!") + dict.Remove(x2) |> equal true + dict.Remove(x4) |> equal false + dict.Count |> equal 1 + // --- TryGetValue --- [] diff --git a/tests/Beam/EncodingTests.fs b/tests/Beam/EncodingTests.fs index de1eaed3f0..5a54b9ed1e 100644 --- a/tests/Beam/EncodingTests.fs +++ b/tests/Beam/EncodingTests.fs @@ -192,6 +192,55 @@ let ``test Base64 empty array works`` () = let encoded = Convert.ToBase64String(bytes) encoded |> equal "" +// --- Hex --- + +[] +let ``test Convert.ToHexString works`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexString(bytes) |> equal "FAFBFCFDFE" + Convert.ToHexString([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010F10" + Convert.ToHexString([||]) |> equal "" + Convert.ToHexString(bytes, 1, 3) |> equal "FBFCFD" + Convert.ToHexString(bytes, 5, 0) |> equal "" + Convert.ToHexString(List.toArray [ 200uy; 3uy ]) |> equal "C803" + +[] +let ``test Convert.ToHexStringLower works`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexStringLower(bytes) |> equal "fafbfcfdfe" + Convert.ToHexStringLower([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010f10" + Convert.ToHexStringLower([||]) |> equal "" + Convert.ToHexStringLower(bytes, 1, 3) |> equal "fbfcfd" + +[] +let ``test Convert.ToHexString checks its arguments`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + raisedExceptionType (fun () -> Convert.ToHexString(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString((null: byte[]), 0, 0)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 1, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 6, 0)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Convert.FromHexString works`` () = + Convert.FromHexString("fafBFCFDFE") |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.FromHexString("0123456789abcdefABCDEF") + |> equal [| 1uy; 35uy; 69uy; 103uy; 137uy; 171uy; 205uy; 239uy; 171uy; 205uy; 239uy |] + Convert.FromHexString("") |> equal [||] + +[] +let ``test Convert.FromHexString checks its input`` () = + raisedExceptionType (fun () -> Convert.FromHexString(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.FromHexString("abc")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("zz")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("0x10")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("00 1")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("+1")) |> equal "FormatException" + // --- Guid --- [] diff --git a/tests/Beam/EnumerableTests.fs b/tests/Beam/EnumerableTests.fs index ad27923e0f..259565e84f 100644 --- a/tests/Beam/EnumerableTests.fs +++ b/tests/Beam/EnumerableTests.fs @@ -67,3 +67,97 @@ let ``test Enumerator can be converted to seq`` () = |> Seq.toList |> List.sum |> equal 55 + +#if FABLE_COMPILER_BEAM +// decision: enumerates in another process to detect process-local sequence wrapper state +[ + Parent = self(), F = $0, + {Pid, Ref} = spawn_monitor(fun() -> + Result = try + {ok, case erlang:fun_info(F, arity) of {arity, 0} -> F(); _ -> F(ok) end} + catch Class:Reason:Stack -> {error, Class, Reason, Stack} end, + Parent ! {self(), Result} + end), + receive + {Pid, Result} -> + receive {'DOWN', Ref, process, Pid, normal} -> ok + after 5000 -> erlang:error(sequence_process_timeout) end, + case Result of + {ok, Value} -> Value; + {error, C, E, S} -> erlang:raise(C, E, S) + end; + {'DOWN', Ref, process, Pid, Reason} -> erlang:error({sequence_process_failed, Reason}) + after 5000 -> erlang:error(sequence_process_timeout) + end +end)() +""")>] +let private inSequenceProcess (f: unit -> 'T) : 'T = Fable.Core.Util.nativeOnly + +// decision: calls the runtime override directly because seq-typed ToString uses the general converter +[] +let private sequenceToString (values: seq<'T>) : string = Fable.Core.Util.nativeOnly + +[] +let ``test lazy sequence wrappers are portable across processes`` () = + let values = seq { 1..3 } + inSequenceProcess (fun () -> Seq.toList values) |> equal [1; 2; 3] + inSequenceProcess (fun () -> Seq.toList values) |> equal [1; 2; 3] + inSequenceProcess (fun () -> sequenceToString values) |> equal "seq [1; 2; 3]" + +[] +let ``test mapped sequence wrappers are portable across processes`` () = + let values = seq { 1..3 } |> Seq.map (fun x -> x * 2) + inSequenceProcess (fun () -> Seq.toList values) |> equal [2; 4; 6] + +[] +let ``test lazy sequence survives its creating process`` () = + let values = inSequenceProcess (fun () -> seq { 1..3 }) + Seq.toList values |> equal [1; 2; 3] + inSequenceProcess (fun () -> Seq.toList values) |> equal [1; 2; 3] + +[] +let ``test sequence ToString preserves formatting`` () = + Seq.empty |> sequenceToString |> equal "seq []" + Seq.singleton 1 |> sequenceToString |> equal "seq [1]" + seq { 1..3 } |> sequenceToString |> equal "seq [1; 2; 3]" + seq { 1..4 } |> sequenceToString |> equal "seq [1; 2; 3; 4; ...]" + Seq.initInfinite (fun i -> i + 1) |> sequenceToString |> equal "seq [1; 2; 3; 4; ...]" + +[] +let ``test sequence ToString is lazy repeatable and disposes its enumerator`` () = + let mutable evaluated = 0 + let mutable disposed = 0 + let values = seq { + try + for value in 1..5 do + evaluated <- evaluated + 1 + yield value + finally + disposed <- disposed + 1 + } + evaluated |> equal 0 + disposed |> equal 0 + sequenceToString values |> equal "seq [1; 2; 3; 4; ...]" + evaluated |> equal 4 + disposed |> equal 1 + Seq.toList values |> equal [1; 2; 3; 4; 5] + evaluated |> equal 9 + disposed |> equal 2 + sequenceToString values |> equal "seq [1; 2; 3; 4; ...]" + evaluated |> equal 13 + disposed |> equal 3 + +[] +let ``test sequence ToString disposes its enumerator when enumeration throws`` () = + let mutable disposed = 0 + let values = seq { + try + yield 1 + failwith "sequence failure" + finally + disposed <- disposed + 1 + } + Util.throwsError "sequence failure" (fun () -> sequenceToString values) + disposed |> equal 1 +#endif diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index 55b6e145e4..b9bf615daa 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -6,6 +6,14 @@ open Util.Testing exception MyError of string exception MyError2 of code: int * message: string exception EmptyError +exception NumericError of int + +type ExitReason = + | Stopped + | Failed of string + +exception ExitError of ExitReason +exception NamedNumericError of message: int [] let ``test custom exception can be raised and caught`` () = @@ -73,6 +81,46 @@ let ``test custom exception Message contains field value`` () = // .NET formats as 'MyError "custom msg"', Beam uses the raw field value msg.Contains("custom msg") |> equal true +[] +let ``test numeric exception Message is printable and preserves its payload`` () = + let error = NumericError 42 + let message = error.Message + message.Contains("42") |> equal true + message.Split([| '\n' |]).Length > 0 |> equal true + match error with + | NumericError value -> value |> equal 42 + | _ -> failwith "Expected NumericError" + +[] +let ``test union exception Message is printable and preserves its payload`` () = + let reason = Failed "worker stopped" + let error = ExitError reason + let message = error.Message + message.Contains("worker stopped") |> equal true + message.Split([| '\n' |]).Length > 0 |> equal true + match error with + | ExitError value -> value |> equal reason + | _ -> failwith "Expected ExitError" + +[] +let ``test named numeric exception field stays numeric when Message is read`` () = + let error = NamedNumericError 17 + error.Message.Contains("17") |> equal true + match error with + | NamedNumericError value -> value |> equal 17 + | _ -> failwith "Expected NamedNumericError" + +[] +let ``test aggregate exception formats non-string custom exception messages`` () = + let error = ExitError(Failed "inner worker stopped") + let aggregate = System.AggregateException("outer failure", [| error |]) + aggregate.Message.Contains("outer failure") |> equal true + aggregate.Message.Contains("inner worker stopped") |> equal true + obj.ReferenceEquals(error, aggregate.InnerException) |> equal true + match aggregate.InnerException with + | ExitError(Failed message) -> message |> equal "inner worker stopped" + | _ -> failwith "Expected the original ExitError" + [] let ``test empty custom exception can be caught`` () = let result = @@ -122,3 +170,76 @@ let ``test invalidArg formats the message like .NET`` () = throwsError "This is invalid (Parameter 'arg')" (fun () -> invalidArg "arg" "This is invalid" ) + +[] +let ``test invalidArg raises ArgumentException`` () = + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid (Parameter 'arg')" + +[] +let ``test invalidOp raises InvalidOperationException`` () = + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid" + +[] +let ``test nullArg raises ArgumentNullException`` () = + let caught = + try + nullArg "arg" + with + | :? System.ArgumentNullException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "Value cannot be null. (Parameter 'arg')" + +let private paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +[] +let ``test ArgumentException.ParamName works`` () = + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", box 5, "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + +[] +let ``test ArgumentException.ParamName of FSharp.Core errors works`` () = + paramName (fun () -> List.exactlyOne [ 1; 2 ] |> ignore) |> equal "source" + paramName (fun () -> Seq.head Seq.empty |> ignore) |> equal "source" + paramName (fun () -> System.Array.BinarySearch([| 1; 3; 5 |], -1, 2, 5) |> ignore) |> equal "index" + paramName (fun () -> Array.sub [| 1 |] 0 5 |> ignore) |> equal "count" + paramName (fun () -> Array.sub [| 1 |] -1 1 |> ignore) |> equal "startIndex" + +[] +let ``test ArgumentException parameter name is evaluated once`` () = + let calls = ref 0 + let name () = + calls.Value <- calls.Value + 1 + "x" + paramName (fun () -> invalidArg (name ()) "m") |> equal "x" + paramName (fun () -> nullArg (name ())) |> equal "x" + System.ArgumentException("m", name ()).ParamName |> equal "x" + System.ArgumentNullException(name (), "m").ParamName |> equal "x" + calls.Value |> equal 4 diff --git a/tests/Beam/Fable.Tests.Beam.fsproj b/tests/Beam/Fable.Tests.Beam.fsproj index f8344cb3ff..f310f1716d 100644 --- a/tests/Beam/Fable.Tests.Beam.fsproj +++ b/tests/Beam/Fable.Tests.Beam.fsproj @@ -1,6 +1,10 @@ Exe + + false + false + false net10.0 Major false @@ -10,17 +14,24 @@ - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + + + @@ -101,6 +112,6 @@ - + diff --git a/tests/Beam/HashSetTests.fs b/tests/Beam/HashSetTests.fs index 3782dca90b..306a29e2bc 100644 --- a/tests/Beam/HashSetTests.fs +++ b/tests/Beam/HashSetTests.fs @@ -391,32 +391,34 @@ let ``test HashSet.CopyTo works`` () = arr3[3..] |> Array.distinct |> Array.length |> equal 4 arr3.Length |> equal 7 -// [] -// let ``test HashSet IReadOnlyCollection.Count works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> IReadOnlyCollection<_> -// coll.Count |> equal 3 - -// [] -// let ``test HashSet ICollection.IsReadOnly works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.IsReadOnly |> equal false +[] +let ``test HashSet IReadOnlyCollection.Count works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> IReadOnlyCollection<_> + coll.Count |> equal 3 -// [] -// let ``test HashSet ICollection.Count works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.Count |> equal 3 +[] +let ``test HashSet ICollection.IsReadOnly works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.IsReadOnly |> equal false -// [] -// let ``test HashSet ICollection.Contains works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.Contains(("B", 3)) |> equal false -// coll.Contains(("D", 3)) |> equal false -// coll.Contains(("B", 2)) |> equal true +[] +let ``test HashSet ICollection.Count works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.Count |> equal 3 +[] +let ``test HashSet ICollection.Contains works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.Contains(("B", 3)) |> equal false + coll.Contains(("D", 3)) |> equal false + coll.Contains(("B", 2)) |> equal true + +// TODO: Interface-typed collection calls do not retain the concrete HashSet kind, +// so ICollection.CopyTo cannot select fable_hashset:copy_to yet. // [] // let ``test HashSet ICollection.CopyTo works`` () = // let xs = [| ("A", 1); ("B", 2); ("C", 3) |] @@ -425,13 +427,15 @@ let ``test HashSet.CopyTo works`` () = // coll.CopyTo(ys, 0) // ys = xs |> equal true -// [] -// let ``test HashSet ICollection.Clear works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.Clear() -// coll.Count |> equal 0 - +[] +let ``test HashSet ICollection.Clear works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.Clear() + coll.Count |> equal 0 + +// TODO: Interface-typed collection calls do not retain the concrete HashSet kind, +// so ICollection.Add cannot select fable_hashset:add yet. // [] // let ``test HashSet ICollection.Add works`` () = // let xs = [| ("A", 1); ("B", 2); ("C", 3) |] @@ -441,11 +445,11 @@ let ``test HashSet.CopyTo works`` () = // coll.Add(("D", 4)) // coll.Count |> equal 5 -// [] -// let ``test HashSet ICollection.Remove works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.Remove(("B", 3)) |> equal false -// coll.Remove(("D", 3)) |> equal false -// coll.Remove(("B", 2)) |> equal true -// coll.Count |> equal 2 +[] +let ``test HashSet ICollection.Remove works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.Remove(("B", 3)) |> equal false + coll.Remove(("D", 3)) |> equal false + coll.Remove(("B", 2)) |> equal true + coll.Count |> equal 2 diff --git a/tests/Beam/InteropTests.fs b/tests/Beam/InteropTests.fs index 7c4ac25c19..749714d495 100644 --- a/tests/Beam/InteropTests.fs +++ b/tests/Beam/InteropTests.fs @@ -104,6 +104,13 @@ type ErasedUnion = type ErasedUnionWithMultipleFields = | ErasedUnionWithMultipleFields of string * int +type ErasedPayload = { Description: string; Membership: int } + +[] +type ErasedWrapper = + | ErasedSuccess of ErasedPayload + | ErasedFailure of string + // ============================================================ // StringEnum tests // ============================================================ @@ -408,6 +415,19 @@ let ``test Erased types can have members`` () = () #endif +[] +let ``test Erased union payload fields can be read`` () = +#if FABLE_COMPILER + let payload = { Description = "a"; Membership = 2 } + + match ErasedSuccess payload with + | ErasedSuccess details -> details.Membership + | ErasedFailure _ -> -1 + |> equal 2 +#else + () +#endif + [] let ``test Erased unions with multiple fields work`` () = #if FABLE_COMPILER @@ -436,6 +456,23 @@ let ``test !^ implicit cast for erased unions works`` () = () #endif +[] +let ``test implicit conversion to erased unions works`` () = +#if FABLE_COMPILER + let strify (x: U3) = + match x with + | U3.Case1 i -> "i: " + string i + | U3.Case2 s -> "s: " + s + | U3.Case3 b -> if b then "b: yes" else "b: no" + let s: U2 = "foo" + s |> equal (U2.Case2 "foo") + strify 42 |> equal "i: 42" + strify "foo" |> equal "s: foo" + strify true |> equal "b: yes" +#else + () +#endif + [] let ``test StringEnum works`` () = #if FABLE_COMPILER diff --git a/tests/Beam/ListTests.fs b/tests/Beam/ListTests.fs index 2733453992..a3031010d9 100644 --- a/tests/Beam/ListTests.fs +++ b/tests/Beam/ListTests.fs @@ -690,6 +690,12 @@ let ``test List.randomSampleBy works`` () = throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) +[] +let ``test List random functions match FSharp.Core`` () = + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + throwsErrorContaining "The input sequence was empty." (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + [] let ``test List.updateAt works`` () = equal [0; 2; 3; 4; 5] (List.updateAt 0 0 [1..5]) @@ -906,3 +912,67 @@ let ``test List.range works`` () = [0..2..9] |> List.reduce (+) |> equal 20 + +[] +let ``test List.take raises InvalidOperationException when the list is too short`` () = + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + +[] +let ``test List.exactlyOne raises ArgumentException unless the list has one element`` () = + raisedExceptionType (fun () -> List.exactlyOne []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.exactlyOne [1; 2]) |> equal "ArgumentException" + List.exactlyOne [1] |> equal 1 + +[] +let ``test List.find and friends raise KeyNotFoundException when nothing matches`` () = + raisedExceptionType (fun () -> List.find (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.findBack (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.findIndex (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.findIndexBack (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.pick (fun _ -> None: int option) [1]) |> equal "KeyNotFoundException" + +[] +let ``test List random functions raise ArgumentOutOfRangeException when the randomizer is out of range`` () = + raisedExceptionType (fun () -> List.randomShuffleBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> -0.1) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + +[] +let ``test List random functions raise ArgumentException for invalid inputs`` () = + raisedExceptionType (fun () -> List.randomChoice []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomChoices -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomChoices 1 []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomSample -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomSample 3 [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomSample 1 []) |> equal "ArgumentException" + +[] +let ``test List.forall2 and List.exists2 stop once the result is known`` () = + List.forall2 (fun _ _ -> false) [1; 2] [1; 2; 3] |> equal false + List.exists2 (fun _ _ -> true) [1; 2] [1; 2; 3] |> equal true + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> true) [1; 2] [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> true) [1; 2; 3] [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.exists2 (fun _ _ -> false) [1; 2] [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.exists2 (fun _ _ -> false) [1] []) |> equal "ArgumentException" + +[] +let ``test List.chunkBySize, List.windowed and List.splitInto reject a size below 1`` () = + raisedExceptionType (fun () -> List.chunkBySize 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.chunkBySize -1 []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.windowed 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.windowed -1 []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitInto 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitInto -4 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitInto -1 []) |> equal "ArgumentException" + +[] +let ``test List.splitInto works with an empty list`` () = + List.splitInto 1 [] |> equal [] + List.splitInto 3 [] |> equal [] + +[] +let ``test List.transpose raises ArgumentException for lists of different lengths`` () = + raisedExceptionType (fun () -> List.transpose [[1]; [2; 3]]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[1; 2]; [3]]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[]; [1]]) |> equal "ArgumentException" diff --git a/tests/Beam/Main.fs b/tests/Beam/Main.fs deleted file mode 100644 index b00af3eaac..0000000000 --- a/tests/Beam/Main.fs +++ /dev/null @@ -1,6 +0,0 @@ -#if FABLE_COMPILER -module Program -() -#else -module Program = let [] main _ = 0 -#endif diff --git a/tests/Beam/MapTests.fs b/tests/Beam/MapTests.fs index 68c4fcf07a..a99b869105 100644 --- a/tests/Beam/MapTests.fs +++ b/tests/Beam/MapTests.fs @@ -409,3 +409,9 @@ let ``test Map.toSeq works`` () = let zs = Map.toSeq ys (Seq.item 2 xs |> snd) = (Seq.item 2 zs |> snd) |> equal true + +[] +let ``test Map.findKey and Map.pick raise KeyNotFoundException when nothing matches`` () = + let m = Map.ofList [ (1, "a"); (2, "b") ] + raisedExceptionType (fun () -> Map.findKey (fun k _ -> k > 5) m) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Map.pick (fun _ _ -> None: int option) m) |> equal "KeyNotFoundException" diff --git a/tests/Beam/MemberNameCollisionLibrary/Library.fs b/tests/Beam/MemberNameCollisionLibrary/Library.fs new file mode 100644 index 0000000000..36fcc7b668 --- /dev/null +++ b/tests/Beam/MemberNameCollisionLibrary/Library.fs @@ -0,0 +1,26 @@ +module Fable.Tests.MemberNameCollisionLibrary + +type Response() = + let mutable status = 0 + + member _.StatusCode + with get () = status + and set (value: int) = status <- value + + member _.SetStatusCode(value: int) = status <- value + 1 + member _.GetStatusCode() = status + 2 + +let propertyPath () = + let response = Response() + response.StatusCode <- 201 + response.StatusCode + +let methodPath () = + let response = Response() + response.SetStatusCode 201 + response.StatusCode + +let getterMethodPath () = + let response = Response() + response.StatusCode <- 201 + response.GetStatusCode() diff --git a/tests/Beam/MemberNameCollisionLibrary/MemberNameCollisionLibrary.fsproj b/tests/Beam/MemberNameCollisionLibrary/MemberNameCollisionLibrary.fsproj new file mode 100644 index 0000000000..df9b203b99 --- /dev/null +++ b/tests/Beam/MemberNameCollisionLibrary/MemberNameCollisionLibrary.fsproj @@ -0,0 +1,9 @@ + + + net10.0 + false + + + + + diff --git a/tests/Beam/MemberNamingTests.fs b/tests/Beam/MemberNamingTests.fs new file mode 100644 index 0000000000..2c193a4e5c --- /dev/null +++ b/tests/Beam/MemberNamingTests.fs @@ -0,0 +1,42 @@ +module Fable.Tests.MemberNamingTests + +open Fable.Tests.Util +open Util.Testing +open Fable.Tests.MemberNameCollisionLibrary + +[] +let ``test Same-file property setter keeps its own body`` () = + propertyPath () |> equal 201 + +[] +let ``test Same-file ordinary setter method keeps its own body`` () = + methodPath () |> equal 202 + +[] +let ``test Same-file ordinary getter method keeps its own body`` () = + getterMethodPath () |> equal 203 + +[] +let ``test Referenced-library property setter resolves across files`` () = + let response = Response() + response.StatusCode <- 201 + response.StatusCode |> equal 201 + +[] +let ``test Referenced-library ordinary setter method resolves across files`` () = + let response = Response() + response.SetStatusCode 201 + response.StatusCode |> equal 202 + +[] +let ``test Referenced-library ordinary getter method resolves across files`` () = + let response = Response() + response.StatusCode <- 201 + response.GetStatusCode() |> equal 203 + +[] +let ``test Referenced-library method reference keeps the ordinary method body`` () = + let response = Response() + let setStatus = response.SetStatusCode + setStatus 201 + response.StatusCode |> equal 202 diff --git a/tests/Beam/MiscTests.fs b/tests/Beam/MiscTests.fs index f57f446add..764d23b0e0 100644 --- a/tests/Beam/MiscTests.fs +++ b/tests/Beam/MiscTests.fs @@ -30,6 +30,12 @@ let log2 (a: string) (b: string) = String.Format("a = {0}, b = {1}", a, b) let logItem1 = log2 "item1" let logItem2 = log2 "item2" +let ``punctuation-name`` () = 1 +let punctuation_name () = 2 + +let ``abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh`` () = + 3 + type PartialFunctions() = member _.logItem1 = log2 "item1" member _.logItem2 = log2 "item2" @@ -357,7 +363,6 @@ let ``test Custom computation expressions work`` () = execMaybe 99 |> equal None // -- Self References in Constructors -- -// TODO: "as myself" self-reference in constructors generates {badkey,x} — class constructor codegen issue [] let ``test Self references in constructors work`` () = @@ -445,6 +450,15 @@ let ``test Conversion to Action works`` () = f3'.Invoke(10) equal 40 myField +[] +let ``test module functions with punctuation remain distinct`` () = + (``punctuation-name`` (), punctuation_name ()) |> equal (1, 2) + +[] +let ``test escaped function names stay within the Erlang atom limit`` () = + ``abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh`` () + |> equal 3 + // -- Object Expressions -- [] @@ -489,15 +503,13 @@ let ``test Composition with recursive this works`` () = RecursiveType(fun f -> x <- f()) |> ignore equal 11 x -// TODO: Re-enable when Beam target disambiguates local let bindings from same-named members -// (MyComponent has `let work i = ...` and `member _.Work i = work i` which collide after sanitization) -// [] -// let ``test References to enclosing type from object expression work`` () = -// MyComponent("TestA").works1().doWork() |> equal "TestA-1" -// MyComponent("TestB").works2().doWork() |> equal "TestB-2" -// MyComponent("TestC").works3().doWork() |> equal "TestC-3" -// MyComponent("TestD").works4().doWork() |> equal "TestD-4" -// MyComponent("TestE").works5().doWork() |> equal "TestE-5" +[] +let ``test References to enclosing type from object expression work`` () = + MyComponent("TestA").works1().doWork() |> equal "TestA-1" + MyComponent("TestB").works2().doWork() |> equal "TestB-2" + MyComponent("TestC").works3().doWork() |> equal "TestC-3" + MyComponent("TestD").works4().doWork() |> equal "TestD-4" + MyComponent("TestE").works5().doWork() |> equal "TestE-5" [] let ``test Inlined object expression doesn't change argument this context`` () = @@ -734,6 +746,21 @@ let ``test try-with with unmatched exception type reraises`` () = | _ -> caught <- "other" caught |> equal "arg" +[] +let ``test built-in exception type tests respect nominal hierarchy`` () = + let invalidOperation: exn = System.InvalidOperationException("invalid") + let argumentNull: exn = System.ArgumentNullException("value") + let divideByZero: exn = System.DivideByZeroException() + let aggregate: exn = System.AggregateException() + + (invalidOperation :? System.InvalidOperationException) |> equal true + (invalidOperation :? System.ArgumentException) |> equal false + (box invalidOperation :? System.Exception) |> equal true + (argumentNull :? System.ArgumentException) |> equal true + (divideByZero :? System.ArithmeticException) |> equal true + (aggregate :? System.AggregateException) |> equal true + (aggregate :? System.ArgumentException) |> equal false + [] let ``test ArgumentException with message and inner exception works`` () = let inner = exn "the inner cause" @@ -741,11 +768,105 @@ let ``test ArgumentException with message and inner exception works`` () = ex.Message |> equal "outer message" ex.InnerException.Message |> equal "the inner cause" +[] +let ``test ArgumentException default message matches .NET`` () = + let ex = System.ArgumentException() + ex.Message |> equal "Value does not fall within the expected range." + +[] +let ``test ArgumentException with message and parameter name works`` () = + let ex = System.ArgumentException("outer message", "arg") + ex.Message |> equal "outer message (Parameter 'arg')" + +[] +let ``test ArgumentException with message, parameter name and inner exception works`` () = + let inner = exn "the inner cause" + let ex = System.ArgumentException("outer message", "arg", inner) + ex.Message |> equal "outer message (Parameter 'arg')" + ex.InnerException.Message |> equal "the inner cause" + +[] +let ``test ArgumentNullException with parameter name works`` () = + let ex = System.ArgumentNullException("arg") + ex.Message |> equal "Value cannot be null. (Parameter 'arg')" + +[] +let ``test ArgumentNullException with parameter name and message works`` () = + let ex = System.ArgumentNullException("arg", "custom message") + ex.Message |> equal "custom message (Parameter 'arg')" + +[] +let ``test ArgumentOutOfRangeException with parameter name works`` () = + let ex = System.ArgumentOutOfRangeException("arg") + ex.Message |> equal "Specified argument was out of the range of valid values. (Parameter 'arg')" + [] let ``test Exception InnerException is null when not provided`` () = let ex = System.ArgumentException("no inner") isNull (box ex.InnerException) |> equal true +[] +let ``test AggregateException retains all supplied exceptions`` () = + let first = Exception("first") + let second = Exception("second") + let aggregate = AggregateException(first, second) + + aggregate.Message |> equal "One or more errors occurred. (first) (second)" + aggregate.InnerException.Message |> equal "first" + + aggregate.InnerExceptions + |> Seq.map (fun ex -> ex.Message) + |> Seq.toList + |> equal [ "first"; "second" ] + +[] +let ``test AggregateException practical constructors work`` () = + let first = Exception("first") + let second = Exception("second") + let empty = AggregateException() + let messageOnly = AggregateException("explicit") + let exceptions = AggregateException([| first; second |]) + let messageAndExceptions = AggregateException("explicit", [| first; second |]) + + empty.Message |> equal "One or more errors occurred." + empty.InnerExceptions |> Seq.isEmpty |> equal true + messageOnly.Message |> equal "explicit" + messageOnly.InnerExceptions |> Seq.isEmpty |> equal true + exceptions.InnerException.Message |> equal "first" + messageAndExceptions.Message |> equal "explicit (first) (second)" + +[] +let ``test raised AggregateException retains its details when caught`` () = + let first = Exception("first") + let second = Exception("second") + let aggregate = AggregateException("explicit", [| first; second |]) + + let caughtMessage, caughtInnerMessages = + try + raise aggregate + failwith "unreachable" + with caught -> + let caught = unbox caught + + caught.Message, + (caught.InnerExceptions + |> Seq.map (fun ex -> ex.Message) + |> Seq.toList) + + caughtMessage |> equal "explicit (first) (second)" + caughtInnerMessages |> equal [ "first"; "second" ] + +[] +let ``test ordinary Exception and ArgumentException retain inner exceptions`` () = + let inner = Exception("inner") + let plain = Exception("plain", inner) + let argument = ArgumentException("argument", inner) + + plain.Message |> equal "plain" + plain.InnerException.Message |> equal "inner" + argument.Message |> equal "argument" + argument.InnerException.Message |> equal "inner" + // -- General / Misc -- [] diff --git a/tests/Beam/ModuleNamingTests.fs b/tests/Beam/ModuleNamingTests.fs index 1ff43a4717..f84f7f92ed 100644 --- a/tests/Beam/ModuleNamingTests.fs +++ b/tests/Beam/ModuleNamingTests.fs @@ -31,3 +31,107 @@ let ``test Module named String does not resolve to OTP string`` () = let ``test Same-named files in different directories both survive`` () = Naming.First.Types.area (Naming.First.Types.Circle 2.0) |> equal 12.0 Naming.Second.Types.name Naming.Second.Types.Red |> equal "red" + +// --- Names longer than Erlang's 255-character atom limit --- + +// Both names are 263 characters and share their first 260, so the truncated prefix alone +// cannot tell them apart; an overlong atom fails in `erlc`, a merged one calls the wrong body. +let private overlongfunctionnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxone () = 1 + +let private overlongfunctionnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtwo () = 2 + +[] +let ``test Names over the atom length limit stay valid and distinct`` () = + overlongfunctionnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxone () |> equal 1 + overlongfunctionnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtwo () |> equal 2 + +// The atom limiter caps each sanitized name at 223, so anything joined onto a capped name +// (`_reflection`, `_`) overflows the limit again unless re-capped. +type Overlongrecordnamerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrend = { Value: int } + +type Overlongclassnamecccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccend(value: int) = + member _.Overlongpropertynameppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppend = value + +[] +let ``test Atoms composed from capped names stay within the limit`` () = + let record = { Overlongrecordnamerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrend.Value = 1 } + record.Value |> equal 1 + Overlongclassnamecccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccend(2).Overlongpropertynameppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppend |> equal 2 + +// A `[]` is used as the runtime tag verbatim, so it has to be capped like any +// other atom. Codegen and the reflection `erl_tag` must agree on the capped form. +type OverlongTag = + | [] Tagged + | Other + +[] +let ``test Overlong CompiledName union tags stay valid and distinct`` () = + Tagged = Other |> equal false + Tagged = Tagged |> equal true + + match Tagged with + | Tagged -> () + | Other -> failwith "wrong case" + +// These names collide under djb2 once the emitting module name is added to the state key. +let mutable private aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2 = 1 +let mutable private aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafp = 2 + +[] +let ``test Bounded mutable state keys preserve distinct values`` () = + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2 |> equal 1 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafp |> equal 2 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2 <- 11 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafp <- 22 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2 |> equal 11 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafp |> equal 22 + +type ILongProperty = + abstract pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp: int with get, set + +type LongPropertyClass() = + let mutable value = 0 + + interface ILongProperty with + member _.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp + with get () = value + and set v = value <- v + +[] +let ``test Long interface setters work on object expressions`` () = + let mutable value = 0 + + let instance = + { new ILongProperty with + member _.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp + with get () = value + and set v = value <- v + } + + instance.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp <- 42 + instance.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp |> equal 42 + value |> equal 42 + +[] +let ``test Long interface setters work on classes`` () = + let instance = LongPropertyClass() :> ILongProperty + instance.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp <- 42 + instance.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp |> equal 42 + +type Ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc() = + override _.ToString() = "ok" + override _.GetHashCode() = 42 + +[] +let ``test Long override entity names resolve consistently`` () = + let instance = Ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc() + string instance |> equal "ok" + instance.ToString() |> equal "ok" + instance.GetHashCode() |> equal 42 + +type HashOverride() = + override _.GetHashCode() = 43 + +[] +let ``test Direct hash code calls use class overrides`` () = + HashOverride().GetHashCode() |> equal 43 diff --git a/tests/Beam/NonRegressionTests.fs b/tests/Beam/NonRegressionTests.fs index db7275e807..ab410f7209 100644 --- a/tests/Beam/NonRegressionTests.fs +++ b/tests/Beam/NonRegressionTests.fs @@ -164,3 +164,32 @@ module IssueInlineMutualRec = [] let ``test inline outer with nested mutual recursion captures outer scope`` () = IssueInlineMutualRec.outer 0 |> equal "ok" + +let spawn first second = first, second +let start first second = spawn first second + +[] +let erlangSpawn (moduleName: obj) (functionName: obj) (args: obj) : obj = nativeOnly + +let sendSpawned parent = Fable.Core.BeamInterop.emitErlExpr parent "$0 ! spawned" + +[] +let ``test local spawn function is not replaced by Erlang BIF`` () = + start 1 2 |> equal (1, 2) + +[] +let ``test explicitly imported Erlang spawn BIF stays qualified`` () = +#if FABLE_COMPILER + let parent: obj = Fable.Core.BeamInterop.emitErlExpr () "erlang:self()" + let moduleName: obj = Fable.Core.BeamInterop.emitErlExpr () "?MODULE" + let functionName: obj = Fable.Core.BeamInterop.emitErlExpr () "send_spawned" + let args: obj = Fable.Core.BeamInterop.emitErlExpr parent "[$0]" + erlangSpawn moduleName functionName args |> ignore + + let received: bool = + Fable.Core.BeamInterop.emitErlExpr () "receive spawned -> true after 1000 -> false end" + + received |> equal true +#else + () +#endif diff --git a/tests/Beam/QuotationTests.fs b/tests/Beam/QuotationTests.fs index f0a5d41a22..6e29661cdd 100644 --- a/tests/Beam/QuotationTests.fs +++ b/tests/Beam/QuotationTests.fs @@ -4,6 +4,7 @@ open Fable.Tests.Util open Util.Testing open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Microsoft.FSharp.Linq.RuntimeHelpers type QuotationTestUnion = @@ -283,3 +284,33 @@ let ``test Call on an instance whose value is null keeps a Some instance`` () = if not (hasSomeInstancePropertyGet q) then failwith "Expected a PropertyGet with a Some instance, even though its value is null" + +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + +[] +let ``test DerivedPatterns AndAlso, OrElse and SpecificCall work`` () = + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" diff --git a/tests/Beam/RandomTests.fs b/tests/Beam/RandomTests.fs index 06eeacfe95..0f5a3091ef 100644 --- a/tests/Beam/RandomTests.fs +++ b/tests/Beam/RandomTests.fs @@ -112,3 +112,20 @@ let ``test System.Random.NextBytes seeded works`` () = Random(5432).NextBytes(buffer1) Random(5432).NextBytes(buffer2) buffer1 |> equal buffer2 + +[] +let ``test System.Random gives .NET's argument errors`` () = + for rnd in [Random(42); Random()] do + throwsErrorContaining "maxValue ('-1') must be a non-negative value. (Parameter 'maxValue')" (fun () -> rnd.Next(-1)) + throwsErrorContaining "'minValue' cannot be greater than maxValue. (Parameter 'minValue')" (fun () -> rnd.Next(5, 3)) + raisedExceptionType (fun () -> rnd.Next(-1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> rnd.Next(5, 3)) |> equal "ArgumentOutOfRangeException" + rnd.Next(5, 5) |> equal 5 + +[] +let ``test System.Random.Shared works`` () = + let x = Random.Shared.Next(5) + (x >= 0 && x < 5) |> equal true + let x = Random.Shared.NextDouble() + (x >= 0.0 && x < 1.0) |> equal true + obj.ReferenceEquals(Random.Shared, Random.Shared) |> equal true diff --git a/tests/Beam/RecordTests.fs b/tests/Beam/RecordTests.fs index 685e89ee6c..8c408a97fd 100644 --- a/tests/Beam/RecordTests.fs +++ b/tests/Beam/RecordTests.fs @@ -1,6 +1,7 @@ module Fable.Tests.RecordTests open Fable.Tests.Util +open Fable.Tests.UnionTypes open Util.Testing type Person = { Name: string; Age: int } @@ -17,8 +18,9 @@ type JSKiller = type ErlangKeywordRecord = { Receive: string; Case: int; Fun: bool } -// Note: Records with spaces/symbols in field names are not supported in Beam -// as Erlang atoms can't contain these characters +type SpecialCharacterRecord = + { ``s p a c e``: float + ``s*y*m*b*o*l``: float } type MutatingRecord = { uniqueA: int; uniqueB: int } @@ -41,6 +43,43 @@ type RecordWithProperty = { items: string list } member this.fullName = String.concat " - " this.items +type IntegrityRecord = + { Level: string } + + override this.ToString() = this.Level + +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +[] +type StructRecordOrdered = { OrdA: int; OrdB: int; OrdC: int } + +type ByRefClassRecord = { BcP: int; BcQ: int } + +type ByRefClass = + val mutable BkF: int + val BkV: int + new() = { BkF = 0; BkV = 3 } + +let sumStructRecordByRef (r: byref) = r.OrdA + r.OrdC + +let copyStructRecordByRef (r: byref) = { r with OrdB = 99 } + +let sumClassRecordByRef (r: byref) = r.BcP + r.BcQ + +let setClassRecordOutRef (r: outref) = + r <- { BcP = 4; BcQ = 1 } + r.BcP + r.BcQ + +let sumClassByRef (r: byref) = r.BkF + r.BkV + +let sumAnonymousStructRecordByRef (x: byref) = x.AnX + x.AnY + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + [] let ``test simple record creation works`` () = let p = { Name = "Alice"; Age = 30 } @@ -123,7 +162,14 @@ let ``test records with Erlang reserved word fields work`` () = equal "world" r2.Receive equal 42 r2.Case -// test records with special characters - skipped for Beam (Erlang atoms can't have spaces/symbols) +[] +let ``test records with special characters are mapped correctly`` () = + let x = + { ``s p a c e`` = 1.0 + ``s*y*m*b*o*l`` = 2.0 } + + equal 1.0 x.``s p a c e`` + equal 2.0 x.``s*y*m*b*o*l`` [] let ``test mutating records work`` () = @@ -181,6 +227,24 @@ let ``test Record property access uses correct naming`` () = let x = { items = ["Hello"; "World"] } equal "Hello - World" x.fullName +[] +let ``test record ToString override works`` () = + let value: IntegrityRecord = { Level = "Untrusted" } + value.ToString() |> equal "Untrusted" + string value |> equal "Untrusted" + +[] +let ``test percent O uses record ToString override`` () = + let value: IntegrityRecord = { Level = "Untrusted" } + sprintf "%O" value |> equal "Untrusted" + +[] +let ``test ToString override can be called across files`` () = + let value = IntegrityLevel.Trusted + value.ToString() |> equal "Trusted" + string value |> equal "Trusted" + sprintf "%O" value |> equal "Trusted" + [] let ``test Anonymous records can have optional fields`` () = let add (o: {| bar: int option; zas: string option; foo: int option option |}) = @@ -279,3 +343,58 @@ let ``test Records with ref-type interior mutability`` () = x.MutRefValue |> equal "ab" x.MutRefValue <- x.MutRefValue + "c" x.MutRefValue |> equal "abc" + +[] +let ``test copy-and-update of a let mutable struct record keeps the unchanged fields`` () = + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + +[] +let ``test copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields`` () = + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + +[] +let ``test copy-and-update of a module-level mutable struct record keeps the unchanged fields`` () = + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + +[] +let ``test field reads and copy-and-update through a byref struct record parameter work`` () = + let mutable r = { OrdA = 1; OrdB = 2; OrdC = 3 } + sumStructRecordByRef &r |> equal 4 + let x = copyStructRecordByRef &r + (x.OrdA, x.OrdB, x.OrdC) |> equal (1, 99, 3) + +[] +let ``test field reads through a byref or outref to a reference type work`` () = + let mutable c = { BcP = 1; BcQ = 2 } + sumClassRecordByRef &c |> equal 3 + let mutable o = { BcP = 0; BcQ = 0 } + setClassRecordOutRef &o |> equal 5 + let mutable k = ByRefClass() + k.BkF <- 4 + sumClassByRef &k |> equal 7 + +[] +let ``test copy-and-update of a let mutable anonymous struct record keeps the unchanged fields`` () = + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) + +[] +let ``test field reads through a byref to an anonymous struct record work`` () = + let mutable a = struct {| AnX = 1; AnY = 2 |} + sumAnonymousStructRecordByRef &a |> equal 3 diff --git a/tests/Beam/RegexTests.fs b/tests/Beam/RegexTests.fs index 15e8d85953..4b0d15432b 100644 --- a/tests/Beam/RegexTests.fs +++ b/tests/Beam/RegexTests.fs @@ -164,6 +164,14 @@ let ``test Regex.Unescape works`` () = // --- Options --- +[] +let ``test RegexOptions.NonBacktracking is accepted`` () = + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + [] let ``test Regex.Options works`` () = let r = Regex("\\d+", RegexOptions.IgnoreCase ||| RegexOptions.Multiline) diff --git a/tests/Beam/ResizeArrayTests.fs b/tests/Beam/ResizeArrayTests.fs index 77391ca97f..e8d8c3e7d8 100644 --- a/tests/Beam/ResizeArrayTests.fs +++ b/tests/Beam/ResizeArrayTests.fs @@ -65,6 +65,72 @@ let ``test ResizeArray.IndexOf works`` () = ra.IndexOf(20) |> equal 1 ra.IndexOf(99) |> equal -1 +// --- BinarySearch --- + +[] +let ``test ResizeArray.BinarySearch works`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let desc = { new IComparer with member _.Compare(a, b) = compare b a } + // Beam has no Comparer.Default, so this stands in for it + let asc = { new IComparer with member _.Compare(a, b) = compare a b } + ra.BinarySearch(7) |> equal 3 + ra.BinarySearch(8) |> equal -5 + (ResizeArray [9; 7; 5; 3; 1]).BinarySearch(3, desc) |> equal 3 + ra.BinarySearch(1, 3, 7, asc) |> equal 3 + ra.BinarySearch(1, 2, 8, asc) |> equal -4 + ra.BinarySearch(1, 2, 3, null) |> equal 1 + +[] +let ``test ResizeArray.BinarySearch works with an empty list, duplicates and a null range comparer`` () = + ResizeArray().BinarySearch(1) |> equal -1 + (ResizeArray [2; 2; 2; 2]).BinarySearch(2) |> equal 1 + (ResizeArray [1; 2; 2; 2; 2; 3]).BinarySearch(2) |> equal 2 + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(0, 5, 2, null) |> equal -2 + ra.BinarySearch(2, 3, 9, null) |> equal 4 + +[] +let ``test ResizeArray.BinarySearch with a range passes the element to the comparer first`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + ra.BinarySearch(1, 3, 7, logging) |> equal 3 + List.ofSeq calls |> equal [(5, 7); (7, 7)] + +[] +let ``test ResizeArray.BinarySearch with a range checks index and count together`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(6, 0, 3, null)) |> equal "ArgumentException" + raised (fun () -> ra.BinarySearch(0, 6, 3, null)) |> equal "ArgumentException" + +[] +let ``test ResizeArray.BinarySearch with a range checks its arguments`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised index count = + try + ra.BinarySearch(index, count, 3, null) |> ignore + "none" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentException -> "ArgumentException" + | _ -> "other" + raised -1 2 |> equal "ArgumentOutOfRangeException" + raised 1 -1 |> equal "ArgumentOutOfRangeException" + raised 3 3 |> equal "ArgumentException" + // --- Remove --- [] diff --git a/tests/Beam/SeqExpressionTests.fs b/tests/Beam/SeqExpressionTests.fs index 8562a99984..5b44092b9e 100644 --- a/tests/Beam/SeqExpressionTests.fs +++ b/tests/Beam/SeqExpressionTests.fs @@ -111,25 +111,23 @@ let ``test try...with in seq expressions preserves yielded elements before the e equal [1; 3] xs equal true caught -// TODO: enable when Beam correctly rethrows exceptions not matched by a filtered -// try/with handler in seq expressions (see PR #4719 review discussion) -// [] -// let ``test try...with in seq expressions rethrows unmatched exceptions`` () = -// let mutable propagated = false -// try -// seq { -// try -// yield 1 -// raise (System.InvalidOperationException "boom") -// yield 2 -// with :? System.ArgumentException -> -// yield 0 -// } -// |> Seq.toList -// |> ignore -// with :? System.InvalidOperationException -> -// propagated <- true -// equal true propagated +[] +let ``test try...with in seq expressions rethrows unmatched exceptions`` () = + let mutable propagated = false + try + seq { + try + yield 1 + raise (System.InvalidOperationException "boom") + yield 2 + with :? System.ArgumentException -> + yield 0 + } + |> Seq.toList + |> ignore + with :? System.InvalidOperationException -> + propagated <- true + equal true propagated [] let ``test array expressions work`` () = diff --git a/tests/Beam/SeqTests.fs b/tests/Beam/SeqTests.fs index 1e43a350b3..7b5d6e83b5 100644 --- a/tests/Beam/SeqTests.fs +++ b/tests/Beam/SeqTests.fs @@ -622,6 +622,20 @@ let ``test Seq.randomSampleBy works`` () = throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) +[] +let ``test Seq.randomChoicesBy draws on enumeration`` () = + let mutable calls = 0 + let xs = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + xs |> Seq.toList |> equal [5; 5; 5] + xs |> Seq.toList |> equal [5; 5; 5] + calls |> equal 6 + let rng = System.Random(42) + let ys = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal (System.Random(42).Next()) + ys |> Seq.length |> equal 3 + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Seq.randomChoicesWith rng -1 [1]) + [] let ``test Seq.average works`` () = [1.0; 2.0; 3.0] |> Seq.average |> equal 2.0 @@ -1161,3 +1175,73 @@ let ``test Seq.toList on string works`` () = let text = "ABC" let chars = text |> Seq.toList chars |> List.length |> equal 3 + +[] +let ``test Seq.reduce and Seq.reduceBack raise ArgumentException on an empty sequence`` () = + raisedExceptionType (fun () -> Seq.reduce (+) Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) Seq.empty) |> equal "ArgumentException" + +[] +let ``test Seq.min and friends raise ArgumentException on an empty sequence`` () = + raisedExceptionType (fun () -> Seq.min Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id Seq.empty) |> equal "ArgumentException" + +[] +let ``test Seq.take and Seq.skip raise InvalidOperationException past the end`` () = + raisedExceptionType (fun () -> Seq.take 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + +[] +let ``test Seq.tail raises ArgumentException on an empty sequence`` () = + raisedExceptionType (fun () -> Seq.tail Seq.empty |> Seq.toList) |> equal "ArgumentException" + +[] +let ``test Seq random functions raise ArgumentOutOfRangeException when the randomizer is out of range`` () = + raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoicesBy (fun () -> 1.0) 1 [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Range expressions raise ArgumentException for a zero step`` () = + raisedExceptionType (fun () -> [1 .. 0 .. 5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [1L .. 0L .. 5L]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [1.0 .. 0.0 .. 5.0]) |> equal "ArgumentException" + +[] +let ``test Seq.init, Seq.replicate and Seq.take reject a negative count when called`` () = + raisedExceptionType (fun () -> Seq.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.replicate -1 0) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.take -1 [1; 2]) |> equal "ArgumentException" + +[] +let ``test Seq.chunkBySize, Seq.windowed and Seq.splitInto reject a size below 1 when called`` () = + raisedExceptionType (fun () -> Seq.chunkBySize 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.chunkBySize -1 Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed -1 Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto -1 Seq.empty) |> equal "ArgumentException" + +[] +let ``test Seq.splitInto works with an empty sequence`` () = + Seq.splitInto 2 Seq.empty |> Seq.toList |> equal [] + +[] +let ``test Seq.removeManyAt raises only when the sequence is too short for index`` () = + raisedExceptionType (fun () -> Seq.removeManyAt -1 1 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 [1; 2; 3] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 0 0 Seq.empty |> Seq.toList) |> equal "ArgumentException" + Seq.removeManyAt 1 3 [1; 2; 3] |> Seq.toList |> equal [1] + Seq.removeManyAt 2 4 [1; 2; 3] |> Seq.toList |> equal [1; 2] + Seq.removeManyAt 1 0 [1; 2; 3] |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 0 -1 [1; 2; 3] |> Seq.toList |> equal [1; 2; 3] + +[] +let ``test Seq.transpose groups jagged input by index`` () = + [[1; 2]; [3]] |> Seq.transpose |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 3]; [2]] + [|[|1|]; [|2; 3|]|] |> Seq.transpose |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 2]; [3]] + [[1; 2; 3]; [4]; [5; 6]] |> Seq.transpose |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 4; 5]; [2; 6]; [3]] + [[]; [1]] |> Seq.transpose |> Seq.map Seq.toList |> Seq.toList |> equal [[1]] diff --git a/tests/Beam/StringTests.fs b/tests/Beam/StringTests.fs index 3cee1934f6..1ecc913dd8 100644 --- a/tests/Beam/StringTests.fs +++ b/tests/Beam/StringTests.fs @@ -311,6 +311,21 @@ let ``test String.Split works`` () = parts.[1] |> equal "b" parts.[2] |> equal "c" +[] +let ``test String.Split with character array separators works`` () = + "one,twoéthree".Split([| ','; 'é' |]) + |> equal [| "one"; "two"; "three" |] + +[] +let ``test String.Split with character array removes empty entries`` () = + " one two ".Split([| ' ' |], StringSplitOptions.RemoveEmptyEntries) + |> equal [| "one"; "two" |] + +[] +let ``test String.Split with string array separators and count works`` () = + "one,two.three".Split([| ","; "." |], 2, StringSplitOptions.None) + |> equal [| "one"; "two.three" |] + [] let ``test String.Split with char works`` () = "a b c d".Split(' ') diff --git a/tests/Beam/TryCatchTests.fs b/tests/Beam/TryCatchTests.fs index 2d6ab7d4b7..388023d10b 100644 --- a/tests/Beam/TryCatchTests.fs +++ b/tests/Beam/TryCatchTests.fs @@ -3,6 +3,16 @@ module Fable.Tests.TryCatch open Fable.Tests.Util open Util.Testing +#if FABLE_COMPILER_BEAM +open Fable.Core + +[] +let private throwNative () : unit = nativeOnly + +[ true; _:_ -> false end")>] +let private catchesNativeThrow (f: unit -> unit) : bool = nativeOnly +#endif + [] let ``test try-catch returns body value on success`` () = let result = @@ -96,6 +106,18 @@ let ``test reraise in try-catch with side effect`` () = sideEffectRan |> equal true msg |> equal "boom" +#if FABLE_COMPILER_BEAM +[] +let ``test reraise preserves native Erlang exception class`` () = + let rethrow () = + try + throwNative () + with _ -> + reraise () + + catchesNativeThrow rethrow |> equal true +#endif + [] let ``test try-catch result used in match`` () = // Regression: try/catch result fed into pattern match (the Seq.fs generateWhileSome pattern) diff --git a/tests/Beam/TypeTests.fs b/tests/Beam/TypeTests.fs index 51a3d08ffb..0160ed16d0 100644 --- a/tests/Beam/TypeTests.fs +++ b/tests/Beam/TypeTests.fs @@ -60,6 +60,32 @@ type Point2D = new(xy: float) = { X = xy; Y = xy } end +type ClassWithDefaultValueInstanceFields = + val X: int + [] + val mutable D: int + [] + val mutable S: string + val Y: string + new(x, y) = { X = x; Y = y } + +[] +type StructWithDefaultValueInstanceField = + val X: int + [] + val mutable D: int + val Y: string + new(x, y) = { X = x; Y = y } + +type ClassWithDefaultValueStaticAndInstanceFields = + [] + static val mutable private count: int + val X: int + [] + val mutable D: int + val Y: int + new(x, y) = { X = x; Y = y } + exception MyEx of int * string type MyEx2(f: float) = @@ -652,6 +678,32 @@ let ``test Other Value Types work`` () = let p = Point2D(2.) p.Y |> equal 2. +[] +let ``test Instance DefaultValue fields are skipped by explicit constructors`` () = + let c = ClassWithDefaultValueInstanceFields(1, "y") + c.X |> equal 1 + c.D |> equal 0 + c.S |> equal null + c.Y |> equal "y" + c.D <- 5 + c.S <- "s" + c.D |> equal 5 + c.S |> equal "s" + + let mutable s = StructWithDefaultValueInstanceField(2, "z") + s.X |> equal 2 + s.D |> equal 0 + s.Y |> equal "z" + s.D <- 6 + s.D |> equal 6 + +[] +let ``test Explicit constructors skip static fields next to DefaultValue fields`` () = + let c = ClassWithDefaultValueStaticAndInstanceFields(1, 2) + c.X |> equal 1 + c.D |> equal 0 + c.Y |> equal 2 + [] let ``test Struct with mutable fields works`` () = let x = ValueTypeR(-10.0) diff --git a/tests/Beam/UnionTypeTests.fs b/tests/Beam/UnionTypeTests.fs index c0d71a8dc5..0a88809eef 100644 --- a/tests/Beam/UnionTypeTests.fs +++ b/tests/Beam/UnionTypeTests.fs @@ -14,31 +14,120 @@ type Shape = | Square of side: float | Rectangle of width: float * height: float -type MyUnion = +type TestUnion = | Case0 | Case1 of string | Case2 of string * string | Case3 of string * string * string -type MyUnion2 = +type TestUnion2 = | Tag of string | NewTag of string -type IntUnion = - | IntCase1 of int - | IntCase2 of int - | IntCase3 of int +type IntegrityLevel = + | Untrusted + | Trusted + + override this.ToString() = + match this with + | Untrusted -> "Untrusted" + | Trusted -> "Trusted" + +type T1 = T1 +type T2 = T2 +type T3 = T3 +type T4 = T4 +type T5 = T5 +type T6 = T6 +type T7 = T7 +type T8 = T8 +type T9 = T9 +type 'a more = More of 'a + +[] +type LangCode = + | EN + | ``pt-Br`` + +type S = S of string + +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id + +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +// [] +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First [] type StructPoint2D = | Planar2D of float * float | Infinity +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + // member this.Area = // TODO: support instance members + static member Area this = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + // member this.Area = // TODO: support instance members + static member Area this = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + type WrappedUnion = | AString of string -type T1 = T1 - type DeepRecord = { Value: string } type DeepWrappedUnion = @@ -48,7 +137,167 @@ type DeepWrappedUnion = | DeepWrappedD of DeepRecord | DeepWrappedE of int * int | DeepWrappedF of WrappedUnion - | DeepWrappedG of {| X: DeepRecord; Y: int |} + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" + +type ForwardedValue = ForwardedValue of obj + +type GenericForwardedValue<'T> = GenericForwardedValue of 'T + +type ForwardedPayload = { Pid: obj; Reason: obj } + +type RuntimeTaggedUnion = + | [] RuntimeEmpty + | [] RuntimePayload of obj + | [] RuntimePair of obj * obj + +let private unwrapForwardedValue (message: obj) = + match message with + | :? ForwardedValue as forwarded -> + let (ForwardedValue value) = forwarded + Some value + | _ -> None + +let private unwrapGenericForwardedValue<'T> (message: obj) = + match message with + | :? GenericForwardedValue<'T> as forwarded -> + let (GenericForwardedValue value) = forwarded + Some value + | _ -> None + +let private isMyUnion (message: obj) = message :? TestUnion +let private isEither (message: obj) = message :? Either +let private isGender (message: obj) = message :? Gender +let private isT1 (message: obj) = message :? T1 +let private isRuntimeTaggedUnion (message: obj) = message :? RuntimeTaggedUnion + +[] +let ``test boxed single case union type test unwraps payload`` () = + unwrapForwardedValue (box (ForwardedValue (box 42))) |> equal (Some (box 42)) + +[] +let ``test boxed generic union type test unwraps payload`` () = + unwrapGenericForwardedValue (box (GenericForwardedValue 42)) |> equal (Some 42) + unwrapGenericForwardedValue (box (GenericForwardedValue "value")) |> equal (Some "value") + +[] +let ``test boxed multi case union type tests recognize every case`` () = + [ Case0; Case1 "one"; Case2 ("one", "two"); Case3 ("one", "two", "three") ] + |> List.iter (fun value -> isMyUnion (box value) |> equal true) + [ Left 42; Right "value" ] + |> List.iter (fun value -> isEither (box value) |> equal true) + +[] +let ``test boxed fieldless union type tests recognize atoms`` () = + isGender (box Male) |> equal true + isGender (box Female) |> equal true + isT1 (box T1) |> equal true + isT1 (box Male) |> equal false + isGender (box T1) |> equal false + +[] +let ``test boxed union type tests honor compiled case names`` () = + [ RuntimeEmpty; RuntimePayload (box 42); RuntimePair (box 42, null) ] + |> List.iter (fun value -> isRuntimeTaggedUnion (box value) |> equal true) + +[] +let ``test boxed union type test evaluates its input once`` () = + let mutable calls = 0 + let next () = + calls <- calls + 1 + box (RuntimePair (box 42, null)) + (next () :? RuntimeTaggedUnion) |> equal true + calls |> equal 1 + +[] +let ``test boxed union wrappers preserve null and child exit shaped payloads`` () = + unwrapForwardedValue (box (ForwardedValue null)) |> equal (Some null) + unwrapGenericForwardedValue (box (GenericForwardedValue null)) |> equal (Some null) + let payload = { Pid = box 42; Reason = null } + unwrapForwardedValue (box (ForwardedValue (box payload))) |> equal (Some (box payload)) + unwrapGenericForwardedValue (box (GenericForwardedValue payload)) |> equal (Some payload) + unwrapForwardedValue (box payload) |> equal None + unwrapGenericForwardedValue (box payload) |> equal None + +[] +let ``test boxed union type tests reject unrelated values`` () = + [ null; box 42; box true; box "value"; box (ref 42); box [| 42 |] + box {| Pid = 42; Reason = "normal" |}; box (Map.ofList [ "pid", 42 ]) + box (42, "value"); box Male; box T1 ] + |> List.iter (fun value -> + unwrapForwardedValue value |> equal None + unwrapGenericForwardedValue value |> equal None + isMyUnion value |> equal false + isEither value |> equal false + isRuntimeTaggedUnion value |> equal false) + +#if FABLE_COMPILER_BEAM +[ erlang:self(), reason => normal}, erlang:make_ref(), unrelated, {}, forwarded_value, {forwarded_value}, {forwarded_value, 42, extra}, generic_forwarded_value, {generic_forwarded_value}, {generic_forwarded_value, 42, extra}, {other, 42}, {42, forwarded_value}, {case0}, case1, {case1}, {case1, 42, extra}, {case2, 42}, {case2, 42, 43, extra}, {case3, 42, 43}, {case3, 42, 43, 44, extra}, {left}, {left, 42, extra}, {'FORWARDED.Empty'}, 'FORWARDED.Payload', {'FORWARDED.Payload'}, {'FORWARDED.Payload', 42, extra}, {'FORWARDED.Pair', 42}]")>] +let private unrelatedNativeUnionTerms () : obj list = Fable.Core.Util.nativeOnly + +[] +let private compiledNativeUnionTerms () : obj list = Fable.Core.Util.nativeOnly + +[ erlang:self(), reason => normal}")>] +let private nativeChildExitPayload () : ForwardedPayload = Fable.Core.Util.nativeOnly + +[] +let ``test boxed union wrappers preserve native child exit shaped records`` () = + let payload = nativeChildExitPayload () + unwrapForwardedValue (box (ForwardedValue (box payload))) |> equal (Some (box payload)) + unwrapGenericForwardedValue (box (GenericForwardedValue payload)) |> equal (Some payload) + unwrapForwardedValue (box payload) |> equal None + unwrapGenericForwardedValue (box payload) |> equal None + +[] +let ``test boxed union type tests reject malformed native terms safely`` () = + unrelatedNativeUnionTerms () + |> List.iter (fun value -> + unwrapForwardedValue value |> equal None + unwrapGenericForwardedValue value |> equal None + isMyUnion value |> equal false + isEither value |> equal false + isGender value |> equal false + isT1 value |> equal false + isRuntimeTaggedUnion value |> equal false) + +[] +let ``test boxed union type tests recognize native compiled tags`` () = + compiledNativeUnionTerms () + |> List.iter (fun value -> isRuntimeTaggedUnion value |> equal true) +#endif + +type CollectionOrder = + | Zebra + | Alpha + | Middle + +type CollectionOrderItem = + { Key: CollectionOrder + Value: string } + +type CollectionArray = CollectionArray of int array let (|Functional|NonFunctional|) (s: string) = match s with @@ -65,15 +314,116 @@ let (|FSharp|_|) (document : string) = let (|A|) n = n +[] +let ``test collection ordering uses union declaration order`` () = + let values = [ Middle; Zebra; Alpha ] + let expected = [ Zebra; Alpha; Middle ] + values |> List.sortBy id |> equal expected + values |> List.sortByDescending id |> equal (List.rev expected) + values |> List.min |> equal Zebra + values |> List.max |> equal Middle + + let items = + [ { Key = Middle; Value = "middle" } + { Key = Zebra; Value = "zebra" } + { Key = Alpha; Value = "alpha" } ] + + items + |> List.sortBy (fun item -> item.Key) + |> List.map (fun item -> item.Value) + |> equal [ "zebra"; "alpha"; "middle" ] + items |> List.minBy (fun item -> item.Key) |> equal items.[1] + items |> List.maxBy (fun item -> item.Key) |> equal items.[0] + + let arrayValues = values |> List.toArray + arrayValues |> Array.sortBy id |> equal (List.toArray expected) + arrayValues |> Array.sortByDescending id |> equal (expected |> List.rev |> List.toArray) + arrayValues |> Array.min |> equal Zebra + arrayValues |> Array.max |> equal Middle + + let arrayItems = items |> List.toArray + arrayItems |> Array.minBy (fun item -> item.Key) |> equal arrayItems.[1] + arrayItems |> Array.maxBy (fun item -> item.Key) |> equal arrayItems.[0] + + let inPlaceValues = values |> List.toArray + Array.sortInPlace inPlaceValues + inPlaceValues |> equal (List.toArray expected) + + let inPlaceItems = items |> List.toArray + Array.sortInPlaceBy (fun item -> item.Key) inPlaceItems + inPlaceItems + |> Array.map (fun item -> item.Value) + |> equal [| "zebra"; "alpha"; "middle" |] + +[] +let ``test collection extrema preserve first ties and project each item once`` () = + let items = + [ { Key = Zebra; Value = "first" } + { Key = Zebra; Value = "second" } + { Key = Alpha; Value = "third" } ] + + let mutable listMinProjections = 0 + + let listMin = + items + |> List.minBy (fun item -> + listMinProjections <- listMinProjections + 1 + item.Key) + + listMin.Value |> equal "first" + listMinProjections |> equal items.Length + + let mutable listMaxProjections = 0 + + items + |> List.maxBy (fun item -> + listMaxProjections <- listMaxProjections + 1 + item.Key) + |> ignore + + listMaxProjections |> equal items.Length + + let arrayItems = List.toArray items + let mutable arrayMinProjections = 0 + + let arrayMin = + arrayItems + |> Array.minBy (fun item -> + arrayMinProjections <- arrayMinProjections + 1 + item.Key) + + arrayMin.Value |> equal "first" + arrayMinProjections |> equal arrayItems.Length + + let mutable arrayMaxProjections = 0 + + arrayItems + |> Array.maxBy (fun item -> + arrayMaxProjections <- arrayMaxProjections + 1 + item.Key) + |> ignore + + arrayMaxProjections |> equal arrayItems.Length + + let first = [| 1 |] + let second = [| 1 |] + let values = [ CollectionArray first; CollectionArray second ] + + let (CollectionArray listMinValue) = List.min values + obj.ReferenceEquals(listMinValue, first) |> equal true + + let (CollectionArray arrayMinValue) = values |> List.toArray |> Array.min + obj.ReferenceEquals(arrayMinValue, first) |> equal true + [] -type MyUnion3 = -| Case1 -| Case2 -| Case3 +type MyUnion = + | Case1 + | Case2 + | Case3 type R = { Name: string - UnionCase: MyUnion3 + Case: MyUnion } [] @@ -84,6 +434,20 @@ let ``test Union cases matches with no arguments can be generated`` () = | Male -> false |> equal false +[] +let ``test union ToString override works`` () = + let value = IntegrityLevel.Untrusted + value.ToString() |> equal "Untrusted" + string value |> equal "Untrusted" + +[] +let ``test percent O uses union ToString override`` () = + sprintf "%O" IntegrityLevel.Untrusted |> equal "Untrusted" + sprintf "%d %O" 42 IntegrityLevel.Untrusted |> equal "42 Untrusted" + + let formatIntegrityLevel = sprintf "%O" + formatIntegrityLevel IntegrityLevel.Trusted |> equal "Trusted" + [] let ``test Union cases matches with one argument can be generated`` () = let x = Left "abc" @@ -116,11 +480,11 @@ let ``test Union case with no data works`` () = let ``test Shape union works`` () = let area shape = match shape with - | Circle r -> 3.14159 * r * r - | Square s -> s * s - | Rectangle(w, h) -> w * h - area (Square 5.0) |> equal 25.0 - area (Rectangle(3.0, 4.0)) |> equal 12.0 + | Shape.Circle r -> 3.14159 * r * r + | Shape.Square s -> s * s + | Shape.Rectangle(w, h) -> w * h + area (Shape.Square 5.0) |> equal 25.0 + area (Shape.Rectangle(3.0, 4.0)) |> equal 12.0 [] let ``test Either union works`` () = @@ -170,12 +534,12 @@ let ``test Union cases matches with many arguments can be generated`` () = [] let ``test Pattern matching with common targets works`` () = - let x = MyUnion.Case2("a", "b") + let x = TestUnion.Case2("a", "b") match x with - | MyUnion.Case0 -> failwith "unexpected" - | MyUnion.Case1 _ - | MyUnion.Case2 _ -> "a" - | MyUnion.Case3(a, b, c) -> a + b + c + | TestUnion.Case0 -> failwith "unexpected" + | TestUnion.Case1 _ + | TestUnion.Case2 _ -> "a" + | TestUnion.Case3(a, b, c) -> a + b + c |> equal "a" [] @@ -226,13 +590,75 @@ let ``test Active patterns can be combined with union case matching`` () = [] let ``test Equality works in filter`` () = - let original = [| { Name = "1"; UnionCase = MyUnion3.Case1 } ; { Name = "2"; UnionCase = MyUnion3.Case1 }; { Name = "3"; UnionCase = MyUnion3.Case2 }; { Name = "4"; UnionCase = MyUnion3.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + original + |> Array.filter (fun r -> r.Case = MyUnion.Case1) + |> Array.length + |> equal 2 + +[] +let ``test Pattern matching works in filter`` () = + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original - |> Array.filter (fun r -> r.UnionCase = MyUnion3.Case1) + |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) |> Array.length |> equal 2 -// --- Tests ported from Rust UnionTests --- +[] +let ``test Union field can have the same name as its type`` () = + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + +[] +let ``test Verify that union cases can have `-` in their names`` () = + // See https://github.com/fable-compiler/Fable/issues/4071 + let x = LangCode.``pt-Br`` + equal x LangCode.``pt-Br`` + +[] +let ``test sprintf formats strings cases correctly`` () = + let s = sprintf "%A" (S "1") + s |> equal "S \"1\"" + +[] +let ``test Union cases with no fields are physically equal`` () = + // Nullary cases compile to plain atoms, which the BEAM VM interns, so this matches fsc. + obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true + obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false + obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true +#if FABLE_COMPILER + // Unlike fsc, Erlang has no reference identity for compound terms (only atoms are + // interned), so `=:=` here is structural equality, unlike .NET reference equality. + obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal true +#else + obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false +#endif + obj.ReferenceEquals(T1, T1) |> equal true + +[] +let ``Named union case fields work when a field is called name`` () = + let value = NamedCase("v1", "v2") + match value with + | NamedCase(name, optional) -> + name |> equal "v1" + optional |> equal "v2" + +[] +let ``test Static union member returning the union type works`` () = + Example.All |> Array.length |> equal 2 + Example.All.[0] |> equal Example.First + Example.All.[1] |> equal Example.Second + +[] +let ``test Static union member with nested self-reference works`` () = + Example.Head |> equal (Some Example.First) + Example.Nested |> List.head |> Array.head |> equal Example.First + Example.Pair |> fst |> equal Example.First + Example.Pair |> snd |> equal Example.Second + Example.Factory() |> equal Example.First [] let ``test Struct unions work`` () = @@ -244,6 +670,31 @@ let ``test Struct unions work`` () = let res = distance p res |> equal 5. +[] +let ``test Struct unions with fields work`` () = + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + // circle.Area |> equal (System.Math.PI * radius * radius) + // square.Area |> equal (4.0 * radius * radius) + StructShape.Area circle |> equal (System.Math.PI * radius * radius) + StructShape.Area square |> equal (4.0 * radius * radius) + +[] +let ``test Struct unions without fields work`` () = + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + // circle.Area |> equal (System.Math.PI * radius * radius) + // square.Area |> equal (4.0 * radius * radius) + StructShape2.Area circle |> equal (System.Math.PI * radius * radius) + StructShape2.Area square |> equal (4.0 * radius * radius) + +[] +let ``test Nested union match keeps case-specific field access`` () = + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + [] let ``test Union case matching works`` () = let x = IntCase1 5 @@ -262,15 +713,6 @@ let ``test Union case equality works`` () = IntCase2 1 = IntCase2 1 |> equal true IntCase3 1 = IntCase3 1 |> equal true -let unionFnAlways1 = function - | IntCase1 x -> x - | _ -> -1 - -let unionFnRetNum = function - | IntCase1 a -> a - | IntCase2 b -> b - | IntCase3 c -> c - [] let ``test Union fn call works`` () = let x = IntCase1 3 @@ -289,24 +731,6 @@ let ``test Union with wrapped type works`` () = let b = match a with AString s -> s + " world" b |> equal "hello world" -let matchStrings = function - | DeepWrappedA (s, d) -> d.Value + s - | DeepWrappedB s -> s - | DeepWrappedC _ -> "nothing" - | DeepWrappedD d -> d.Value - | DeepWrappedE _ -> "nothing2" - | DeepWrappedF (AString s) -> s - | DeepWrappedG x -> x.X.Value - -let matchNumbers = function - | DeepWrappedA _ -> 0 - | DeepWrappedB _ -> 0 - | DeepWrappedC c -> c - | DeepWrappedD _ -> 0 - | DeepWrappedE(a, b) -> a + b - | DeepWrappedF _ -> 0 - | DeepWrappedG x -> x.Y - [] let ``test Deep union with wrapped type works`` () = let a = DeepWrappedA (" world", { Value = "hello" }) @@ -325,34 +749,21 @@ let ``test Deep union with wrapped type works`` () = [] let ``test Deep union with tuped prim type works`` () = let e = DeepWrappedE (3, 2) - let c = DeepWrappedC 42 + let c = DeepWrappedC 42 //todo to string! let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} e |> matchNumbers |> equal 5 c |> matchNumbers |> equal 42 g |> matchNumbers |> equal 365 -let matchStringWhenNotHello = function - | DeepWrappedB s when s <> "hello" -> "not hello" - | _ -> "hello" +[] +let ``test Multi-case Union with wrapped type works`` () = + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" [] let ``test Match with condition works`` () = let b1 = DeepWrappedB "hello" let b2 = DeepWrappedB "not" - b1 |> matchStringWhenNotHello |> equal "hello" - b2 |> matchStringWhenNotHello |> equal "not hello" - -[] -let ``test Union cases with no fields are physically equal`` () = - // Nullary cases compile to plain atoms, which the BEAM VM interns, so this matches fsc. - obj.ReferenceEquals(Male, Male) |> equal true - obj.ReferenceEquals(Male, Female) |> equal false - obj.ReferenceEquals(Case0, Case0) |> equal true - obj.ReferenceEquals(T1, T1) |> equal true -#if FABLE_COMPILER - // Unlike fsc, Erlang has no reference identity for compound terms (only atoms are - // interned), so `=:=` here is structural equality, unlike .NET reference equality. - obj.ReferenceEquals(Case1 "a", Case1 "a") |> equal true -#else - obj.ReferenceEquals(Case1 "a", Case1 "a") |> equal false -#endif + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" diff --git a/tests/Beam/UnsupportedNumericLiteral/Program.fs b/tests/Beam/UnsupportedNumericLiteral/Program.fs new file mode 100644 index 0000000000..04e12ac1d3 --- /dev/null +++ b/tests/Beam/UnsupportedNumericLiteral/Program.fs @@ -0,0 +1,9 @@ +module Fable.Tests.UnsupportedNumericLiteral + +let value = Unchecked.defaultof + +type CollidingRecord = + { ``foo-bar``: int + foo_bar: int } + +let collidingAnonymousRecord = {| ``foo-bar`` = 1; foo_bar = 2 |} diff --git a/tests/Beam/UnsupportedNumericLiteral/UnsupportedNumericLiteral.fsproj b/tests/Beam/UnsupportedNumericLiteral/UnsupportedNumericLiteral.fsproj new file mode 100644 index 0000000000..705031e61e --- /dev/null +++ b/tests/Beam/UnsupportedNumericLiteral/UnsupportedNumericLiteral.fsproj @@ -0,0 +1,13 @@ + + + net10.0 + false + preview + + + + + + + + diff --git a/tests/Beam/Util.fs b/tests/Beam/Util.fs index 279a109b8c..8a8512c097 100644 --- a/tests/Beam/Util.fs +++ b/tests/Beam/Util.fs @@ -69,3 +69,17 @@ let throwsAnyError (f: unit -> 'a): unit = let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess + +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentNullException -> "ArgumentNullException" + | :? ArgumentException -> "ArgumentException" + | :? InvalidOperationException -> "InvalidOperationException" + | :? Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | :? FormatException -> "FormatException" + | e -> "Unexpected exception: " + e.Message diff --git a/tests/Beam/erl/async_lifecycle_tests.erl b/tests/Beam/erl/async_lifecycle_tests.erl new file mode 100644 index 0000000000..0c6e8c748a --- /dev/null +++ b/tests/Beam/erl/async_lifecycle_tests.erl @@ -0,0 +1,264 @@ +%% Native CPS probes complement the F# tests: they exercise cancellation continuations, +%% callback exceptions, and deterministic cross-process handshakes without timing sleeps. +-module(async_lifecycle_tests). +-export([ + test_finalizers_settle_once/0, + test_finalizers_with_bind_propagate_continuation_exceptions/0, + test_bind_evaluation_failure_uses_error_continuation/0, + test_nested_cancellation_finalizers/0, + test_cancellation_finalizer_preserves_channel_and_exception/0, + test_using_settles_once/0, + test_using_binder_failure/0, + test_run_synchronously_pending_cancellation/0, + test_try_with_preserves_channels/0, + test_failure_racing_cancellation/0, + test_run_synchronously_timeout/0, + test_run_synchronously_timeout_preserves_supplied_token/0, + pending/1 +]). + +outcome(Comp) -> + try fable_async:run_synchronously(Comp) of + Value -> {ok, Value} + catch error:Reason -> {error, Reason} + end. + +body(success) -> fable_async_builder:return(42); +body(failure) -> fun(Ctx) -> (maps:get(on_error, Ctx))(body_defect) end; +body(throw) -> fun(_) -> erlang:error(body_defect) end; +body(cancel) -> fun(Ctx) -> (maps:get(on_cancel, Ctx))(ok) end. + +test_finalizers_settle_once() -> + lists:foreach(fun(Path) -> + lists:foreach(fun(Throws) -> + put(cleanups, 0), + Comp = fable_async_builder:try_finally(body(Path), fun(_) -> + put(cleanups, get(cleanups) + 1), + case Throws of true -> erlang:error(cleanup_defect); false -> ok end + end), + Result = outcome(Comp), + 1 = erase(cleanups), + case {Throws, Path, Result} of + {_, cancel, {error, #{exn_type := operation_canceled_exception}}} -> ok; + {true, _, {error, cleanup_defect}} when Path =/= cancel -> ok; + {false, success, {ok, 42}} -> ok; + {false, _, {error, body_defect}} when Path =/= cancel -> ok; + _ -> erlang:error({wrong_finalizer_result, Path, Throws, Result}) + end + end, [false, true]) + end, [success, failure, throw, cancel]), + %% A caller's throwing continuation cannot re-enter compensation. + put(cleanups, 0), + Comp = fable_async_builder:try_finally(body(success), fun(_) -> + put(cleanups, get(cleanups) + 1) + end), + {error, callback_defect} = + try Comp(#{ + on_success => fun(_) -> erlang:error(callback_defect) end, + on_error => fun(_) -> erlang:error(unexpected_error_callback) end, + on_cancel => fun(_) -> erlang:error(unexpected_cancel_callback) end, + cancel_token => undefined + }) + catch error:Reason -> {error, Reason} + end, + 1 = erase(cleanups), + ok. + +test_finalizers_with_bind_propagate_continuation_exceptions() -> + lists:foreach(fun(Path) -> + put(cleanups, 0), + put(callbacks, []), + Comp = fable_async_builder:try_finally( + fable_async_builder:bind(fable_async_builder:return(ok), fun(_) -> body(Path) end), + fun(_) -> put(cleanups, get(cleanups) + 1) end + ), + Callback = fun(Channel) -> fun(_) -> + put(callbacks, [Channel | get(callbacks)]), + erlang:error(callback_defect) + end end, + {error, callback_defect} = + try Comp(#{ + on_success => Callback(on_success), + on_error => Callback(on_error), + on_cancel => Callback(on_cancel), + cancel_token => undefined + }) + catch error:Reason -> {error, Reason} + end, + Expected = case Path of success -> on_success; failure -> on_error; cancel -> on_cancel end, + [Expected] = erase(callbacks), + 1 = erase(cleanups) + end, [success, failure, cancel]). + +test_bind_evaluation_failure_uses_error_continuation() -> + Comp = fable_async_builder:bind( + fable_async_builder:return(ok), fun(_) -> erlang:error(binder_defect) end + ), + Ref = make_ref(), + Comp(#{ + on_success => fun(_) -> erlang:error(unexpected_success_callback) end, + on_error => fun(Error) -> put(Ref, {error, Error}) end, + on_cancel => fun(_) -> erlang:error(unexpected_cancel_callback) end, + cancel_token => undefined + }), + {error, binder_defect} = erase(Ref), + ok. + +test_nested_cancellation_finalizers() -> + lists:foreach(fun(OuterThrows) -> + put(cleanups, []), + Inner = fable_async_builder:try_finally(body(cancel), fun(_) -> + put(cleanups, [inner | get(cleanups)]), + erlang:error(inner_defect) + end), + Outer = fable_async_builder:try_finally(Inner, fun(_) -> + put(cleanups, [outer | get(cleanups)]), + case OuterThrows of true -> erlang:error(outer_defect); false -> ok end + end), + {error, #{exn_type := operation_canceled_exception}} = outcome(Outer), + [outer, inner] = erase(cleanups) + end, [false, true]). + +test_cancellation_finalizer_preserves_channel_and_exception() -> + Cancellation = #{exn_type => operation_canceled_exception, message => <<"original cancellation">>}, + put(cleanups, 0), + Comp = fable_async_builder:try_finally( + fun(Ctx) -> (maps:get(on_cancel, Ctx))(Cancellation) end, + fun(_) -> put(cleanups, get(cleanups) + 1), erlang:error(cleanup_defect) end + ), + Ref = make_ref(), + Comp(#{ + on_success => fun(_) -> erlang:error(unexpected_success_callback) end, + on_error => fun(_) -> erlang:error(unexpected_error_callback) end, + on_cancel => fun(Error) -> put(Ref, {cancelled, Error}) end, + cancel_token => undefined + }), + {cancelled, Cancellation} = erase(Ref), + 1 = erase(cleanups), + ok. + +test_using_settles_once() -> + lists:foreach(fun(Path) -> + lists:foreach(fun(Arity) -> + lists:foreach(fun(Throws) -> + put(disposals, 0), + Dispose = fun() -> + put(disposals, get(disposals) + 1), + case Throws of true -> erlang:error(disposal_defect); false -> ok end + end, + Callback = case Arity of 0 -> Dispose; 1 -> fun(_) -> Dispose() end end, + Resource = #{dispose => Callback}, + Result = outcome(fable_async_builder:using(Resource, fun(_) -> body(Path) end)), + 1 = erase(disposals), + case {Throws, Path, Result} of + {_, cancel, {error, #{exn_type := operation_canceled_exception}}} -> ok; + {true, _, {error, disposal_defect}} when Path =/= cancel -> ok; + {false, success, {ok, 42}} -> ok; + {false, _, {error, body_defect}} when Path =/= cancel -> ok; + _ -> erlang:error({wrong_using_result, Path, Arity, Throws, Result}) + end + end, [false, true]) + end, [0, 1]) + end, [success, failure, throw, cancel]), + {ok, 42} = outcome(fable_async_builder:using(undefined, fun(_) -> body(success) end)), + ok. + +test_using_binder_failure() -> + put(disposals, 0), + Resource = #{dispose => fun(_) -> put(disposals, get(disposals) + 1) end}, + Comp = fable_async_builder:using(Resource, fun(_) -> erlang:error(binder_defect) end), + 0 = get(disposals), + {error, binder_defect} = outcome(Comp), + 1 = erase(disposals), + ok. + +%% Signal readiness only after registering the wake-up; cancellation can now interrupt +%% an admitted pending operation, regardless of scheduler interleaving. +pending(Parent) -> + fun(Ctx) -> + Owner = self(), + Ref = make_ref(), + Registration = fable_cancellation:register(maps:get(cancel_token, Ctx), fun(_) -> + Owner ! {cancel, Ref} + end), + Parent ! {pending, Owner}, + receive {cancel, Ref} -> ok after 5000 -> erlang:error(pending_not_cancelled) end, + fable_utils:safe_dispose(Registration), + (maps:get(on_cancel, Ctx))(ok) + end. + +test_run_synchronously_pending_cancellation() -> + lists:foreach(fun pending_cancellation/1, [undefined, -1, 5000]). + +pending_cancellation(Timeout) -> + Parent = self(), + Token = fable_cancellation:create(), + {Pid, Monitor} = spawn_monitor(fun() -> + Comp = fable_async_builder:bind(fable_async:cancellation_token(), fun(Actual) -> + Token = Actual, + pending(Parent) + end), + Result = try fable_async:run_synchronously(Comp, Timeout, Token) + catch error:Reason -> Reason end, + exit({result, Result}) + end), + try + receive + {pending, _Worker} -> ok; + {'DOWN', Monitor, process, Pid, EarlyReason} -> erlang:error({worker_failed_before_pending, EarlyReason}) + after 5000 -> erlang:error(worker_not_pending) + end, + fable_cancellation:cancel(Token), + receive + {'DOWN', Monitor, process, Pid, {result, #{exn_type := operation_canceled_exception}}} -> ok; + {'DOWN', Monitor, process, Pid, Reason} -> erlang:error({wrong_cancellation, Reason}) + after 5000 -> erlang:error(worker_not_completed) + end + after + exit(Pid, kill), + erlang:demonitor(Monitor, [flush]) + end. + +test_try_with_preserves_channels() -> + Comp = fable_async_builder:try_with(body(cancel), fun(_) -> + erlang:error(cancellation_entered_error_handler) + end), + {error, #{exn_type := operation_canceled_exception}} = outcome(Comp), + {error, #{exn_type := operation_canceled_exception}} = outcome(fable_async:catch_async(body(cancel))), + {ok, handled} = outcome(fable_async_builder:try_with(body(failure), fun(_) -> + fable_async_builder:return(handled) + end)), + ok. + +test_failure_racing_cancellation() -> + Token = fable_cancellation:create(), + Comp = fun(_) -> + fable_cancellation:cancel(Token), + erlang:error(real_defect) + end, + {error, real_defect} = try fable_async:run_synchronously(Comp, undefined, Token) + catch error:Reason -> {error, Reason} end, + false = fable_utils:is_exception_type(fable_async:wrap_error(<<"operation_cancelled">>), operation_canceled_exception), + ok. + +test_run_synchronously_timeout() -> + Parent = self(), + Comp = fable_async_builder:try_finally(pending(Parent), fun(_) -> Parent ! timeout_cleanup end), + {error, #{exn_type := timeout_exception}} = + try fable_async:run_synchronously(Comp, 1000, undefined) + catch error:Reason -> {error, Reason} end, + receive {pending, _} -> ok after 5000 -> erlang:error(timeout_worker_not_started) end, + receive timeout_cleanup -> ok after 5000 -> erlang:error(timeout_skipped_cleanup) end, + %% No late result or DOWN message may remain in the caller mailbox. + receive Message -> erlang:error({timeout_mailbox_leak, Message}) after 0 -> ok end. + +test_run_synchronously_timeout_preserves_supplied_token() -> + Token = fable_cancellation:create(), + Comp = fable_async_builder:bind(fable_async:cancellation_token(), fun(Actual) -> + Token = Actual, + fable_async_builder:return(42) + end), + 42 = fable_async:run_synchronously(Comp, 5000, Token), + {choice1_of2, 42} = fable_async:run_synchronously(fable_async:catch_async(Comp), 5000, Token), + false = fable_cancellation:is_cancellation_requested(Token), + ok. diff --git a/tests/Beam/erl/async_stacktrace_tests.erl b/tests/Beam/erl/async_stacktrace_tests.erl index 4b452701a7..46a52ea212 100644 --- a/tests/Beam/erl/async_stacktrace_tests.erl +++ b/tests/Beam/erl/async_stacktrace_tests.erl @@ -7,7 +7,9 @@ -export([ test_run_synchronously_keeps_original_stacktrace/0, test_handled_stacktrace_is_not_reused/0, - test_nested_run_keeps_own_stacktrace/0 + test_nested_run_keeps_own_stacktrace/0, + test_finalizer_failure_keeps_original_stacktrace/0, + test_start_immediate_downstream_failure_keeps_original_stacktrace/0 ]). %% The frame every test below looks for: an error raised at a known place in this module. @@ -73,3 +75,32 @@ test_nested_run_keeps_own_stacktrace() -> true -> ok; false -> erlang:error({expected_original_frame_on_top, Stacktrace}) end. + +test_finalizer_failure_keeps_original_stacktrace() -> + Comp = fable_async_builder:try_finally( + fable_async_builder:return(ok), fun(_) -> boom(cleanup_defect) end + ), + {error, cleanup_defect, Stacktrace} = run_catching(Comp), + case has_boom_on_top(Stacktrace) of + true -> ok; + false -> erlang:error({expected_finalizer_frame_on_top, Stacktrace}) + end. + +test_start_immediate_downstream_failure_keeps_original_stacktrace() -> + Comp = fable_async_builder:bind( + fable_async_builder:try_finally( + fable_async_builder:bind( + fable_async_builder:return(ok), fun(_) -> fable_async_builder:return(ok) end + ), + fun(_) -> ok end + ), + fun(_) -> fun(_) -> boom(downstream_defect) end end + ), + {error, downstream_defect, Stacktrace} = + try fable_async:start_immediate(Comp) + catch error:Err:St -> {error, Err, St} + end, + case has_boom_on_top(Stacktrace) of + true -> ok; + false -> erlang:error({expected_downstream_frame_on_top, Stacktrace}) + end. diff --git a/tests/Beam/xunit.runner.json b/tests/Beam/xunit.runner.json new file mode 100644 index 0000000000..8641732346 --- /dev/null +++ b/tests/Beam/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeTestCollections": false, + "maxParallelThreads": 1 +} diff --git a/tests/Dart/main.dart b/tests/Dart/main.dart deleted file mode 100644 index a6b380a18a..0000000000 --- a/tests/Dart/main.dart +++ /dev/null @@ -1,65 +0,0 @@ -import './src/ArithmeticTests.dart' as arithmetic; -import './src/RandomTests.dart' as random; -import './src/ArrayTests.dart' as array; -import './src/ComparisonTests.dart' as comparison; -import './src/CustomOperatorTests.dart' as custom_operator; -import './src/DateTimeTests.dart' as date; -import './src/DictionaryTests.dart' as dictionary; -import './src/EnumTests.dart' as enum_; -import './src/EnumerableTests.dart' as enumerable; -import './src/HashSetTests.dart' as hash_set; -import './src/InlineIfLambdaTests.dart' as inline_if_lambda; -import './src/ListTests.dart' as list; -import './src/MapTests.dart' as map; -import './src/MiscTests.dart' as misc; -// import './src/NullnessTests.dart' as nullness; -import './src/OptionTests.dart' as option; -import './src/QuotationTests.dart' as quotation; -import './src/RecordTests.dart' as record; -import './src/RegexTests.dart' as regex; -import './src/ResizeArrayTests.dart' as resize_array; -import './src/ResultTests.dart' as result; -import './src/SeqTests.dart' as seq; -import './src/SeqExpressionTests.dart' as seq_expression; -import './src/SetTests.dart' as set_; -import './src/StringTests.dart' as string; -import './src/SudokuTests.dart' as sudoku; -import './src/TailCallTests.dart' as tailcall; -import './src/TimeSpanTests.dart' as timespan; -import './src/TupleTests.dart' as tuple; -import './src/TypeTests.dart' as type_; -import './src/UnionTests.dart' as union; - -void main() { - arithmetic.tests(); - random.tests(); - array.tests(); - comparison.tests(); - custom_operator.tests(); - date.tests(); - dictionary.tests(); - enum_.tests(); - enumerable.tests(); - hash_set.tests(); - inline_if_lambda.tests(); - list.tests(); - map.tests(); - misc.tests(); - // nullness.tests(); - option.tests(); - quotation.tests(); - record.tests(); - regex.tests(); - resize_array.tests(); - result.tests(); - seq.tests(); - seq_expression.tests(); - set_.tests(); - string.tests(); - sudoku.tests(); - tailcall.tests(); - timespan.tests(); - tuple.tests(); - type_.tests(); - union.tests(); -} diff --git a/tests/Dart/src/ArrayTests.fs b/tests/Dart/src/ArrayTests.fs index 54aaa76232..f2307c51a3 100644 --- a/tests/Dart/src/ArrayTests.fs +++ b/tests/Dart/src/ArrayTests.fs @@ -36,6 +36,16 @@ type Things = type Animal = Duck of int | Dog of int +type DescendingComparer() = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = compare y x + +type LoggingComparer(calls: ResizeArray) = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y + let tests () = testCase "Pattern matching with arrays works" <| fun () -> match [||] with [||] -> true | _ -> false @@ -440,6 +450,118 @@ let tests () = // System.Array.IndexOf(myArray, Dog 3, 0, 1) |> equal -1 // System.Array.IndexOf(myArray, Duck 5, 1) |> equal -1 + testCase "System.Array.BinarySearch works" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 0) |> equal -1 + System.Array.BinarySearch(xs, 1) |> equal 0 + System.Array.BinarySearch(xs, 2) |> equal -2 + System.Array.BinarySearch(xs, 7) |> equal 3 + System.Array.BinarySearch(xs, 8) |> equal -5 + System.Array.BinarySearch(xs, 10) |> equal -6 + System.Array.BinarySearch(([||]: int[]), 3) |> equal -1 + + testCase "System.Array.BinarySearch works with duplicates" <| fun () -> + System.Array.BinarySearch([|2; 2; 2; 2; 2; 2; 2; 2|], 2) |> equal 3 + System.Array.BinarySearch([|1; 2; 2; 2; 2; 3; 4; 5; 6|], 2) |> equal 4 + + testCase "System.Array.BinarySearch works with a range" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2) |> equal -2 + System.Array.BinarySearch(xs, 1, 3, 9) |> equal -5 + System.Array.BinarySearch(xs, 1, 2, 9) |> equal -4 + System.Array.BinarySearch(xs, 2, 0, 5) |> equal -3 + System.Array.BinarySearch(xs, 5, 0, 5) |> equal -6 + + testCase "System.Array.BinarySearch works with a comparer" <| fun () -> + let ys = [|9; 7; 5; 3; 1|] + System.Array.BinarySearch(ys, 3, DescendingComparer()) |> equal 3 + System.Array.BinarySearch(ys, 4, DescendingComparer()) |> equal -4 + System.Array.BinarySearch(ys, 1, 3, 3, DescendingComparer()) |> equal 3 + System.Array.BinarySearch([|1; 3; 5; 7; 9|], 7, (null: System.Collections.Generic.IComparer)) |> equal 3 + + testCase "System.Array.BinarySearch passes the array element to the comparer first" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + System.Array.BinarySearch(xs, 9, LoggingComparer(calls)) |> equal 4 + List.ofSeq calls |> equal [(5, 9); (7, 9); (9, 9)] + calls.Clear() + System.Array.BinarySearch(xs, 4, LoggingComparer(calls)) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (1, 4); (3, 4)] + + testCase "System.Array.BinarySearch works with other element types" <| fun () -> + let strings = [|"apple"; "banana"; "cherry"|] + System.Array.BinarySearch(strings, "banana") |> equal 1 + System.Array.BinarySearch(strings, "blue") |> equal -3 + let floats = [|1.5; 2.5; 3.5|] + System.Array.BinarySearch(floats, 2.5) |> equal 1 + System.Array.BinarySearch(floats, 3.0) |> equal -3 + let int64s = [|1L; 5L; 10L|] + System.Array.BinarySearch(int64s, 10L) |> equal 2 + System.Array.BinarySearch(int64s, 6L) |> equal -3 + let things = [|{ MainThing = 1; OtherThing = "x" }; { MainThing = 2; OtherThing = "a" }; { MainThing = 2; OtherThing = "b" }|] + System.Array.BinarySearch(things, { MainThing = 2; OtherThing = "b" }) |> equal 2 + System.Array.BinarySearch(things, { MainThing = 1; OtherThing = "z" }) |> equal -2 + let animals = [|Duck 1; Duck 4; Dog 0|] + System.Array.BinarySearch(animals, Dog 0) |> equal 2 + System.Array.BinarySearch(animals, Duck 2) |> equal -2 + + testCase "System.Array.BinarySearch works with a range and a null comparer" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7, null) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2, null) |> equal -2 + + testCase "System.Array.BinarySearch works with a single element and a large array" <| fun () -> + System.Array.BinarySearch([|5|], 5) |> equal 0 + System.Array.BinarySearch([|5|], 3) |> equal -1 + System.Array.BinarySearch([|5|], 7) |> equal -2 + let big = Array.init 1000 (fun i -> i * 2) + System.Array.BinarySearch(big, 998) |> equal 499 + System.Array.BinarySearch(big, 999) |> equal -501 + + testCase "System.Array.BinarySearch works with more element types" <| fun () -> + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 3uy) |> equal 1 + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 4uy) |> equal -3 + System.Array.BinarySearch([|-5s; 0s; 5s|], 0s) |> equal 1 + System.Array.BinarySearch([|-5s; 0s; 5s|], -6s) |> equal -1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'c') |> equal 1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'd') |> equal -3 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "b")) |> equal 1 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "c")) |> equal -3 + let dates = [|System.DateTime(2020, 1, 1); System.DateTime(2021, 1, 1)|] + System.Array.BinarySearch(dates, System.DateTime(2021, 1, 1)) |> equal 1 + System.Array.BinarySearch(dates, System.DateTime(2020, 6, 1)) |> equal -2 + + testCase "System.Array.BinarySearch with a range passes the array element to the comparer first" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + LoggingComparer(calls) + System.Array.BinarySearch(xs, 1, 3, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (3, 4)] + + testCase "System.Array.BinarySearch non-generic overloads work" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs :> System.Array, box 7) |> equal 3 + System.Array.BinarySearch(xs :> System.Array, box 4) |> equal -3 + System.Array.BinarySearch(xs :> System.Array, 1, 3, box 5) |> equal 2 + + testCase "System.Array.BinarySearch with a range checks its arguments" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> System.Array.BinarySearch(xs, -1, 2, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, -1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 3, 3, 5)) |> equal "ArgumentException" + raised (fun () -> System.Array.BinarySearch(xs, 6, 0, 5)) |> equal "ArgumentException" + testCase "Array.findIndex works" <| fun () -> let xs = [|1.f; 2.f; 3.f; 4.f|] xs |> Array.findIndex ((=) 2.f) @@ -1316,6 +1438,56 @@ let tests () = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) + testCase "Array.randomShuffleBy matches FSharp.Core" <| fun () -> + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + float ((calls * 37) % 100) / 100.0 + Array.randomShuffleBy next [|0 .. 9|] |> equal [|3; 7; 2; 6; 9; 0; 8; 4; 5; 1|] + calls |> equal 9 + + testCase "Array.randomSampleBy matches FSharp.Core" <| fun () -> + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + Array.randomSampleBy (fun () -> 0.0) 0 ([||]: int[]) |> equal [||] + // Above the max set size it draws again on a repeated index + let values = [|0.0; 0.0; 0.5|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + values[calls - 1] + Array.randomSampleBy next 2 [|0 .. 21|] |> equal [|0; 11|] + calls |> equal 3 + + testCase "Array random functions draw from the given Random like FSharp.Core" <| fun () -> + let rng = System.Random(42) + let shuffled = Array.randomShuffleWith rng [|0 .. 9|] + Array.randomShuffleWith (System.Random(42)) [|0 .. 9|] |> equal shuffled + let reference = System.Random(42) + for i in 0 .. 8 do reference.Next(i, 10) |> ignore + rng.Next() |> equal (reference.Next()) + let rng = System.Random(42) + let sample = Array.randomSampleWith rng 3 [|0 .. 9|] + Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|] |> equal sample + let reference = System.Random(42) + for i in 0 .. 2 do reference.Next(0, 10 - i) |> ignore + rng.Next() |> equal (reference.Next()) + + testCase "Array random functions give FSharp.Core's error messages" <| fun () -> + let message = "randomizer returned NaN, should be in range [0.0, 1.0)." + throwsErrorContaining message (fun () -> Array.randomShuffleBy (fun () -> System.Double.NaN) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoiceBy (fun () -> System.Double.NaN) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoicesBy (fun () -> System.Double.NaN) 2 [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomSampleBy (fun () -> System.Double.NaN) 1 [|1; 2|]) + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> System.Double.NaN) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + throwsErrorContaining "The input array was empty." (fun () -> Array.randomChoiceWith (System.Random(1)) ([||]: int[])) + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + testCase "Array.compareWith works" <| fun () -> // See #2961 let a = [|1;3|] let b = [|1;2;3|] @@ -1328,6 +1500,103 @@ let tests () = equal c2 -1 equal c3 1 + testCase "Array.reduce and Array.min on an empty array raise ArgumentException" <| fun () -> + let xs: int[] = [||] + raisedExceptionType (fun () -> Array.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.Parallel.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.maxBy id xs) |> equal "ArgumentException" + + testCase "Array.take and Array.splitAt past the end raise InvalidOperationException" <| fun () -> + raisedExceptionType (fun () -> Array.take 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.splitAt 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.take -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.splitAt -1 [|1|]) |> equal "ArgumentException" + + testCase "Array.skip past the end raises ArgumentOutOfRangeException" <| fun () -> + raisedExceptionType (fun () -> Array.skip 5 [|1|]) |> equal "ArgumentOutOfRangeException" + + testCase "Array functions on arrays of different lengths raise ArgumentException" <| fun () -> + let xs = [|1|] + let ys = [|1; 2|] + raisedExceptionType (fun () -> Array.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.zip3 xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.mapi2 (fun i x y -> i + x + y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map3 (fun x y z -> x + y + z) xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iter2 (fun _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iteri2 (fun _ _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.foldBack2 (fun x y acc -> x + y + acc) xs ys 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (=) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.exists2 (=) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [| xs; ys |]) |> equal "ArgumentException" + + testCase "Array.find and Array.pick raise KeyNotFoundException when nothing matches" <| fun () -> + let xs = [|1; 2|] + raisedExceptionType (fun () -> Array.find (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findBack (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndex (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndexBack (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.pick (fun x -> if x > 5 then Some x else None) xs) |> equal "KeyNotFoundException" + + testCase "Array.windowed and Array.permute with invalid arguments raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Array.windowed 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun _ -> 5) [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun _ -> 0) [|1; 2|]) |> equal "ArgumentException" + + testCase "Array random functions raise ArgumentOutOfRangeException when the randomizer is out of range" <| fun () -> + raisedExceptionType (fun () -> Array.randomShuffleInPlaceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomShuffleBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 1 [|1|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + + testCase "Array.sub with an index or count out of range raises" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.sub xs -1 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub xs 1 -1) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub xs 2 5) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub xs 4 0) |> equal "ArgumentOutOfRangeException" + Array.sub xs 3 0 |> equal [||] + + testCase "Array.fill with an index or count out of range raises" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.fill xs -1 2 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fill xs 0 -1 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fill xs 3 1 0) |> equal "IndexOutOfRangeException" + Array.fill xs 4 0 0 + xs |> equal [|1; 2; 3|] + + testCase "Array.blit and System.Array.Copy with an index or count out of range raise" <| fun () -> + let source = [|1; 2; 3|] + let target = Array.zeroCreate 5 + raisedExceptionType (fun () -> Array.blit source 0 target 0 -1) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit source -1 target 0 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit source 0 target -1 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit source 2 target 0 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit source 0 target 4 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit source 2 target -1 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy(source, 0, target, 0, 4)) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy(source, target, -1)) |> equal "ArgumentOutOfRangeException" + target |> equal [|0; 0; 0; 0; 0|] + Array.blit source 3 target 5 0 + target |> equal [|0; 0; 0; 0; 0|] + + testCase "Array.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 2 2 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 4 0 xs) |> equal "ArgumentException" + Array.removeManyAt 3 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 1 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 0 0 ([||]: int[]) |> equal [||] + Array.removeManyAt 1 2 xs |> equal [|1|] + // testCase "System.Array.Resize works" <| fun () -> // let mutable xs = [|1; 2; 3; 4; 5|] // System.Array.Resize(&xs, 3) diff --git a/tests/Dart/src/AsyncTests.fs b/tests/Dart/src/AsyncTests.fs new file mode 100644 index 0000000000..368aeec5a4 --- /dev/null +++ b/tests/Dart/src/AsyncTests.fs @@ -0,0 +1,842 @@ +module Fable.Tests.Dart.Async + +open System +open Fable.Core.Dart +open Util + +type DisposableAction(f) = + interface IDisposable with + member _.Dispose() = f () + +type MyException(value) = + inherit Exception() + member _.Value: int = value + +let successWork: Async = Async.FromContinuations(fun (onSuccess, _, _) -> onSuccess "success") + +let errorWork: Async = Async.FromContinuations(fun (_, onError, _) -> onError (exn "error")) + +let cancelWork: Async = + Async.FromContinuations(fun (_, _, onCancel) -> System.OperationCanceledException("cancelled") |> onCancel) + +let asyncMap f a = + async { + let! a = a + return f a + } + +let sleepAndAssign token (res: bool ref) = + Async.StartImmediate( + async { + do! Async.Sleep 200 + res.Value <- true + }, + token + ) + +let tests () = + testCase "Simple async translates without exception" + <| fun () -> async { return () } |> Async.StartImmediate + + testCase "Async while binding works correctly" + <| fun () -> + let mutable result = 0 + + async { + while result < 10 do + result <- result + 1 + } + |> Async.StartImmediate + + equal 10 result + + testCase "Async for binding works correctly" + <| fun () -> + let inputs = [| 1; 2; 3 |] + let mutable result = 0 + + async { + for inp in inputs do + result <- result + inp + } + |> Async.StartImmediate + + equal 6 result + + testCase "Async exceptions are handled correctly" + <| fun () -> + let mutable result = 0 + + let f shouldThrow = + async { + try + if shouldThrow then + failwith "boom!" + else + result <- 12 + with _ -> + result <- 10 + } + |> Async.StartImmediate + + result + + f true + f false |> equal 22 + +#if FABLE_COMPILER + testCase "Non captured exception in async is propagated when using Async.StartImmediate" + <| fun () -> throwsAnyError (fun () -> async { failwith "boom!" } |> Async.StartImmediate) + + // Behaviour of Async.Start is the same as Async.StartImmediate in Dart + testCase "Non captured exception in async is propagated when using Async.Start" + <| fun () -> throwsAnyError (fun () -> async { failwith "boom!" } |> Async.Start) +#endif + + testCase "Simple async is executed correctly" + <| fun () -> + let mutable result = false + let x = async { return 99 } + + async { + let! x = x + let y = 99 + result <- x = y + } + |> Async.StartImmediate + + equal true result + + testCase "async use statements should dispose of resources when they go out of scope" + <| fun () -> + let mutable isDisposed = false + let mutable step1ok = false + let mutable step2ok = false + + let resource = async { return new DisposableAction(fun () -> isDisposed <- true) } + + async { + use! _r = resource + step1ok <- not isDisposed + } + |> Async.StartImmediate + + step2ok <- isDisposed + + (step1ok && step2ok) |> equal true + + testCase "Try ... with ... expressions inside async expressions work the same" + <| fun () -> + let mutable result = "" + + let throw () : unit = raise (exn "Boo!") + + let append x = result <- result + x + + let innerAsync () = + async { + append "b" + + try + append "c" + throw () + append "1" + with _ -> + append "d" + + append "e" + } + + async { + append "a" + + try + do! innerAsync () + with _ -> + append "2" + + append "f" + } + |> Async.StartImmediate + + equal "abcdef" result + + testCaseAsync "Async cancellation works" + <| fun () -> + async { + let res1, res2, res3 = ref false, ref false, ref false + let tcs1 = new System.Threading.CancellationTokenSource(50) + let tcs2 = new System.Threading.CancellationTokenSource() + let tcs3 = new System.Threading.CancellationTokenSource() + sleepAndAssign tcs1.Token res1 + sleepAndAssign tcs2.Token res2 + sleepAndAssign tcs3.Token res3 + tcs2.Cancel() + tcs3.CancelAfter(1000) + do! Async.Sleep 500 + equal false res1.Value + equal false res2.Value + equal true res3.Value + } + + testCaseAsync "CancellationTokenSource.Register works" + <| fun () -> + async { + let mutable x = 0 + let res1 = ref false + let tcs1 = new System.Threading.CancellationTokenSource(50) + let _foo = tcs1.Token.Register(fun () -> x <- x + 1) + sleepAndAssign tcs1.Token res1 + do! Async.Sleep 500 + equal false res1.Value + equal 1 x + } + + testCaseAsync "CancellationToken can be disposed" + <| fun () -> // See #2879 + async { + let res1 = ref false + + do + use tcs1 = new System.Threading.CancellationTokenSource() + sleepAndAssign tcs1.Token res1 + + do! Async.Sleep 300 + // Disposing the CancellationToken doesn't cancel the operation + equal true res1.Value + } + + testCase "Async.StartWithContinuations works" + <| fun () -> + let mutable res1 = "" + let mutable res2 = "" + let mutable res3 = "" + Async.StartWithContinuations(successWork, (fun x -> res1 <- x), ignore, ignore) + Async.StartWithContinuations(errorWork, ignore, (fun x -> res2 <- x.Message), ignore) + Async.StartWithContinuations(cancelWork, ignore, ignore, (fun x -> res3 <- x.Message)) + equal "success" res1 + equal "error" res2 + equal "cancelled" res3 + + testCase "Async.Catch works" + <| fun () -> + let mutable res1 = "" + let mutable res2 = "" + + let assign = + function + | Choice1Of2 msg -> msg + | Choice2Of2(ex: Exception) -> "ERROR: " + ex.Message + + async { + let! x1 = successWork |> Async.Catch + res1 <- assign x1 + let! x2 = errorWork |> Async.Catch + res2 <- assign x2 + } + |> Async.StartImmediate + + equal "success" res1 + equal "ERROR: error" res2 + + testCase "Async.Ignore works" + <| fun () -> + let mutable res = false + + async { + do! successWork |> Async.Ignore + res <- true + } + |> Async.StartImmediate + + equal true res + + testCaseAsync "Async.Parallel works" + <| fun () -> + async { + let makeWork i = + async { + do! Async.Sleep 200 + return i + } + + let mutable res: int[] = [||] + let works = [ makeWork 1; makeWork 2; makeWork 3 ] + + async { + let! x = Async.Parallel works + res <- x + } + |> Async.StartImmediate + + do! Async.Sleep 500 + res |> Array.sum |> equal 6 + } + + testCaseAsync "Async.Parallel is lazy" + <| fun () -> + async { + let mutable x = 0 + + let add i = x <- x + i + + let a = Async.Parallel [ async { add 1 }; async { add 2 } ] + + do! Async.Sleep 100 + + equal 0 x + + let! _ = a + + equal 3 x + } + + testCaseAsync "Async.Sequential works" + <| fun () -> + async { + let mutable aggregate = 0 + + let makeWork i = + async { + // check that the individual work items run sequentially and not interleaved + aggregate <- aggregate + i + let copyOfI = aggregate + do! Async.Sleep 100 + equal copyOfI aggregate + do! Async.Sleep 100 + equal copyOfI aggregate + return i + } + + let works = [ for i in 1..5 -> makeWork i ] + let! result = Async.Sequential works + result |> equal [| 1..5 |] + result |> Array.sum |> equal aggregate + } + + testCaseAsync "Async.Sequential is lazy" + <| fun () -> + async { + let mutable x = 0 + + let a = Async.Sequential [ async { x <- x + 1 }; async { x <- x + 2 } ] + + do! Async.Sleep 100 + + equal 0 x + + let! _ = a + + equal 3 x + } + +#if FABLE_COMPILER + testCaseAsync "Interaction between Async and Future works" + <| fun () -> + async { + let mutable res = false + + async { res <- true } + |> Async.StartAsFuture + |> Async.AwaitFuture + |> Async.StartImmediate + + equal true res + } + + testCaseAsync "Futures can be cancelled" + <| fun () -> + async { + let mutable res = 0 + let tcs = new System.Threading.CancellationTokenSource(50) + + let work = + let work = + async { + do! Async.Sleep 75 + res <- -1 + } + + Async.StartAsFuture(work, tcs.Token) |> Async.AwaitFuture + + Async.StartWithContinuations(work, ignore, ignore, (fun _ -> res <- 1)) + do! Async.Sleep 100 + equal 1 res + } + + testCaseAsync "Async.AwaitFuture propagates Future errors" + <| fun () -> + async { + let future = + async { + failwith "boom" + return 0 + } + |> Async.StartAsFuture + + let mutable message = "" + + try + let! _ = Async.AwaitFuture future + () + with error -> + message <- error.Message + + equal "boom" message + } +#endif + + testCase "Async try .. with returns correctly from 'with' branch" + <| fun () -> + let work = + async { + try + failwith "testing" + return -1 + with e -> + return 42 + } + + let mutable result = 0 + Async.StartWithContinuations(work, (fun r -> result <- r), ignore, ignore) + equal 42 result + + testCaseAsync "Deep recursion with async doesn't cause stack overflow" + <| fun () -> + async { + let mutable result = false + + let rec trampolineTest i = + async { + if i > 100000 then + result <- true + else + return! trampolineTest (i + 1) + } + + do! trampolineTest 0 + equal true result + } + + testCaseAsync "Nested failure propagates in async expressions" + <| fun () -> + async { + let mutable data = "" + + let f1 x = + async { + try + failwith "1" + return x + with e -> + return! failwith ("2 " + e.Message) + } + + let f2 x = + async { + try + return! f1 x + with e -> + return! failwith ("3 " + e.Message) + } + + let f () = + async { + try + let! _y = f2 4 + return () + with e -> + data <- e.Message + } + |> Async.StartImmediate + + f () + do! Async.Sleep 100 + equal "3 2 1" data + } + + testCaseAsync "Try .. finally expressions inside async expressions work" + <| fun () -> + async { + let mutable data = "" + + async { + try + data <- data + "1 " + finally + data <- data + "2 " + } + |> Async.StartImmediate + + async { + try + try + failwith "boom!" + finally + data <- data + "3" + with _ -> + () + } + |> Async.StartImmediate + + do! Async.Sleep 100 + equal "1 2 3" data + } + + testCaseAsync "Final statement inside async expressions can throw" + <| fun () -> + async { + let mutable data = "" + + let f () = + async { + try + data <- data + "1 " + finally + failwith "boom!" + } + + async { + try + do! f () + return () + with e -> + data <- data + e.Message + } + |> Async.StartImmediate + + do! Async.Sleep 100 + equal "1 boom!" data + } + + testCaseAsync "Async.Bind propagates exceptions" + <| fun () -> // See #724 + async { + let task1 name = + async { + if name = "fail" then + failwith "Invalid access credentials" + + return "Ok" + } + + let task2 name = + async { + do! Async.Sleep 100 //difference between task1 and task2 + + if name = "fail" then + failwith "Invalid access credentials" + + return "Ok" + } + + let doWork _name task = + let catch comp = + async { + let! res = Async.Catch comp + + return + match res with + | Choice1Of2 str -> str + | Choice2Of2 ex -> ex.Message + } + + async { + let! a = task "work" |> catch + let! b = task "fail" |> catch + return a, b + } + + let! res1 = doWork "task1" task1 + let! res2 = doWork "task2" task2 + equal ("Ok", "Invalid access credentials") res1 + equal ("Ok", "Invalid access credentials") res2 + } + + testCaseAsync "Async.StartChild works" + <| fun () -> + async { + let mutable x = "" + + let taskA = + async { + do! Async.Sleep 500 + x <- x + "D" + return "E" + } + + let taskB = + async { + do! Async.Sleep 100 + x <- x + "C" + return "F" + } + + let! result1Async = taskA |> Async.StartChild // start first request but do not wait + let! result2Async = taskB |> Async.StartChild // start second request in parallel + x <- x + "AB" + let! result1 = result1Async + let! result2 = result2Async + x <- x + result1 + result2 + equal "ABCDEF" x + } + + testCaseAsync "Async.StartChild applies timeout" + <| fun () -> + async { + let mutable x = "" + + let task = + async { + x <- x + "A" + do! Async.Sleep 1_000 + x <- x + "X" // Never hit + } + + try + let! childTask = Async.StartChild(task, 200) + + do! childTask + with :? TimeoutException -> + x <- x + "B" + + x <- x + "C" + + equal "ABC" x + } + + testCaseAsync "Async.StartChild with timeout completes when computation finishes before timeout" + <| fun () -> // See #4481 + async { + let fast = async { do! Async.Sleep 10 } + + try + let! child = Async.StartChild(fast, 1_000) + do! child + equal true true // should reach here + with :? TimeoutException -> + failwith "should not time out" + } + + testCaseAsync "Unit arguments are erased" + <| fun () -> // See #1832 + let mutable token = 0 + + async { + let! res = async.Return 5 |> asyncMap (fun x -> token <- x) + equal 5 token + res + } + + testCaseAsync "Can use custom exceptions in async workflows #2396" + <| fun () -> + let workflow () : Async = async { return MyException(7) |> raise } + + let parentWorkflow () = + async { + try + do! workflow () + return 100 + with :? MyException as ex -> + return ex.Value + } + + async { + let! res = parentWorkflow () + equal 7 res + } + + testCase "Async try .. with does not run 'with' branch when body succeeds" + <| fun () -> + let work = + async { + try + return 1 + with _ -> + return 99 + } + + let mutable result = 0 + Async.StartWithContinuations(work, (fun r -> result <- r), ignore, ignore) + equal 1 result + + testCase "Exception thrown by the continuation is not redirected to the 'with' branch" + <| fun () -> + let work = + async { + try + return 1 + with _ -> + return 99 + } + + let mutable calls = 0 + + throwsAnyError (fun () -> + Async.StartWithContinuations( + work, + (fun _ -> + calls <- calls + 1 + failwith "boom from continuation"), + ignore, + ignore + ) + ) + + equal 1 calls + + testCaseAsync "Async.StartChild cancels the child when the parent's token is cancelled" + <| fun () -> + async { + let mutable finallyRan = false + let mutable completed = false + let cts = new System.Threading.CancellationTokenSource() + + let child = + async { + try + do! Async.Sleep 500 + completed <- true + finally + finallyRan <- true + } + + let parent = + async { + let! childResult = Async.StartChild child + do! childResult + } + + Async.StartImmediate(parent, cts.Token) + do! Async.Sleep 200 + cts.Cancel() + do! Async.Sleep 600 + equal true finallyRan + equal false completed + } + + testCaseAsync "Async.Parallel children are cancelled when the parent's token is cancelled" + <| fun () -> + async { + let mutable completed = 0 + let cts = new System.Threading.CancellationTokenSource() + + let mkChild () = + async { + do! Async.Sleep 500 + completed <- completed + 1 + } + + let work = + async { + let! _ = Async.Parallel [ mkChild (); mkChild (); mkChild () ] + return () + } + + Async.StartImmediate(work, cts.Token) + do! Async.Sleep 200 + cts.Cancel() + do! Async.Sleep 600 + equal 0 completed + } + + testCaseAsync "Async.Sequential children are cancelled when the parent's token is cancelled" + <| fun () -> + async { + let mutable completed = 0 + let cts = new System.Threading.CancellationTokenSource() + + let mkChild () = + async { + do! Async.Sleep 500 + completed <- completed + 1 + } + + let work = + async { + let! _ = Async.Sequential [ mkChild (); mkChild (); mkChild () ] + return () + } + + Async.StartImmediate(work, cts.Token) + do! Async.Sleep 200 + cts.Cancel() + do! Async.Sleep 600 + equal 0 completed + } + + testCase "CancellationTokenSource.Cancel invokes registrations only once" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + let mutable calls = 0 + + cts.Token.Register(fun () -> calls <- calls + 1) |> ignore + + cts.Cancel() + cts.Cancel() + + equal 1 calls + + testCase "CancellationToken registration can be disposed" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + let mutable called = false + + let registration = cts.Token.Register(fun () -> called <- true) + + registration.Dispose() + + cts.Cancel() + + equal false called + + testCase "CancellationToken registration after cancellation runs immediately" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + let mutable called = false + + cts.Cancel() + + cts.Token.Register(fun () -> called <- true) |> ignore + + equal true called + + testCase "ThrowIfCancellationRequested throws after cancellation" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + + // Does not throw before cancellation. + cts.Token.ThrowIfCancellationRequested() + + cts.Cancel() + + throwsAnyError (fun () -> cts.Token.ThrowIfCancellationRequested()) + + testCase "Async.CancellationToken returns current cancellation token" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + + let work = + async { + let! token = Async.CancellationToken + token = cts.Token |> equal true + } + + Async.StartWithContinuations(work, ignore, raise, raise, cts.Token) + + testCaseAsync "CancellationTokenSource.CancelAfter can be reset" + <| fun () -> + async { + let cts = new System.Threading.CancellationTokenSource() + + cts.CancelAfter 300 + do! Async.Sleep 30 + + cts.CancelAfter 500 + do! Async.Sleep 350 + + equal false cts.IsCancellationRequested + + do! Async.Sleep 200 + + equal true cts.IsCancellationRequested + } diff --git a/tests/Dart/src/DictionaryTests.fs b/tests/Dart/src/DictionaryTests.fs index 6addc9e00d..544d1197f6 100644 --- a/tests/Dart/src/DictionaryTests.fs +++ b/tests/Dart/src/DictionaryTests.fs @@ -186,6 +186,15 @@ let tests() = dic.Add("B", "World!") dic.Remove("A") |> equal true dic.Remove("C") |> equal false + dic.Count |> equal 1 + + testCase "Dictionary.Remove with complex keys works" <| fun () -> + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 testCase "Dictionary.Remove with records as keys works" <| fun () -> let x1 = { a = 5 } @@ -197,6 +206,7 @@ let tests() = dic.Add(x3, "World!") dic.Remove(x2) |> equal true dic.Remove(x4) |> equal false + dic.Count |> equal 1 testCase "Interface IDictionary.Count works" <| fun () -> let dic = dict <| seq { for i in 1. .. 10. -> i, i*i } diff --git a/tests/Dart/src/Fable.Tests.Dart.fsproj b/tests/Dart/src/Fable.Tests.Dart.fsproj index 2fe141bc5a..cb8aec2cdd 100644 --- a/tests/Dart/src/Fable.Tests.Dart.fsproj +++ b/tests/Dart/src/Fable.Tests.Dart.fsproj @@ -1,14 +1,17 @@ net10.0 + Exe Major + + @@ -23,12 +26,14 @@ + + @@ -39,6 +44,6 @@ - + diff --git a/tests/Dart/src/ListTests.fs b/tests/Dart/src/ListTests.fs index 58cd87c5df..ccc400dc40 100644 --- a/tests/Dart/src/ListTests.fs +++ b/tests/Dart/src/ListTests.fs @@ -1063,3 +1063,104 @@ let tests() = List.distinct sample |> List.length |> equal 3 throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + + testCase "List random functions match FSharp.Core" <| fun () -> + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + throwsErrorContaining "randomizer returned NaN, should be in range [0.0, 1.0)." (fun () -> List.randomChoiceBy (fun () -> System.Double.NaN) [1; 2]) + throwsErrorContaining "The input sequence was empty." (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + + testCase "List.randomShuffleWith and randomSampleWith draw like Array's" <| fun () -> + List.randomShuffleWith (System.Random(42)) [0 .. 9] |> equal (List.ofArray (Array.randomShuffleWith (System.Random(42)) [|0 .. 9|])) + List.randomSampleWith (System.Random(42)) 3 [0 .. 9] |> equal (List.ofArray (Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|])) + + testCase "List.reduce and List.min on an empty list raise ArgumentException" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.maxBy id xs) |> equal "ArgumentException" + + testCase "List.head, List.tail and List.last on an empty list raise ArgumentException" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.tail xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.last xs) |> equal "ArgumentException" + + testCase "list.Head and list.Tail on an empty list raise InvalidOperationException" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> xs.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> xs.Tail) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + + testCase "List.take and List.splitAt past the end raise InvalidOperationException" <| fun () -> + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.splitAt 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitAt -1 [1]) |> equal "ArgumentException" + + testCase "List.skip past the end raises ArgumentOutOfRangeException" <| fun () -> + raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" + + testCase "List functions with invalid arguments raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> List.windowed 0 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun _ -> 5) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun _ -> 0) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) [1] [1; 2] 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[1]; [2; 3]]) |> equal "ArgumentException" + + testCase "List random functions raise ArgumentOutOfRangeException when the randomizer is out of range" <| fun () -> + raisedExceptionType (fun () -> List.randomShuffleBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> 1.0) 1 [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + + testCase "List functions on lists of different lengths raise ArgumentException" <| fun () -> + let xs = [1; 2] + let ys = [1; 2; 3] + raisedExceptionType (fun () -> List.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 xs ys ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 ys ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map2 (+) [] [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.mapi2 (fun i x y -> i + x + y) ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x y z -> x + y + z) ys xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iter2 (fun _ _ -> ()) [1] []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iteri2 (fun _ _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> true) xs ys) |> equal "ArgumentException" + + testCase "List functions on lists of different lengths call the function on the common prefix first" <| fun () -> + let calls = ResizeArray() + raisedExceptionType (fun () -> List.iter2 (fun x y -> calls.Add(x + y)) [1; 2] [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x y z -> calls.Add(x + y + z)) [1; 2; 3] [1; 2] [1; 2]) |> equal "ArgumentException" + calls |> Seq.toList |> equal [2; 4; 3; 6] + + testCase "List.forall2 returns false before reaching the end of lists of different lengths" <| fun () -> + List.forall2 (fun x y -> x + y < 3) [1; 2] [1; 2; 3] |> equal false + List.forall2 (fun _ _ -> false) [1; 2; 3] [1; 2] |> equal false + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> false) [] [1]) |> equal "ArgumentException" + + testCase "List.init and List.replicate with a negative count raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> List.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.replicate -1 "a") |> equal "ArgumentException" + List.init 0 id |> equal [] + + testCase "List.average and List.averageBy on an empty list raise ArgumentException" <| fun () -> + let xs: float list = [] + raisedExceptionType (fun () -> List.average xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.averageBy (fun x -> x * 2.) xs) |> equal "ArgumentException" + + testCase "List.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [1; 2; 3] + raisedExceptionType (fun () -> List.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 2 2 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 4 0 xs) |> equal "ArgumentException" + List.removeManyAt 3 0 xs |> equal [1; 2; 3] + List.removeManyAt 1 0 xs |> equal [1; 2; 3] + List.removeManyAt 2 -1 xs |> equal [1; 2; 3] + List.removeManyAt 0 0 ([]: int list) |> equal [] + List.removeManyAt 1 2 xs |> equal [1] diff --git a/tests/Dart/src/Main.fs b/tests/Dart/src/Main.fs new file mode 100644 index 0000000000..df367d8380 --- /dev/null +++ b/tests/Dart/src/Main.fs @@ -0,0 +1,54 @@ +module Fable.Tests.Dart.Main + +let allTests = + [| + "Arithmetic", Arithmetic.tests + "Async", Async.tests + "Random", RandomTests.tests + "Array", Array.tests + "Comparison", Comparison.tests + "CustomOperator", CustomOperator.tests + "DateTime", DateTime.tests + "Dictionary", Dictionary.tests + "Enum", Enum.tests + "Enumerable", Enumerable.tests + "HashSet", HashSet.tests + "InlineIfLambda", InlineIfLambda.tests + "List", List.tests + "Map", Map.tests + "Misc", Misc.tests + "Nullable", Nullable.tests + "Option", Option.tests + "Quotation", Quotation.tests + "Record", Record.tests + "Regex", Regex.tests + "ResizeArray", ResizeArray.tests + "Result", Result.tests + "Rune", Rune.tests + "Seq", Seq.tests + "SeqExpression", SeqExpression.tests + "Set", Set.tests + "String", String.tests + "Sudoku", Sudoku.tests + "TailCall", TailCall.tests + "TimeSpan", TimeSpan.tests + "Tuple", Tuple.tests + "Type", Type.tests + "Union", Union.tests + |] + +#if FABLE_COMPILER +let main () = + for _, test in allTests do + test () + 0 +#else +open Expecto + +[] +let main args = + for name, test in allTests do + Util.collectExpectoModule name test + Util.getExpectoTests () + |> runTestsWithCLIArgs [] args +#endif \ No newline at end of file diff --git a/tests/Dart/src/MapTests.fs b/tests/Dart/src/MapTests.fs index 68161e9c95..366f6b9dab 100644 --- a/tests/Dart/src/MapTests.fs +++ b/tests/Dart/src/MapTests.fs @@ -295,3 +295,11 @@ let tests() = |> Map.add { Bar = "a"; Foo = 10 } 2 |> Map.count |> equal 1 + + testCase "Map enumerator Current outside the enumeration raises InvalidOperationException" <| fun () -> + let e = (Map.ofList [(1, "a")] :> seq>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + e.MoveNext() |> equal true + e.Current.Key |> equal 1 + e.MoveNext() |> equal false + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Dart/src/MiscTests.fs b/tests/Dart/src/MiscTests.fs index c6278ff2ec..ecee4fd97d 100644 --- a/tests/Dart/src/MiscTests.fs +++ b/tests/Dart/src/MiscTests.fs @@ -63,7 +63,22 @@ type ElmishWidget<'Model, 'View, 'Msg>(view: 'Model -> ('Msg -> unit) -> 'View) let _view = view member _.Render(model, dispatch) = _view model dispatch +let paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +type GenericNameRecord<'a> = + { Value: 'a } + + static member Stringify v = string v + let tests() = + testCase "Compiler-generated generic names do not clash with the type's own" <| fun () -> + GenericNameRecord.Stringify 456 |> equal "456" + { Value = "x" }.Value |> equal "x" + testCase "ref works" <| fun () -> let x = ref 5 increase x @@ -155,6 +170,45 @@ let tests() = invalidArg "arg" "This is invalid" ) + testCase "invalidArg raises ArgumentException" <| fun () -> + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid (Parameter 'arg')" caught + + testCase "ArgumentException.ParamName works" <| fun () -> + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + // Dart strings are not nullable, so an absent name reads as an empty string + System.ArgumentException("m").ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException().ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException("m", exn "i").ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> System.String.IsNullOrEmpty |> equal true + + testCase "ArgumentException.ParamName of FSharp.Core errors works" <| fun () -> + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + + testCase "invalidOp raises InvalidOperationException" <| fun () -> + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid" caught + testCase "Compiler target flags have correct value per target" <| fun () -> equal false Compiler.isJavaScript equal false Compiler.isTypeScript diff --git a/tests/Dart/src/NullableTests.fs b/tests/Dart/src/NullableTests.fs new file mode 100644 index 0000000000..1a8b13689b --- /dev/null +++ b/tests/Dart/src/NullableTests.fs @@ -0,0 +1,60 @@ +module Fable.Tests.Dart.Nullable + +open System +open Util +open FSharp.Linq + +let private none<'T when 'T: struct and 'T: (new: unit -> 'T) and 'T :> ValueType> = Nullable<'T>() + +let tests() = + testCase "Nullable.char works" <| fun () -> + Nullable.char (Nullable 49uy) |> equal (Nullable '1') + Nullable.char (Nullable 53) |> equal (Nullable '5') + Nullable.char (Nullable 55L) |> equal (Nullable '7') + Nullable.char (Nullable 58.0) |> equal (Nullable ':') + Nullable.char (Nullable 'a') |> equal (Nullable 'a') + Nullable.char none |> equal none + + testCase "Nullable.enum works" <| fun () -> + (Nullable.enum (Nullable 2): Nullable) |> equal (Nullable DayOfWeek.Tuesday) + (Nullable.enum none: Nullable) |> equal none + + testCase "Nullable.float and Nullable.float32 work" <| fun () -> + Nullable.float (Nullable 1uy) |> equal (Nullable 1.0) + Nullable.float (Nullable 5) |> equal (Nullable 5.0) + Nullable.float (Nullable 7L) |> equal (Nullable 7.0) + Nullable.float (Nullable 'c') |> equal (Nullable 99.0) + Nullable.float none |> equal none + Nullable.float32 (Nullable 5) |> equal (Nullable 5.0f) + Nullable.float32 (Nullable 10.0) |> equal (Nullable 10.0f) + Nullable.float32 none |> equal none + Nullable.double (Nullable 5) |> equal (Nullable 5.0) + Nullable.single (Nullable 9.5) |> equal (Nullable 9.5f) + + testCase "Nullable integer conversions work" <| fun () -> + Nullable.int (Nullable 1uy) |> equal (Nullable 1) + Nullable.int (Nullable 7L) |> equal (Nullable 7) + Nullable.int (Nullable 10.9) |> equal (Nullable 10) + Nullable.int (Nullable -11.9) |> equal (Nullable -11) + Nullable.int (Nullable 'c') |> equal (Nullable 99) + Nullable.int none |> equal none + Nullable.int16 (Nullable -7L) |> equal (Nullable -7s) + Nullable.int32 (Nullable 5L) |> equal (Nullable 5) + Nullable.int64 (Nullable -9.9) |> equal (Nullable -9L) + Nullable.int64 none |> equal none + Nullable.int8 (Nullable 9.9) |> equal (Nullable 9y) + Nullable.sbyte (Nullable -7.9) |> equal (Nullable -7y) + Nullable.byte (Nullable 5) |> equal (Nullable 5uy) + Nullable.uint8 (Nullable 7L) |> equal (Nullable 7uy) + Nullable.uint16 (Nullable 9.9) |> equal (Nullable 9us) + Nullable.uint (Nullable 5) |> equal (Nullable 5u) + Nullable.uint32 (Nullable 7L) |> equal (Nullable 7u) + Nullable.uint64 none |> equal none + + testCase "Nullable conversions evaluate their argument once" <| fun () -> + let mutable calls = 0 + let next () = + calls <- calls + 1 + Nullable 5 + Nullable.float (next ()) |> equal (Nullable 5.0) + calls |> equal 1 diff --git a/tests/Dart/src/QuotationTests.fs b/tests/Dart/src/QuotationTests.fs index 5553bd3bec..80a93538a6 100644 --- a/tests/Dart/src/QuotationTests.fs +++ b/tests/Dart/src/QuotationTests.fs @@ -3,6 +3,7 @@ module Fable.Tests.Dart.Quotation open Util open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Microsoft.FSharp.Linq.RuntimeHelpers type C = @@ -14,6 +15,27 @@ type Cfg = type MutableRec = { mutable Value: int } + +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + let tests() = testCase "Simple integer value quotation" <| fun () -> let q = <@ 42 @> @@ -134,6 +156,7 @@ let tests() = let result = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> (fun y -> x + y)) 3 4 @> equal 7 (result :?> int) +#if FABLE_COMPILER testCase "Evaluate tuple" <| fun () -> // NOTE: the canonical suite casts the result to `(int * int)`, but Dart // reifies generic type arguments, so `obj :?> (int * int)` fails for a @@ -143,6 +166,7 @@ let tests() = let arr = result :?> obj[] equal 1 (arr.[0] :?> int) equal 2 (arr.[1] :?> int) +#endif testCase "Expr.GetFreeVars returns empty for closed expr" <| fun () -> let q = <@ fun x -> x + 1 @> @@ -207,6 +231,7 @@ let tests() = | NewRecord(_ty, values) -> equal 1 (List.length values) | _ -> failwith "Expected NewRecord" +#if FABLE_COMPILER testCase "Evaluate mutable variable set (VarSet)" <| fun () -> let result = LeafExpressionConverter.EvaluateQuotation <@ let mutable v = 1 in (v <- 2); v @> equal 2 (result :?> int) @@ -217,3 +242,11 @@ let tests() = <@ let r = { Value = 1 }: MutableRec in r.Value <- 5; r.Value @> equal 5 (result :?> int) +#endif + + testCase "DerivedPatterns AndAlso, OrElse and SpecificCall work" <| fun () -> + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" diff --git a/tests/Dart/src/RandomTests.fs b/tests/Dart/src/RandomTests.fs index 1f403d1ef3..7fe0212ea0 100644 --- a/tests/Dart/src/RandomTests.fs +++ b/tests/Dart/src/RandomTests.fs @@ -106,3 +106,17 @@ let tests () = Random(5432).NextBytes(buffer1) Random(5432).NextBytes(buffer2) buffer1 |> equal buffer2 + + testCase "System.Random gives .NET's argument errors" <| fun () -> + for rnd in [Random(42); Random()] do + throwsErrorContaining "maxValue ('-1') must be a non-negative value. (Parameter 'maxValue')" (fun () -> rnd.Next(-1)) + throwsErrorContaining "'minValue' cannot be greater than maxValue. (Parameter 'minValue')" (fun () -> rnd.Next(5, 3)) + raisedExceptionType (fun () -> rnd.Next(-1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> rnd.Next(5, 3)) |> equal "ArgumentOutOfRangeException" + + testCase "System.Random.Shared works" <| fun () -> + let x = Random.Shared.Next(5) + (x >= 0 && x < 5) |> equal true + let x = Random.Shared.NextDouble() + (x >= 0.0 && x < 1.0) |> equal true + obj.ReferenceEquals(Random.Shared, Random.Shared) |> equal true diff --git a/tests/Dart/src/RecordTests.fs b/tests/Dart/src/RecordTests.fs index 179801b326..04d12ebe71 100644 --- a/tests/Dart/src/RecordTests.fs +++ b/tests/Dart/src/RecordTests.fs @@ -42,6 +42,30 @@ type Time = type CarInterior = { Seats: int } type Car = { Interior: CarInterior } +type RecordWithIdField = { Id: Id } + +type Wrapped = { Value: int } + +let addOne (x: int) = x + 1 + +type IWrap = + abstract Wrap: unit -> Wrapped + abstract Add: unit -> int + +type RecordShadowingFileScope = + { Wrapped: int; addOne: int } + interface IWrap with + member this.Wrap() = { Value = this.Wrapped } + member this.Add() = addOne this.addOne + +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + let tests() = testCase "Anonymous records work" <| fun () -> let r = makeAnonRec() @@ -155,3 +179,42 @@ let tests() = let car2 = {| car with Interior.Seats = 5 |} equal 5 car2.Interior.Seats + + testCase "Record field can have the same name as its type" <| fun () -> + let record = { Id = Id "foo" } + match record.Id with + | Id value -> value |> equal "foo" + + testCase "Record fields can have the same name as file scope declarations" <| fun () -> + let record = { Wrapped = 3; addOne = 7 } :> IWrap + record.Wrap().Value |> equal 3 + record.Add() |> equal 8 + + testCase "Copy-and-update of a let mutable struct record keeps the unchanged fields" <| fun () -> + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + + testCase "Copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields" <| fun () -> + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + + testCase "Copy-and-update of a module-level mutable struct record keeps the unchanged fields" <| fun () -> + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + + testCase "Copy-and-update of a let mutable anonymous struct record keeps the unchanged fields" <| fun () -> + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) diff --git a/tests/Dart/src/RegexTests.fs b/tests/Dart/src/RegexTests.fs index 6c7020f958..acc8186619 100644 --- a/tests/Dart/src/RegexTests.fs +++ b/tests/Dart/src/RegexTests.fs @@ -41,6 +41,13 @@ let tests() = let r = Regex("[a-z]", options) int r.Options |> equal 257 + testCase "RegexOptions.NonBacktracking is accepted" <| fun _ -> + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + testCase "Regex.IsMatch with IgnoreCase and Multiline works" <| fun _ -> let str = "ab\ncd" let option1 = RegexOptions.IgnoreCase diff --git a/tests/Dart/src/ResizeArrayTests.fs b/tests/Dart/src/ResizeArrayTests.fs index 706e5f788e..61788d980c 100644 --- a/tests/Dart/src/ResizeArrayTests.fs +++ b/tests/Dart/src/ResizeArrayTests.fs @@ -2,6 +2,20 @@ open Util +type AscendingComparer() = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = compare x y + +type DescendingComparer() = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = compare y x + +type LoggingComparer(calls: ResizeArray) = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y + let tests() = testCase "ResizeArray zero creation works" <| fun () -> let li = ResizeArray() @@ -171,6 +185,60 @@ let tests() = li.IndexOf("ab") |> equal 1 li.IndexOf("cd") |> equal -1 + testCase "ResizeArray.BinarySearch works" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(7) |> equal 3 + ra.BinarySearch(8) |> equal -5 + (ResizeArray [9; 7; 5; 3; 1]).BinarySearch(3, DescendingComparer()) |> equal 3 + // Comparer.Default isn't supported in Dart + ra.BinarySearch(1, 3, 7, AscendingComparer()) |> equal 3 + ra.BinarySearch(1, 2, 8, AscendingComparer()) |> equal -4 + ra.BinarySearch(1, 2, 3, null) |> equal 1 + + testCase "ResizeArray.BinarySearch works with an empty list, duplicates and a null range comparer" <| fun () -> + ResizeArray().BinarySearch(1) |> equal -1 + (ResizeArray [2; 2; 2; 2]).BinarySearch(2) |> equal 1 + (ResizeArray [1; 2; 2; 2; 2; 3]).BinarySearch(2) |> equal 2 + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(0, 5, 2, null) |> equal -2 + ra.BinarySearch(2, 3, 9, null) |> equal 4 + + testCase "ResizeArray.BinarySearch with a range passes the element to the comparer first" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let calls = ResizeArray() + let logging = + LoggingComparer(calls) + ra.BinarySearch(1, 3, 7, logging) |> equal 3 + List.ofSeq calls |> equal [(5, 7); (7, 7)] + + testCase "ResizeArray.BinarySearch with a range checks index and count together" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(6, 0, 3, null)) |> equal "ArgumentException" + raised (fun () -> ra.BinarySearch(0, 6, 3, null)) |> equal "ArgumentException" + + testCase "ResizeArray.BinarySearch with a range checks its arguments" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, 2, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(3, 3, 3, null)) |> equal "ArgumentException" + testCase "ResizeArray.Remove works" <| fun () -> let li = ResizeArray<_>() li.Add("ab") diff --git a/tests/Dart/src/RuneTests.fs b/tests/Dart/src/RuneTests.fs new file mode 100644 index 0000000000..4b2b967bd0 --- /dev/null +++ b/tests/Dart/src/RuneTests.fs @@ -0,0 +1,124 @@ +module Fable.Tests.Dart.Rune + +open System.Text +open Fable.Core +open Util + +[] +let private stringFromCharCode (_: int) : string = + nativeOnly + +let tests () = + testCase "Rune.GetRuneAt works with BMP characters" <| fun () -> + let rune = Rune.GetRuneAt("ABC", 1) + + rune.Value |> equal 0x42 + rune.ToString() |> equal "B" + + testCase "Rune.GetRuneAt works with supplementary characters" <| fun () -> + let rune = Rune.GetRuneAt("𠀀", 0) + + rune.Value |> equal 0x20000 + rune.ToString() |> equal "𠀀" + + testCase "Rune.GetRuneAt uses UTF-16 code unit indexes" <| fun () -> + Rune.GetRuneAt("A𠀀B", 0).ToString() + |> equal "A" + + Rune.GetRuneAt("A𠀀B", 1).ToString() + |> equal "𠀀" + + Rune.GetRuneAt("A𠀀B", 3).ToString() + |> equal "B" + + testCase "Rune.GetRuneAt throws when index points inside a surrogate pair" <| fun () -> + throwsAnyError <| fun () -> + Rune.GetRuneAt("A𠀀B", 2) + |> ignore + + testCase "Rune.GetRuneAt throws when index is out of range" <| fun () -> + throwsAnyError <| fun () -> + Rune.GetRuneAt("ABC", -1) + |> ignore + + throwsAnyError <| fun () -> + Rune.GetRuneAt("ABC", 3) + |> ignore + +#if FABLE_COMPILER + testCase "Rune.GetRuneAt throws for unpaired surrogate characters" <| fun () -> + let highSurrogate = stringFromCharCode 0xD800 + let lowSurrogate = stringFromCharCode 0xDC00 + + throwsAnyError <| fun () -> + Rune.GetRuneAt(highSurrogate, 0) + |> ignore + + throwsAnyError <| fun () -> + Rune.GetRuneAt(lowSurrogate, 0) + |> ignore +#endif + + testCase "Rune.Value returns the Unicode scalar value" <| fun () -> + Rune.GetRuneAt("A", 0).Value + |> equal 0x41 + + Rune.GetRuneAt("𠀀", 0).Value + |> equal 0x20000 + + testCase "Rune.Utf16SequenceLength returns the number of UTF-16 code units" <| fun () -> + Rune.GetRuneAt("A", 0).Utf16SequenceLength + |> equal 1 + + Rune.GetRuneAt("𠀀", 0).Utf16SequenceLength + |> equal 2 + + testCase "Rune.Utf16SequenceLength works at the BMP boundary" <| fun () -> + Rune.GetRuneAt("\uFFFF", 0).Utf16SequenceLength + |> equal 1 + + Rune.GetRuneAt("\U00010000", 0).Utf16SequenceLength + |> equal 2 + + testCase "Rune.ToString returns the represented Unicode scalar as a string" <| fun () -> + Rune.GetRuneAt("A", 0).ToString() + |> equal "A" + + Rune.GetRuneAt("𠀀", 0).ToString() + |> equal "𠀀" + + testCase "F# string conversion works with Rune" <| fun () -> + Rune.GetRuneAt("A", 0) + |> string + |> equal "A" + + Rune.GetRuneAt("𠀀", 0) + |> string + |> equal "𠀀" + + testCase "String.EnumerateRunes enumerates Unicode scalar values" <| fun () -> + "A𠀀B".EnumerateRunes() + |> Seq.map (fun rune -> rune.Value) + |> Seq.toList + |> equal [ 0x41; 0x20000; 0x42 ] + + testCase "String.EnumerateRunes works with Rune string conversion" <| fun () -> + "A𠀀B".EnumerateRunes() + |> Seq.map string + |> Seq.toList + |> equal [ "A"; "𠀀"; "B" ] + +#if FABLE_COMPILER + testCase "String.EnumerateRunes replaces unpaired surrogates" <| fun () -> + // .NET String.EnumerateRunes replaces invalid UTF-16 sequences with U+FFFD + // https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.replacementchar + let input = + stringFromCharCode 0xD800 + + "A" + + stringFromCharCode 0xDC00 + + input.EnumerateRunes() + |> Seq.map _.Value + |> Seq.toList + |> equal [ 0xFFFD; 0x41; 0xFFFD ] +#endif diff --git a/tests/Dart/src/SeqTests.fs b/tests/Dart/src/SeqTests.fs index ce53aaf7c8..a9ef1bf50a 100644 --- a/tests/Dart/src/SeqTests.fs +++ b/tests/Dart/src/SeqTests.fs @@ -1163,3 +1163,91 @@ let tests() = Array.distinct sample |> Array.length |> equal 3 throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + + testCase "Seq.randomShuffleWith and randomSampleWith draw like Array's" <| fun () -> + Seq.randomShuffleWith (System.Random(42)) [0 .. 9] |> Seq.toArray |> equal (Array.randomShuffleWith (System.Random(42)) [|0 .. 9|]) + Seq.randomSampleWith (System.Random(42)) 3 [0 .. 9] |> Seq.toArray |> equal (Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|]) + + testCase "Seq.randomChoicesBy draws on enumeration" <| fun () -> + let mutable calls = 0 + let xs = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + xs |> Seq.toList |> equal [5; 5; 5] + xs |> Seq.toList |> equal [5; 5; 5] + calls |> equal 6 + let rng = System.Random(42) + let ys = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal (System.Random(42).Next()) + ys |> Seq.length |> equal 3 + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Seq.randomChoicesWith rng -1 [1]) + + testCase "Seq.reduce and Seq.min on an empty sequence raise ArgumentException" <| fun () -> + let xs: int seq = Seq.empty + raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id xs) |> equal "ArgumentException" + + testCase "Seq.take and Seq.skip past the end raise InvalidOperationException" <| fun () -> + raisedExceptionType (fun () -> Seq.take 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + + testCase "Seq.tail on an empty sequence raises ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Seq.tail Seq.empty |> Seq.toList) |> equal "ArgumentException" + Seq.tail [1; 2; 3] |> Seq.toList |> equal [2; 3] + + testCase "Seq functions with invalid arguments raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Seq.windowed 0 [1] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun _ -> 5) [1; 2] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun _ -> 0) [1; 2] |> Seq.toList) |> equal "ArgumentException" + + testCase "Seq random functions raise ArgumentOutOfRangeException when the randomizer is out of range" <| fun () -> + raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoicesBy (fun () -> 1.0) 1 [1] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + + testCase "Ranges with a zero step raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> [1 .. 0 .. 5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [|1L .. 0L .. 5L|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [1. .. 0. .. 5.]) |> equal "ArgumentException" + + testCase "Seq.init, Seq.replicate and Seq.take with a negative count raise when called" <| fun () -> + raisedExceptionType (fun () -> Seq.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.replicate -1 "a") |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.take -1 [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.take -1 (seq { 1; 2 })) |> equal "ArgumentException" + + testCase "Seq.chunkBySize, Seq.windowed and Seq.splitInto check their size when called" <| fun () -> + raisedExceptionType (fun () -> Seq.chunkBySize 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.chunkBySize -1 Seq.empty) |> equal "ArgumentException" + + testCase "Seq.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [1; 2; 3] + raisedExceptionType (fun () -> Seq.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 0 0 Seq.empty |> Seq.toList) |> equal "ArgumentException" + let yielded = ResizeArray() + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 xs |> Seq.iter (fun x -> yielded.Add x)) |> equal "ArgumentException" + yielded |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 2 2 xs |> Seq.toList |> equal [1; 2] + Seq.removeManyAt 1 3 xs |> Seq.toList |> equal [1] + Seq.removeManyAt 1 -1 xs |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 1 1 xs |> Seq.toList |> equal [1; 3] + + testCase "Seq.transpose groups the elements of sequences of different lengths by index" <| fun () -> + let toLists (xss: seq>) = xss |> Seq.map Seq.toList |> Seq.toList + Seq.transpose [[1; 2]; [3]] |> toLists |> equal [[1; 3]; [2]] + Seq.transpose [[1]; [2; 3]] |> toLists |> equal [[1; 2]; [3]] + Seq.transpose [[]; [1]] |> toLists |> equal [[1]] + Seq.transpose [[1; 2; 3]; [4]; [5; 6]] |> toLists |> equal [[1; 4; 5]; [2; 6]; [3]] + Seq.transpose (seq [seq { 1; 2 }; seq { 3 }]) |> toLists |> equal [[1; 3]; [2]] + Seq.transpose [|[|1|]; [||]|] |> toLists |> equal [[1]] + Seq.transpose ([[]]: int list list) |> toLists |> equal [] + let enumerated = ResizeArray() + let xss = Seq.transpose (seq { enumerated.Add 1; yield [1; 2]; yield [3] }) + enumerated.Count |> equal 0 + xss |> toLists |> equal [[1; 3]; [2]] diff --git a/tests/Dart/src/SetTests.fs b/tests/Dart/src/SetTests.fs index 21d9ea6fb0..bd1b5c7241 100644 --- a/tests/Dart/src/SetTests.fs +++ b/tests/Dart/src/SetTests.fs @@ -289,3 +289,21 @@ let tests() = largeSetB.IsSupersetOf(largeSetA) |> equal true largeSetA.IsProperSubsetOf(largeSetB) |> equal false largeSetA.IsSubsetOf(largeSetB) |> equal true + + testCase "Set.minElement and Set.maxElement on an empty set raise ArgumentException" <| fun () -> + let xs: Set = Set.empty + raisedExceptionType (fun () -> Set.minElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.maxElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MinimumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MaximumElement) |> equal "ArgumentException" + + testCase "Set.intersectMany with no sets raises ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Set.intersectMany ([]: Set list)) |> equal "ArgumentException" + + testCase "Set enumerator Current outside the enumeration raises InvalidOperationException" <| fun () -> + let e = (Set.ofList [1] :> seq).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + e.MoveNext() |> equal true + e.Current |> equal 1 + e.MoveNext() |> equal false + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Dart/src/StringTests.fs b/tests/Dart/src/StringTests.fs index 62f24fa4b7..d3722dbd79 100644 --- a/tests/Dart/src/StringTests.fs +++ b/tests/Dart/src/StringTests.fs @@ -77,11 +77,13 @@ let tests() = s.[2..] |> equal "t and dog" s.[..8] |> equal "cat and d" +#if FABLE_COMPILER testCase "String literal addition is optimized" <| fun () -> "bar" + aLiteral |> equal "barfoo" "bar" + notALiteral |> equal "barfoo" obj.ReferenceEquals("bar" + aLiteral, "barfoo") |> equal true obj.ReferenceEquals("bar" + notALiteral, "barfoo") |> equal false +#endif testCase "String chunkBySize works" <| fun () -> // See #1296 "fffff" |> Seq.chunkBySize 3 |> Seq.map String |> Seq.toList @@ -1119,6 +1121,46 @@ let tests() = String.filter (fun x -> x <> '.') "" |> equal "" + testCase "Convert.ToHexString works" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexString(bytes) + |> equal "FAFBFCFDFE" + Convert.ToHexString([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010F10" + Convert.ToHexString([||]) |> equal "" + Convert.ToHexString(bytes, 1, 3) |> equal "FBFCFD" + Convert.ToHexString(bytes, 5, 0) |> equal "" + + testCase "Convert.ToHexStringLower works" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexStringLower(bytes) + |> equal "fafbfcfdfe" + Convert.ToHexStringLower([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010f10" + Convert.ToHexStringLower([||]) |> equal "" + Convert.ToHexStringLower(bytes, 1, 3) |> equal "fbfcfd" + + testCase "Convert.ToHexString checks its arguments" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + raisedExceptionType (fun () -> Convert.ToHexString(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 1, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 6, 0)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + + testCase "Convert.FromHexString works" <| fun () -> + Convert.FromHexString("fafBFCFDFE") + |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.FromHexString("0123456789abcdefABCDEF") + |> equal [| 1uy; 35uy; 69uy; 103uy; 137uy; 171uy; 205uy; 239uy; 171uy; 205uy; 239uy |] + Convert.FromHexString("") |> equal [||] + + testCase "Convert.FromHexString checks its input" <| fun () -> + raisedExceptionType (fun () -> Convert.FromHexString("abc")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("zz")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("0x10")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("00 1")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("+1")) |> equal "FormatException" + // // TODO: Environment // #if FABLE_COMPILER // testCase "System.Environment.NewLine works" <| fun () -> diff --git a/tests/Dart/src/Tests.Util.fs b/tests/Dart/src/Tests.Util.fs index 9091e038e1..cb60e2c7a4 100644 --- a/tests/Dart/src/Tests.Util.fs +++ b/tests/Dart/src/Tests.Util.fs @@ -1,6 +1,9 @@ module Fable.Tests.Dart.Util +#if FABLE_COMPILER + open Fable.Core +open Fable.Core.Dart type Assertion = interface end @@ -8,17 +11,85 @@ type Assertion = [] type Test = static member test(msg: string, f: unit -> unit): unit = nativeOnly + static member test(msg: string, f: unit -> Future): unit = nativeOnly static member expect(actual: obj, assertion: Assertion): unit = nativeOnly static member equals(value: obj): Assertion = nativeOnly static member isNot(assertion: Assertion): Assertion = nativeOnly static member throwsException: Assertion = nativeOnly let testCase (msg: string) (f: unit -> unit) = Test.test(msg, f) +let testCaseAsync (msg: string) (f: unit -> Async) = Test.test(msg, fun () -> f () |> Async.StartAsFuture) let testList (msg: string) (tests: unit list) = () let equal (expected: 'T) (actual: 'T) = Test.expect(actual, Test.equals(expected)) let notEqual (expected: 'T) (actual: 'T) = Test.expect(actual, Test.isNot(Test.equals(expected))) let throwsAnyError (f: unit -> 'a): unit = Test.expect(f, Test.throwsException) +#else + +open Expecto +let private expectoTestCase = testCase +let private expectoTestCaseAsync = testCaseAsync +let private expectoTestList = testList + +let mutable private registeredTests = [] +let mutable private registeredTestNames = Map.empty + +let private uniqueTestName (name: string) = + match Map.tryFind name registeredTestNames with + | Some count -> + registeredTestNames <- Map.add name (count + 1) registeredTestNames + $"{name} ({count + 1})" + | None -> + registeredTestNames <- Map.add name 1 registeredTestNames + name + +let private register test = + registeredTests <- test :: registeredTests + +let testCase (msg: string) (f: unit -> unit) = + register (expectoTestCase (uniqueTestName msg) f) + +let testCaseAsync (msg: string) (f: unit -> Async) = + register (expectoTestCaseAsync (uniqueTestName msg) (f ())) + +let testList (msg: string) (tests: unit list) = + let testCount = List.length tests + let childTests = registeredTests |> List.take testCount |> List.rev + registeredTests <- registeredTests |> List.skip testCount + register (expectoTestList (uniqueTestName msg) childTests) + +let getExpectoTests () = expectoTestList "All" (List.rev registeredTests) +let collectExpectoModule (name: string) (registerTests: unit -> unit) = + let previousTests = registeredTests + let previousNames = registeredTestNames + registeredTests <- [] + registeredTestNames <- Map.empty + registerTests () + let moduleTests = registeredTests |> List.rev + registeredTests <- expectoTestList name moduleTests :: previousTests + registeredTestNames <- previousNames + +let equal expected actual: unit = Expecto.Expect.equal actual expected "" +let notEqual expected actual: unit = Expecto.Expect.notEqual actual expected "" +let throwsAnyError (f: unit -> 'a): unit = Expecto.Expect.throws (fun () -> f () |> ignore) "" + +#endif + +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentNullException -> "ArgumentNullException" + | :? System.ArgumentException -> "ArgumentException" + | :? System.InvalidOperationException -> "InvalidOperationException" + | :? System.Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | :? System.IndexOutOfRangeException -> "IndexOutOfRangeException" + | :? System.FormatException -> "FormatException" + | e -> "Unexpected exception: " + e.Message + let private run (f: unit -> 'a) = try f() diff --git a/tests/Dart/src/TypeTests.fs b/tests/Dart/src/TypeTests.fs index 5a498c02f4..fb6e41d578 100644 --- a/tests/Dart/src/TypeTests.fs +++ b/tests/Dart/src/TypeTests.fs @@ -595,6 +595,17 @@ type Model = unit let update (model: Model) = model, () +type Id = Id of string + +type IHasId = + abstract Id: Id + abstract GetId: unit -> Id + +type IdHolder(id: Id) = + interface IHasId with + member _.Id = id + member _.GetId() = id + let tests() = testCase "Unit arguments work" <| fun () -> @@ -614,6 +625,13 @@ let tests() = ex.Message |> equal "outer message" ex.InnerException.Message |> equal "the inner cause" + testCase "Interface members can have the same name as their type" <| fun () -> + let holder = IdHolder(Id "foo") :> IHasId + match holder.Id with + | Id value -> value |> equal "foo" + match holder.GetId() with + | Id value -> value |> equal "foo" + // testCase "Can implement interface optional properties" <| fun () -> // let veryOptionalValue = VeryOptionalClass() :> VeryOptionalInterface // veryOptionalValue.Bar |> equal (Some 3) diff --git a/tests/Dart/src/UnionTests.fs b/tests/Dart/src/UnionTests.fs index ef89a7f466..55d70fefe9 100644 --- a/tests/Dart/src/UnionTests.fs +++ b/tests/Dart/src/UnionTests.fs @@ -18,7 +18,6 @@ type TestUnion = | Case2 of string * string | Case3 of string * string * string - type TestUnion2 = | Tag of string | NewTag of string @@ -64,9 +63,9 @@ type Wrapper(s: string) = [] type MyUnion = -| Case1 -| Case2 -| Case3 + | Case1 + | Case2 + | Case3 type R = { Name: string @@ -89,6 +88,123 @@ type T8 = T8 type T9 = T9 type 'a more = More of 'a +[] +type LangCode = + | EN + | ``pt-Br`` + +type S = S of string + +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id + +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +// [] +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First + +[] +type StructPoint2D = + | Planar2D of float * float + | Infinity + +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + // member this.Area = // TODO: support instance members + static member Area this = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + // member this.Area = // TODO: support instance members + static member Area this = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + +type WrappedUnion = + | AString of string + +type DeepRecord = { Value: string } + +type DeepWrappedUnion = + | DeepWrappedA of string * DeepRecord + | DeepWrappedB of string + | DeepWrappedC of int + | DeepWrappedD of DeepRecord + | DeepWrappedE of int * int + | DeepWrappedF of WrappedUnion + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" + let tests() = testCase "Union cases matches with no arguments can be generated" <| fun () -> let x = Male @@ -187,19 +303,26 @@ let tests() = ex.Message.Contains("bar") |> equal false testCase "Equality works in filter" <| fun () -> - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> r.Case = MyUnion.Case1) |> Array.length |> equal 2 testCase "Pattern matching works in filter" <| fun () -> - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) |> Array.length |> equal 2 + testCase "Union field can have the same name as its type" <| fun () -> + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + // TODO // testCase "Case testing with erased unions works" <| fun () -> // let strify = function @@ -207,3 +330,134 @@ let tests() = // | Str s -> "s: " + s // Str "foo" |> strify |> equal "s: foo" // Int 42 |> strify |> equal "i: 42" + + testCase "Verify that union cases can have `-` in their names" <| fun () -> + // See https://github.com/fable-compiler/Fable/issues/4071 + let x = LangCode.``pt-Br`` + equal x LangCode.``pt-Br`` + + // testCase "sprintf formats strings cases correctly" <| fun () -> + // let s = sprintf "%A" (S "1") + // equal s "S \"1\"" + + // testCase "Union cases with no fields are physically equal" <| fun () -> + // obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true + // obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false + // obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true + // obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false + // obj.ReferenceEquals(T1, T1) |> equal true + + testCase "Named union case fields work when a field is called name" <| fun () -> + let value = NamedCase("v1", "v2") + match value with + | NamedCase(name, optional) -> + name |> equal "v1" + optional |> equal "v2" + + testCase "Static union member returning the union type works" <| fun () -> + Example.All |> Array.length |> equal 2 + Example.All.[0] |> equal Example.First + Example.All.[1] |> equal Example.Second + + testCase "Static union member with nested self-reference works" <| fun () -> + Example.Head |> equal (Some Example.First) + Example.Nested |> List.head |> Array.head |> equal Example.First + Example.Pair |> fst |> equal Example.First + Example.Pair |> snd |> equal Example.Second + Example.Factory() |> equal Example.First + + testCase "Struct unions work" <| fun () -> + let distance p = + match p with + | Planar2D (x, y) -> sqrt (x * x + y * y) + | Infinity -> System.Double.PositiveInfinity //TODO: infinity literal + let p = Planar2D (3, 4) + let res = distance p + res |> equal 5. + + testCase "Struct unions with fields work" <| fun () -> + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + // circle.Area |> equal (System.Math.PI * radius * radius) + // square.Area |> equal (4.0 * radius * radius) + StructShape.Area circle |> equal (System.Math.PI * radius * radius) + StructShape.Area square |> equal (4.0 * radius * radius) + + testCase "Struct unions without fields work" <| fun () -> + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + // circle.Area |> equal (System.Math.PI * radius * radius) + // square.Area |> equal (4.0 * radius * radius) + StructShape2.Area circle |> equal (System.Math.PI * radius * radius) + StructShape2.Area square |> equal (4.0 * radius * radius) + + testCase "Nested union match keeps case-specific field access" <| fun () -> + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + + testCase "Union case matching works" <| fun () -> + let x = IntCase1 5 + let res = + match x with + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + res |> equal 5 + + testCase "Union case equality works" <| fun () -> + IntCase1 5 = IntCase1 5 |> equal true + IntCase1 5 = IntCase2 5 |> equal false + IntCase3 2 = IntCase3 3 |> equal false + IntCase2 1 = IntCase2 1 |> equal true + IntCase3 1 = IntCase3 1 |> equal true + + testCase "Union fn call works" <| fun () -> + let x = IntCase1 3 + let res = unionFnAlways1 x + let res2 = unionFnAlways1 x //deliberately force clone/borrow break + let res3 = unionFnRetNum x + let res4 = unionFnRetNum (IntCase2 24) + res |> equal 3 + res2 |> equal 3 + res3 |> equal 3 + res4 |> equal 24 + + testCase "Union with wrapped type works" <| fun () -> + let a = AString "hello" + let b = match a with AString s -> s + " world" + b |> equal "hello world" + + testCase "Deep union with wrapped type works" <| fun () -> + let a = DeepWrappedA (" world", {Value = "hello"}) + let b = DeepWrappedB "world" + let c = DeepWrappedC 42 + let d = DeepWrappedD { Value = "hello" } + let f = DeepWrappedF (AString "doublewrapped") + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + a |> matchStrings |> equal "hello world" + b |> matchStrings |> equal "world" + c |> matchStrings |> equal "nothing" + d |> matchStrings |> equal "hello" + f |> matchStrings |> equal "doublewrapped" + g |> matchStrings |> equal "G" + + testCase "Deep union with tuped prim type works" <| fun () -> + let e = DeepWrappedE (3, 2) + let c = DeepWrappedC 42 //todo to string! + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + e |> matchNumbers |> equal 5 + c |> matchNumbers |> equal 42 + g |> matchNumbers |> equal 365 + + testCase "Multi-case Union with wrapped type works" <| fun () -> + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" + + testCase "Match with condition works" <| fun () -> + let b1 = DeepWrappedB "hello" + let b2 = DeepWrappedB "not" + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" diff --git a/tests/Integration/Compiler/BehaviorWarningTests.fs b/tests/Integration/Compiler/BehaviorWarningTests.fs new file mode 100644 index 0000000000..dd60f88717 --- /dev/null +++ b/tests/Integration/Compiler/BehaviorWarningTests.fs @@ -0,0 +1,208 @@ +module Fable.Tests.Compiler.BehaviorWarning + +open Fable.Core +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private compile source = Compiler.Cached.compile Compiler.Settings.standard source + +(* + Warnings about code that compiles but behaves differently from .NET. Each one carries a + code so that a user who accepts the difference can silence it at the call site. +*) + +let tests = + testList "Behavior Warnings" [ + testCase "Console.Write is reported" <| fun _ -> + let source = + """ +System.Console.Write("x") +""" + compile source + |> Assert.Code.warning "FABLE0108" + |> ignore + + testCase "Console.Write can be suppressed by code" <| fun _ -> + let source = + """ +System.Console.Write("x") // fable-disable-line FABLE0108 +""" + compile source + |> Assert.Code.noWarning "FABLE0108" + |> ignore + + testCase "Async.Start is reported" <| fun _ -> + let source = + """ +Async.Start(async { () }) +""" + compile source + |> Assert.Code.warning "FABLE0109" + |> ignore + + testCase "Task.Result is reported" <| fun _ -> + let source = + """ +let t = task { return 1 } +let r: int = t.Result +""" + compile source + |> Assert.Code.warning "FABLE0110" + |> ignore + + testCase "GetType on obj is reported" <| fun _ -> + let source = + """ +let x: obj = box 1 +x.GetType() |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0111" + |> ignore + + testCase "A type test against an interface is reported" <| fun _ -> + let source = + """ +type IFoo = + abstract X: int + +let isFoo (o: obj) = match o with :? IFoo -> true | _ -> false +""" + compile source + |> Assert.Code.warning "FABLE0112" + |> ignore + + testCase "Inheriting through a secondary constructor is reported" <| fun _ -> + let source = + """ +type Base = + val X: int + new() = { X = 1 } + +type Derived() = + inherit Base() +""" + compile source + |> Assert.Code.warning "FABLE0114" + |> ignore + + testCase "An inlined argument at risk of double evaluation is reported" <| fun _ -> + let source = + """ +let inline orZero (o: int option) = + defaultArg o 0 + (if o.IsSome then 1 else 0) + +orZero (Some(System.Random().Next())) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0115" + |> ignore + + testCase "CaseTest outside a TypeScriptTaggedUnion is reported" <| fun _ -> + let source = + """ +open Fable.Core + +type Id = + | [] Name of string + | [] Slug of string + +let isName (id: Id) = match id with Name _ -> true | Slug _ -> false +""" + compile source + |> Assert.Code.warning "FABLE0116" + |> ignore + + testCase "Duplicate CompiledName on union cases is reported" <| fun _ -> + let source = + """ +type Prop = + | [] RefHook of int + | [] RefValue of string +""" + compile source + |> Assert.Code.warning "FABLE0121" + |> Assert.Exists.warningWith "Cases 'RefValue' and 'RefHook' of 'Prop' both compile to 'ref'" + |> ignore + + testCase "CompiledName clashing with another case name is reported" <| fun _ -> + let source = + """ +type Id = + | [] Name + | Slug +""" + compile source + |> Assert.Code.warning "FABLE0121" + |> ignore + + testCase "Duplicate CompiledName on field-less erased union cases is reported" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type X = + | [] A + | [] B +""" + compile source + |> Assert.Code.warning "FABLE0121" + |> Assert.Exists.warningWith "Cases 'B' and 'A' of 'X' both compile to 'ff'" + |> ignore + + testCase "Duplicate CompiledName on erased union cases with a field is not reported" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type X = + | [] A of int + | [] B of string +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "StringEnum values clashing through the case rule are reported" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Kind = + | [] S + | Small +""" + compile source + |> Assert.Code.warning "FABLE0121" + |> Assert.Exists.warningWith "Cases 'Small' and 'S' of 'Kind' both compile to 'small'" + |> ignore + + testCase "Duplicate CompiledName can be suppressed by code" <| fun _ -> + let source = + """ +type Prop = + | [] RefHook of int + | [] RefValue of string // fable-disable-line FABLE0121 +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag are not reported as duplicates" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Terminal = + | [] Done of value: float + | [] DoneError of error: string +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + ] diff --git a/tests/Integration/Compiler/CodeServicesProject/.gitignore b/tests/Integration/Compiler/CodeServicesProject/.gitignore new file mode 100644 index 0000000000..19ee8b3cc9 --- /dev/null +++ b/tests/Integration/Compiler/CodeServicesProject/.gitignore @@ -0,0 +1 @@ +Program.fs diff --git a/tests/Integration/Compiler/CodeServicesProject/CodeServicesProject.fsproj b/tests/Integration/Compiler/CodeServicesProject/CodeServicesProject.fsproj new file mode 100644 index 0000000000..ce84c87b1b --- /dev/null +++ b/tests/Integration/Compiler/CodeServicesProject/CodeServicesProject.fsproj @@ -0,0 +1,17 @@ + + + + Exe + net10.0 + Major + + + + + + + + + + + diff --git a/tests/Integration/Compiler/CodeServicesTests.fs b/tests/Integration/Compiler/CodeServicesTests.fs new file mode 100644 index 0000000000..3431f0a7aa --- /dev/null +++ b/tests/Integration/Compiler/CodeServicesTests.fs @@ -0,0 +1,157 @@ +module Fable.Tests.Compiler.CodeServicesApi + +open System +open Expecto +open Fable +open Fable.Compiler.Util +open Fable.Compiler.ProjectCracker +open Fable.Transforms.State +open FSharp.Compiler.SourceCodeServices + +/// Drives `Fable.Compiler.CodeServices` directly, the way a host embedding the compiler does, +/// instead of going through `Fable.Cli.Main`. +module private Fixture = + + let private projDir = + IO.Path.Join(__SOURCE_DIRECTORY__, "CodeServicesProject") |> Path.normalizeFullPath + + let private projFile = + IO.Path.Join(projDir, "CodeServicesProject.fsproj") |> Path.normalizeFullPath + + let private sourceFile = IO.Path.Join(projDir, "Program.fs") |> Path.normalizeFullPath + + let private cliArgs = + { + CliArgs.ProjectFile = projFile + // These tests only look at what the compiler reports, they never run the emitted + // JavaScript, so point at a fixed directory instead of depending on a fable-library + // build being present. + FableLibraryPath = Some(IO.Path.Join(projDir, "fable_modules", "fable-library-js")) + RootDir = projDir + Configuration = "Debug" + OutDir = None + IsWatch = false + Precompile = false + PrecompiledLib = None + PrintAst = false + SourceMaps = false + SourceMapsRoot = None + NoRestore = false + NoCache = false + NoGitignore = false + NoParallelTypeCheck = false + Exclude = [ "Fable.Core" ] + Replace = Map.empty + RunProcess = None + CompilerOptions = CompilerOptionsHelper.Make() + Verbosity = Verbosity.Silent + } + + /// Cracking is expensive and the project options never change, only the source does. + let private crackerResponse = + lazy + (let resolver: ProjectCrackerResolver = Fable.Compiler.MSBuildCrackerResolver() + CrackerOptions(cliArgs, false) |> getFullProjectOpts resolver) + + let private pathResolver = + { new PathResolver with + member _.TryPrecompiledOutPath(_sourceDir, _relativePath) = None + member _.GetOrAddDeduplicateTargetDir(_importDir, _addTargetDir) = "" + } + + /// Writes `source` as the single file of the project and hands back what the compiler needs. + /// NOTE: NOT threadsafe, that file is rewritten on every call. + let private prepare (source: string) = + IO.File.WriteAllText(sourceFile, source) + let crackerResponse = crackerResponse.Value + + let _, sourceReader = + crackerResponse.ProjectOptions.SourceFiles + |> Array.map Fable.Compiler.File + |> Fable.Compiler.File.MakeSourceReader + + let checker = InteractiveChecker.Create(crackerResponse.ProjectOptions) + crackerResponse, sourceReader, checker + + let compile (source: string) = + let crackerResponse, sourceReader, checker = prepare source + + async { + let! typeCheckResult = + Fable.Compiler.CodeServices.typeCheckProject sourceReader checker cliArgs crackerResponse + + return! + Fable.Compiler.CodeServices.compileMultipleFilesToJavaScript + pathResolver + cliArgs + crackerResponse + typeCheckResult + [ sourceFile ] + } + |> Async.RunSynchronously + + let compileToFableAST (source: string) = + let crackerResponse, sourceReader, checker = prepare source + + Fable.Compiler.CodeServices.compileFileToFableAST + sourceReader + checker + cliArgs + crackerResponse + sourceFile + |> Async.RunSynchronously + +/// `Async.RunSynchronously` type-checks fine but has no translation, so Fable reports it through +/// `addErrorAndReturnNull` and emits `return null`. +let private unsupportedByFable = + """module Program + +let blocking () = Async.RunSynchronously(async { return 42 }) +""" + +let private fsharpTypeError = + """module Program + +let wrong: int = "not an int" +""" + +let tests = + testList + "CodeServices" + [ + testCase "Fable errors are reported" + <| fun _ -> + let result = Fixture.compile unsupportedByFable + + let errors = + result.Logs |> Array.filter (fun log -> log.Severity = Severity.Error) + + Expect.exists + errors + (fun log -> log.Tag = "FABLE" && log.Message.Contains "not supported by Fable") + "Fable should report that Async.RunSynchronously is not supported" + + testCase "F# errors are reported" + <| fun _ -> + let result = Fixture.compile fsharpTypeError + + let errors = + result.Logs |> Array.filter (fun log -> log.Severity = Severity.Error) + + Expect.exists + errors + (fun log -> log.Tag = "FSHARP") + "The F# type error should be reported" + + testCase "Fable errors are reported when only transforming to the Fable AST" + <| fun _ -> + let result = Fixture.compileToFableAST unsupportedByFable + + let errors = + result.Logs |> Array.filter (fun log -> log.Severity = Severity.Error) + + Expect.exists + errors + (fun log -> log.Tag = "FABLE" && log.Message.Contains "not supported by Fable") + "Fable should report that Async.RunSynchronously is not supported" + ] diff --git a/tests/Integration/Compiler/CompilerHelpersTests.fs b/tests/Integration/Compiler/CompilerHelpersTests.fs index 5b4bd5dfa7..a3a2bfa393 100644 --- a/tests/Integration/Compiler/CompilerHelpersTests.fs +++ b/tests/Integration/Compiler/CompilerHelpersTests.fs @@ -5,8 +5,69 @@ open Util.Testing open Fable.Tests.Compiler.Util open Fable.Tests.Compiler.Util.Compiler +open System +open System.Reflection +open Fable.Transforms + +/// Every warning code `WarningCodes` can emit, read from the module itself so that this test +/// has no list of its own to fall out of date. +let private codesTheRegistryCanProduce () = + let flags = BindingFlags.Public ||| BindingFlags.Static + let moduleType = typeof.Assembly.GetType("Fable.Transforms.WarningCodes") + let codeOf (pair: obj) = fst (pair :?> string * string) + + // The parameterised warnings interpolate their argument into the message, never into the + // code, so anything of the right type will do. + let throwaway (p: ParameterInfo) = + if p.ParameterType = typeof then box "" + elif p.ParameterType.IsValueType then Activator.CreateInstance p.ParameterType + else null + + // A warning is anything shaped `string * string`: plain ones compile to static properties, + // parameterised ones to static methods. Nothing else in the module has that shape. + let fromValues = + moduleType.GetProperties(flags) + |> Array.filter (fun p -> p.PropertyType = typeof) + |> Array.map (fun p -> codeOf (p.GetValue(null))) + + let fromFactories = + moduleType.GetMethods(flags) + |> Array.filter (fun m -> not m.IsSpecialName && m.ReturnType = typeof) + |> Array.map (fun m -> codeOf (m.Invoke(null, m.GetParameters() |> Array.map throwaway))) + + Set.ofArray (Array.append fromValues fromFactories) + let tests = testList "Compiler Helpers" [ + // The goal of this test is to act as a safe guard for us, so we remember to add + // warning code to the knownCodes list. + testCase "knownCodes lists every warning the registry can produce" <| fun _ -> + // Forgetting to add a code is otherwise silent until somebody writes a directive for + // it and gets told it's a typo - the one thing knownCodes exists to prevent. + let produced = codesTheRegistryCanProduce () + + // Guards against the reflection above quietly finding nothing, which would make every + // assertion below pass for the wrong reason. + if Set.isEmpty produced then + failwith "Found no warnings in WarningCodes - the reflection in this test has gone stale" + + let missing = Set.difference produced WarningCodes.knownCodes + let stale = Set.difference WarningCodes.knownCodes produced + + if not (Set.isEmpty missing) || not (Set.isEmpty stale) then + failwithf "knownCodes is out of sync. Missing: %A. Listed but unused: %A" missing stale + + testCase "isSuppressible splits the code bands at FABLE0100" <| fun _ -> + // Directive warnings sit below the boundary and can never be silenced by a directive. + WarningCodes.isSuppressible (Some "FABLE0001") |> equal false + WarningCodes.isSuppressible (Some "FABLE0002") |> equal false + WarningCodes.isSuppressible (Some "FABLE0003") |> equal false + WarningCodes.isSuppressible (Some "FABLE0099") |> equal false + WarningCodes.isSuppressible (Some "FABLE0100") |> equal true + WarningCodes.isSuppressible (Some "FABLE9999") |> equal true + // A warning with no code at all is still caught by a bare `// fable-disable-line`. + WarningCodes.isSuppressible None |> equal true + testCase "expectedVersionMatchesActual works for same major version" <| fun _ -> Fable.CompilerExt.expectedVersionMatchesActual "5.0.0" "5.0.0" |> equal true Fable.CompilerExt.expectedVersionMatchesActual "5.0.0" "5.0.1" |> equal true diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index 3289c6baff..7fe26be375 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -7,6 +7,12 @@ open Fable.Tests.Compiler.Util.Compiler let private compile source = Compiler.Cached.compile Compiler.Settings.standard source +let private compileWithLibrary library source = + Compiler.Cached.compileWithLibrary Compiler.Settings.standard library source + +let private compileWithSignedLibrary signature library source = + Compiler.Cached.compileWithSignedLibrary Compiler.Settings.standard signature library source + let tests = testList "Compiler Messages" [ testCase "Compile Console.WriteLine" <| fun _ -> @@ -14,6 +20,255 @@ let tests = compile source |> Assert.Is.success |> ignore + + testCase "Explicit constructor with a self identifier and no base compiles" <| fun _ -> + let source = + """ +type Base = + val mutable Count: int + new (a: int) as self = { Count = a } then self.Count <- self.Count + 1 +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Explicit child of a class with a self identifier results in error" <| fun _ -> + let source = + """ +type Base = + val mutable Count: int + new (a: int) as self = { Count = a } then self.Count <- self.Count + 1 + +type Derived = + inherit Base + val X: int + new () = { inherit Base(1); X = 1 } +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Primary constructor child of a class with a self identifier results in error" <| fun _ -> + let source = + """ +type Base = + val mutable Count: int + new (a: int) as self = { Count = a } then self.Count <- self.Count + 1 + +type Derived() = + inherit Base(5) +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Object expression over a class with a self identifier results in error" <| fun _ -> + let source = + """ +type Base = + val mutable Count: int + new (a: int) as self = { Count = a } then self.Count <- self.Count + 1 + +let o = { new Base(5) with member _.ToString() = "o" } +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Child of a primary constructor class with a secondary self constructor results in error" <| fun _ -> + let source = + """ +type Base(a: int) = + member val V = a with get, set + new () as self = Base(1) then self.V <- self.V + 10 + +type Derived = + inherit Base + val E: int + new () = { inherit Base(); E = 1 } +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Self identifier with an explicit constructor base results in error" <| fun _ -> + let source = + """ +type Base = + val N: int + new (n: int) = { N = n } + +type Derived = + inherit Base + val mutable Y: int + new () as self = { inherit Base(1); Y = 0 } then self.Y <- self.N +""" + compile source + |> Assert.Exists.errorWith "uses a self identifier" + |> ignore + + testCase "Self identifier with a primary constructor base results in error" <| fun _ -> + let source = + """ +type Base(a: int) = + member _.A = a + +type Derived = + inherit Base + val mutable Y: int + new (a: int) as self = { inherit Base(a); Y = 0 } then self.Y <- self.A + 1 +""" + compile source + |> Assert.Exists.errorWith "uses a self identifier" + |> ignore + + testCase "Self identifier with a BCL base results in error" <| fun _ -> + let source = + """ +type Derived = + inherit System.Exception + val Code: int + new (m: string) as self = { inherit System.Exception(m); Code = 1 } +""" + compile source + |> Assert.Exists.errorWith "uses a self identifier" + |> ignore + + testCase "Self identifier with AttachMembers results in error" <| fun _ -> + let source = + """ +[] +type C = + val mutable V: int + new (v: int) as self = { V = v } then self.V <- self.V + 1 +""" + compile source + |> Assert.Exists.errorWith "uses a self identifier" + |> ignore + + testCase "Child of a primary constructor class with a self identifier compiles" <| fun _ -> + let source = + """ +type Base() as this = + let mutable n = 0 + do this.Set(1) + member _.Set(v: int) = n <- v + member _.N = n + +type Derived() = + inherit Base() +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Child of a primary constructor class with a self identifier and a secondary constructor compiles" <| fun _ -> + let source = + """ +type Base(x: int) as this = + do this.Touch() + new () = Base(1) + member _.X = x + member _.Touch() = () + +type Derived() = + inherit Base(2) +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Primary constructor child of a secondary constructor with a self identifier results in error" <| fun _ -> + let source = + """ +type Base(a: int) = + member val V = a with get, set + new () as self = Base(1) then self.V <- self.V + 10 + +type Derived() = + inherit Base() +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Child of an imported class with a self identifier compiles" <| fun _ -> + let source = + """ +[] +type Base = + val mutable X: int + new () as self = { X = 1 } then self.X <- 2 + +type Derived() = + inherit Base() +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Imported child of a class with a self identifier compiles" <| fun _ -> + let source = + """ +type Base = + val mutable X: int + new () as self = { X = 1 } then self.X <- 2 + +[] +type Child() = + inherit Base() +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Explicit child of a class without a self identifier compiles" <| fun _ -> + let source = + """ +type Base = + val N: int + new (n: int) = { N = n } + +type Derived = + inherit Base + val X: int + new () = { inherit Base(1); X = 1 } +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Child of a self identifier class behind a signature file results in error" <| fun _ -> + let signature = + """ +module Library + +type Base = + val mutable N: int + new: n: int -> Base +""" + + let library = + """ +module Library + +type Base = + val mutable N: int + new (n: int) as self = { N = n } then self.N <- self.N + 1 +""" + + let source = + """ +type Derived = + inherit Library.Base + val X: int + new () = { inherit Library.Base(1); X = 1 } +""" + + compileWithSignedLibrary signature library source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + testCase "Compile printfn" <| fun _ -> let source = "printfn \"Hello %s\" \"World\"" compile source @@ -25,6 +280,7 @@ let tests = compile source |> Assert.Is.Single.error |> ignore + testCase "Compiling incomplete pattern match results in warning" <| fun _ -> let source = "match None with | Some n -> 42 |> ignore" // without `ignore`: Warning: Result of Expression is implicitly ignored compile source @@ -36,6 +292,7 @@ let tests = compile source |> Assert.Exists.errorWith "This expression was expected to have type" |> ignore + testCase "Compiling incomplete pattern match results in specific warning" <| fun _ -> let source = "match None with | Some n -> 42" compile source @@ -107,6 +364,149 @@ let version = Semver.SemVersion.Parse("1.0.0", 1024) |> Assert.Exists.errorWith "Cannot reference member from .dll reference, Fable packages must include F# sources" |> ignore + testCase "Private inline function referencing private value succeeds" <| fun _ -> + let source = + """ +let private x = 1 +let inline private y () = x +let z = y () +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Private inline function referencing private function succeeds" <| fun _ -> + let source = + """ +let private add a b = a + b +let inline private addOne x = add x 1 +let z = addOne 41 +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Private inline function in nested module referencing private value succeeds" <| fun _ -> + let source = + """ +module Nested = + let private x = 1 + let inline private y () = x + let z = y () +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Private inline function referencing private value errors when inlined in another file" <| fun _ -> + let library = + """ +module Library + +let private x = 1 +let inline private y () = x +let inline z () = y () +""" + let source = "let res = Library.z ()" + compileWithLibrary library source + |> Assert.Are.errors 1 + |> Assert.Exists.errorWith "was marked inline but its implementation makes use of an internal or private function" + |> ignore + + testCase "Inline function referencing private value is reported once, not once per call site" <| fun _ -> + let library = + """ +module Library + +let private x = 1 +let inline w () = x +let inline z () = w () +""" + let source = + """ +let a = Library.z () +let b = Library.z () +""" + compileWithLibrary library source + |> Assert.Are.errors 1 + |> Assert.Exists.errorWith "was marked inline but its implementation makes use of an internal or private function" + |> ignore + + testCase "Inline function referencing non-private value from another file succeeds" <| fun _ -> + let library = + """ +module Library + +let x = 1 +let internal w = 2 +let inline private y () = x + w +let inline z () = y () +""" + let source = "let res = Library.z ()" + compileWithLibrary library source + |> Assert.Is.success + |> ignore + + testCase "Inline function referencing value hidden by a signature file errors when inlined in another file" <| fun _ -> + let signature = + """ +module Library + +val inline z: unit -> int +""" + let library = + """ +module Library + +let x = 1 +let inline z () = x +""" + let source = "let res = Library.z ()" + compileWithSignedLibrary signature library source + |> Assert.Are.errors 1 + |> Assert.Exists.errorWith "was marked inline but its implementation makes use of an internal or private function" + |> ignore + + testCase "Inline function hidden by a signature file referencing hidden value succeeds" <| fun _ -> + let signature = + """ +module Library + +val res: int +""" + let library = + """ +module Library + +let x = 1 +let inline z () = x +let res = z () +""" + let source = "let res = Library.res" + compileWithSignedLibrary signature library source + |> Assert.Is.success + |> ignore + + testCase "Inline function referencing value exposed by a signature file succeeds" <| fun _ -> + let signature = + """ +module Library + +val x: int +val inline z: unit -> int +""" + let library = + """ +module Library + +let x = 1 +let inline z () = x +""" + let source = "let res = Library.z ()" + compileWithSignedLibrary signature library source + |> Assert.Is.success + |> ignore + testCase "Duplicate attached member names emit a warning" <| fun _ -> let source = """ @@ -134,4 +534,584 @@ type MyClass() = compile source |> Assert.Is.success |> ignore + + testCase "CaseTest on a regular union emits warning" <| fun _ -> + let source = + """ +open Fable.Core + +type Id = + | [] Name of string + | [] Slug of string +""" + compile source + |> Assert.Exists.warningWith "CaseTest attribute on 'Name' is ignored: it is only supported on TypeScriptTaggedUnion and Erase union cases" + |> Assert.Exists.warningWith "CaseTest attribute on 'Slug' is ignored: it is only supported on TypeScriptTaggedUnion and Erase union cases" + |> ignore + + testCase "CaseTest in a TypeScriptTaggedUnion does not emit warning" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Terminal = + | [] Done of value: float + | [] DoneError of error: string + +let isError (t: Terminal) = match t with DoneError _ -> true | Done _ -> false +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Type test against a Pojo class emits warning" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Options(name: string) = + member val name = name + +let isOptions (o: obj) = match o with :? Options -> true | _ -> false +""" + compile source + |> Assert.Exists.warningWith "Cannot type test (evals to false): Program.Options (pojo)" + |> ignore + + testCase "The formatted output carries the warning code" <| fun _ -> + // Without this there is no way to discover which code to put in a directive. + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + let formatted = + compile source + |> List.filter (fun log -> log.Code = Some "FABLE0100") + |> List.map (Fable.Cli.Main.Util.formatLog Compiler.Cached.projDir) + + match formatted with + | [] -> failwith "Expected a FABLE0100 warning" + | messages -> + equal true (messages |> List.forall (fun m -> m.Contains "warning FABLE0100:")) + equal false (messages |> List.exists (fun m -> m.Contains "FABLE FABLE")) + + testCase "TypeScriptTaggedUnion cases sharing a tag compile when their fields tell them apart" <| fun _ -> + let source = + """ +open Fable.Core + +type Payload(x: int) = + member _.X = x + +[] +type Terminal = + | [] Done of value: float + | [] DoneError of error: string + | [] LogString of message: string + | [] LogPayload of message: Payload + +let describe (t: Terminal) = + match t with + | DoneError _ -> "error" + | LogString _ -> "string" + | LogPayload _ -> "payload" + | Done _ -> "done" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when their fields cannot tell them apart" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Result = + | [] Ok of ok: bool + | [] Failed of ok: bool + +let isFailed (r: Result) = match r with Failed _ -> true | Ok _ -> false +""" + compile source + |> Assert.Exists.errorWith "of 'Result' share the tag value \"done\" and have the same fields. Add a CaseTest attribute to" + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag compile when the other case has a CaseTest" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Result = + | [] Ok of ok: bool + | [] Failed of ok: bool + +let isOk (r: Result) = r.IsOk +let isFailed (r: Result) = match r with Failed _ -> true | Ok _ -> false +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when more than one case has no CaseTest" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Job = + | [] Build of kind: string + | [] Test of kind: string + | [] Other of kind: string + +let isOther (j: Job) = j.IsOther +""" + compile source + |> Assert.Exists.errorWith "Cases 'Other' and 'Test' of 'Job' share the tag value \"run\" and have the same fields. Add a CaseTest attribute to 'Other' or 'Test'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when one field type includes the other" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Log = + | [] LogString of message: string + | [] LogObject of message: obj + +let isObject (l: Log) = l.IsLogObject +let isString (l: Log) = l.IsLogString +""" + compile source + |> Assert.Exists.errorWith "Cases 'LogObject' and 'LogString' of 'Log' share the tag value \"log\" and cannot be told apart at runtime: field 'message' has type 'obj', which can hold any value. Add a CaseTest attribute to 'LogObject' or 'LogString'." + |> Assert.Exists.errorWith "Cases 'LogString' and 'LogObject' of 'Log' share the tag value \"log\" and cannot be told apart at runtime: field 'message' has type 'obj', which can hold any value. Add a CaseTest attribute to 'LogString' or 'LogObject'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when one field type is a base class of the other" <| fun _ -> + let source = + """ +open Fable.Core + +type Animal() = class end +type Dog() = inherit Animal() + +[] +type Pet = + | [] PetAnimal of pet: Animal + | [] PetDog of pet: Dog + +let isAnimal (p: Pet) = p.IsPetAnimal +let isDog (p: Pet) = p.IsPetDog +""" + compile source + |> Assert.Exists.errorWith "Cases 'PetAnimal' and 'PetDog' of 'Pet' share the tag value \"pet\" and cannot be told apart at runtime: field 'pet' has types 'Animal' and 'Dog', and 'Dog' is a subtype of 'Animal'. Add a CaseTest attribute to 'PetAnimal' or 'PetDog'." + |> Assert.Exists.errorWith "Cases 'PetDog' and 'PetAnimal' of 'Pet' share the tag value \"pet\" and cannot be told apart at runtime: field 'pet' has types 'Dog' and 'Animal', and 'Dog' is a subtype of 'Animal'. Add a CaseTest attribute to 'PetDog' or 'PetAnimal'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag compile when a CaseTest narrows a base class field" <| fun _ -> + let source = + """ +open Fable.Core + +type Animal() = class end + +[] +type Dog() = + inherit Animal() + member _.Bark() = "woof" + +[] +type Pet = + | [] PetAnimal of pet: Animal + | [] PetDog of pet: Dog + +let isAnimal (p: Pet) = p.IsPetAnimal +let isDog (p: Pet) = p.IsPetDog +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when only a Pojo field type tells them apart" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Options(name: string) = + member val name = name + +[] +type Log = + | [] LogString of message: string + | [] LogOptions of message: Options + +let isOptions (l: Log) = l.IsLogOptions +let isString (l: Log) = l.IsLogString +""" + compile source + |> Assert.Exists.errorWith "Cases 'LogOptions' and 'LogString' of 'Log' share the tag value \"log\" and cannot be told apart at runtime: field 'message' has type 'Options', a Pojo that is a plain object at runtime. Add a CaseTest attribute to 'LogOptions' or 'LogString'." + |> Assert.Exists.errorWith "Cases 'LogString' and 'LogOptions' of 'Log' share the tag value \"log\" and cannot be told apart at runtime: field 'message' has type 'Options', a Pojo that is a plain object at runtime. Add a CaseTest attribute to 'LogString' or 'LogOptions'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when only interfaces tell them apart" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract radius: float + +type Rect = + abstract width: float + +[] +type Shape = + | [] Circle of shape: Circle + | [] Rect of shape: Rect + +let isRect (s: Shape) = s.IsRect +""" + compile source + |> Assert.Exists.errorWith "Cases 'Rect' and 'Circle' of 'Shape' share the tag value \"shape\" and cannot be told apart at runtime: field 'shape' has type 'Rect', an interface that does not exist at runtime. Add a CaseTest attribute to 'Rect' or 'Circle'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a number tag emit error showing the tag value" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Status = + | [] Ok of value: string + | [] Failed of value: string + +let isOk (s: Status) = s.IsOk +""" + compile source + |> Assert.Exists.errorWith "Cases 'Ok' and 'Failed' of 'Status' share the tag value 1 and have the same fields. Add a CaseTest attribute to 'Ok' or 'Failed'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a boolean tag emit error showing the tag value" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Status = + | [] Ok of value: string + | [] Warn of value: string + +let isOk (s: Status) = s.IsOk +""" + compile source + |> Assert.Exists.errorWith "Cases 'Ok' and 'Warn' of 'Status' share the tag value true and have the same fields. Add a CaseTest attribute to 'Ok' or 'Warn'." + |> ignore + + testCase "Erased union cases of interface type are told apart by their members" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract radius: float + +type Rect = + abstract width: float + abstract area: unit -> float + +type Animal = + abstract name: string + +type Dog = + inherit Animal + abstract bark: string + +type Marker = interface end + +[] +type Shape = + | Circle of Circle + | Rect of Rect + | Count of int + | Label of string + +[] +type Pet = + | Animal of Animal + | Dog of Dog + +[] +type Tagged = + | Mark of Marker + | Name of string + +let describe (s: Shape) = match s with Circle _ -> "circle" | Rect _ -> "rect" | Count _ -> "count" | Label _ -> "label" +let pet (p: Pet) = match p with Animal _ -> "animal" | Dog _ -> "dog" +let tagged (t: Tagged) = match t with Mark _ -> "mark" | Name _ -> "name" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Erased union cases of interfaces with the same members emit error" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract size: float + +type Rect = + abstract size: float + abstract label: string option + +[] +type Shape = + | Circle of Circle + | Rect of Rect + +let isRect (s: Shape) = match s with Rect _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Cases 'Rect' and 'Circle' of 'Shape' cannot be told apart at runtime: interfaces 'Rect' and 'Circle' have no required member that tells them apart. Add a CaseTest attribute to 'Rect' or 'Circle'." + |> ignore + + testCase "Erased union case of mangled interface type emits error" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Circle = + abstract radius: float + +[] +type Shape = + | Circle of Circle + | Count of int + +let isCircle (s: Shape) = match s with Circle _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Case 'Circle' of 'Shape' cannot be tested at runtime: type 'Circle' is an interface that does not exist at runtime. Add a CaseTest attribute to 'Circle'." + |> ignore + + testCase "Erased union case of interface type next to a Pojo case emits error" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract radius: float + +[] +type Options(name: string) = + member val name = name + +[] +type Shape = + | Circle of Circle + | Opts of Options + +let isCircle (s: Shape) = match s with Circle _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Cases 'Circle' and 'Opts' of 'Shape' cannot be told apart at runtime: type 'Options' is a Pojo that is a plain object at runtime. Add a CaseTest attribute to 'Circle' or 'Opts'." + |> ignore + + testCase "Erased union cases of the same type emit error" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Id = + | Name of string + | Slug of string + +let isName (id: Id) = match id with Name _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Cases 'Name' and 'Slug' of 'Id' cannot be told apart at runtime: both have type 'string'. Add a CaseTest attribute to 'Name' or 'Slug'." + |> ignore + + testCase "Erased union cases of types with the same runtime test emit error" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Quantity = + | Count of int + | Amount of float + +let isCount (q: Quantity) = match q with Count _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Cases 'Count' and 'Amount' of 'Quantity' cannot be told apart at runtime: types 'int' and 'float' look the same at runtime. Add a CaseTest attribute to 'Count' or 'Amount'." + |> ignore + + testCase "Erased union case whose type includes another case emits error" <| fun _ -> + let source = + """ +open Fable.Core + +type Animal() = + member _.Kind = "animal" + +type Dog() = + inherit Animal() + +[] +type Pet = + | Animal of Animal + | Dog of Dog + +let isAnimal (p: Pet) = match p with Animal _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Cases 'Animal' and 'Dog' of 'Pet' cannot be told apart at runtime: 'Dog' is a subtype of 'Animal'. Add a CaseTest attribute to 'Animal' or 'Dog'." + |> ignore + + testCase "Erased union cases of distinct types do not emit error" <| fun _ -> + let source = + """ +open Fable.Core + +type Animal() = + member _.Kind = "animal" + +type Dog() = + inherit Animal() + +[] +type Pet = + | Animal of Animal + | Dog of Dog + +[] +type Value = + | Str of string + | Num of float + | Flag of bool + | Many of float[] + +let isDog (p: Pet) = match p with Dog _ -> true | _ -> false +let describe (v: Value) = match v with Str _ -> "str" | Num _ -> "num" | Flag _ -> "flag" | Many _ -> "many" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Erased union cases with CaseTest do not emit error" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract radius: float + +type Rect = + abstract width: float + +[] +type Shape = + | [] Circle of Circle + | [] Rect of Rect + +[] +type Id = + | [] Name of string + | Slug of string + +[] +type Raw = + | [] Obj of obj + | Text of string + +let describe (s: Shape) = match s with Circle _ -> "circle" | Rect _ -> "rect" +let kind (id: Id) = match id with Name _ -> "name" | Slug _ -> "slug" +let raw (r: Raw) = match r with Obj _ -> "obj" | Text _ -> "text" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TS tagged union case with an interface field compiles when the interface declares the tag" <| fun _ -> + let source = + """ +open Fable.Core + +type Take = + abstract ``type``: string + abstract fromIndex: int + +[] +type Command = + | Take of Take + | Save + +let fromIndex (c: Command) = match c with Take t -> t.fromIndex | Save -> 0 +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TS tagged union case with an interface field reports an interface without the tag member" <| fun _ -> + let source = + """ +open Fable.Core + +type Take = + abstract fromIndex: int + +[] +type Command = + | Take of Take + | Save +""" + compile source + |> Assert.Exists.errorWith "Case 'Take' of 'Command' compiles to its 'Take' value, so the interface must declare a 'type' member" + |> ignore + + testCase "TS tagged union case with a named interface field does not need the tag member" <| fun _ -> + let source = + """ +open Fable.Core + +type Take = + abstract fromIndex: int + +[] +type Command = + | Take of take: Take + | Save + +let fromIndex (c: Command) = match c with Take t -> t.fromIndex | Save -> 0 +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TS tagged union case with a single primitive field compiles" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type AuthState = + | Zero + | One of string + +let one = AuthState.One "a" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore ] diff --git a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj index 49253cae8a..ab4f65a052 100644 --- a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj +++ b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj @@ -7,7 +7,7 @@ $(DefineConstants);DOTNET_FILE_SYSTEM - + @@ -23,9 +23,15 @@ + + + + + + diff --git a/tests/Integration/Compiler/IgnoredArgumentTests.fs b/tests/Integration/Compiler/IgnoredArgumentTests.fs new file mode 100644 index 0000000000..39b4b60a6b --- /dev/null +++ b/tests/Integration/Compiler/IgnoredArgumentTests.fs @@ -0,0 +1,215 @@ +module Fable.Tests.Compiler.IgnoredArgument + +open Fable.Core +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private compile source = Compiler.Cached.compile Compiler.Settings.standard source + +(* + Verify that warnings are generated when needed, and skipped in the scenario where Fable + has the same behavior as .NET. + + The later makes the logs less verbose and warnings more meaningful +*) + +let tests = + testList "Ignored Arguments" [ + testCase "Discarding a format provider on a date parse is reported" <| fun _ -> + // JS, Dart and Rust used to drop this argument silently while Python warned. Aligning them + // is a behaviour change: code that compiled quietly now raises FABLE0102. + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.GetCultureInfo "fr-FR") |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0102" + |> ignore + + testCase "Passing InvariantCulture to a parse is not reported" <| fun _ -> + // Fable parses with a fixed culture-independent implementation, so InvariantCulture asks + // for exactly what it gets. Warning here would fire on every correct call - and does, if + // the exemption is removed: 96 sites across the repo's own test suites. + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0102" + |> ignore + + testCase "A discarded DateTimeStyles is reported even when the culture is exempt" <| fun _ -> + // Two codes rather than one bundled message: the style is still discarded regardless of + // which culture was passed, so exempting the culture must not silence it. + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0104" + |> Assert.Code.noWarning "FABLE0102" + |> ignore + + testCase "DateTimeStyles.None is not reported" <| fun _ -> + // `None` means "no special handling", which is what Fable does - same exemption as + // InvariantCulture. Every one of the 12 sites in tests/Js passes exactly this. + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.InvariantCulture, DateTimeStyles.None) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0102" + |> Assert.Code.noWarning "FABLE0104" + |> ignore + + testCase "A numeric parse given InvariantCulture is not reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +Double.Parse("10.5", CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0102" + |> ignore + + testCase "A numeric parse given a real culture is reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +Double.Parse("10.5", CultureInfo.GetCultureInfo "fr-FR") |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0102" + |> ignore + + testCase "A real culture and a style are both reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.GetCultureInfo "fr-FR", DateTimeStyles.AssumeUniversal) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0102" + |> Assert.Code.warning "FABLE0104" + |> ignore + + testCase "A date parse with no extra argument discards nothing and is silent" <| fun _ -> + let source = + """ +open System +DateTime.Parse("2026-01-01") |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0102" + |> Assert.Code.noWarning "FABLE0104" + |> ignore + + testCase "A discarded NumberStyles is reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +Double.Parse("1.5", NumberStyles.Currency, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0103" + |> ignore + + testCase "A supported NumberStyles on a float is not reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +Double.Parse("1.5", NumberStyles.Float ||| NumberStyles.AllowParentheses, CultureInfo.InvariantCulture) |> ignore +Decimal.Parse("1.5", NumberStyles.Number, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0103" + |> ignore + + testCase "A TimeSpan built with microseconds is reported" <| fun _ -> + // The runtime representation only carries milliseconds, so the finer component is dropped. + let source = + """ +open System +TimeSpan.FromMilliseconds(1L, 500L) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0105" + |> ignore + + testCase "A TimeSpan with no microseconds is silent" <| fun _ -> + let source = + """ +open System +TimeSpan.FromMilliseconds(1L, 0L) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0105" + |> ignore + + testCase "A private-representation flag on FSharpType is reported" <| fun _ -> + let source = + """ +open Microsoft.FSharp.Reflection +FSharpType.IsUnion(typeof, true) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0106" + |> ignore + + testCase "A private-representation flag on GetRecordFields is reported" <| fun _ -> + // This one dropped the flag silently while its seven siblings warned, all in one function. + let source = + """ +open Microsoft.FSharp.Reflection +type R = { A: int } +FSharpType.GetRecordFields(typeof, true) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0106" + |> ignore + + testCase "FSharpType without the flag is silent" <| fun _ -> + let source = + """ +open Microsoft.FSharp.Reflection +FSharpType.IsUnion(typeof) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0106" + |> ignore + + testCase "Base64 offset and length arguments are reported" <| fun _ -> + // The slice is silently encoded whole, so this is a wrong value rather than a formatting nit. + let source = + """ +open System +Convert.ToBase64String([| 1uy; 2uy; 3uy; 4uy |], 0, 2) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0107" + |> ignore + + testCase "Base64 with only the array is silent" <| fun _ -> + let source = + """ +open System +Convert.ToBase64String([| 1uy; 2uy; 3uy; 4uy |]) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0107" + |> ignore + ] diff --git a/tests/Integration/Compiler/Main.fs b/tests/Integration/Compiler/Main.fs index 18c778cfa4..86f7f71ef4 100644 --- a/tests/Integration/Compiler/Main.fs +++ b/tests/Integration/Compiler/Main.fs @@ -6,9 +6,15 @@ open Expecto let allTests = [ CompilerMessages.tests + CodeServicesApi.tests + WarningSuppression.tests + IgnoredArgument.tests + BehaviorWarning.tests AnonRecordInInterface.tests CompilerHelpers.tests Inflate.tests + WatchDependency.tests + RegexOptions.tests ] diff --git a/tests/Integration/Compiler/RegexOptionsTests.fs b/tests/Integration/Compiler/RegexOptionsTests.fs new file mode 100644 index 0000000000..0924e9fb36 --- /dev/null +++ b/tests/Integration/Compiler/RegexOptionsTests.fs @@ -0,0 +1,56 @@ +module Fable.Tests.Compiler.RegexOptions + +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private compile source = Compiler.Cached.compile Compiler.Settings.standard source + +let tests = + testList "Regex Options" [ + testCase "An unsupported option on the constructor is a compile error" <| fun _ -> + let source = + """ +open System.Text.RegularExpressions + +let r = Regex("a", RegexOptions.RightToLeft) +""" + compile source + |> Assert.Is.error + |> Assert.Exists.errorWith "RegexOptions.RightToLeft is not supported" + |> ignore + + testCase "Only the unsupported options of a combination are reported" <| fun _ -> + let source = + """ +open System.Text.RegularExpressions + +let found = Regex.IsMatch("abc", "b", RegexOptions.IgnoreCase ||| RegexOptions.ExplicitCapture ||| RegexOptions.CultureInvariant) +""" + compile source + |> Assert.Exists.errorWith "RegexOptions.ExplicitCapture, RegexOptions.CultureInvariant is not supported" + |> ignore + + testCase "Options that only affect performance compile without a diagnostic" <| fun _ -> + let source = + """ +open System.Text.RegularExpressions + +let r = Regex("a", RegexOptions.Compiled ||| RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase) +let parts = Regex.Split("a1b", "\\d", RegexOptions.ECMAScript) +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Options that are only known at run time are left to the runtime check" <| fun _ -> + let source = + """ +open System.Text.RegularExpressions + +let make (options: RegexOptions) = Regex("a", options) +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + ] diff --git a/tests/Integration/Compiler/TestProject/.gitignore b/tests/Integration/Compiler/TestProject/.gitignore index 19c254c5db..437dc5f5ac 100644 --- a/tests/Integration/Compiler/TestProject/.gitignore +++ b/tests/Integration/Compiler/TestProject/.gitignore @@ -1 +1,2 @@ -Program.fs \ No newline at end of file +Program.fs +Library.fs diff --git a/tests/Integration/Compiler/TestProject/TestProject.fsproj b/tests/Integration/Compiler/TestProject/TestProject.fsproj index 1b7d67737c..80a9298133 100644 --- a/tests/Integration/Compiler/TestProject/TestProject.fsproj +++ b/tests/Integration/Compiler/TestProject/TestProject.fsproj @@ -7,6 +7,7 @@ + diff --git a/tests/Integration/Compiler/TestProjectSignature/.gitignore b/tests/Integration/Compiler/TestProjectSignature/.gitignore new file mode 100644 index 0000000000..9baf46c59f --- /dev/null +++ b/tests/Integration/Compiler/TestProjectSignature/.gitignore @@ -0,0 +1,3 @@ +Library.fsi +Library.fs +Program.fs diff --git a/tests/Integration/Compiler/TestProjectSignature/TestProjectSignature.fsproj b/tests/Integration/Compiler/TestProjectSignature/TestProjectSignature.fsproj new file mode 100644 index 0000000000..7e8a49ecca --- /dev/null +++ b/tests/Integration/Compiler/TestProjectSignature/TestProjectSignature.fsproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + Major + + + + + + + + + + + + + diff --git a/tests/Integration/Compiler/TestProjectWatch/.gitignore b/tests/Integration/Compiler/TestProjectWatch/.gitignore new file mode 100644 index 0000000000..9baf46c59f --- /dev/null +++ b/tests/Integration/Compiler/TestProjectWatch/.gitignore @@ -0,0 +1,3 @@ +Library.fsi +Library.fs +Program.fs diff --git a/tests/Integration/Compiler/TestProjectWatch/TestProjectWatch.fsproj b/tests/Integration/Compiler/TestProjectWatch/TestProjectWatch.fsproj new file mode 100644 index 0000000000..7e8a49ecca --- /dev/null +++ b/tests/Integration/Compiler/TestProjectWatch/TestProjectWatch.fsproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + Major + + + + + + + + + + + + + diff --git a/tests/Integration/Compiler/Util/Compiler.fs b/tests/Integration/Compiler/Util/Compiler.fs index 4a6b62ffa7..5880d9839e 100644 --- a/tests/Integration/Compiler/Util/Compiler.fs +++ b/tests/Integration/Compiler/Util/Compiler.fs @@ -27,36 +27,81 @@ module Compiler = /// NOTE: NOT threadsafe /// -> don't use `parallel` module Cached = - let projDir = IO.Path.Join(__SOURCE_DIRECTORY__, "../TestProject" ) |> Path.normalizeFullPath - let projFile = IO.Path.Join(projDir, "TestProject.fsproj" ) |> Path.normalizeFullPath - let sourceFile = IO.Path.Join(projDir, "Program.fs" ) |> Path.normalizeFullPath - - let cliArgs = - let compilerOptions = CompilerOptionsHelper.Make() - { CliArgs.ProjectFile = projFile - FableLibraryPath = None - RootDir = projDir - Configuration = "Debug" - OutDir = None - IsWatch = false - Precompile = false - PrecompiledLib = None - PrintAst = false - SourceMaps = false - SourceMapsRoot = None - NoRestore = false - NoCache = false - NoGitignore = false - NoParallelTypeCheck = false - Exclude = ["Fable.Core"] - Replace = Map.empty - RunProcess = None - CompilerOptions = compilerOptions - Verbosity = Verbosity.Normal } - - let mutable private state = State.Create(cliArgs, recompileAllFiles=true) - - let compile settings source = + type private CachedProject(name: string, ?isWatch: bool, ?watchDelay: int, ?recompileAllFiles: bool) = + let projDir = IO.Path.Join(__SOURCE_DIRECTORY__, "../" + name) |> Path.normalizeFullPath + let projFile = IO.Path.Join(projDir, name + ".fsproj") |> Path.normalizeFullPath + + let cliArgs = + // The real CLI always defines these (Entry.fs), and code guarded by them is exactly where + // Fable warnings - and the comments suppressing them - live. + let compilerOptions = CompilerOptionsHelper.Make(define = ["FABLE_COMPILER"; "FABLE_COMPILER_JAVASCRIPT"]) + { CliArgs.ProjectFile = projFile + FableLibraryPath = None + RootDir = projDir + Configuration = "Debug" + OutDir = None + IsWatch = defaultArg isWatch false + Precompile = false + PrecompiledLib = None + PrintAst = false + SourceMaps = false + SourceMapsRoot = None + NoRestore = false + NoCache = false + NoGitignore = false + NoParallelTypeCheck = false + Exclude = ["Fable.Core"] + Replace = Map.empty + RunProcess = None + CompilerOptions = compilerOptions + Verbosity = Verbosity.Normal } + + let state = State.Create(cliArgs, ?watchDelay = watchDelay, recompileAllFiles = defaultArg recompileAllFiles true) + let mutable watchDependencies = Map.empty + + member _.ProjDir = projDir + + member _.WatchDependencies = watchDependencies + + member _.WriteFiles(files: (string * string) list) = + for (fileName, content) in files do + IO.File.WriteAllText(IO.Path.Join(projDir, fileName), content) + + member _.ReadOutput(fileName: string) = + let path = IO.Path.Join(projDir, fileName + ".js") + if IO.File.Exists path then IO.File.ReadAllText path else "" + + /// Never returns: the watcher keeps the session alive until the process exits. + member this.StartWatching(files: (string * string) list) = + for (fileName, _) in files do + let output = IO.Path.Join(projDir, fileName + ".js") + if IO.File.Exists output then IO.File.Delete output + + this.WriteFiles files + state |> startCompilationAsync |> Async.Ignore |> Async.Start + + member this.Compile(files: (string * string) list) = + this.WriteFiles files + + let result = + state + |> startCompilationAsync + |> Async.RunSynchronously + + match result with + | Error(_msg, logs) -> Array.toList logs + | Ok(newState, logs) -> + watchDependencies <- newState.WatchDependencies + Array.toList logs + + let private project = CachedProject("TestProject") + let private projectWithSignature = CachedProject("TestProjectSignature") + + let projDir = project.ProjDir + + let private emptyLibrary = "module Library" + Environment.NewLine + + let private programFile settings source = let preamble = [ yield! settings.Opens |> List.map (sprintf "open %s") @@ -65,19 +110,58 @@ module Compiler = ] |> String.concat Environment.NewLine - let source = preamble + source - IO.File.WriteAllText(sourceFile, source) + preamble + source + + let compileWithLibrary settings (library: string) source = + project.Compile [ + "Library.fs", library + "Program.fs", programFile settings source + ] + + let compile settings source = compileWithLibrary settings emptyLibrary source + + let compileWithSignedLibrary settings (signature: string) (library: string) source = + projectWithSignature.Compile [ + "Library.fsi", signature + "Library.fs", library + "Program.fs", programFile settings source + ] + + let private projectWithSignatureInWatchMode = CachedProject("TestProjectSignature", isWatch = true) + + let watchDependenciesWithSignedLibrary settings (signature: string) (library: string) source = + let logs = + projectWithSignatureInWatchMode.Compile [ + "Library.fsi", signature + "Library.fs", library + "Program.fs", programFile settings source + ] - let result = - state - |> startCompilationAsync - |> Async.RunSynchronously + let projDir = projectWithSignatureInWatchMode.ProjDir - match result with - | Error(_msg, logs) -> Array.toList logs - | Ok(newState, logs) -> - state <- newState - Array.toList logs + let dependencies = + projectWithSignatureInWatchMode.WatchDependencies + |> Map.toList + |> List.map (fun (file, deps) -> + IO.Path.GetRelativePath(projDir, file), deps |> Array.map (fun d -> IO.Path.GetRelativePath(projDir, d)) |> Array.toList + ) + |> Map.ofList + + logs, dependencies + + let private watchedProject = CachedProject("TestProjectWatch", isWatch = true, watchDelay = 100, recompileAllFiles = false) + + let startWatchingSignedLibrary settings (signature: string) (library: string) source = + watchedProject.StartWatching [ + "Library.fsi", signature + "Library.fs", library + "Program.fs", programFile settings source + ] + + let rewriteWatchedLibrary (library: string) = + watchedProject.WriteFiles [ "Library.fs", library ] + + let watchedOutput fileName = watchedProject.ReadOutput fileName module Assert = open Util.Testing @@ -141,6 +225,8 @@ module Compiler = let errorOrWarning: Result -> bool = List.isEmpty >> not let matching = List.exists let withMsg (txt: string) = List.exists (fun m -> m.Message.Contains txt) + module Code = + let is (code: string) (msg: LogEntry) = msg.Code = Some code module Text = let contains (txt: string) msg = msg.Message.Contains(txt, StringComparison.InvariantCultureIgnoreCase) let isMatch (regex: System.Text.RegularExpressions.Regex) (msg: LogEntry) = @@ -205,6 +291,16 @@ module Compiler = Test.All.warning |> orFail expected.All.Warning + /// Assertions on the stable `FABLExxxx` codes rather than on message text: what a + /// `// fable-disable` comment targets is the code, so that's what the tests must pin. + module Code = + let warning (code: string) = + Test.Exists.matching (Test.Is.warning >&> Test.Code.is code) + |> orFail (expected.Warning.With code) + let noWarning (code: string) = + (not << Test.Exists.matching (Test.Is.warning >&> Test.Code.is code)) + |> orFail (expected.No.Warning.With code) + module Exists = let errorWith (txt: string) = Test.Exists.matching (Test.Is.error >&> Test.Text.contains txt) diff --git a/tests/Integration/Compiler/WarningSuppressionTests.fs b/tests/Integration/Compiler/WarningSuppressionTests.fs new file mode 100644 index 0000000000..bda6ca3d3b --- /dev/null +++ b/tests/Integration/Compiler/WarningSuppressionTests.fs @@ -0,0 +1,322 @@ +module Fable.Tests.Compiler.WarningSuppression + +open Fable.Core +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private compile source = Compiler.Cached.compile Compiler.Settings.standard source + +/// Exercises the `// fable-disable` machinery itself - directive parsing, block scoping, which +/// warnings it can reach, and the FABLE0001-band reports about the directives. FABLE0100 stands +/// in as the subject warning throughout; what is under test is the mechanism, not that code. +let tests = + testList "Warning Suppression" [ + testCase "CultureInfo argument warning is not suppressed by default" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Exists.warningWith "CultureInfo argument is ignored" + |> ignore + + testCase "The same code covers both StartsWith and EndsWith call sites" <| fun _ -> + // StartsWith and EndsWith raise the same logical "CultureInfo argument is ignored" warning + // from two separate call sites in Replacements.fs, sharing WarningCodes.CultureInfoIgnored. + // One code must suppress both, otherwise the registry has failed at its only job. + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +"abc".EndsWith("c", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "fable-disable-line suppresses a warning on the same line" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "fable-disable-next-line suppresses a warning on the following line" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable-next-line FABLE0100 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "fable-disable/fable-enable suppresses warnings in a block" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable FABLE0100 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +"abc".EndsWith("c", true, CultureInfo.InvariantCulture) |> ignore +// fable-enable FABLE0100 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Are.warnings 1 + |> ignore + + testCase "A mismatched code does not suppress the warning" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line SOME_OTHER_CODE +""" + compile source + |> Assert.Exists.warningWith "CultureInfo argument is ignored" + |> ignore + + testCase "A bare fable-disable-line suppresses regardless of code" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "A string literal that looks like a directive is not treated as one" <| fun _ -> + let source = + """ +open System.Globalization +let s = "// fable-disable-line FABLE0100" +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Exists.warningWith "CultureInfo argument is ignored" + |> ignore + + testCase "A directive inside a #if FABLE_COMPILER block is honoured" <| fun _ -> + let source = + """ +open System.Globalization +#if FABLE_COMPILER +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +#endif +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A warning inside a #if FABLE_COMPILER block still fires without a directive" <| fun _ -> + // Guards the test above from passing vacuously because the block was compiled out. + let source = + """ +open System.Globalization +#if FABLE_COMPILER +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +#endif +""" + compile source + |> Assert.Code.warning "FABLE0100" + |> ignore + + testCase "Errors are never suppressed, not even by a blanket fable-disable" <| fun _ -> + let source = + """ +open Fable.Core.JsInterop + +type Response = + abstract fn: int -> int + abstract prop: bool with get, set + +// fable-disable +let res = jsOptions (fun o -> o.fn <- (fun i -> i)) +""" + compile source + |> Assert.Exists.errorWith "Cannot set a non-property member in 'jsOptions'" + |> ignore + + testCase "A directive written as a block comment is honoured" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore (* fable-disable-line FABLE0100 *) +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A trailing directive suppresses a warning spanning several lines" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith( + "a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A fable-disable block with no fable-enable runs to the end of the file" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable FABLE0100 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +"abc".EndsWith("c", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A colon separator and a lower-case code are accepted" <| fun _ -> + // What people coming from `# noqa: E501` and `@ts-ignore` will write. + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line: fable0100 +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A justification after -- is not parsed as codes" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 -- culture is irrelevant here +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> Assert.Code.noWarning "FABLE0001" + |> ignore + + testCase "A typo'd code is reported instead of silently suppressing nothing" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABEL0001 +""" + compile source + |> Assert.Code.warning "FABLE0001" + |> Assert.Code.warning "FABLE0100" + // The typo report is the actionable one; don't pile "and it's unused" on top of it. + |> Assert.Code.noWarning "FABLE0002" + |> ignore + + testCase "A directive that suppresses nothing is reported as unused" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable-next-line FABLE0100 +let answer = 42 +""" + compile source + |> Assert.Code.warning "FABLE0002" + |> ignore + + testCase "A directive that does its job is not reported as unused" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Code.noWarning "FABLE0002" + |> ignore + + testCase "A fable-disable block with no codes is reported" <| fun _ -> + // It would otherwise silence every Fable warning to the end of the file, unnoticed. + let source = + """ +open System.Globalization +// fable-disable +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0003" + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "Directive warnings cannot be suppressed by a directive" <| fun _ -> + // FABLE0001-0099 flag comment text that is wrong to keep, so the only correct answer is to + // edit it. Silencing "this directive is broken" with another directive is self-defeating. + let source = + """ +open System.Globalization +// fable-disable-next-line FABLE0002 +// fable-disable-next-line FABLE0100 +let answer = 42 +""" + compile source + |> Assert.Code.warning "FABLE0002" + |> ignore + + testCase "A blanket fable-disable cannot suppress its own report" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable +let answer = 42 +""" + compile source + |> Assert.Code.warning "FABLE0003" + |> ignore + + testCase "A word merely starting with a directive name is not a directive" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disabled for now +""" + compile source + |> Assert.Code.warning "FABLE0100" + |> ignore + + testCase "Directives are found in CRLF sources" <| fun _ -> + let source = + [ "" + "open System.Globalization" + "\"abc\".StartsWith(\"a\", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100" + "" ] + |> String.concat "\r\n" + + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A warning from an inlined function is suppressed at its definition" <| fun _ -> + // The warning is attributed to the file the inline function is *defined* in, so that is + // where the directive has to go - the call site can't suppress it. + let source = + """ +open System.Globalization +let inline startsWithCulture (s: string) = + s.StartsWith("a", true, CultureInfo.InvariantCulture) // fable-disable-line FABLE0100 + +startsWithCulture "abc" |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A directive at the call site does not suppress an inlined function's warning" <| fun _ -> + let source = + """ +open System.Globalization +let inline startsWithCulture (s: string) = + s.StartsWith("a", true, CultureInfo.InvariantCulture) + +startsWithCulture "abc" |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Code.warning "FABLE0100" + |> ignore + ] diff --git a/tests/Integration/Compiler/WatchDependencyTests.fs b/tests/Integration/Compiler/WatchDependencyTests.fs new file mode 100644 index 0000000000..b913e89aeb --- /dev/null +++ b/tests/Integration/Compiler/WatchDependencyTests.fs @@ -0,0 +1,88 @@ +module Fable.Tests.Compiler.WatchDependency + +open System +open System.Threading +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private signature = + """ +module Library + +val inline triple: x: int -> int +""" + +let private library (factor: int) = + $""" +module Library + +let inline triple (x: int) = x * {factor} +""" + +let private source = + """ +let six = Library.triple 2 +""" + +let private watchDependencies signature library source = + Compiler.Cached.watchDependenciesWithSignedLibrary Compiler.Settings.standard signature library source + +let private waitForOutput (fileName: string) (expected: string) = + let deadline = DateTime.UtcNow + TimeSpan.FromSeconds 30.0 + let mutable output = Compiler.Cached.watchedOutput fileName + + while not (output.Contains expected) && DateTime.UtcNow < deadline do + Thread.Sleep 200 + output <- Compiler.Cached.watchedOutput fileName + + if not (output.Contains expected) then + failwith $"Timed out waiting for '{expected}' in {fileName}.js, last content:\n{output}" + +let tests = + testList "Watch Dependencies" [ + testCase "A file that inlines a function from a file with a signature depends on the .fs file" <| fun _ -> + let logs, deps = watchDependencies signature (library 3) source + + logs |> Assert.Is.strictSuccess |> ignore + deps |> Map.tryFind "Program.fs" |> Option.defaultValue [] |> equal [ "Library.fs" ] + + testCase "A file that inherits a parameterless explicit constructor depends on the file of its body" <| fun _ -> + let signature = + """ +module Library + +type Base = + val N: int + new: unit -> Base +""" + + let library = + """ +module Library + +type Base = + val N: int + new () = { N = 1 } +""" + + let source = + """ +type Derived = + inherit Library.Base + val M: int + new () = { inherit Library.Base(); M = 2 } +""" + + let logs, deps = watchDependencies signature library source + + logs |> Assert.Is.strictSuccess |> ignore + deps |> Map.tryFind "Program.fs" |> Option.defaultValue [] |> equal [ "Library.fs" ] + + testCase "Editing an inline function in a file with a signature recompiles the files that inlined it" <| fun _ -> + Compiler.Cached.startWatchingSignedLibrary Compiler.Settings.standard signature (library 3) source + waitForOutput "Program.fs" "2 * 3" + + Compiler.Cached.rewriteWatchedLibrary (library 4) + waitForOutput "Program.fs" "2 * 4" + ] diff --git a/tests/Integration/Integration/CacheInvalidationTests.fs b/tests/Integration/Integration/CacheInvalidationTests.fs new file mode 100644 index 0000000000..c19e7dfc8b --- /dev/null +++ b/tests/Integration/Integration/CacheInvalidationTests.fs @@ -0,0 +1,102 @@ +module Fable.Tests.CacheInvalidation + +open System +open System.IO +open Expecto + +/// A project whose compile order lives in an imported .props, so adding a source file +/// leaves the .fsproj timestamp untouched. In a temp dir: these tests mutate the project. +let private createProject (dir: string) = + Directory.CreateDirectory(dir) |> ignore + + File.WriteAllText( + Path.Combine(dir, "Test.fsproj"), + """ + + net10.0 + Major + + + +""" + ) + + File.WriteAllText( + Path.Combine(dir, "Tests.props"), + """ + + + + + +""" + ) + + File.WriteAllText(Path.Combine(dir, "Lib.fs"), "module Lib\n\nlet greeting = \"hello\"\n") + + File.WriteAllText(Path.Combine(dir, "Main.fs"), "module Main\n\nlet run () = printfn \"%s\" Lib.greeting\n") + +/// Cache invalidation compares timestamps, so a file rewritten within the same tick as the +/// previous compilation could look unchanged. Nudge it forward to keep the test from flaking. +let private writeNewer (path: string) (content: string) = + File.WriteAllText(path, content) + File.SetLastWriteTime(path, DateTime.Now.AddSeconds(2.0)) + +let private compile (dir: string) (outDir: string) = + Fable.Cli.Entry.main + [| + Path.Combine(dir, "Test.fsproj") + "--cwd" + dir + "--lang" + "javascript" + "--outDir" + outDir + |] + +let private withTempProject name (f: string -> string -> unit) = + let unique = Guid.NewGuid().ToString("N") + let dir = Path.Combine(Path.GetTempPath(), $"fable-cache-tests-%s{name}-%s{unique}") + + try + createProject dir + f dir (Path.Combine(dir, "out")) + finally + try + Directory.Delete(dir, true) + with _ -> + () + +let tests = + testList + "CacheInvalidation" + [ + // A `` arriving through an imported .props touches no .fsproj + // timestamp, so the options cache used to be reused with a stale source list. + testCase "Source file added via an imported .props is compiled" + <| fun () -> + withTempProject + "added" + (fun dir outDir -> + Expect.equal (compile dir outDir) 0 "First compilation should succeed" + + writeNewer (Path.Combine(dir, "Scalars.fs")) "module Scalars\n\nlet tests = \"scalars\"\n" + + writeNewer + (Path.Combine(dir, "Tests.props")) + """ + + + + + + +""" + + Expect.equal (compile dir outDir) 0 "Second compilation should succeed" + + Expect.isTrue + (File.Exists(Path.Combine(outDir, "Scalars.js"))) + "The file added through the imported .props should have been generated" + ) + ] diff --git a/tests/Integration/Integration/CompilationTests.fs b/tests/Integration/Integration/CompilationTests.fs index 8d592785df..180dfa25e3 100644 --- a/tests/Integration/Integration/CompilationTests.fs +++ b/tests/Integration/Integration/CompilationTests.fs @@ -56,6 +56,46 @@ let private checkBrowserInlineExprs (precompiledDir: string) = (Fable.BrowserInlineExprs.toString [| name, fromCompact |]) $"%s{name} differs between the two copies" +let private precompiledBaseTest name fixture shouldCompile = + testCaseAsync + name + (async { + let testCaseDir = Path.Combine(__SOURCE_DIRECTORY__, "precompiledBases", fixture) + let libraryProject = Path.Combine(testCaseDir, "library", "library.fsproj") + let project = Path.Combine(testCaseDir, fixture + ".fsproj") + let tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + let precompiledDir = Path.Combine(tempDir, "precompiled") + + try + let precompileExitCode = + Fable.Cli.Entry.main [| "precompile"; libraryProject; "--outDir"; precompiledDir; "--noCache" |] + + Expect.equal precompileExitCode 0 "Expected precompile exit code to be 0" + + let exitCode = + Fable.Cli.Entry.main + [| + project + "--cwd" + testCaseDir + "--outDir" + Path.Combine(tempDir, "out") + "--exclude" + "Fable.Core" + "--noCache" + "--precompiledLib" + precompiledDir + |] + + if shouldCompile then + Expect.equal exitCode 0 "Expected the compilation to succeed" + else + Expect.notEqual exitCode 0 "Expected the compilation to fail" + finally + if Directory.Exists tempDir then + Directory.Delete(tempDir, true) + }) + let tests = Directory.EnumerateDirectories(data) |> Seq.map (fun testCaseDir -> // @@ -63,7 +103,10 @@ let tests = testCaseDir (async { let project = - Directory.GetFileSystemEntries(testCaseDir, "*.fsproj") |> Seq.exactlyOne + match Directory.GetFileSystemEntries(testCaseDir, "*.fsproj") with + | [| project |] -> project + | [||] -> Path.Combine(testCaseDir, "main.fsx") + | projects -> failwith $"Expected one project in {testCaseDir}, found {projects.Length}" // clean up old actual files for f in Directory.GetFileSystemEntries(testCaseDir, "*.actual") do File.Delete f @@ -90,17 +133,27 @@ let tests = else [||] + let outputArgs = + if Directory.GetFileSystemEntries(testCaseDir, "*.ts.expected") |> Array.isEmpty then + [| "-e"; ".jsx.actual" |] + else + [| "--lang"; "ts"; "-e"; ".ts.actual" |] + // Compile project let exitCode = - Array.append - [| project; "--cwd"; $"'%s{testCaseDir}'"; "-e"; ".jsx.actual" |] - precompiledLibArgs + Array.concat + [ + [| project; "--cwd"; $"'%s{testCaseDir}'" |] + outputArgs + [| "--exclude"; "Fable.Core" |] + precompiledLibArgs + ] |> Fable.Cli.Entry.main Expect.equal exitCode 0 "Expected exit code to be 0" let normalize content = - Regex.Replace(content, @"(/fable-library-js)[^/]+", "$1") + Regex.Replace(content, @"(/fable-library-(?:js|ts))[^/]+", "$1") |> fun c -> Regex.Replace(c, @"[^""]*/precompiled/", "precompiled/") |> _.ReplaceLineEndings() |> _.Trim() @@ -116,4 +169,9 @@ let tests = })) |> Seq.toList + |> List.append + [ + precompiledBaseTest "Inheriting a precompiled class with a self identifier is an error" "selfBase" false + precompiledBaseTest "Inheriting a precompiled class with a static let binding compiles" "staticInit" true + ] |> testList "Compilation" diff --git a/tests/Integration/Integration/Fable.Tests.Integration.fsproj b/tests/Integration/Integration/Fable.Tests.Integration.fsproj index a87aacd988..33b0ec33ad 100644 --- a/tests/Integration/Integration/Fable.Tests.Integration.fsproj +++ b/tests/Integration/Integration/Fable.Tests.Integration.fsproj @@ -6,7 +6,7 @@ Major - + @@ -17,6 +17,7 @@ + diff --git a/tests/Integration/Integration/Main.fs b/tests/Integration/Integration/Main.fs index 3346c5e75f..3ff92dce0a 100644 --- a/tests/Integration/Integration/Main.fs +++ b/tests/Integration/Integration/Main.fs @@ -6,6 +6,7 @@ let allTests = Cli.tests FileWatcher.tests CompilationTests.tests + CacheInvalidation.tests ] open Expecto diff --git a/tests/Integration/Integration/data/fsxImportExtension/a.fsx b/tests/Integration/Integration/data/fsxImportExtension/a.fsx new file mode 100644 index 0000000000..f0a06fd5c8 --- /dev/null +++ b/tests/Integration/Integration/data/fsxImportExtension/a.fsx @@ -0,0 +1 @@ +let a v = v + 1 diff --git a/tests/Integration/Integration/data/fsxImportExtension/main.fsx b/tests/Integration/Integration/data/fsxImportExtension/main.fsx new file mode 100644 index 0000000000..ba10438697 --- /dev/null +++ b/tests/Integration/Integration/data/fsxImportExtension/main.fsx @@ -0,0 +1,4 @@ +#load "a.fsx" +open A + +let b v = a v + 1 diff --git a/tests/Integration/Integration/data/fsxImportExtension/main.jsx.expected b/tests/Integration/Integration/data/fsxImportExtension/main.jsx.expected new file mode 100644 index 0000000000..3003d78626 --- /dev/null +++ b/tests/Integration/Integration/data/fsxImportExtension/main.jsx.expected @@ -0,0 +1,7 @@ + +import { a } from "./a.jsx.actual"; + +export function b(v) { + return (a(v) + 1) | 0; +} + diff --git a/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected index c439bf61a3..8ff5422f59 100644 --- a/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected +++ b/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected @@ -11,11 +11,11 @@ export let sum = createAtom(0); (function () { const array_1 = arr; for (let i = 0; i <= (array_1.length - 1); i++) { - const x = item(i, array_1) | 0; + const x = (array_1[i] ?? item(i, array_1)) | 0; sum(sum() + x); } for (let i_1 = 0; i_1 <= (array_1.length - 1); i_1++) { - const x = item(i_1, array_1) | 0; + const x = (array_1[i_1] ?? item(i_1, array_1)) | 0; sum(sum() + x); } })(); diff --git a/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected index 6cef943fe9..89cd4cfb84 100644 --- a/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected +++ b/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected @@ -15,11 +15,11 @@ export function addToSum(x) { (function () { const array_1 = arr; for (let i = 0; i <= (array_1.length - 1); i++) { - const x_1 = item(i, array_1) | 0; + const x_1 = (array_1[i] ?? item(i, array_1)) | 0; sum(sum() + x_1); } for (let i_1 = 0; i_1 <= (array_1.length - 1); i_1++) { - const x_1 = item(i_1, array_1) | 0; + const x_1 = (array_1[i_1] ?? item(i_1, array_1)) | 0; sum(sum() + x_1); } })(); diff --git a/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected index 08f3dd85bc..6875caa3f0 100644 --- a/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected +++ b/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected @@ -15,11 +15,11 @@ export function myAction(x) { (function () { const array_1 = arr; for (let i = 0; i <= (array_1.length - 1); i++) { - const x_1 = item(i, array_1) | 0; + const x_1 = (array_1[i] ?? item(i, array_1)) | 0; sum(sum() + x_1); } for (let i_1 = 0; i_1 <= (array_1.length - 1); i_1++) { - const x_1 = item(i_1, array_1) | 0; + const x_1 = (array_1[i_1] ?? item(i_1, array_1)) | 0; sum(sum() + x_1); } })(); diff --git a/tests/Integration/Integration/data/jsxListOptimisation/Components.fs b/tests/Integration/Integration/data/jsxListOptimisation/Components.fs index 8760bc1439..f01bd728f4 100644 --- a/tests/Integration/Integration/data/jsxListOptimisation/Components.fs +++ b/tests/Integration/Integration/data/jsxListOptimisation/Components.fs @@ -40,6 +40,8 @@ type prop = static member inline className (cls : string) : JSX_IReactProperty = "className", cls + static member inline title (value : string) : JSX_IReactProperty = "title", value + [] type Html = @@ -197,3 +199,20 @@ let divWithLongClassName = Html.div [ prop.className "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ] + +let spreadProps: obj = + createObj [ "data-testid" ==> "spread-target"; "title" ==> "from-spread" ] + +let divWithSpread = Html.div [ JSX.spread spreadProps ] + +let divWithSpreadBetweenProps = + Html.div [ prop.className "before"; JSX.spread spreadProps; prop.title "after" ] + +let divWithSpreadOverridingProps = + Html.div [ prop.title "before"; JSX.spread spreadProps ] + +let divWithTwoSpreads = + Html.div [ JSX.spread spreadProps; JSX.spread (createObj [ "title" ==> "second" ]) ] + +let divWithSpreadAndChildren = + Html.div [ JSX.spread spreadProps; prop.children [ Html.div "Child" ] ] diff --git a/tests/Integration/Integration/data/jsxListOptimisation/Components.jsx.expected b/tests/Integration/Integration/data/jsxListOptimisation/Components.jsx.expected index 8f8366f3c5..f0e4fcbb3b 100644 --- a/tests/Integration/Integration/data/jsxListOptimisation/Components.jsx.expected +++ b/tests/Integration/Integration/data/jsxListOptimisation/Components.jsx.expected @@ -1,7 +1,8 @@ + import * as react from "react"; -import { empty, map, singleton, append, delay, toList } from "./fable_modules/fable-library-js.5.0.0-alpha.13/Seq.js"; -import { int32ToString } from "./fable_modules/fable-library-js.5.0.0-alpha.13/Util.js"; -import { rangeDouble } from "./fable_modules/fable-library-js.5.0.0-alpha.13/Range.js"; +import { empty, map, singleton, append, delay, toList } from "./fable_modules/fable-library-js.5.23.0/Seq.js"; +import { int32ToString } from "./fable_modules/fable-library-js.5.23.0/Util.js"; +import { rangeDouble } from "./fable_modules/fable-library-js.5.23.0/Range.js"; export const React = react; @@ -152,3 +153,29 @@ export function divWithElementBeforeMatchContainingForLoop(counter) { } export const divWithLongClassName =
; + +export const spreadProps = { + "data-testid": "spread-target", + title: "from-spread", +}; + +export const divWithSpread =
; + +export const divWithSpreadBetweenProps =
; + +export const divWithSpreadOverridingProps =
; + +export const divWithTwoSpreads =
; + +export const divWithSpreadAndChildren =
+
+ Child +
+
; + diff --git a/tests/Integration/Integration/data/jsxListOptimisation/jsxListOptimisation.fsproj b/tests/Integration/Integration/data/jsxListOptimisation/jsxListOptimisation.fsproj index 63e38f9aad..51a2e75b45 100644 --- a/tests/Integration/Integration/data/jsxListOptimisation/jsxListOptimisation.fsproj +++ b/tests/Integration/Integration/data/jsxListOptimisation/jsxListOptimisation.fsproj @@ -11,7 +11,7 @@ - + diff --git a/tests/Integration/Integration/data/optionalArgs/OptionalArgs.jsx.expected b/tests/Integration/Integration/data/optionalArgs/OptionalArgs.jsx.expected index 793aff95ad..e12cfc938a 100644 --- a/tests/Integration/Integration/data/optionalArgs/OptionalArgs.jsx.expected +++ b/tests/Integration/Integration/data/optionalArgs/OptionalArgs.jsx.expected @@ -1,6 +1,6 @@ import { class_type } from "./fable_modules/fable-library-js.5.4.0/Reflection.js"; import { int32ToString } from "./fable_modules/fable-library-js.5.4.0/Util.js"; -import { toString } from "./fable_modules/fable-library-js.5.4.0/Types.js"; +import { toStringOrEmpty } from "./fable_modules/fable-library-js.5.4.0/Types.js"; import { some, defaultArg } from "./fable_modules/fable-library-js.5.4.0/Option.js"; export function jsBinding(v, sp) { @@ -33,7 +33,7 @@ export function Native_$ctor() { } export function Native_Show_21A309C4(x, y) { - return int32ToString(x) + toString(defaultArg(y, "-")); + return int32ToString(x) + toStringOrEmpty(defaultArg(y, "-")); } export function fSharpMember(n) { diff --git a/tests/Integration/Integration/data/precompiledLib/App.fs b/tests/Integration/Integration/data/precompiledLib/App.fs index 9730e7add9..a5fbc031f7 100644 --- a/tests/Integration/Integration/data/precompiledLib/App.fs +++ b/tests/Integration/Integration/data/precompiledLib/App.fs @@ -3,3 +3,4 @@ module App let greeting = Library.greeting "world" let doubled = Library.double 21 let plain = Library.plain 1 +let circles = Library.circles [ Library.Circle 1.0; Library.Square 2.0 ] diff --git a/tests/Integration/Integration/data/precompiledLib/App.jsx.expected b/tests/Integration/Integration/data/precompiledLib/App.jsx.expected index c58d20b05d..48ca2adfd8 100644 --- a/tests/Integration/Integration/data/precompiledLib/App.jsx.expected +++ b/tests/Integration/Integration/data/precompiledLib/App.jsx.expected @@ -1,5 +1,6 @@ -import { plain as plain_1 } from "./precompiled/Library.js"; +import { Shape, circles as circles_1, plain as plain_1 } from "./precompiled/Library.js"; +import { ofArray } from "./precompiled/fable_modules/fable-library-js.5.21.0/List.js"; export const greeting = { Greet(helpers) { @@ -11,3 +12,5 @@ export const doubled = 21 * 2; export const plain = plain_1(1); +export const circles = circles_1(ofArray([new Shape(/* Circle */ 0, [1]), new Shape(/* Square */ 1, [2])])); + diff --git a/tests/Integration/Integration/data/precompiledLib/library/Library.fs b/tests/Integration/Integration/data/precompiledLib/library/Library.fs index dc2e64a93f..fbada775bb 100644 --- a/tests/Integration/Integration/data/precompiledLib/library/Library.fs +++ b/tests/Integration/Integration/data/precompiledLib/library/Library.fs @@ -15,3 +15,10 @@ let inline greeting (name: string) = let inline double (value: int) = value * 2 let plain (value: int) = value + 1 + +type Shape = + | Circle of float + | Square of float + +/// Regression shape: F# 9 union case testers, which the F# compiler emits as IL helpers +let circles (shapes: Shape list) = shapes |> List.filter (fun shape -> shape.IsCircle) diff --git a/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.fs b/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.fs new file mode 100644 index 0000000000..9c41313ad7 --- /dev/null +++ b/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.fs @@ -0,0 +1,16 @@ +module TaskBuilder + +open System.Threading.Tasks + +let run shouldThrow : Task> = + task { + let! value = Task.FromResult 42 + + try + if shouldThrow then + failwith "boom" + + return Ok value + with error -> + return Error error + } diff --git a/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.jsx.expected b/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.jsx.expected new file mode 100644 index 0000000000..33b008a4da --- /dev/null +++ b/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.jsx.expected @@ -0,0 +1,13 @@ + +import { task } from "./fable_modules/fable-library-js.5.15.0/TaskBuilder.js"; +import { fromResult } from "./fable_modules/fable-library-js.5.15.0/Task.js"; +import { Exception } from "./fable_modules/fable-library-js.5.15.0/Util.js"; +import { FSharpResult$2 } from "./fable_modules/fable-library-js.5.15.0/Result.js"; + +export function run(shouldThrow) { + const builder$0040 = task(); + return builder$0040.Run(builder$0040.Delay(() => builder$0040.Bind(fromResult(42), (_arg) => builder$0040.TryWith(builder$0040.Delay(() => builder$0040.Combine(shouldThrow ? (((() => { + throw new Exception("boom"); + })(), builder$0040.Zero())) : builder$0040.Zero(), builder$0040.Delay(() => builder$0040.Return(new FSharpResult$2(/* Ok */ 0, [_arg]))))), (_arg_1) => builder$0040.Return(new FSharpResult$2(/* Error */ 1, [_arg_1])))))); +} + diff --git a/tests/Integration/Integration/data/taskNetstandard/taskNetstandard.fsproj b/tests/Integration/Integration/data/taskNetstandard/taskNetstandard.fsproj new file mode 100644 index 0000000000..024261748d --- /dev/null +++ b/tests/Integration/Integration/data/taskNetstandard/taskNetstandard.fsproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + + + + + + + + + + + diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.fs b/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.fs new file mode 100644 index 0000000000..f932a11e32 --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.fs @@ -0,0 +1,25 @@ +module TaggedUnion + +open Fable.Core + +type Take = + abstract ``type``: string + abstract fromIndex: int + abstract toIndex: int + +type Edit = + abstract text: string + +[] +type Command = + | Take of Take + | Edit of edit: Edit + | Save + +let make (t: Take) = Take t + +let describe (c: Command) = + match c with + | Take t -> t.fromIndex + t.toIndex + | Edit e -> e.text.Length + | Save -> 0 diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.jsx.expected b/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.jsx.expected new file mode 100644 index 0000000000..ebd309f60a --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.jsx.expected @@ -0,0 +1,19 @@ + + +export function make(t) { + return t; +} + +export function describe(c) { + switch (c.type) { + case "edit": + return c.edit.text.length | 0; + case "save": + return 0; + default: { + const t = c; + return (t.fromIndex + t.toIndex) | 0; + } + } +} + diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterface/tsTaggedUnionInterface.fsproj b/tests/Integration/Integration/data/tsTaggedUnionInterface/tsTaggedUnionInterface.fsproj new file mode 100644 index 0000000000..151946452c --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterface/tsTaggedUnionInterface.fsproj @@ -0,0 +1,16 @@ + + + + net10.0 + Major + + + + + + + + + + + diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.fs b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.fs new file mode 100644 index 0000000000..f932a11e32 --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.fs @@ -0,0 +1,25 @@ +module TaggedUnion + +open Fable.Core + +type Take = + abstract ``type``: string + abstract fromIndex: int + abstract toIndex: int + +type Edit = + abstract text: string + +[] +type Command = + | Take of Take + | Edit of edit: Edit + | Save + +let make (t: Take) = Take t + +let describe (c: Command) = + match c with + | Take t -> t.fromIndex + t.toIndex + | Edit e -> e.text.Length + | Save -> 0 diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.ts.expected b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.ts.expected new file mode 100644 index 0000000000..30f02a1924 --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.ts.expected @@ -0,0 +1,35 @@ + +import { int32 } from "./fable_modules/fable-library-ts.5.23.0/Int32.ts"; + +export interface Take { + get fromIndex(): int32, + get toIndex(): int32, + get type(): string +} + +export interface Edit { + get text(): string +} + +export type Command = + | Take & { type: "take" } + | { type: "edit", edit: Edit } + | { type: "save" } + +export function make(t: Take): Command { + return t as Command; +} + +export function describe(c: Command): int32 { + switch (c.type) { + case "edit": + return c.edit.text.length | 0; + case "save": + return 0; + default: { + const t = c as Take; + return (t.fromIndex + t.toIndex) | 0; + } + } +} + diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/tsTaggedUnionInterfaceTs.fsproj b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/tsTaggedUnionInterfaceTs.fsproj new file mode 100644 index 0000000000..151946452c --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/tsTaggedUnionInterfaceTs.fsproj @@ -0,0 +1,16 @@ + + + + net10.0 + Major + + + + + + + + + + + diff --git a/tests/Integration/Integration/precompiledBases/selfBase/App.fs b/tests/Integration/Integration/precompiledBases/selfBase/App.fs new file mode 100644 index 0000000000..1b320a101b --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/selfBase/App.fs @@ -0,0 +1,6 @@ +module App + +type Derived = + inherit Library.Base + val M: int + new () = { inherit Library.Base(1); M = 2 } diff --git a/tests/Integration/Integration/precompiledBases/selfBase/library/Library.fs b/tests/Integration/Integration/precompiledBases/selfBase/library/Library.fs new file mode 100644 index 0000000000..f520508840 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/selfBase/library/Library.fs @@ -0,0 +1,7 @@ +module Library + +let registered = ResizeArray() + +type Base = + val N: int + new (n: int) as self = { N = n } then registered.Add(box self) diff --git a/tests/Integration/Integration/precompiledBases/selfBase/library/library.fsproj b/tests/Integration/Integration/precompiledBases/selfBase/library/library.fsproj new file mode 100644 index 0000000000..6fc5f67a94 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/selfBase/library/library.fsproj @@ -0,0 +1,12 @@ + + + net10.0 + Major + + + + + + + + diff --git a/tests/Integration/Integration/precompiledBases/selfBase/selfBase.fsproj b/tests/Integration/Integration/precompiledBases/selfBase/selfBase.fsproj new file mode 100644 index 0000000000..912d3e71a2 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/selfBase/selfBase.fsproj @@ -0,0 +1,13 @@ + + + net10.0 + Major + + + + + + + + + diff --git a/tests/Integration/Integration/precompiledBases/staticInit/App.fs b/tests/Integration/Integration/precompiledBases/staticInit/App.fs new file mode 100644 index 0000000000..9a0033d8aa --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/staticInit/App.fs @@ -0,0 +1,4 @@ +module App + +type Child() = + inherit Library.Counter(1) diff --git a/tests/Integration/Integration/precompiledBases/staticInit/library/Library.fs b/tests/Integration/Integration/precompiledBases/staticInit/library/Library.fs new file mode 100644 index 0000000000..7fa0cb4149 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/staticInit/library/Library.fs @@ -0,0 +1,6 @@ +module Library + +type Counter(n: int) = + static let zero = 0 + new () = Counter(zero) + member _.N = n diff --git a/tests/Integration/Integration/precompiledBases/staticInit/library/library.fsproj b/tests/Integration/Integration/precompiledBases/staticInit/library/library.fsproj new file mode 100644 index 0000000000..6fc5f67a94 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/staticInit/library/library.fsproj @@ -0,0 +1,12 @@ + + + net10.0 + Major + + + + + + + + diff --git a/tests/Integration/Integration/precompiledBases/staticInit/staticInit.fsproj b/tests/Integration/Integration/precompiledBases/staticInit/staticInit.fsproj new file mode 100644 index 0000000000..912d3e71a2 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/staticInit/staticInit.fsproj @@ -0,0 +1,13 @@ + + + net10.0 + Major + + + + + + + + + diff --git a/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj b/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj index c1a6a7e835..d5485bf1af 100644 --- a/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj +++ b/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj @@ -10,7 +10,7 @@ - - + + diff --git a/tests/Js/Main/ApplicativeTests.fs b/tests/Js/Main/ApplicativeTests.fs index 867f7ea897..1077f7f75a 100644 --- a/tests/Js/Main/ApplicativeTests.fs +++ b/tests/Js/Main/ApplicativeTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Applicative open System open Util.Testing +open Fable.Tests.Util let inline (|HasLength|) x = fun () -> (^a: (member Length: int) x) @@ -1190,6 +1191,12 @@ let tests7 = [ apply3 r.myFunction 5 7 4 |> equal 8 apply (r.myFunction 1 1 |> Some) (Some 5) |> equal (Some -3) + testCase "Null functions in record fields can be read back" <| fun () -> + let r = { myFunction = Unchecked.defaultof<_> } + let f = r.myFunction + throwsAnyError (fun () -> f 1 2 3) + throwsAnyError (fun () -> r.myFunction 1 2 3) + testCase "Uncurried functions in record fields can be partially applied" <| fun () -> // Test also non-record functions just in case let result = getStrLen () "hello" @@ -1720,6 +1727,53 @@ module MultipleInlines = let private mapMyList (xs: NonEmptyList) : NonEmptyList = map (fun s -> s + "_") xs + let mutable codecCalls = 0 + + type Codec<'a> = Codec of (unit -> int) + + type Codecs = + static member inline Codecs(_output: int, _mthd: Codecs) : Codec = Codec(fun () -> 0) + static member inline Codecs(_output: bool, _mthd: Codecs) : Codec = Codec(fun () -> 0) + + static member inline Invoke() = + let inline call_2 (a: ^a, b: ^b) = + ((^a or ^b): (static member Codecs: _ * _ -> _) b, a) + + let inline call (a: 'a) = + call_2 (a, Unchecked.defaultof<'r>): Codec<'r> + + call Unchecked.defaultof + + type Codecs with + static member inline Codecs< ^a, ^b + when (^a): (static member codecInner: Codec< ^b > -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + (_output: ^a, _mthd: Codecs) + : Codec< ^a > = + codecCalls <- codecCalls + 1 + let x = codecCalls * 10 + let (Codec inner) = 'a.codecInner (Codecs.Invoke()) + Codec(fun () -> x + inner ()) + + let inline getCodecs<'a, 'b + when (Codecs or ^a): (static member Codecs: ^a * Codecs -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + () + : Codec< ^a > * Codec< ^b > = + let a: Codec<'a> = Codecs.Invoke() + let b: Codec<'b> = Codecs.Invoke() + (a, b) + + type CodecContainer<'T> = + | CodecContainer + + static member codecInner(Codec f: Codec<'T>) : Codec> = Codec(fun () -> f () + 1) + + let getContainerCodecs () = + codecCalls <- 0 + let (Codec a), (Codec b) = (getCodecs () : Codec> * Codec>) + a (), b () + let tests = [ testCase "Trait call works with multiple inlined functions I" <| fun () -> // See #2809 let account = Some { Details = { Tag = "Test"; InsurancePolicyData = None }} @@ -1741,6 +1795,9 @@ module MultipleInlines = testCase "Identifiers from witnesses don't get duplicated when resolving inline expressions" <| fun () -> // See #2855 NonEmptyList("a", ["b"; "c"]) |> mapMyList |> equal (NonEmptyList("a_", ["b_"; "c_"])) + + testCase "Identifiers from a witness inlined twice in the same scope don't get duplicated" <| fun () -> // See #3921 + getContainerCodecs () |> equal (11, 21) ] module AccessorFunctionShorthand = diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index 53bf938f92..dc94da202f 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -1,6 +1,7 @@ module Fable.Tests.Arithmetic open System +open System.Globalization open Util.Testing open Fable.Tests.Util @@ -195,6 +196,38 @@ let tests = testCase "Decimal division precision is kept" <| fun () -> string (8M / 3M) |> equal "2.6666666666666666666666666667" + testCase "Decimal ToString keeps the sign of values below 0.1" <| fun () -> + string (decimal -0.001) |> equal "-0.001" + string (decimal -0.05) |> equal "-0.05" + string (decimal -0.1) |> equal "-0.1" + string (decimal -1.5) |> equal "-1.5" + string -0.001M |> equal "-0.001" + string (decimal 0) |> equal "0" + + testCase "Decimal ToString round-trips negative values parsed from a string" <| fun () -> + string (decimal "-0.000001") |> equal "-0.000001" + string (decimal "-0.001") |> equal "-0.001" + string (decimal "-0.05") |> equal "-0.05" + string (decimal "-0.5") |> equal "-0.5" + string (decimal "-0.000") |> equal "0.000" + + testCase "Decimal fixed-point formatting respects the precision" <| fun () -> + let inv = System.Globalization.CultureInfo.InvariantCulture + (decimal 0.001).ToString("F2", inv) |> equal "0.00" + (decimal 0.05).ToString("F1", inv) |> equal "0.1" + (decimal 0.001).ToString("N2", inv) |> equal "0.00" + (decimal "0.001").ToString("F2", inv) |> equal "0.00" + sprintf "%.2f" (decimal 0.001) |> equal "0.00" + + testCase "Decimal fixed-point formatting omits the sign when the result rounds to zero" <| fun () -> + let inv = System.Globalization.CultureInfo.InvariantCulture + (-0.001M).ToString("F2", inv) |> equal "0.00" + (-0.1M).ToString("F0", inv) |> equal "0" + (-0.4M).ToString("F0", inv) |> equal "0" + (-0.001M).ToString("N2", inv) |> equal "0.00" + (-0.6M).ToString("F0", inv) |> equal "-1" + (-1.5M).ToString("F0", inv) |> equal "-2" + testCase "Decimal division works" <| fun () -> let a = decimal 0.00001 let b = 1000.M @@ -300,6 +333,29 @@ let tests = Math.Round(-1.426M, 2) |> equal -1.43M Math.Round(-1.426M, 1) |> equal -1.4M + testCase "Decimal round with MidpointRounding works" <| fun () -> + let round mode = [ 2.5M; -2.5M; 3.5M; -3.5M; 2.4M; -2.6M ] |> List.map (fun x -> Math.Round(x, (mode: MidpointRounding))) + round MidpointRounding.ToEven |> equal [ 2.M; -2.M; 4.M; -4.M; 2.M; -3.M ] + round MidpointRounding.AwayFromZero |> equal [ 3.M; -3.M; 4.M; -4.M; 2.M; -3.M ] + round MidpointRounding.ToZero |> equal [ 2.M; -2.M; 3.M; -3.M; 2.M; -2.M ] + round MidpointRounding.ToNegativeInfinity |> equal [ 2.M; -3.M; 3.M; -4.M; 2.M; -3.M ] + round MidpointRounding.ToPositiveInfinity |> equal [ 3.M; -2.M; 4.M; -3.M; 3.M; -2.M ] + let roundDigits mode = [ 1.25M, 1; -1.25M, 1; 1.235M, 2; -1.235M, 2; 1.426M, 2 ] |> List.map (fun (x, d) -> Math.Round(x, d, (mode: MidpointRounding))) + roundDigits MidpointRounding.ToEven |> equal [ 1.2M; -1.2M; 1.24M; -1.24M; 1.43M ] + roundDigits MidpointRounding.AwayFromZero |> equal [ 1.3M; -1.3M; 1.24M; -1.24M; 1.43M ] + roundDigits MidpointRounding.ToZero |> equal [ 1.2M; -1.2M; 1.23M; -1.23M; 1.42M ] + roundDigits MidpointRounding.ToNegativeInfinity |> equal [ 1.2M; -1.3M; 1.23M; -1.24M; 1.42M ] + roundDigits MidpointRounding.ToPositiveInfinity |> equal [ 1.3M; -1.2M; 1.24M; -1.23M; 1.43M ] + Decimal.Round(2.5M, MidpointRounding.AwayFromZero) |> equal 3.M + Decimal.Round(-1.25M, 1, MidpointRounding.AwayFromZero) |> equal -1.3M + + testCase "Decimal round with MidpointRounding validates its arguments" <| fun () -> + (fun () -> Math.Round(1.5M, enum 5)) |> throwsAnyError + (fun () -> Math.Round(1.5M, 2, enum -1)) |> throwsAnyError + (fun () -> Math.Round(1.5M, 29, MidpointRounding.AwayFromZero)) |> throwsAnyError + (fun () -> Math.Round(1.5M, -1, MidpointRounding.AwayFromZero)) |> throwsAnyError + Math.Round(1.5M, 28, MidpointRounding.AwayFromZero) |> equal 1.5M + testCase "Decimal truncate works" <| fun () -> truncate 11.0M |> equal 11.M truncate 11.01M |> equal 11.M @@ -345,6 +401,243 @@ let tests = Math.Floor 11.25M |> equal 11.M Math.Floor -11.25M |> equal -12.M + testCase "Decimal rounding works for values below 0.1" <| fun () -> + Math.Floor (decimal -0.0836) |> equal -1.M + Math.Floor (decimal (sin (-5. * Math.PI / 180.))) |> equal -1.M + Math.Floor (decimal "-0.0836") |> equal -1.M + Math.Floor (decimal "-0.0000123") |> equal -1.M + Math.Floor -0.0836M |> equal -1.M + Math.Floor (decimal 0.0836) |> equal 0.M + Math.Ceiling (decimal 0.0836) |> equal 1.M + Math.Ceiling (decimal "0.0000123") |> equal 1.M + Math.Ceiling (decimal -0.0836) |> equal 0.M + Math.Round (decimal 0.06) |> equal 0.M + Math.Round (decimal 0.0836, 2) |> equal 0.08M + Math.Round (decimal -0.0836, 3) |> equal -0.084M + Math.Truncate (decimal -0.0836) |> equal 0.M + Math.Floor (0.M - 0.0836M) |> equal -1.M + + testCase "Decimal values below 0.1 hash the same however they are built" <| fun () -> + (decimal -0.0836).GetHashCode() |> equal ((0.M - 0.0836M).GetHashCode()) + (decimal "0.05").GetHashCode() |> equal ((0.1M / 2.M).GetHashCode()) + let set = Collections.Generic.HashSet() + set.Add(decimal -0.0836) |> equal true + set.Add(0.M - 0.0836M) |> equal false + let dict = Collections.Generic.Dictionary() + dict.[decimal "0.05"] <- "a" + dict.ContainsKey(0.1M / 2.M) |> equal true + + testCase "Decimal hash ignores trailing zeros and the sign of zero" <| fun () -> + (decimal "1.0").GetHashCode() |> equal ((decimal "1").GetHashCode()) + (decimal "0.050").GetHashCode() |> equal ((0.1M / 2.M).GetHashCode()) + (decimal "0.00").GetHashCode() |> equal ((decimal "0").GetHashCode()) + (-(0.M)).GetHashCode() |> equal ((0.M).GetHashCode()) + let set = Collections.Generic.HashSet([ 1.M ]) + set.Contains 1.0M |> equal true + set.Add 0.M |> equal true + set.Add(Math.Truncate -0.0836M) |> equal false + let dict = Collections.Generic.Dictionary() + dict.[0.050M] <- "a" + dict.ContainsKey(0.1M / 2.M) |> equal true + + testCase "Decimal parsing rounds to 28 places and keeps the scale of zero" <| fun () -> + string (decimal "0.0000000000000000000000000000004") |> equal "0.0000000000000000000000000000" + string (decimal "0.00000000000000000000000000005") |> equal "0.0000000000000000000000000000" + string (decimal "0.00000000000000000000000000015") |> equal "0.0000000000000000000000000002" + string (decimal "0.000000000000000000000000000012") |> equal "0.0000000000000000000000000000" + Decimal.GetBits(decimal "0.0000000000000000000000000000004") |> equal [| 0; 0; 0; 1835008 |] + Decimal.GetBits(decimal "0.00") |> equal [| 0; 0; 0; 131072 |] + string (-(0.00M)) |> equal "0.00" + string (abs 0.00M) |> equal "0.00" + string (Math.Round(0.004M, 2)) |> equal "0.00" + (0.00M).ToString("E3", System.Globalization.CultureInfo.InvariantCulture) |> equal "0.000E+000" + (decimal "0.00").GetHashCode() |> equal (Decimal(0, 0, 0, false, 2uy).GetHashCode()) + + testCase "Decimal addition and subtraction keep the larger scale" <| fun () -> + string (1.50M + 0.5M) |> equal "2.00" + string (1.5M + 0.50M) |> equal "2.00" + string (0.1M + 0.2M) |> equal "0.3" + string (1M + 1M) |> equal "2" + string (0.00M + 0M) |> equal "0.00" + string (0.00M + 1.5M) |> equal "1.50" + string (1.0M + (-1.0M)) |> equal "0.0" + string (0.05M - 0.05M) |> equal "0.00" + string (1.00M - 0.50M) |> equal "0.50" + string (100.5M - 0.5M) |> equal "100.0" + string (1.5M - 0.00M) |> equal "1.50" + + testCase "Decimal multiplication adds the scales up to 28 places" <| fun () -> + string (2.0M * 3.0M) |> equal "6.00" + string (1.5M * 2M) |> equal "3.0" + string (0.5M * 0.5M) |> equal "0.25" + string (1.50M * 1.0M) |> equal "1.500" + string (10M * 0.1M) |> equal "1.0" + string (1.10M * 1.10M) |> equal "1.2100" + string (0.0M * 5M) |> equal "0.0" + string (0M * 1.50M) |> equal "0.00" + string (pown 1.50M 2) |> equal "2.2500" + string (0.1234567890123456789012345678M * 0.1M) |> equal "0.0123456789012345678901234568" + string (0.0000000000000000000000000025M * 0.1M) |> equal "0.0000000000000000000000000002" + string (0.0000000000000000000000000035M * 0.1M) |> equal "0.0000000000000000000000000004" + string (123456789012345678901234567.89M * 1.1M) |> equal "135802467913580246791358024.68" + + testCase "Decimal division keeps the scale of an exact quotient" <| fun () -> + string (1.00M / 2M) |> equal "0.50" + string (10M / 2M) |> equal "5" + string (1M / 4M) |> equal "0.25" + string (1.0M / 0.5M) |> equal "2" + string (2M / 1.0M) |> equal "2" + string (5.0M / 2.0M) |> equal "2.5" + string (1.000M / 8M) |> equal "0.125" + string (1.23M / 0.1M) |> equal "12.3" + string (0.0M / 5M) |> equal "0.0" + + testCase "Decimal division rounds an inexact quotient half to even to fit a decimal" <| fun () -> + string (1M / 3M) |> equal "0.3333333333333333333333333333" + string (2M / 3M) |> equal "0.6666666666666666666666666667" + string (10M / 3M) |> equal "3.3333333333333333333333333333" + string (100M / 3M) |> equal "33.333333333333333333333333333" + string (1M / 30000M) |> equal "0.0000333333333333333333333333" + string (1M / 7M) |> equal "0.1428571428571428571428571429" + string (80M / 9M) |> equal "8.888888888888888888888888889" + string (1000000000000000000000000000M / 3M) |> equal "333333333333333333333333333.33" + string (10000000000000000000000000001M / 2M) |> equal "5000000000000000000000000000.5" + string (0.0000000000000000000000000005M / 2M) |> equal "0.0000000000000000000000000002" + string (0.0000000000000000000000000007M / 2M) |> equal "0.0000000000000000000000000004" + string (0.0000000000000000000000000001M / 2M) |> equal "0" + + testCase "Decimal remainder keeps the larger scale" <| fun () -> + string (5.0M % 2M) |> equal "1.0" + string (5M % 2.0M) |> equal "1.0" + string (5.5M % 2M) |> equal "1.5" + string (7.00M % 2.5M) |> equal "2.00" + string (-5.0M % 2M) |> equal "-1.0" + string (1.5M % 2.00M) |> equal "1.5" + + testCase "Decimal arithmetic and parsing throw on overflow" <| fun () -> + (fun () -> Decimal.MaxValue + 1M) |> throwsError "" + (fun () -> Decimal.MaxValue + 0.5M) |> throwsError "" + string (Decimal.MaxValue + 0.4M) |> equal "79228162514264337593543950335" + string ((Decimal.MaxValue - 1M) + 1M) |> equal "79228162514264337593543950335" + (fun () -> Decimal.MinValue - 1M) |> throwsError "" + (fun () -> Decimal.MaxValue * 1.1M) |> throwsError "" + string (Decimal.MaxValue * 1.0M) |> equal "79228162514264337593543950335" + (fun () -> Decimal.MaxValue / 0.5M) |> throwsError "" + (fun () -> decimal "1e30") |> throwsError "" + (fun () -> decimal "79228162514264337593543950336") |> throwsError "" + (fun () -> decimal "79228162514264337593543950335.5") |> throwsError "" + string (decimal "79228162514264337593543950335.4") |> equal "79228162514264337593543950335" + (fun () -> decimal 1e30) |> throwsError "" + + testCase "Decimal from float rounds to 15 significant digits" <| fun () -> + let inv = CultureInfo.InvariantCulture + string (decimal 0.1) |> equal "0.1" + string (decimal (0.1 + 0.2)) |> equal "0.3" + string (decimal 123456789.123456789) |> equal "123456789.123457" + string (decimal (1.0 / 3.0)) |> equal "0.333333333333333" + string (decimal 123456789012345.6789) |> equal "123456789012346" + string (decimal 123456789012345678.0) |> equal "123456789012346000" + string (decimal 1e20) |> equal "100000000000000000000" + string (decimal 1e28) |> equal "10000000000000000000000000000" + string (decimal 1e-10) |> equal "0.0000000001" + string (decimal 1.5e-28) |> equal "0.0000000000000000000000000002" + string (decimal 5e-29) |> equal "0" + string (decimal 1e-30) |> equal "0" + string (decimal -0.0) |> equal "0" + string (decimal 100000000000001.5) |> equal "100000000000002" + string (decimal 100000000000002.5) |> equal "100000000000002" + (fun () -> decimal 1e29) |> throwsError "" + (fun () -> decimal nan) |> throwsError "" + (fun () -> decimal infinity) |> throwsError "" + + testCase "Decimal from float32 rounds to 7 significant digits" <| fun () -> + let inv = CultureInfo.InvariantCulture + string (decimal 1.1f) |> equal "1.1" + string (decimal (0.1f + 0.2f)) |> equal "0.3" + string (decimal 16777216.f) |> equal "16777220" + string (decimal 1.23456789f) |> equal "1.234568" + string (decimal 123456.789f) |> equal "123456.8" + string (decimal 1000001.5f) |> equal "1000002" + string (decimal 1000002.5f) |> equal "1000002" + string (decimal 1e-30f) |> equal "0" + (fun () -> decimal 3.4e38f) |> throwsError "" + + testCase "Decimal.Parse follows NumberStyles" <| fun () -> + let inv = CultureInfo.InvariantCulture + Decimal.Parse("+5", inv) |> equal 5M + Decimal.Parse(" 5 ", inv) |> equal 5M + Decimal.Parse("5-", inv) |> equal -5M + Decimal.Parse("1,234.5", inv) |> equal 1234.5M + Decimal.Parse("1,23,4", inv) |> equal 1234M + Decimal.Parse(".5", inv) |> equal 0.5M + Decimal.Parse("5.", inv) |> equal 5M + string (Decimal.Parse("-0.00", inv)) |> equal "0.00" + (fun () -> Decimal.Parse("1e3", inv)) |> throwsError "" + (fun () -> Decimal.Parse(",5", inv)) |> throwsError "" + (fun () -> Decimal.Parse("1,234.5,6", inv)) |> throwsError "" + (fun () -> Decimal.Parse("(5)", inv)) |> throwsError "" + (fun () -> Decimal.Parse("", inv)) |> throwsError "" + Decimal.Parse("1.5E+2", NumberStyles.Float, inv) |> equal 150M + string (Decimal.Parse("1.50e1", NumberStyles.Float, inv)) |> equal "15.0" + Decimal.Parse("1e3", NumberStyles.Any, inv) |> equal 1000M + Decimal.Parse("(5)", NumberStyles.Any, inv) |> equal -5M + (fun () -> Decimal.Parse("1,234", NumberStyles.Float, inv)) |> throwsError "" + (fun () -> Decimal.Parse("1e30", NumberStyles.Float, inv)) |> throwsError "" + let ok1, v1 = Decimal.TryParse("1,5", NumberStyles.Number, inv) + ok1 |> equal true + v1 |> equal 15M + let ok2, _ = Decimal.TryParse("1e3", NumberStyles.Number, inv) + ok2 |> equal false + + testCase "Decimal G format keeps the scale without a precision" <| fun () -> + let inv = CultureInfo.InvariantCulture + (1.10M).ToString("G", inv) |> equal "1.10" + (0.00M).ToString("G", inv) |> equal "0.00" + (1.10M).ToString("G5", inv) |> equal "1.1" + (123456M).ToString("G3", inv) |> equal "1.23E+05" + (0.0001234M).ToString("G3", inv) |> equal "0.000123" + (2.5M).ToString("G1", inv) |> equal "3" + (1.5M).ToString("G1", inv) |> equal "2" + + testCase "Decimal max returns the first of two equal values" <| fun () -> + let inv = CultureInfo.InvariantCulture + string (max 1.0M 1.00M) |> equal "1.0" + string (min 1.0M 1.00M) |> equal "1.00" + string (Math.Max(1.0M, 1.00M)) |> equal "1.0" + string (Math.Min(1.0M, 1.00M)) |> equal "1.00" + + testCase "Decimal static conversions truncate and check the range" <| fun () -> + Decimal.ToInt32 2.9M |> equal 2 + Decimal.ToInt32 -2.9M |> equal -2 + Decimal.ToInt64 -2.9M |> equal -2L + Decimal.ToInt64 9223372036854775807.5M |> equal 9223372036854775807L + Decimal.ToUInt64 18446744073709551615M |> equal 18446744073709551615UL + Decimal.ToInt16 32767.5M |> equal 32767s + Decimal.ToUInt16 65535.9M |> equal 65535us + Decimal.ToByte 255.9M |> equal 255uy + Decimal.ToSByte -128.9M |> equal -128y + Decimal.ToUInt32 4294967295.9M |> equal 4294967295u + Decimal.ToDouble 0.1M |> equal 0.1 + Decimal.ToSingle 1.5M |> equal 1.5f + (fun () -> Decimal.ToInt32 2147483648M) |> throwsError "" + (fun () -> Decimal.ToByte -1M) |> throwsError "" + Decimal.ToUInt64 -0.5M |> equal 0UL + + testCase "Decimal rounding keeps the requested scale" <| fun () -> + string (Math.Round(1.20001M, 2)) |> equal "1.20" + string (Math.Round(1.2M, 3)) |> equal "1.2" + string (Math.Round(1.99M, 1)) |> equal "2.0" + string (Math.Floor 1.50M) |> equal "1" + string (Math.Ceiling 1.50M) |> equal "2" + string (Math.Truncate 1.50M) |> equal "1" + + testCase "Decimal rounding is half to even when a trailing zero follows the 5" <| fun () -> + Math.Round 2.50M |> equal 2.M + Math.Round 0.50M |> equal 0.M + Math.Round(1.250M, 1) |> equal 1.2M + Math.Round(0.050M, 1) |> equal 0.0M + Math.Round(0.0050M, 2) |> equal 0.00M + testCase "Decimal pown works" <| fun () -> pown 2.2M 3 |> equal 10.648M @@ -495,6 +788,30 @@ let tests = let result = Double.Parse("10.5", culture) result |> equal 10.5 + testCase "float32 arithmetic rounds every operation like .NET" <| fun () -> + let a, b, c = 1.5f, 1.2f, 3.3f + (a + b) * c |> equal 8.9099998f + a + b + c |> equal 6.f + 0.1f + 0.2f |> equal 0.3f + 16777216.f + 1.f |> equal 16777216.f + 1.f / 3.f |> equal 0.33333334f + 5.5f % 2.2f |> equal 1.0999999f + -(a + b) |> equal -2.7f + let arr = [| 0.f |] + arr.[0] <- a * c + arr.[0] |> equal 4.95f + + testCase "float32 conversions round like .NET" <| fun () -> + float32 2.7 |> equal 2.7f + float32 16777217 |> equal 16777216.f + float32 16777217L |> equal 16777216.f + float32 2.7M |> equal 2.7f + float32 "2.7" |> equal 2.7f + Single.Parse("2.7", CultureInfo.InvariantCulture) |> equal 2.7f + let mutable parsed = 0.f + Single.TryParse("2.7", NumberStyles.Float, CultureInfo.InvariantCulture, &parsed) |> equal true + parsed |> equal 2.7f + testCase "Single.Parse works with IFormatProvider" <| fun () -> // culture compiles to { } for now and it is ignored on the call-site let culture = Globalization.CultureInfo.InvariantCulture @@ -586,6 +903,99 @@ let tests = Math.Round(-1.426, 2) |> equal -1.43 Math.Round(-1.426, 1) |> equal -1.4 + testCase "Math.Round rounds the exact value" <| fun () -> // See #5251 + Math.Round 2.500000001 |> equal 3. + Math.Round 3.4999999999 |> equal 3. + Math.Round -2.500000001 |> equal -3. + Math.Round -3.4999999999 |> equal -3. + Math.Round 2.5 |> equal 2. + Math.Round 3.5 |> equal 4. + Math.Round 0.49999999999999994 |> equal 0. + 1. / Math.Round(-0.5) |> equal -infinity + Math.Round(1.2500000001, 1) |> equal 1.3 + Math.Round(2.0000000050001, 8) |> equal 2.00000001 + Math.Round(2.675, 2) |> equal 2.68 + Math.Round(123456.785, 2) |> equal 123456.78 + round 2.500000001 |> equal 3. + round 3.4999999999 |> equal 3. + Convert.ToInt32 2.500000001 |> equal 3 + Convert.ToInt32 3.4999999999 |> equal 3 + MathF.Round 2.500001f |> equal 3.f + MathF.Round 3.499999f |> equal 3.f + MathF.Round 2.5f |> equal 2.f + + testCase "Math.Round validates its digits" <| fun () -> + (fun () -> Math.Round(1., 16)) |> throwsAnyError + (fun () -> Math.Round(1., -1)) |> throwsAnyError + (fun () -> MathF.Round(1.f, 7)) |> throwsAnyError + Math.Round(1.2345678901234567, 15) |> equal 1.234567890123457 + Math.Round(1e16, 2) |> equal 1e16 + + testCase "Math.Round with MidpointRounding works" <| fun () -> + let round mode = [ 2.5; -2.5; 3.5; -3.5; 2.4; -2.6; 0.5; -0.5 ] |> List.map (fun x -> Math.Round(x, (mode: MidpointRounding))) + round MidpointRounding.ToEven |> equal [ 2.; -2.; 4.; -4.; 2.; -3.; 0.; -0. ] + round MidpointRounding.AwayFromZero |> equal [ 3.; -3.; 4.; -4.; 2.; -3.; 1.; -1. ] + round MidpointRounding.ToZero |> equal [ 2.; -2.; 3.; -3.; 2.; -2.; 0.; -0. ] + round MidpointRounding.ToNegativeInfinity |> equal [ 2.; -3.; 3.; -4.; 2.; -3.; 0.; -1. ] + round MidpointRounding.ToPositiveInfinity |> equal [ 3.; -2.; 4.; -3.; 3.; -2.; 1.; -0. ] + Math.Round(0.49999999999999994, MidpointRounding.AwayFromZero) |> equal 0. + Math.Round(4503599627370497., MidpointRounding.AwayFromZero) |> equal 4503599627370497. + 1. / Math.Round(-0.4, MidpointRounding.AwayFromZero) |> equal -infinity + + testCase "Math.Round with digits and MidpointRounding works" <| fun () -> + let round mode = [ 1.25, 1; -1.25, 1; 1.235, 2; 2.675, 2; -2.675, 2; 1.426, 2; 0.125, 2; -1.005, 2 ] |> List.map (fun (x, d) -> Math.Round(x, d, (mode: MidpointRounding))) + round MidpointRounding.ToEven |> equal [ 1.2; -1.2; 1.24; 2.68; -2.68; 1.43; 0.12; -1. ] + round MidpointRounding.AwayFromZero |> equal [ 1.3; -1.3; 1.24; 2.68; -2.68; 1.43; 0.13; -1. ] + round MidpointRounding.ToZero |> equal [ 1.2; -1.2; 1.23; 2.67; -2.67; 1.42; 0.12; -1. ] + round MidpointRounding.ToNegativeInfinity |> equal [ 1.2; -1.3; 1.23; 2.67; -2.68; 1.42; 0.12; -1.01 ] + round MidpointRounding.ToPositiveInfinity |> equal [ 1.3; -1.2; 1.24; 2.68; -2.67; 1.43; 0.13; -1. ] + Math.Round(1e17, 2, MidpointRounding.AwayFromZero) |> equal 1e17 + + testCase "Math.Round with MidpointRounding.ToEven rounds the exact value" <| fun () -> + Math.Round(2.500000001, MidpointRounding.ToEven) |> equal 3. + Math.Round(-2.500000001, MidpointRounding.ToEven) |> equal -3. + Math.Round(2.499999999, MidpointRounding.ToEven) |> equal 2. + Math.Round(1.015, 2, MidpointRounding.ToEven) |> equal 1.01 + Math.Round(1.015, 2, MidpointRounding.AwayFromZero) |> equal 1.01 + Math.Round(1.025, 2, MidpointRounding.ToEven) |> equal 1.02 + 1. / Math.Round(-0.5, MidpointRounding.ToEven) |> equal -infinity + 1. / Math.Round(-0.04, 1, MidpointRounding.ToEven) |> equal -infinity + + testCase "Math.Round with MidpointRounding keeps NaN and infinities" <| fun () -> + let modes = [ MidpointRounding.ToEven; MidpointRounding.AwayFromZero; MidpointRounding.ToZero; MidpointRounding.ToNegativeInfinity; MidpointRounding.ToPositiveInfinity ] + for mode in modes do + Double.IsNaN(Math.Round(nan, mode)) |> equal true + Double.IsNaN(Math.Round(nan, 2, mode)) |> equal true + Math.Round(infinity, mode) |> equal infinity + Math.Round(-infinity, 2, mode) |> equal -infinity + + testCase "Math.Round with MidpointRounding validates its arguments" <| fun () -> + (fun () -> Math.Round(1.5, enum 5)) |> throwsAnyError + (fun () -> Math.Round(1e17, enum 5)) |> throwsAnyError + (fun () -> Math.Round(nan, enum 5)) |> throwsAnyError + (fun () -> Math.Round(1.5, 2, enum -1)) |> throwsAnyError + (fun () -> Math.Round(1.5, 16, MidpointRounding.AwayFromZero)) |> throwsAnyError + (fun () -> Math.Round(1.5, -1, MidpointRounding.ToEven)) |> throwsAnyError + (fun () -> Math.Round(nan, 16, MidpointRounding.ToEven)) |> throwsAnyError + Math.Round(1.5, 15, MidpointRounding.AwayFromZero) |> equal 1.5 + + testCase "Math.Round with digits and MidpointRounding skips mode validation for unrounded values" <| fun () -> + Math.Round(1e17, 2, enum 5) |> equal 1e17 + Math.Round(infinity, 2, enum 5) |> equal infinity + Double.IsNaN(Math.Round(nan, 2, enum 5)) |> equal true + + testCase "MathF.Round with MidpointRounding works" <| fun () -> + MathF.Round(2.5f, MidpointRounding.AwayFromZero) |> equal 3.f + MathF.Round(-2.5f, MidpointRounding.AwayFromZero) |> equal -3.f + MathF.Round(2.5f, MidpointRounding.ToEven) |> equal 2.f + MathF.Round(2.5f, MidpointRounding.ToZero) |> equal 2.f + MathF.Round(-2.5f, MidpointRounding.ToNegativeInfinity) |> equal -3.f + MathF.Round(-2.5f, MidpointRounding.ToPositiveInfinity) |> equal -2.f + MathF.Round(1.5f, 6, MidpointRounding.AwayFromZero) |> equal 1.5f + MathF.Round(1e9f, 2, enum 5) |> equal 1e9f + (fun () -> MathF.Round(1.5f, 7, MidpointRounding.AwayFromZero)) |> throwsAnyError + (fun () -> MathF.Round(1e9f, enum 5)) |> throwsAnyError + testCase "Math.Truncate works" <| fun () -> Math.Truncate -12.5 |> equal -12. Math.Truncate 1.425 |> equal 1. diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index b0b90a14a6..0d70eb736f 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -14,6 +14,10 @@ open Fable.Core [] let jsConstructorIs (s: string) (ar: 'T[]) = true + +/// Expected JS constructor name of a numeric array, depending on the --typedArrays option +let typedArrayOr (typedArrayName: string) = + if Compiler.typedArrays then typedArrayName else "Array" #endif type ExceptFoo = { Bar:string } @@ -102,6 +106,77 @@ let tests = xs |> Array.sum |> equal 6 #endif +#if FABLE_COMPILER + testCase "Numeric array literals respect --typedArrays" <| fun () -> + [| 1y |] |> jsConstructorIs (typedArrayOr "Int8Array") |> equal true + [| 1uy |] |> jsConstructorIs (typedArrayOr "Uint8Array") |> equal true + [| 1s |] |> jsConstructorIs (typedArrayOr "Int16Array") |> equal true + [| 1us |] |> jsConstructorIs (typedArrayOr "Uint16Array") |> equal true + [| 1 |] |> jsConstructorIs (typedArrayOr "Int32Array") |> equal true + [| 1u |] |> jsConstructorIs (typedArrayOr "Uint32Array") |> equal true + [| 1L |] |> jsConstructorIs (typedArrayOr "BigInt64Array") |> equal true + [| 1UL |] |> jsConstructorIs (typedArrayOr "BigUint64Array") |> equal true + [| 1.f |] |> jsConstructorIs (typedArrayOr "Float32Array") |> equal true + [| 1. |] |> jsConstructorIs (typedArrayOr "Float64Array") |> equal true + [| 1M |] |> jsConstructorIs "Array" |> equal true + [| 1I |] |> jsConstructorIs "Array" |> equal true + [| 'a' |] |> jsConstructorIs "Array" |> equal true + + testCase "Array.zeroCreate respects --typedArrays" <| fun () -> + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Int8Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Uint8Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Int16Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Uint16Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Int32Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Uint32Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "BigInt64Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "BigUint64Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Float32Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Float64Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs "Array" |> equal true + Array.zeroCreate 2 |> equal [| 0L; 0L |] + Array.zeroCreate 2 |> equal [| 0.; 0. |] + Array.zeroCreate 2 |> equal [| 0M; 0M |] + + testCase "Array module functions respect --typedArrays" <| fun () -> + let xs = Array.init 3 int8 + xs |> jsConstructorIs (typedArrayOr "Int8Array") |> equal true + xs |> equal [| 0y; 1y; 2y |] + let xs = Array.create 2 7us + xs |> jsConstructorIs (typedArrayOr "Uint16Array") |> equal true + xs |> equal [| 7us; 7us |] + let xs = Array.ofSeq (seq { 1L; 2L }) + xs |> jsConstructorIs (typedArrayOr "BigInt64Array") |> equal true + xs |> Array.sum |> equal 3L + let xs = Array.ofSeq (seq { 1UL; 2UL }) + xs |> jsConstructorIs (typedArrayOr "BigUint64Array") |> equal true + xs |> Array.sum |> equal 3UL + let xs = [| 1; 2 |] |> Array.map float32 + xs |> jsConstructorIs (typedArrayOr "Float32Array") |> equal true + xs |> equal [| 1.f; 2.f |] + let xs = [| 1.5; 2.5 |] |> Array.map string + xs |> jsConstructorIs "Array" |> equal true + xs |> equal [| "1.5"; "2.5" |] + + testCase "Byte arrays respect --typedArrays" <| fun () -> + let xs = System.Text.Encoding.UTF8.GetBytes("ab") + xs |> jsConstructorIs (typedArrayOr "Uint8Array") |> equal true + xs |> equal [| 97uy; 98uy |] + let xs = System.BitConverter.GetBytes(1s) + xs |> jsConstructorIs (typedArrayOr "Uint8Array") |> equal true + xs |> equal [| 1uy; 0uy |] +#endif + + testCase "Array.zeroCreate and Array.create evaluate the count once" <| fun () -> + let mutable calls = 0 + let size () = + calls <- calls + 1 + 2 + Array.zeroCreate (size ()) |> equal [| 0M; 0M |] + calls |> equal 1 + Array.create (size ()) "a" |> equal [| "a"; "a" |] + calls |> equal 2 + testCase "Mapping from values to functions works" <| fun () -> let a = [| "a"; "b"; "c" |] let b = [| 1; 2; 3 |] @@ -165,6 +240,21 @@ let tests = a.[3..3] <- [|999|] a |> equal [|999; 2; 3; 999; 5; 6|] + testCase "Array slices clamp the bounds like .NET" <| fun () -> // See #5164 + let xs = [| 1; 2; 3 |] + xs.[-1..1] |> equal [| 1; 2 |] + xs.[-5..] |> equal [| 1; 2; 3 |] + xs.[1..5] |> equal [| 2; 3 |] + xs.[..5] |> equal [| 1; 2; 3 |] + xs.[.. -2] |> equal [||] + xs.[-5.. -1] |> equal [||] + xs.[5..6] |> equal [||] + xs.[2..1] |> equal [||] + let lower, upper = -1, 1 + xs.[lower..upper] |> equal [| 1; 2 |] + let strings = [| "a"; "b"; "c" |] + strings.[-1..1] |> equal [| "a"; "b" |] + testCase "Array literals work" <| fun () -> let x = [| 1; 2; 3; 4; 5 |] equal 5 x.Length @@ -206,6 +296,42 @@ let tests = let b = [| |] throwsAnyError (fun () -> b.[0]) + testCase "Array indexer evaluates the array once" <| fun () -> + let mutable calls = 0 + let size () = + calls <- calls + 1 + 3 + (Array.zeroCreate (size ())).[1] |> equal 0 + calls |> equal 1 + throwsAnyError (fun () -> (Array.zeroCreate (size ())).[5]) + calls |> equal 2 + + testCase "Array getter returns None for an in-bounds option element" <| fun () -> + let xs = [| Some 1; None; Some 3 |] + Array.get xs 1 |> equal None + xs[1] |> equal None + xs.[1] |> equal None + Array.item 1 xs |> equal None + xs[2] |> equal (Some 3) + throwsAnyError (fun () -> xs[3]) + + testCase "Array getter evaluates the array and the index once" <| fun () -> + let mutable calls = 0 + let xs = [| 10; 20; 30 |] + let next () = + calls <- calls + 1 + calls + xs.[next ()] |> equal 20 + calls |> equal 1 + let ys = ResizeArray [ "a"; "b" ] + let pick () = + calls <- calls + 1 + ys + (pick ()).[1] |> equal "b" + calls |> equal 2 + throwsAnyError (fun () -> xs.[next ()]) + calls |> equal 3 + testCase "Array setter works" <| fun () -> let x = [| 1.; 2.; 3.; 4.; 5. |] Array.set x 3 10. @@ -347,6 +473,14 @@ let tests = Array.blit xs 3 ys 5 4 ys.[5] + ys.[6] + ys.[7] + ys.[8] |> equal "defg" + testCase "Array.blit works with overlapping ranges in the same array" <| fun () -> + let xs = [| "a"; "b"; "c"; "d"; "e" |] + Array.blit xs 0 xs 1 3 + xs |> equal [| "a"; "a"; "b"; "c"; "e" |] + let ys = [| "a"; "b"; "c"; "d"; "e" |] + Array.blit ys 1 ys 0 3 + ys |> equal [| "b"; "c"; "d"; "d"; "e" |] + #if FABLE_COMPILER testCase "Array.blit works with deceiving typed arrays" <| fun () -> let xs = Fable.Core.JsInterop.emitJsExpr () "[1,2,3,4,5,6,7,8,9,10]" @@ -563,6 +697,133 @@ let tests = System.Array.IndexOf(myArray, Dog 3, 0, 1) |> equal -1 System.Array.IndexOf(myArray, Duck 5, 1) |> equal -1 + testCase "System.Array.BinarySearch works" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 0) |> equal -1 + System.Array.BinarySearch(xs, 1) |> equal 0 + System.Array.BinarySearch(xs, 2) |> equal -2 + System.Array.BinarySearch(xs, 7) |> equal 3 + System.Array.BinarySearch(xs, 8) |> equal -5 + System.Array.BinarySearch(xs, 10) |> equal -6 + System.Array.BinarySearch(([||]: int[]), 3) |> equal -1 + + testCase "System.Array.BinarySearch works with duplicates" <| fun () -> + System.Array.BinarySearch([|2; 2; 2; 2; 2; 2; 2; 2|], 2) |> equal 3 + System.Array.BinarySearch([|1; 2; 2; 2; 2; 3; 4; 5; 6|], 2) |> equal 4 + + testCase "System.Array.BinarySearch works with a range" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2) |> equal -2 + System.Array.BinarySearch(xs, 1, 3, 9) |> equal -5 + System.Array.BinarySearch(xs, 1, 2, 9) |> equal -4 + System.Array.BinarySearch(xs, 2, 0, 5) |> equal -3 + System.Array.BinarySearch(xs, 5, 0, 5) |> equal -6 + + testCase "System.Array.BinarySearch works with a comparer" <| fun () -> + let desc = ComparisonIdentity.FromFunction(fun (a: int) b -> compare b a) + let ys = [|9; 7; 5; 3; 1|] + System.Array.BinarySearch(ys, 3, desc) |> equal 3 + System.Array.BinarySearch(ys, 4, desc) |> equal -4 + System.Array.BinarySearch(ys, 1, 3, 3, desc) |> equal 3 + System.Array.BinarySearch([|1; 3; 5; 7; 9|], 7, (null: IComparer)) |> equal 3 + + testCase "System.Array.BinarySearch passes the array element to the comparer first" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 9, logging) |> equal 4 + List.ofSeq calls |> equal [(5, 9); (7, 9); (9, 9)] + calls.Clear() + System.Array.BinarySearch(xs, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (1, 4); (3, 4)] + + testCase "System.Array.BinarySearch works with other element types" <| fun () -> + let strings = [|"apple"; "banana"; "cherry"|] + System.Array.BinarySearch(strings, "banana") |> equal 1 + System.Array.BinarySearch(strings, "blue") |> equal -3 + let floats = [|1.5; 2.5; 3.5|] + System.Array.BinarySearch(floats, 2.5) |> equal 1 + System.Array.BinarySearch(floats, 3.0) |> equal -3 + let longs = [|1L; 5L; 10L|] + System.Array.BinarySearch(longs, 10L) |> equal 2 + System.Array.BinarySearch(longs, 6L) |> equal -3 + let things = [|{ MainThing = 1; OtherThing = "x" }; { MainThing = 2; OtherThing = "a" }; { MainThing = 2; OtherThing = "b" }|] + System.Array.BinarySearch(things, { MainThing = 2; OtherThing = "b" }) |> equal 2 + System.Array.BinarySearch(things, { MainThing = 1; OtherThing = "z" }) |> equal -2 + let animals = [|Duck 1; Duck 4; Dog 0|] + System.Array.BinarySearch(animals, Dog 0) |> equal 2 + System.Array.BinarySearch(animals, Duck 2) |> equal -2 + + testCase "System.Array.BinarySearch works with a range and a null comparer" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7, null) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2, null) |> equal -2 + + testCase "System.Array.BinarySearch works with a single element and a large array" <| fun () -> + System.Array.BinarySearch([|5|], 5) |> equal 0 + System.Array.BinarySearch([|5|], 3) |> equal -1 + System.Array.BinarySearch([|5|], 7) |> equal -2 + let big = Array.init 1000 (fun i -> i * 2) + System.Array.BinarySearch(big, 998) |> equal 499 + System.Array.BinarySearch(big, 999) |> equal -501 + + testCase "System.Array.BinarySearch works with more element types" <| fun () -> + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 3uy) |> equal 1 + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 4uy) |> equal -3 + System.Array.BinarySearch([|-5s; 0s; 5s|], 0s) |> equal 1 + System.Array.BinarySearch([|-5s; 0s; 5s|], -6s) |> equal -1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'c') |> equal 1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'd') |> equal -3 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "b")) |> equal 1 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "c")) |> equal -3 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 2.5M) |> equal 1 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 3.0M) |> equal -3 + let dates = [|System.DateTime(2020, 1, 1); System.DateTime(2021, 1, 1)|] + System.Array.BinarySearch(dates, System.DateTime(2021, 1, 1)) |> equal 1 + System.Array.BinarySearch(dates, System.DateTime(2020, 6, 1)) |> equal -2 + System.Array.BinarySearch([|1I; 5I; 10I|], 5I) |> equal 1 + System.Array.BinarySearch([|1I; 5I; 10I|], 11I) |> equal -4 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 1) |> equal 1 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 2) |> equal -3 + + testCase "System.Array.BinarySearch with a range passes the array element to the comparer first" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 1, 3, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (3, 4)] + + testCase "System.Array.BinarySearch non-generic overloads work" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs :> System.Array, box 7) |> equal 3 + System.Array.BinarySearch(xs :> System.Array, box 4) |> equal -3 + System.Array.BinarySearch(xs :> System.Array, 1, 3, box 5) |> equal 2 + + testCase "System.Array.BinarySearch with a range checks its arguments" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> System.Array.BinarySearch(xs, -1, 2, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, -1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 3, 3, 5)) |> equal "ArgumentException" + raised (fun () -> System.Array.BinarySearch(xs, 6, 0, 5)) |> equal "ArgumentException" + testCase "Array.findIndex works" <| fun () -> let xs = [|1.f; 2.f; 3.f; 4.f|] xs |> Array.findIndex ((=) 2.f) @@ -1200,6 +1461,14 @@ let tests = System.Array.Copy(source, 1, destination, 1, 2) equal [| "a"; "xx"; "xyz" |] destination + testCase "System.Array.Copy works with Int64 indexes and length" <| fun () -> + let source = [| "xy"; "xx"; "xyz" |] + let destination = [| "a"; "b"; "c"; "d" |] + System.Array.Copy(source, destination, 3L) + equal [| "xy"; "xx"; "xyz"; "d" |] destination + System.Array.Copy(source, 1L, destination, 2L, 2L) + equal [| "xy"; "xx"; "xx"; "xyz" |] destination + testCase "Array.splitInto works" <| fun () -> [|1..10|] |> Array.splitInto 3 |> equal [| [|1..4|]; [|5..7|]; [|8..10|] |] [|1..11|] |> Array.splitInto 3 |> equal [| [|1..4|]; [|5..8|]; [|9..11|] |] @@ -1434,6 +1703,64 @@ let tests = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) + testCase "Array.randomShuffleWith gives .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + Array.randomShuffleWith rng [|0 .. 4|] |> equal [|3; 1; 2; 4; 0|] + rng.Next() |> equal 361709742 + let rng = System.Random(12345) + Array.randomShuffleWith rng [|0 .. 9|] |> equal [|0; 1; 8; 6; 2; 9; 3; 5; 4; 7|] + rng.Next() |> equal 1086637143 + let rng = System.Random(42) + let xs = [|0 .. 9|] + Array.randomShuffleInPlaceWith rng xs + xs |> equal [|6; 2; 3; 0; 5; 1; 8; 4; 7; 9|] + rng.Next() |> equal 1634773126 + + testCase "Array.randomShuffleBy gives .NET's results" <| fun () -> + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + + testCase "Array.randomSampleWith gives .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + Array.randomSampleWith rng 3 [|0 .. 4|] |> equal [|3; 0; 4|] + rng.Next() |> equal 1122627734 + let rng = System.Random(12345) + Array.randomSampleWith rng 10 [|0 .. 29|] |> equal [|2; 29; 21; 13; 20; 25; 3; 16; 5; 10|] + rng.Next() |> equal 494509053 + let rng = System.Random(42) + Array.randomSampleWith rng 10 [|0 .. 99|] |> equal [|66; 14; 12; 52; 16; 26; 72; 51; 17; 76|] + rng.Next() |> equal 503774878 + // Draws again on a repeated index + let rng = System.Random(7) + Array.randomSampleWith rng 5 [|0 .. 21|] |> equal [|8; 19; 14; 1; 0|] + rng.Next() |> equal 2048632168 + + testCase "Array.randomSampleBy gives .NET's results" <| fun () -> + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + + testCase "Array.randomChoicesWith gives .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + Array.randomChoicesWith rng 4 [|0 .. 9|] |> equal [|6; 1; 1; 5|] + rng.Next() |> equal 361709742 + + testCase "Array random functions give FSharp.Core's error messages" <| fun () -> + let nanMessage = "The index is outside the legal range.\nrandomizer returned NaN, should be in range [0.0, 1.0). (Parameter 'randomizer')" + throwsError nanMessage (fun () -> Array.randomShuffleBy (fun () -> nan) [|1; 2|]) + throwsError nanMessage (fun () -> Array.randomShuffleInPlaceBy (fun () -> nan) [|1; 2|]) + throwsError nanMessage (fun () -> Array.randomChoiceBy (fun () -> nan) [|1; 2|]) + throwsError nanMessage (fun () -> Array.randomChoicesBy (fun () -> nan) 2 [|1; 2|]) + throwsError nanMessage (fun () -> Array.randomSampleBy (fun () -> nan) 1 [|1; 2|]) + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> nan) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + throwsError "The index is outside the legal range.\nrandomizer returned 1, should be in range [0.0, 1.0). (Parameter 'randomizer')" (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) + throwsError "The input array was empty. (Parameter 'source')" (fun () -> Array.randomChoiceWith (System.Random(1)) ([||]: int[])) + throwsError "The input sequence was empty. (Parameter 'source')" (fun () -> Array.randomSampleWith (System.Random(1)) 1 ([||]: int[])) + throwsError "The input must be non-negative.\ncount = -1 (Parameter 'count')" (fun () -> Array.randomChoicesWith (System.Random(1)) -1 [|1|]) + throwsError "The input must be non-negative.\ncount = -1 (Parameter 'count')" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + testCase "Array.compareWith works" <| fun () -> // See #2961 let a = [|1;3|] let b = [|1;2;3|] @@ -1526,4 +1853,127 @@ let tests = coll.CopyTo(ys, 0) ys = xs |> equal true + testCase "Array functions raise ArgumentException on empty arrays" <| fun () -> + let xs: int[] = [||] + let ys: string[] = [||] + raisedExceptionType (fun () -> Array.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduce (+) ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduceBack (+) ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.Parallel.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.max ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.maxBy id ys) |> equal "ArgumentException" + + testCase "Array functions raise FSharp.Core's exception types for out of range counts" <| fun () -> + raisedExceptionType (fun () -> Array.take 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.take -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.skip 5 [|1|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.splitAt 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.splitAt -1 [|1|]) |> equal "ArgumentException" + + testCase "Array functions raise ArgumentException for arrays of different lengths" <| fun () -> + let xs = [|1|] + let ys = [|1; 2|] + raisedExceptionType (fun () -> Array.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.zip3 xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.mapi2 (fun i x y -> i + x + y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map3 (fun x y z -> x + y + z) xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iter2 (fun _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iteri2 (fun _ _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.foldBack2 (fun x y acc -> x + y + acc) xs ys 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (fun x y -> x = y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.exists2 (fun x y -> x = y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [| xs; ys |]) |> equal "ArgumentException" + + testCase "Array functions raise KeyNotFoundException when nothing satisfies the predicate" <| fun () -> + let xs = [|1; 2|] + raisedExceptionType (fun () -> Array.find (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findBack (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndex (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndexBack (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.pick (fun x -> if x > 2 then Some x else None) xs) |> equal "KeyNotFoundException" + + testCase "Array functions raise FSharp.Core's exception types for invalid arguments" <| fun () -> + raisedExceptionType (fun () -> Array.windowed 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun i -> i + 1) [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun _ -> 0) [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomShuffleInPlaceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomShuffleBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 2 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + + testCase "Array.sub raises FSharp.Core's exception types for out of range arguments" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.sub xs -1 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub xs 1 -1) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub xs 2 5) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub xs 4 0) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub [|"a"; "b"; "c"|] 3 1) |> equal "ArgumentOutOfRangeException" + Array.sub xs 3 0 |> equal [||] + + testCase "Array.fill raises FSharp.Core's exception types for out of range arguments" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.fill xs -1 2 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fill xs 0 -1 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fill xs 3 1 0) |> equal "IndexOutOfRangeException" + raisedExceptionType (fun () -> Array.fill [|"a"; "b"; "c"|] 2 5 "z") |> equal "IndexOutOfRangeException" + xs |> equal [|1; 2; 3|] + Array.fill xs 4 0 0 + xs |> equal [|1; 2; 3|] + + testCase "Array.blit raises System.Array.Copy's exception types for out of range arguments" <| fun () -> + let xs = [|1; 2; 3|] + let ys: int[] = Array.zeroCreate 5 + raisedExceptionType (fun () -> Array.blit xs 0 ys 0 -1) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit xs -1 ys 0 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit xs 0 ys -1 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit xs 2 ys 0 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit xs 0 ys 4 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit xs 4 ys 0 0) |> equal "ArgumentException" + ys |> equal [|0; 0; 0; 0; 0|] + let zs = [|"a"; "b"; "c"|] + let ws = Array.create 5 "_" + raisedExceptionType (fun () -> Array.blit zs 0 ws 0 -1) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit zs -1 ws 0 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit zs 0 ws -1 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit zs 2 ws 0 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit zs 0 ws 4 2) |> equal "ArgumentException" + ws |> equal [|"_"; "_"; "_"; "_"; "_"|] + Array.blit zs 3 ws 5 0 + Array.blit zs 1 ws 3 2 + ws |> equal [|"_"; "_"; "_"; "b"; "c"|] + + testCase "System.Array.Copy raises .NET's exception types for out of range arguments" <| fun () -> + let xs = [|1; 2; 3|] + let ys: int[] = Array.zeroCreate 5 + raisedExceptionType (fun () -> System.Array.Copy(xs, 0, ys, 0, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> System.Array.Copy(xs, -1, ys, 0, 2)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> System.Array.Copy(xs, 0, ys, -1, 2)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> System.Array.Copy(xs, 2, ys, 0, 2)) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy(xs, 0, ys, 4, 2)) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy(xs, ys, 4)) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy([|"a"; "b"|], [|"c"|], 2)) |> equal "ArgumentException" + ys |> equal [|0; 0; 0; 0; 0|] + + testCase "Array.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [|1; 2; 3|] + let empty: int[] = [||] + raisedExceptionType (fun () -> Array.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 1 3 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 3 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 4 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 0 1 empty) |> equal "ArgumentException" + Array.removeManyAt 0 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 1 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 3 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 0 0 empty |> equal [||] + Array.removeManyAt 1 2 xs |> equal [|1|] + Array.removeManyAt 0 2 [|"a"; "b"; "c"|] |> equal [|"c"|] + ] diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index 761d32daa4..bcf3274a14 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -1,11 +1,163 @@ module Fable.Tests.Async open System +open System.Threading open Util open Util.Testing +let private runOffThreadPool action = +#if FABLE_COMPILER + action () +#else + // .NET reschedules deferred FromContinuations callbacks invoked on a pool thread. + // Use a dedicated thread so the probe can observe completion and thrown callbacks. + let mutable failure = None + let thread = Thread(ThreadStart(fun () -> + try action () + with error -> failure <- Some error)) + thread.Start() + thread.Join() + failure |> Option.iter raise +#endif + +let private observeTryCancelled token (work: Async) = + let calls = ResizeArray() + Async.StartWithContinuations( + work, + (fun value -> calls.Add(string value)), + (fun error -> calls.Add("error: " + error.Message)), + (fun error -> calls.Add("cancel: " + error.Message)), + token) + calls + +module private FinalizerRegressions = + open System + open System.Threading + + let private check expected actual = + if expected <> actual then failwithf "Expected %A, got %A" expected actual + + let private observe (token: CancellationToken) work = + let terminal = ResizeArray() + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun e -> terminal.Add e.Message), (fun _ -> terminal.Add "cancel"), token) + terminal + + let preCancelled () = + for throws in [false; true] do + use source = new CancellationTokenSource() + source.Cancel() + let mutable cleanups = 0 + let mutable entered = false + let work = async.TryFinally(async { entered <- true }, fun () -> + cleanups <- cleanups + 1 + if throws then failwith "cleanup") + let terminal = observe source.Token work + check false entered + check 1 cleanups + check [|"cancel"|] (terminal.ToArray()) + + let nestedPreCancelled () = + use source = new CancellationTokenSource() + source.Cancel() + let cleanups = ResizeArray() + let inner = async.TryFinally(async.Return 42, fun () -> cleanups.Add "inner") + let outer = async.TryFinally(inner, fun () -> cleanups.Add "outer") + let terminal = observe source.Token outer + check [|"outer"|] (cleanups.ToArray()) + check [|"cancel"|] (terminal.ToArray()) + + let cancelledAcquisition () = + for acquisition in [0; 1; 2] do + use source = new CancellationTokenSource() + let mutable disposals = 0 + let mutable entered = false + let resource = { new IDisposable with member _.Dispose() = disposals <- disposals + 1 } + let acquire = Async.FromContinuations(fun (ok, _, _) -> + source.Cancel() + ok resource) + let work = + if acquisition = 1 then async { + use! owned = acquire + entered <- true + } + elif acquisition = 0 then async { + let! acquired = acquire + use owned = acquired + entered <- true + } + else async { + use owned = + source.Cancel() + resource + entered <- true + } + let terminal = observe source.Token work + check false entered + check 1 disposals + check [|"cancel"|] (terminal.ToArray()) + + let normalAndFailure () = + for fails in [false; true] do + let mutable disposals = 0 + let resource = { new IDisposable with member _.Dispose() = disposals <- disposals + 1 } + let work = async { + use owned = resource + if fails then failwith "body" + return 42 + } + let terminal = observe CancellationToken.None work + check 1 disposals + check [|if fails then "body" else "success"|] (terminal.ToArray()) + + let trampoline () = async { + let mutable cleanups = 0 + for _ in 1 .. 10000 do + do! async.TryFinally(async { return () }, fun () -> cleanups <- cleanups + 1) + check 10000 cleanups + } + + let downstreamExceptions () = + for channel in ["success"; "error"; "cancel"] do + let mutable cleanups = 0 + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (ok, error, cancelled) -> + match channel with + | "success" -> ok () + | "error" -> error (Exception "body") + | _ -> cancelled (OperationCanceledException())) + let terminal name = calls.Add name; failwith "downstream" + let caught = + try + Async.StartWithContinuations( + async.TryFinally(body, fun () -> cleanups <- cleanups + 1), + (fun () -> terminal "success"), (fun _ -> terminal "error"), (fun _ -> terminal "cancel")) + "no exception" + with e -> e.Message + check "downstream" caught + check 1 cleanups + check [|channel|] (calls.ToArray()) + #if FABLE_COMPILER open Fable.Core + +type private Deferred = + [] + static member startDeferred(computation: Async, token: System.Threading.CancellationToken, onSuccess: int -> unit, onError: exn -> unit, onCancel: exn -> unit): unit -> unit = jsNative + + [] + static member handlerCount(event: Event): int = jsNative + +module private Runtime = + open Fable.Core.JsInterop + + type Timer = + abstract retired: int + abstract invalidHandles: int + abstract fire: unit -> unit + + let withTimers (action: Timer -> unit): unit = importMember "./js/async-runtime.js" + let listenerCount (token: System.Threading.CancellationToken): int = importMember "./js/async-runtime.js" + let cancellationErrorCount (error: exn): int = importMember "./js/async-runtime.js" #endif type DisposableAction(f) = @@ -38,12 +190,406 @@ let asyncMap f a = async { return f a } +let private fromGenericCallback (callback: ('T -> unit) -> (exn -> unit) -> (OperationCanceledException -> unit) -> unit) : Async<'T> = + Async.FromContinuations(fun (ok, error, cancelled) -> callback ok error cancelled) + let tests = testList "Async" [ + testCase "TryCancelled preserves success and ordinary errors" <| fun () -> + let mutable compensations = 0 + let wrap work = Async.TryCancelled(work, fun _ -> compensations <- compensations + 1) + equal [|"42"|] ((observeTryCancelled CancellationToken.None (wrap (async.Return 42))).ToArray()) + for error in [Exception "body"; OperationCanceledException "thrown"] do + let work: Async = async { return raise error } + equal [|"error: " + error.Message|] ((observeTryCancelled CancellationToken.None (wrap work)).ToArray()) + let mutable completed = false + Async.StartWithContinuations( + Async.TryCancelled(async.Return (), fun _ -> compensations <- compensations + 1), + (fun () -> completed <- true), raise, raise) + equal true completed + equal 0 compensations + + testCase "TryCancelled preserves cancellation identity and supports reuse" <| fun () -> + for throws in [false; true] do + let original = OperationCanceledException "original" + let calls = ResizeArray() + let mutable handlerSawOriginal = true + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel original) + let work = Async.TryCancelled(body, fun error -> + handlerSawOriginal <- handlerSawOriginal && obj.ReferenceEquals(original, error) + calls.Add "compensation" + if throws then failwith "compensation defect") + for _ in 1 .. 2 do + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), + (fun _ -> failwith "unexpected error"), + (fun error -> + equal true (obj.ReferenceEquals(original, error)) + calls.Add "cancel")) + equal [|"compensation"; "cancel"; "compensation"; "cancel"|] (calls.ToArray()) + equal true handlerSawOriginal + + testCase "TryCancelled installs nested handlers before cancellation" <| fun () -> + use source = new CancellationTokenSource() + source.Cancel() + let mutable entered = false + let calls = ResizeArray() + let body = async { entered <- true; return 42 } + let inner = Async.TryCancelled(body, fun _ -> calls.Add "inner") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer") + Async.StartWithContinuations(outer, + (fun _ -> failwith "unexpected success"), raise, + (fun _ -> calls.Add "cancel"), source.Token) + equal false entered + equal [|"inner"; "outer"; "cancel"|] (calls.ToArray()) + + testCase "TryCancelled handles deferred cancellation" <| fun () -> + runOffThreadPool (fun () -> + let original = OperationCanceledException "deferred" + let mutable cancel = fun (_: OperationCanceledException) -> failwith "not started" + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, continuation) -> cancel <- continuation) + let work = Async.TryCancelled(body, fun _ -> calls.Add "compensation") + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), raise, + (fun error -> equal true (obj.ReferenceEquals(original, error)); calls.Add "cancel")) + equal 0 calls.Count + cancel original + equal [|"compensation"; "cancel"|] (calls.ToArray())) + + testCase "TryCancelled preserves finalizer order when handlers throw" <| fun () -> + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel (OperationCanceledException "original")) + let finalized = async.TryFinally(body, fun () -> calls.Add "finally") + let inner = Async.TryCancelled(finalized, fun _ -> calls.Add "inner"; failwith "inner defect") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer"; failwith "outer defect") + equal [|"cancel: original"|] ((observeTryCancelled CancellationToken.None outer).ToArray()) + equal [|"finally"; "inner"; "outer"|] (calls.ToArray()) + + testCase "TryCancelled propagates downstream errors once" <| fun () -> + runOffThreadPool (fun () -> + for channel in ["success"; "error"; "cancel"] do + let mutable compensations = 0 + let calls = ResizeArray() + let mutable complete = fun () -> failwith "not started" + let body: Async = Async.FromContinuations(fun (ok, error, cancel) -> + complete <- fun () -> + match channel with + | "success" -> ok 42 + | "error" -> error (Exception "body") + | _ -> cancel (OperationCanceledException "original")) + let work = Async.TryCancelled(body, fun _ -> compensations <- compensations + 1) + let terminal name = calls.Add name; failwith "downstream" + Async.StartWithContinuations(work, + (fun _ -> terminal "success"), + (fun _ -> terminal "error"), + (fun _ -> terminal "cancel")) + let caught = + try + complete () + "no exception" + with error -> error.Message + equal "downstream" caught + equal [|channel|] (calls.ToArray()) + equal (if channel = "cancel" then 1 else 0) compensations) + + testCaseAsync "TryCancelled preserves bounded trampoline execution" <| fun () -> async { + let original = OperationCanceledException "deep cancellation" + let mutable compensations = 0 + let rec loop remaining = + if remaining = 0 then + Async.FromContinuations(fun (_, _, cancel) -> cancel original) + else + Async.TryCancelled(async { + do! async.Return () + return! loop (remaining - 1) + }, fun _ -> compensations <- compensations + 1) + do! Async.FromContinuations(fun (ok, error, _) -> + Async.StartWithContinuations(loop 10000, + (fun () -> error (Exception "unexpected success")), error, + (fun cancelled -> + if obj.ReferenceEquals(original, cancelled) then ok () + else error (Exception "cancellation identity changed")))) + equal 10000 compensations + } + + testCase "TryFinally installs cleanup before pre-cancellation" <| FinalizerRegressions.preCancelled + testCase "Pre-cancelled nested TryFinally only runs outer cleanup" <| FinalizerRegressions.nestedPreCancelled + testCase "Cancelled acquisition disposes use and use bang resources" <| FinalizerRegressions.cancelledAcquisition + testCase "Resource cleanup runs once on success and failure" <| FinalizerRegressions.normalAndFailure + testCase "TryFinally propagates downstream continuation errors once" <| FinalizerRegressions.downstreamExceptions + testCaseAsync "TryFinally preserves bounded trampoline execution" <| FinalizerRegressions.trampoline + + testCase "Generic FromContinuations accepts erased unit callbacks" <| fun () -> + let calls = ResizeArray() + let start work = Async.StartWithContinuations(work, (fun _ -> calls.Add "success"), (fun e -> calls.Add e.Message), (fun e -> calls.Add e.Message)) + let unitWork: Async = fromGenericCallback (fun ok _ _ -> ok ()) + start unitWork + let valueWork = fromGenericCallback (fun ok _ _ -> ok 42) + Async.StartWithContinuations(valueWork, (fun value -> equal 42 value; calls.Add "value"), raise, raise) + let errorWork: Async = fromGenericCallback (fun _ error _ -> error (exn "error")) + start errorWork + let cancelWork: Async = fromGenericCallback (fun _ _ cancelled -> cancelled (OperationCanceledException("cancel"))) + start cancelWork + equal [|"success"; "value"; "error"; "cancel"|] (calls.ToArray()) + + testCase "FromContinuations unit and generic values" <| fun () -> + let fromValue value = Async.FromContinuations(fun (ok, _, _) -> ok value) + let mutable completed = 0 + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok ()) + Async.StartWithContinuations(unitWork, (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 3 completed + + testCase "FromContinuations rejects a second completion" <| fun () -> + let unitCalls = ResizeArray() + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok (); ok ()) + throwsAnyError (fun () -> + Async.StartWithContinuations(unitWork, (fun () -> unitCalls.Add "success"), (fun _ -> unitCalls.Add "error"), (fun _ -> unitCalls.Add "cancel"))) + equal true (unitCalls.Count <= 1) + equal false (unitCalls.Contains "error") + equal false (unitCalls.Contains "cancel") + + let valueCalls = ResizeArray() + let valueWork = Async.FromContinuations(fun (ok, _, _) -> ok 42; ok 43) + throwsAnyError (fun () -> + Async.StartWithContinuations(valueWork, (fun v -> valueCalls.Add (string v)), (fun _ -> valueCalls.Add "error"), (fun _ -> valueCalls.Add "cancel"))) + equal true (valueCalls.Count <= 1) + equal false (valueCalls.Contains "43") + equal false (valueCalls.Contains "error") + equal false (valueCalls.Contains "cancel") + + testCase "Generic unit continuations work through recursive bindings and aliases" <| fun () -> + let mutable recurse = true + let rec fromValue value = + if recurse then + recurse <- false + fromValue value + else Async.FromContinuations(fun (ok, _, _) -> ok value) + let alias = fromValue + let mutable completed = 0 + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + recurse <- true + Async.StartWithContinuations(alias (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(alias 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 3 completed + + testCase "Generic unit continuations work through tuple pattern bindings" <| fun () -> + let fromValue, fromOther = + (fun (value: 'T) -> Async.FromContinuations(fun (ok, _, _) -> ok value)), + (fun (value: 'U) -> Async.FromContinuations(fun (ok, _, _) -> ok value)) + let mutable completed = 0 + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromOther (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromOther "a", (fun value -> equal "a" value; completed <- completed + 1), raise, raise) + equal 4 completed + + testCase "Generic unit continuations work inside nested lambdas and object expressions" <| fun () -> + let fromValue value = Async.FromContinuations(fun (ok, _, _) -> ok value) + let mutable completed = 0 + let startUnit () = Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + let disposable = + { new IDisposable with + member _.Dispose() = + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) } + startUnit () + startUnit () + disposable.Dispose() + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 4 completed + + testCase "Unit continuations preserve captured callbacks" <| fun () -> + let mutable calls = 0 + let rec genericDeadline callback value remaining = + if remaining > 0 then genericDeadline callback value (remaining - 1) + else Async.StartWithContinuations(async { return value }, (fun result -> callback result), raise, raise) + let rec unitDeadline (callback: unit -> unit) remaining = + if remaining > 0 then unitDeadline callback (remaining - 1) + else Async.StartWithContinuations(async { return () }, (fun () -> callback ()), raise, raise) + genericDeadline (fun () -> calls <- calls + 1) () 2 + unitDeadline (fun () -> calls <- calls + 1) 2 + equal 2 calls + + testCase "Finalizer failure overrides success once" <| fun () -> + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! successWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun error -> terminal.Add error.Message), (fun _ -> terminal.Add "cancel")) + equal [| "finalizer" |] (terminal.ToArray()) + equal 1 finalizers + + testCase "Finalizer failure overrides body failure once" <| fun () -> + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! errorWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun error -> terminal.Add error.Message), (fun _ -> terminal.Add "cancel")) + equal [| "finalizer" |] (terminal.ToArray()) + equal 1 finalizers + + testCase "Finalizer failure preserves cancellation once" <| fun () -> + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! cancelWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun _ -> terminal.Add "error"), (fun _ -> terminal.Add "cancel")) + equal [| "cancel" |] (terminal.ToArray()) + equal 1 finalizers + + testCase "Cancellation registrations dispose pending callbacks and invoke late callbacks" <| fun () -> + use source = new System.Threading.CancellationTokenSource() + let calls = ResizeArray() + let first: IDisposable = source.Token.Register(fun () -> calls.Add 1) + let mutable second: IDisposable = null + second <- source.Token.Register(fun () -> + second.Dispose() + first.Dispose() + calls.Add 2) + source.Cancel() + let late: IDisposable = source.Token.Register((fun state -> calls.Add (unbox state)), box 0) + late.Dispose() + second.Dispose() + equal [| 2; 0 |] (calls.ToArray()) +#if FABLE_COMPILER + equal 0 (Runtime.listenerCount source.Token) +#endif + + testCase "Cancellation callback failure preserves remaining listeners" <| fun () -> + use source = new System.Threading.CancellationTokenSource() + let mutable remaining = 0 + let mutable errors = -1 + source.Token.Register(fun () -> remaining <- remaining + 1) |> ignore + source.Token.Register(fun () -> failwith "callback") |> ignore + try source.Cancel() + with error -> +#if FABLE_COMPILER + errors <- Runtime.cancellationErrorCount error +#else + errors <- (error :?> AggregateException).InnerExceptions.Count +#endif + equal 1 errors + equal 1 remaining +#if FABLE_COMPILER + equal 0 (Runtime.listenerCount source.Token) +#endif + +#if FABLE_COMPILER + testCase "Sleep settles once and retires its timer and listener" <| fun () -> + for cancelFirst in [ true; false ] do + for throws in [ true; false ] do + Runtime.withTimers (fun timer -> + use source = new System.Threading.CancellationTokenSource() + let terminal = ResizeArray() + let mutable finalizers = 0 + let work = async { + try do! Async.Sleep 100 + finally + finalizers <- finalizers + 1 + if throws then failwith "finalizer" + } + Async.StartWithContinuations(work, (fun () -> terminal.Add "success"), (fun _ -> terminal.Add "error"), (fun _ -> terminal.Add "cancel"), source.Token) + if cancelFirst then + source.Cancel() + timer.fire() + else + timer.fire() + source.Cancel() + timer.fire() + let expected = if cancelFirst then "cancel" elif throws then "error" else "success" + equal [| expected |] (terminal.ToArray()) + equal 1 finalizers + equal 0 (Runtime.listenerCount source.Token) + equal 1 timer.retired + equal 0 timer.invalidHandles) + + testCase "Idle Receive is cancelled by its token and keeps later posts queued" <| fun () -> + use source = new System.Threading.CancellationTokenSource() + let mailbox = new MailboxProcessor((fun _ -> async { return () }), source.Token) + let terminal = ResizeArray() + Async.StartWithContinuations(mailbox.Receive(), (fun value -> terminal.Add (string value)), raise, (fun _ -> terminal.Add "cancel"), source.Token) + source.Cancel() + equal [| "cancel" |] (terminal.ToArray()) + equal 0 (Runtime.listenerCount source.Token) + mailbox.Post 42 + equal [| "cancel" |] (terminal.ToArray()) + Async.StartWithContinuations(mailbox.Receive(), (fun value -> terminal.Add $"next {value}"), raise, (fun _ -> terminal.Add "next cancel")) + equal [| "cancel"; "next 42" |] (terminal.ToArray()) +#endif + + testCaseAsync "Cancelling a waiting Receive reports cancel and keeps later posts queued" <| fun () -> async { + use source = new System.Threading.CancellationTokenSource() + let mailbox = new MailboxProcessor((fun _ -> async { return () }), source.Token) + let! outcome = + Async.FromContinuations(fun (ok, _, _) -> + Async.StartWithContinuations(mailbox.Receive(), (fun value -> ok $"value {value}"), (fun _ -> ok "error"), (fun _ -> ok "cancel"), source.Token) + source.Cancel()) + equal "cancel" outcome + mailbox.Post 42 + let! next = mailbox.Receive() + equal 42 next + } + + testCaseAsync "Cancelling the mailbox token cancels a body waiting in Receive" <| fun () -> async { + use source = new System.Threading.CancellationTokenSource() + let! outcome = + Async.FromContinuations(fun (ok, _, _) -> + MailboxProcessor.Start((fun inbox -> async { + let received = ref None + try + source.CancelAfter 50 + let! message = inbox.Receive() + received.Value <- Some message + finally + match received.Value with + | Some message -> ok $"received {message}" + | None -> ok "cancelled" }), source.Token) + |> ignore) + equal "cancelled" outcome + } + testCase "Simple async translates without exception" <| fun () -> async { return () } |> Async.StartImmediate + testCase "Cancellation callback errors expose the first InnerException" <| fun () -> + for count in [1; 2] do + use cts = new System.Threading.CancellationTokenSource() + let errors = Array.init count (fun i -> exn (string i)) + let mutable calls = 0 + errors |> Array.iter (fun error -> + cts.Token.Register(fun () -> + calls <- calls + 1 + raise error) |> ignore) + let caught = + try + cts.Cancel() + None + with ex -> Some ex + match caught with + | Some ex -> + equal errors[count - 1].Message ex.InnerException.Message + equal true (obj.ReferenceEquals(errors[count - 1], ex.InnerException)) + | None -> failwith "Expected cancellation callback errors" + equal count calls + equal true cts.IsCancellationRequested + cts.Cancel() + equal count calls + testCase "Async while binding works correctly" <| fun () -> let mutable result = 0 async { @@ -448,6 +994,34 @@ let tests = equal true ran } + testCaseAsync "MailboxProcessor second Reply is ignored" <| fun () -> + async { + let log = ResizeArray() + let completed = new MailboxProcessor(fun _ -> async { return () }) + let agent = MailboxProcessor>.Start(fun inbox -> async { + let! channel = inbox.Receive() + channel.Reply 1 + channel.Reply 2 + log.Add "body done" + completed.Post() }) + let! reply = agent.PostAndAsyncReply(id) + equal 1 reply + do! completed.Receive() + equal [|"body done"|] (log.ToArray()) + } + + testCaseAsync "MailboxProcessor reply computation can be awaited twice" <| fun () -> + async { + let agent = MailboxProcessor>.Start(fun inbox -> async { + let! channel = inbox.Receive() + channel.Reply 42 }) + let work = agent.PostAndAsyncReply(id) + let! first = work + let! second = work + equal 42 first + equal 42 second + } + testCase "Async try .. with returns correctly from 'with' branch" <| fun () -> let work = async { try @@ -684,6 +1258,21 @@ let tests = cts.Cancel() async { equal true cancelCalled } +#if FABLE_COMPILER + testCase "Async.AwaitEvent cancelled before its body runs does not subscribe" <| fun () -> + let ev = Event() + let cts = new System.Threading.CancellationTokenSource() + let calls = ResizeArray() + let resume = + Deferred.startDeferred(Async.AwaitEvent ev.Publish, cts.Token, + (fun v -> calls.Add $"success {v}"), (fun _ -> calls.Add "error"), (fun _ -> calls.Add "cancel")) + cts.Cancel() + resume () + ev.Trigger(42) + equal [ "cancel" ] (List.ofSeq calls) + equal 0 (Deferred.handlerCount ev) +#endif + testCase "Async try .. with does not run 'with' branch when body succeeds" <| fun () -> let work = async { try @@ -770,4 +1359,315 @@ let tests = do! Async.Sleep 600 equal 0 completed } + + testCaseAsync "Async.StartChild with timeout cancels the child when the timeout expires" <| fun () -> + async { + let mutable completed = false + let child = async { + do! Async.Sleep 300 + completed <- true + } + let mutable timedOut = false + try + let! childResult = Async.StartChild(child, 100) + do! childResult + with + | :? TimeoutException -> timedOut <- true + do! Async.Sleep 400 + equal true timedOut + equal false completed + } + + testCaseAsync "Async.StartChild with timeout leaves the parent running when the timeout expires" <| fun () -> + async { + let child = async { do! Async.Sleep 300 } + try + let! childResult = Async.StartChild(child, 100) + do! childResult + with + | :? TimeoutException -> () + let! token = Async.CancellationToken + equal false token.IsCancellationRequested + } + + testCaseAsync "Async.StartChild timeout only runs while the result is awaited" <| fun () -> + async { + let child = async { + do! Async.Sleep 150 + return 42 + } + let! childResult = Async.StartChild(child, 100) + do! Async.Sleep 250 + let! result = childResult + equal 42 result + } + + testCaseAsync "Async.StartChild with a timeout of -1 never times out" <| fun () -> + async { + let child = async { + do! Async.Sleep 50 + return 42 + } + let! childResult = Async.StartChild(child, -1) + let! result = childResult + equal 42 result + } + + testCaseAsync "Async.StartChild with a timeout of 0 returns an available result" <| fun () -> + async { + let child = async { + do! Async.Sleep 50 + return 42 + } + let! childResult = Async.StartChild(child, 0) + do! Async.Sleep 200 + let! result = childResult + equal 42 result + } + + testCaseAsync "Async.StartChild with a timeout of 0 raises when the result isn't available and leaves the child running" <| fun () -> + async { + let mutable completed = false + let child = async { + do! Async.Sleep 300 + completed <- true + } + let mutable timedOut = false + try + let! childResult = Async.StartChild(child, 0) + do! childResult + with + | :? TimeoutException -> timedOut <- true + do! Async.Sleep 400 + equal true timedOut + equal true completed + } + + testCaseAsync "Async.StartChild with timeout raises the child's cancellation callback failure" <| fun () -> + async { + let child = async { + let! token = Async.CancellationToken + token.Register(fun () -> failwith "cleanup") |> ignore + do! Async.Sleep 300 + } + let mutable errors = -1 + try + let! childResult = Async.StartChild(child, 100) + do! childResult + with error -> +#if FABLE_COMPILER + errors <- Runtime.cancellationErrorCount error +#else + errors <- (error :?> AggregateException).InnerExceptions.Count +#endif + equal 1 errors + } + + testCaseAsync "Async.OnCancel ignores a failing interruption" <| fun () -> + async { + let mutable interrupted = false + let mutable cancelled = false + let cts = new CancellationTokenSource() + Async.StartWithContinuations( + async { + use! _ = Async.OnCancel(fun () -> + interrupted <- true + failwith "interruption") + do! Async.Sleep 300 + }, + ignore, + ignore, + (fun _ -> cancelled <- true), + cts.Token) + do! Async.Sleep 50 + cts.Cancel() + do! Async.Sleep 50 + equal true interrupted + equal true cancelled + } + testCaseAsync "Async.Parallel respects maxDegreeOfParallelism" <| fun () -> + async { + let lockObj = obj () + let mutable running = 0 + let mutable peak = 0 + let work i = async { + lock lockObj (fun () -> + running <- running + 1 + peak <- max peak running) + do! Async.Sleep 50 + lock lockObj (fun () -> running <- running - 1) + return i + } + let! results = Async.Parallel([ for i in 1..10 -> work i ], maxDegreeOfParallelism = 3) + equal [| 1..10 |] results + equal 3 peak + } + + testCaseAsync "Async.Parallel cancels the other computations when one fails" <| fun () -> + async { + let mutable completed = 0 + let work i = async { + if i = 2 then + do! Async.Sleep 20 + failwith "boom" + do! Async.Sleep 300 +#if FABLE_COMPILER + completed <- completed + 1 +#else + System.Threading.Interlocked.Increment(&completed) |> ignore +#endif + } + let mutable error = "" + try + let! _ = Async.Parallel [ for i in 0..4 -> work i ] + () + with ex -> error <- ex.Message + do! Async.Sleep 400 + equal "boom" error + equal 0 completed + } + + testCaseAsync "Async.Parallel with an empty sequence returns an empty array" <| fun () -> + async { + let! results = Async.Parallel(([]: Async list), maxDegreeOfParallelism = 2) + equal 0 results.Length + } + + testCase "Async.Parallel rejects a maxDegreeOfParallelism below 1" <| fun () -> + let mutable raised = false + try + Async.Parallel([ async { return 1 } ], maxDegreeOfParallelism = 0) |> ignore + with :? ArgumentException -> raised <- true + equal true raised + + testCaseAsync "Async.Choice returns the first Some and cancels the others" <| fun () -> + async { + let mutable completed = false + let fast = async { + do! Async.Sleep 20 + return Some 1 + } + let slow = async { + do! Async.Sleep 300 + completed <- true + return Some 2 + } + let none = async { return None } + let! result = Async.Choice [ slow; none; fast ] + do! Async.Sleep 400 + equal (Some 1) result + equal false completed + } + + testCaseAsync "Async.Choice returns None when every computation does" <| fun () -> + async { + let none = async { + do! Async.Sleep 10 + return (None: int option) + } + let! result = Async.Choice [ none; none ] + equal None result + let! empty = Async.Choice ([]: Async list) + equal None empty + } + + testCaseAsync "Async.Choice raises when a computation fails" <| fun () -> + async { + let fails = async { + do! Async.Sleep 10 + return failwith "boom" + } + let slow = async { + do! Async.Sleep 300 + return Some 1 + } + let mutable error = "" + try + let! _ = Async.Choice [ slow; fails ] + () + with ex -> error <- ex.Message + equal "boom" error + } + + testCaseAsync "Async.OnCancel runs the interruption when cancelled" <| fun () -> + async { + let mutable interrupted = false + let cts = new CancellationTokenSource() + Async.StartImmediate(async { + use! _ = Async.OnCancel(fun () -> interrupted <- true) + do! Async.Sleep 300 + }, cts.Token) + do! Async.Sleep 50 + cts.Cancel() + do! Async.Sleep 50 + equal true interrupted + } + + testCaseAsync "Async.OnCancel doesn't run the interruption once disposed" <| fun () -> + async { + let mutable interrupted = false + let cts = new CancellationTokenSource() + Async.StartImmediate(async { + let! registration = Async.OnCancel(fun () -> interrupted <- true) + registration.Dispose() + do! Async.Sleep 300 + }, cts.Token) + do! Async.Sleep 50 + cts.Cancel() + do! Async.Sleep 50 + equal false interrupted + } + + testCaseAsync "Async.OnCancel doesn't run the interruption when disposed before the cancellation and again by use!" <| fun () -> + async { + let mutable interrupted = false + let cts = new CancellationTokenSource() + Async.StartImmediate(async { + use! registration = Async.OnCancel(fun () -> interrupted <- true) + registration.Dispose() + do! Async.Sleep 300 + }, cts.Token) + do! Async.Sleep 50 + cts.Cancel() + do! Async.Sleep 50 + equal false interrupted + } + + testCaseAsync "Async.StartAsTask works" <| fun () -> + async { + let t = Async.StartAsTask(async { + do! Async.Sleep 10 + return 42 + }) + let! result = Async.AwaitTask t + equal 42 result + } + + testCaseAsync "Async.StartAsTask passes the cancellation token" <| fun () -> + async { + let mutable completed = false + let cts = new CancellationTokenSource() + let t = Async.StartAsTask(async { + do! Async.Sleep 200 + completed <- true + }, cancellationToken = cts.Token) + cts.Cancel() + let mutable outcome = "pending" + // AwaitTask reports a cancelled task as an exception on .NET and as a cancellation in JS + Async.StartWithContinuations( + Async.AwaitTask t, + (fun () -> outcome <- "completed"), + (fun _ -> outcome <- "cancelled"), + (fun _ -> outcome <- "cancelled")) + do! Async.Sleep 300 + equal "cancelled" outcome + equal false completed + } + + testCaseAsync "Async.StartImmediateAsTask works" <| fun () -> + async { + let t = Async.StartImmediateAsTask(async { return 42 }) + let! result = Async.AwaitTask t + equal 42 result + } ] diff --git a/tests/Js/Main/ComparisonTests.fs b/tests/Js/Main/ComparisonTests.fs index a2f02317a9..0a165d26b2 100644 --- a/tests/Js/Main/ComparisonTests.fs +++ b/tests/Js/Main/ComparisonTests.fs @@ -39,6 +39,25 @@ exception Ex of int [] type RTest2 = { a2: int; b2: int } +[] +type RefEqUnion = RefC of int | RefD of int + +[] +type NoEqRecord = { a3: int } + +[] +type NoEqUnion = NoEqCase of int + +#nowarn "386" +[] +type NoEqRecordWithOverride = + { a4: int } + override _.Equals(_) = true + override _.GetHashCode() = 4 + +[] +type NoEqStruct = { s2: int } + type Test(i: int) = member x.Value = i override x.GetHashCode() = i @@ -115,6 +134,63 @@ let genericCompare (a: 'T) (b: 'T) : int = let tests = testList "Comparison" [ + testCase "ReferenceEquality record equality works" <| fun () -> + let x = { a2 = 1; b2 = 2 } + let y = { a2 = 1; b2 = 2 } + equal true (x = x) + equal false (x = y) + equal true (x <> y) + equal false (x.Equals y) + equal true (genericEquals x x) + equal false (genericEquals x y) + equal false ([ x ] = [ y ]) + equal false ((x, 1) = (y, 1)) + equal false (Some x = Some y) + equal true ([ x ] |> List.contains x) + equal false ([ x ] |> List.contains y) + + testCase "ReferenceEquality record hash works" <| fun () -> + let x = { a2 = 1; b2 = 2 } + let y = { a2 = 1; b2 = 2 } + equal (hash x) (hash x) + equal (genericHash x) (genericHash x) + let d = Dictionary() + d[x] <- 1 + equal true (d.ContainsKey x) + equal false (d.ContainsKey y) + equal 2 (HashSet([ x; y ]).Count) + + testCase "ReferenceEquality union equality works" <| fun () -> + let x = RefC 1 + let y = RefC 1 + equal true (x = x) + equal false (x = y) + equal false (x.Equals y) + equal true (genericEquals x x) + equal false (genericEquals x y) + equal false ([ x ] |> List.contains y) + equal 2 (HashSet([ x; y ]).Count) + + testCase "NoEquality record and union compare by reference" <| fun () -> + let x = { a3 = 1 } + equal true ((box x).Equals(box x)) + equal false ((box x).Equals(box { a3 = 1 })) + equal true (genericEquals x x) + equal false (genericEquals x { a3 = 1 }) + let u = NoEqCase 1 + equal true ((box u).Equals(box u)) + equal false ((box u).Equals(box (NoEqCase 1))) + equal true (genericEquals u u) + equal false (genericEquals u (NoEqCase 1)) + + testCase "NoEquality record keeps an explicit Equals override" <| fun () -> + equal true ((box { a4 = 1 }).Equals(box { a4 = 2 })) + equal 4 ((box { a4 = 1 }).GetHashCode()) + + testCase "NoEquality struct record compares structurally when boxed" <| fun () -> + equal true ((box { s2 = 1 }).Equals(box { s2 = 1 })) + equal false ((box { s2 = 1 }).Equals(box { s2 = 2 })) + testCase "Typed array equality works" <| fun () -> let xs1 = [| 1; 2; 3 |] let xs2 = [| 1; 2; 3 |] @@ -475,6 +551,12 @@ let tests = let s2: String = "hello" isNull s2 |> equal false +#if FABLE_COMPILER + testCase "isNull treats undefined as null" <| fun () -> + let s: String = unbox Fable.Core.JS.undefined + isNull s |> equal true +#endif + testCase "Classes must use identity hashing by default" <| fun () -> // See #2291 let x = MyClass(5) let y = MyClass(5) diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index b9d62e6c3e..933ab8df73 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -26,6 +26,20 @@ let parse f (a: string) = #endif +// The one-argument overload depends on the current culture on .NET, and the explicit style would trigger FABLE0103 on Fable +let tryParseFloat (s: string) = + Double.TryParse(s, NumberStyles.Float ||| NumberStyles.AllowThousands, CultureInfo.InvariantCulture) + +let private toPlainArray (bytes: byte[]) : byte[] = +#if FABLE_COMPILER + ResizeArray bytes |> unbox +#else + bytes +#endif + +// Computed, since the standalone compiler writes a -0. literal as 0 +let private negZero = 1. / Double.NegativeInfinity + let private adaptExponentValue (value: string) = value.ToLowerInvariant().Replace("e+00", "e+").Replace("e+0", "e+") @@ -82,6 +96,186 @@ let tests = tryParse Single.TryParse 0.0f "9SayWhat12Huh" |> equal (false, 0.0f) tryParse Single.TryParse 0.0f "-1.5" |> equal (true, -1.5f) + testCase "System.Double.Parse follows .NET's Float grammar" <| fun () -> + let parse (s: string) = Double.Parse(s, CultureInfo.InvariantCulture) + parse "1." |> equal 1.0 + parse ".5" |> equal 0.5 + parse "+.5" |> equal 0.5 + parse "-1.5e3" |> equal -1500.0 + parse "25E-2" |> equal 0.25 + parse "\t1\n" |> equal 1.0 + parse "\v1\f" |> equal 1.0 + parse "1\u0000" |> equal 1.0 + parse "1 \u0000\u0000" |> equal 1.0 + parse "1e99999" |> equal Double.PositiveInfinity + for s in [ "0x10"; "0X1A"; "0b101"; "0o7"; "\u00a01"; "\ufeff1"; "1\ufeff"; "\u20281"; "\u30001"; "1\u0000 "; "\u00001"; "1e"; "e5"; "."; "+"; "\uff11" ] do + (fun () -> parse s) |> throwsAnyError + + testCase "System.Double.Parse accepts the NaN and infinity symbols ignoring case" <| fun () -> + let parse (s: string) = Double.Parse(s, CultureInfo.InvariantCulture) + for s in [ "NaN"; "nan"; "NAN"; "-NaN"; "+nan"; " NaN\n" ] do + parse s |> Double.IsNaN |> equal true + parse "infinity" |> equal Double.PositiveInfinity + parse "+Infinity" |> equal Double.PositiveInfinity + parse " -INFINITY " |> equal Double.NegativeInfinity + parse " NaN" |> Double.IsNaN |> equal true + parse "\u0085Infinity\u2028" |> equal Double.PositiveInfinity + parse "\u3000-Infinity" |> equal Double.NegativeInfinity + for s in [ "inf"; "\u221e"; "NaN\u0000"; "\ufeffNaN"; "NaN\ufeff"; "- NaN"; "+-Infinity"; "Infinityx"; "NaN0" ] do + (fun () -> parse s) |> throwsAnyError + + testCase "System.Double.TryParse follows .NET's Float grammar" <| fun () -> + tryParseFloat "1." |> equal (true, 1.0) + tryParseFloat " .5\r\n" |> equal (true, 0.5) + tryParseFloat "1\u0000" |> equal (true, 1.0) + tryParseFloat "-infinity" |> equal (true, Double.NegativeInfinity) + tryParseFloat "nan" |> fst |> equal true + tryParseFloat "0x10" |> equal (false, 0.0) + tryParseFloat "\u00a01" |> equal (false, 0.0) + tryParseFloat "1_0" |> equal (false, 0.0) + tryParseFloat "_" |> equal (false, 0.0) + tryParseFloat null |> equal (false, 0.0) + + testCase "System.Single.Parse follows .NET's Float grammar" <| fun () -> + let parse (s: string) = Single.Parse(s, CultureInfo.InvariantCulture) + parse ".5" |> equal 0.5f + parse "-Infinity" |> float |> equal Double.NegativeInfinity + parse "nan" |> float |> Double.IsNaN |> equal true + (fun () -> parse "0x10") |> throwsAnyError + (fun () -> parse "1_0") |> throwsAnyError + + testCase "System.Double.Parse accepts group separators like invariant .NET" <| fun () -> + let parse (s: string) = Double.Parse(s, CultureInfo.InvariantCulture) + parse "1,5" |> equal 15.0 + parse "1,000.5" |> equal 1000.5 + parse "1,,0" |> equal 10.0 + parse "1," |> equal 1.0 + parse "1,.5" |> equal 1.5 + parse "1,e2" |> equal 100.0 + parse "-1,000" |> equal -1000.0 + Single.Parse("1,5", CultureInfo.InvariantCulture) |> equal 15.0f + Convert.ToDouble("1,000", CultureInfo.InvariantCulture) |> equal 1000.0 + tryParseFloat "1,000" |> equal (true, 1000.0) + for s in [ ","; ",1"; "1.5,0"; "1e1,0"; "-,1" ] do + (fun () -> parse s) |> throwsAnyError + (fun () -> float "1,5") |> throwsAnyError + (fun () -> float32 "1,5") |> throwsAnyError + + testCase "System.Double.Parse honors NumberStyles" <| fun () -> + let parse (s: string) (style: NumberStyles) = Double.Parse(s, style, CultureInfo.InvariantCulture) + parse "1e3" NumberStyles.Float |> equal 1000.0 + parse "1,000" NumberStyles.Number |> equal 1000.0 + parse "1.5-" NumberStyles.Number |> equal -1.5 + parse "(1.5)" (NumberStyles.Float ||| NumberStyles.AllowParentheses) |> equal -1.5 + parse "1" NumberStyles.None |> equal 1.0 + for s, style in [ "1,5", NumberStyles.Float; "1e3", NumberStyles.Number; "1.5", NumberStyles.Integer; " 1 ", NumberStyles.None; "-1", NumberStyles.None; "+1", NumberStyles.AllowTrailingSign ] do + (fun () -> parse s style) |> throwsAnyError + let result = ref 0.0 + Double.TryParse("1,5", NumberStyles.Float, CultureInfo.InvariantCulture, result) |> equal false + Double.TryParse("1,5", NumberStyles.Number, CultureInfo.InvariantCulture, result) |> equal true + result.Value |> equal 15.0 + Single.Parse("1,5", NumberStyles.Float ||| NumberStyles.AllowThousands, CultureInfo.InvariantCulture) |> equal 15.0f + (fun () -> Single.Parse("1,5", NumberStyles.Float, CultureInfo.InvariantCulture)) |> throwsAnyError + + testCase "System.Decimal.Parse accepts .NET's group separator placements" <| fun () -> + let parse (s: string) = Decimal.Parse(s, CultureInfo.InvariantCulture) + parse "1,,0" |> equal 10M + parse "1," |> equal 1M + parse "1,.5" |> equal 1.5M + for s in [ ","; ",1"; "1.5,0" ] do + (fun () -> parse s) |> throwsAnyError + + testCase "Double.Parse and Convert.ToDouble reject underscores" <| fun () -> + for s in [ "1_0"; "_1"; "1_"; "_"; "1e1_0"; "1._5" ] do + (fun () -> Double.Parse(s, CultureInfo.InvariantCulture)) |> throwsAnyError + (fun () -> Convert.ToDouble(s, CultureInfo.InvariantCulture)) |> throwsAnyError + + testCase "float and float32 remove underscores like FSharp.Core" <| fun () -> + float "1_0_0" |> equal 100.0 + float "_1" |> equal 1.0 + float "1_" |> equal 1.0 + float "1e1_0" |> equal 1e10 + float " _1" |> equal 1.0 + float "Na_N" |> Double.IsNaN |> equal true + float32 "1__0" |> equal 10.0f + (fun () -> float "_") |> throwsAnyError + (fun () -> float32 "__") |> throwsAnyError + (fun () -> float "0x10") |> throwsAnyError + (fun () -> float "\u00a01") |> throwsAnyError + + testCase "System.Decimal.Parse trims only .NET's white space" <| fun () -> + let parse (s: string) = Decimal.Parse(s, CultureInfo.InvariantCulture) + parse "\t12\r\n" |> equal 12M + parse "12\u0000" |> equal 12M + parse "12 \u0000" |> equal 12M + for s in [ "\u00a012"; "\ufeff12"; "12\u3000"; "12\u0000 " ] do + (fun () -> parse s) |> throwsAnyError + + testCase "decimal parses like System.Decimal.Parse with NumberStyles.Float" <| fun () -> + decimal " 12 " |> equal 12M + decimal "+12" |> equal 12M + decimal "\t12\n" |> equal 12M + decimal "12\u0000" |> equal 12M + decimal "1e3" |> equal 1000M + for s in [ "1,000"; "(1)"; "1-"; "1_0"; "\u00a012"; "0x10"; "NaN" ] do + (fun () -> decimal s) |> throwsAnyError + + testCase "Convert.ToDecimal parses like System.Decimal.Parse" <| fun () -> + Convert.ToDecimal(" 12 ", CultureInfo.InvariantCulture) |> equal 12M + Convert.ToDecimal("1,000", CultureInfo.InvariantCulture) |> equal 1000M + Convert.ToDecimal("1-", CultureInfo.InvariantCulture) |> equal -1M + (fun () -> Convert.ToDecimal("1e3", CultureInfo.InvariantCulture)) |> throwsAnyError + + testCase "BigInteger.Parse follows .NET's Integer grammar" <| fun () -> + bigint.Parse " 12 " |> equal 12I + bigint.Parse "+12" |> equal 12I + bigint.Parse "-12\u0000" |> equal -12I + for s in [ ""; " "; "-"; "0x10"; "1_0"; "1.5"; "1e3"; "\u00a012" ] do + (fun () -> bigint.Parse s) |> throwsAnyError + tryParse bigint.TryParse 0I "" |> equal (false, 0I) + tryParse bigint.TryParse 0I "0x10" |> equal (false, 0I) + tryParse bigint.TryParse 0I " 12 " |> equal (true, 12I) + + testCase "Parsing numbers raises .NET's exception types" <| fun () -> + raisedExceptionType (fun () -> Double.Parse("1e", CultureInfo.InvariantCulture)) |> equal "FormatException" + raisedExceptionType (fun () -> Double.Parse(null, CultureInfo.InvariantCulture)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Single.Parse("abc", CultureInfo.InvariantCulture)) |> equal "FormatException" + raisedExceptionType (fun () -> float "abc") |> equal "FormatException" + raisedExceptionType (fun () -> float32 (null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Int32.Parse "abc") |> equal "FormatException" + raisedExceptionType (fun () -> Int32.Parse(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Int32.Parse "2147483648") |> equal "OverflowException" + raisedExceptionType (fun () -> UInt32.Parse "4294967296") |> equal "OverflowException" + raisedExceptionType (fun () -> Byte.Parse "256") |> equal "OverflowException" + raisedExceptionType (fun () -> SByte.Parse "-129") |> equal "OverflowException" + raisedExceptionType (fun () -> UInt16.Parse "-1") |> equal "OverflowException" + raisedExceptionType (fun () -> Int64.Parse "abc") |> equal "FormatException" + raisedExceptionType (fun () -> Int64.Parse(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> UInt64.Parse "-1") |> equal "OverflowException" + raisedExceptionType (fun () -> int "1.5") |> equal "FormatException" + raisedExceptionType (fun () -> uint32 "4294967296") |> equal "OverflowException" + raisedExceptionType (fun () -> int64 "99999999999999999999") |> equal "OverflowException" + raisedExceptionType (fun () -> Decimal.Parse("abc", CultureInfo.InvariantCulture)) |> equal "FormatException" + raisedExceptionType (fun () -> Decimal.Parse(null, CultureInfo.InvariantCulture)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Decimal.Parse("79228162514264337593543950336", CultureInfo.InvariantCulture)) |> equal "OverflowException" + raisedExceptionType (fun () -> decimal "abc") |> equal "FormatException" + raisedExceptionType (fun () -> decimal "1e30") |> equal "OverflowException" + raisedExceptionType (fun () -> bigint.Parse "abc") |> equal "FormatException" + raisedExceptionType (fun () -> bigint.Parse null) |> equal "ArgumentNullException" + tryParse Int32.TryParse 0 null |> equal (false, 0) + tryParse Decimal.TryParse 0M null |> equal (false, 0M) + + testCase "Parsing numbers raises .NET's exception messages" <| fun () -> + (fun () -> Int32.Parse "abc") |> throwsError "The input string 'abc' was not in a correct format." + (fun () -> Double.Parse("abc", CultureInfo.InvariantCulture)) |> throwsError "The input string 'abc' was not in a correct format." + (fun () -> Int32.Parse(null: string)) |> throwsError "Value cannot be null. (Parameter 's')" + (fun () -> Int32.Parse "2147483648") |> throwsError "Value was either too large or too small for an Int32." + (fun () -> UInt32.Parse "4294967296") |> throwsError "Value was either too large or too small for a UInt32." + (fun () -> Byte.Parse "256") |> throwsError "Value was either too large or too small for an unsigned byte." + (fun () -> SByte.Parse "128") |> throwsError "Value was either too large or too small for a signed byte." + (fun () -> Int64.Parse "9223372036854775808") |> throwsError "Value was either too large or too small for an Int64." + (fun () -> bigint.Parse "abc") |> throwsError "The value could not be parsed." + testCase "System.Boolean.Parse works" <| fun () -> Boolean.Parse "true" |> equal true Boolean.Parse "True" |> equal true @@ -192,6 +386,12 @@ let tests = tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) + testCase "System.UInt64.TryParse works above Int64.MaxValue" <| fun () -> + tryParse UInt64.TryParse 0UL "9223372036854775808" |> equal (true, 9223372036854775808UL) + tryParse UInt64.TryParse 0UL "18446744073709551615" |> equal (true, UInt64.MaxValue) + tryParse UInt64.TryParse 0UL "18446744073709551616" |> equal (false, 0UL) + tryParse UInt64.TryParse 0UL "-1" |> equal (false, 0UL) + testCase "Parsing integers with different radices works" <| fun () -> equal 11 (int "11") equal 17 (int "0x11") @@ -340,6 +540,11 @@ let tests = (235672367).ToString("C2", CultureInfo.InvariantCulture) |> equal "¤235,672,367.00" + // #2582: zero precision must not leave a trailing dot + (255).ToString("C0", CultureInfo.InvariantCulture) |> equal "¤255" + (-255).ToString("C0", CultureInfo.InvariantCulture) |> equal "(¤255)" + (235672367).ToString("C0", CultureInfo.InvariantCulture) |> equal "¤235,672,367" + testCase "uint32.ToString 'C' works" <| fun () -> (255u).ToString("C2", CultureInfo.InvariantCulture) |> equal "¤255.00" (255u).ToString("C15", CultureInfo.InvariantCulture) |> equal "¤255.000000000000000" @@ -605,49 +810,49 @@ let tests = // |> adaptExponentValue // |> equal "2.550000000000000e+2" - // testCase "int64.ToString 'E' works" - // <| fun () -> - // (255L).ToString("E2", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.55e+2" + testCase "int64.ToString 'E' works" + <| fun () -> + (255L).ToString("E2", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.55e+2" - // (255L).ToString("E15", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.550000000000000e+2" + (255L).ToString("E15", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.550000000000000e+2" - // (-255L).ToString("E", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "-2.550000e+2" + (-255L).ToString("E", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "-2.550000e+2" - // (255L).ToString("e2", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.55e+2" + (255L).ToString("e2", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.55e+2" - // (255L).ToString("e15", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.550000000000000e+2" + (255L).ToString("e15", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.550000000000000e+2" - // (-255L).ToString("e", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "-2.550000e+2" + (-255L).ToString("e", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "-2.550000e+2" - // testCase "uint64.ToString 'E' works" - // <| fun () -> - // (255UL).ToString("E2", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.55e+2" + testCase "uint64.ToString 'E' works" + <| fun () -> + (255UL).ToString("E2", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.55e+2" - // (255UL).ToString("E15", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.550000000000000e+2" + (255UL).ToString("E15", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.550000000000000e+2" - // (255UL).ToString("e2", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.55e+2" + (255UL).ToString("e2", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.55e+2" - // (255UL).ToString("e15", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.550000000000000e+2" + (255UL).ToString("e15", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.550000000000000e+2" testCase "float.ToString 'E' works" <| fun () -> @@ -852,6 +1057,11 @@ let tests = |> adaptExponentValue |> equal "2.4e+8" + // JS holds this int as -0 + [ -("0".Length - 1) ] + |> List.map (fun x -> x.ToString("G", CultureInfo.InvariantCulture), x.ToString("g5", CultureInfo.InvariantCulture)) + |> equal [ ("0", "0") ] + testCase "uint32.ToString 'G' works" <| fun () -> (255u).ToString("G2", CultureInfo.InvariantCulture) @@ -910,9 +1120,98 @@ let tests = |> adaptExponentValue |> equal "255.24" - // (-255.2357f).ToString("G", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "-255.2357" + (-255.2357f).ToString("G", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "-255.2357" + + testCase "float.ToString 'G' chooses the notation like .NET" + <| fun () -> + (1e-4).ToString("G", CultureInfo.InvariantCulture) |> equal "0.0001" + (1.5e-5).ToString("G", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1.5e-5).ToString("g", CultureInfo.InvariantCulture) |> equal "1.5e-05" + (1e16).ToString("G", CultureInfo.InvariantCulture) |> equal "10000000000000000" + (1e17).ToString("G", CultureInfo.InvariantCulture) |> equal "1E+17" + (123456789012345678901.0).ToString("G", CultureInfo.InvariantCulture) |> equal "1.2345678901234568E+20" + negZero.ToString("G", CultureInfo.InvariantCulture) |> equal "-0" + (123.456).ToString("G0", CultureInfo.InvariantCulture) |> equal "123.456" + (1.5e-5).ToString("G3", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1.5e-5).ToString("G17", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (5e-5).ToString("G17", CultureInfo.InvariantCulture) |> equal "5.0000000000000002E-05" + negZero.ToString("G17", CultureInfo.InvariantCulture) |> equal "-0" + (1e16).ToString("G16", CultureInfo.InvariantCulture) |> equal "1E+16" + (1e17).ToString("G20", CultureInfo.InvariantCulture) |> equal "100000000000000000" + (0.1 + 0.2).ToString("G20", CultureInfo.InvariantCulture) |> equal "0.30000000000000004441" + $"{1.5e-5:G}" |> equal "1.5E-05" + String.Format(CultureInfo.InvariantCulture, "{0:g}", 1e17) |> equal "1e+17" + + testCase "float32.ToString 'G' chooses the digits and notation like .NET" + <| fun () -> + (0.1f).ToString("G", CultureInfo.InvariantCulture) |> equal "0.1" + (float32 negZero).ToString("G", CultureInfo.InvariantCulture) |> equal "-0" + (1.5e-5f).ToString("G", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1e8f).ToString("G", CultureInfo.InvariantCulture) |> equal "100000000" + (1e9f).ToString("g", CultureInfo.InvariantCulture) |> equal "1e+09" + (123456789.f).ToString("G", CultureInfo.InvariantCulture) |> equal "123456790" + (0.1f).ToString("G9", CultureInfo.InvariantCulture) |> equal "0.100000001" + (1e8f).ToString("G5", CultureInfo.InvariantCulture) |> equal "1E+08" + + testCase "ToString 'G' keeps the trailing zeros of the integer part" + <| fun () -> + (100000).ToString("G", CultureInfo.InvariantCulture) |> equal "100000" + (100).ToString("G", CultureInfo.InvariantCulture) |> equal "100" + (0).ToString("G", CultureInfo.InvariantCulture) |> equal "0" + (-1200).ToString("G", CultureInfo.InvariantCulture) |> equal "-1200" + (100.0).ToString("G", CultureInfo.InvariantCulture) |> equal "100" + (100000L).ToString("G", CultureInfo.InvariantCulture) |> equal "100000" + (100000).ToString("G4", CultureInfo.InvariantCulture) |> equal "1E+05" + (100000).ToString("g4", CultureInfo.InvariantCulture) |> equal "1e+05" + (1.5).ToString("G4", CultureInfo.InvariantCulture) |> equal "1.5" + + testCase "ToString 'E' pads the exponent and keeps its case" + <| fun () -> + (100000).ToString("E", CultureInfo.InvariantCulture) |> equal "1.000000E+005" + (100000).ToString("e", CultureInfo.InvariantCulture) |> equal "1.000000e+005" + (100000).ToString("E0", CultureInfo.InvariantCulture) |> equal "1E+005" + (100000).ToString("E2", CultureInfo.InvariantCulture) |> equal "1.00E+005" + (1234.5678).ToString("E", CultureInfo.InvariantCulture) |> equal "1.234568E+003" + (0.00012).ToString("E2", CultureInfo.InvariantCulture) |> equal "1.20E-004" + (-1234.5).ToString("E2", CultureInfo.InvariantCulture) |> equal "-1.23E+003" + $"{100000L:E}" |> equal "1.000000E+005" + $"{100000L:E2}" |> equal "1.00E+005" + + testCase "ToString 'E' keeps every digit of a 64 bit integer" + <| fun () -> + (0L).ToString("E2", CultureInfo.InvariantCulture) |> equal "0.00E+000" + (100000L).ToString("e4", CultureInfo.InvariantCulture) |> equal "1.0000e+005" + (999999L).ToString("E2", CultureInfo.InvariantCulture) |> equal "1.00E+006" + (9223372036854775807L).ToString("E17", CultureInfo.InvariantCulture) + |> equal "9.22337203685477581E+018" + (-9223372036854775808L).ToString("E2", CultureInfo.InvariantCulture) |> equal "-9.22E+018" + (18446744073709551615UL).ToString("E", CultureInfo.InvariantCulture) |> equal "1.844674E+019" + + testCase "ToString with a custom percent format multiplies by 100" + <| fun () -> + (100000).ToString("0.0%", CultureInfo.InvariantCulture) |> equal "10000000.0%" + (0.1234).ToString("0.00%", CultureInfo.InvariantCulture) |> equal "12.34%" + (0.5).ToString("#.##%", CultureInfo.InvariantCulture) |> equal "50%" + (0.42).ToString("0%", CultureInfo.InvariantCulture) |> equal "42%" + (-0.5).ToString("0.0%", CultureInfo.InvariantCulture) |> equal "-50.0%" + (0.5).ToString("0.0%%", CultureInfo.InvariantCulture) |> equal "5000.0%%" + (0.5).ToString("%0.0", CultureInfo.InvariantCulture) |> equal "%50.0" + + testCase "ToString with a custom format scales by trailing commas" + <| fun () -> + (100000).ToString("0,", CultureInfo.InvariantCulture) |> equal "100" + (100000).ToString("0,,", CultureInfo.InvariantCulture) |> equal "0" + (-123456789).ToString("0,,", CultureInfo.InvariantCulture) |> equal "-123" + (1234567890).ToString("#,#,,", CultureInfo.InvariantCulture) |> equal "1,235" + (123456789).ToString("0,,.0", CultureInfo.InvariantCulture) |> equal "123.5" + (1234567.0).ToString("0,,", CultureInfo.InvariantCulture) |> equal "1" + // A comma that is not next to the decimal point only turns on digit grouping + (123456789).ToString("#,,#", CultureInfo.InvariantCulture) |> equal "123,456,789" + (100000).ToString("#,#", CultureInfo.InvariantCulture) |> equal "100,000" + // .NET ignores commas placed after the decimal placeholders + (123456789).ToString("0.0,,", CultureInfo.InvariantCulture) |> equal "123456789.0" testCase "byte.ToString 'N' works" <| fun () -> @@ -956,6 +1255,11 @@ let tests = (235672367).ToString("N2", CultureInfo.InvariantCulture) |> equal "235,672,367.00" + // #2582: zero precision must not leave a trailing dot + (255).ToString("N0", CultureInfo.InvariantCulture) |> equal "255" + (-255).ToString("N0", CultureInfo.InvariantCulture) |> equal "-255" + (1000000).ToString("N0", CultureInfo.InvariantCulture) |> equal "1,000,000" + testCase "uint32.ToString 'N' works" <| fun () -> (255u).ToString("N2", CultureInfo.InvariantCulture) |> equal "255.00" @@ -990,6 +1294,10 @@ let tests = (-255.2357).ToString("N", CultureInfo.InvariantCulture) |> equal "-255.24" + // #2582: zero precision must not leave a trailing dot + (255.7).ToString("N0", CultureInfo.InvariantCulture) |> equal "256" + (-255.7).ToString("N0", CultureInfo.InvariantCulture) |> equal "-256" + testCase "float32.ToString 'N' works" <| fun () -> (255.2357f).ToString("N2", CultureInfo.InvariantCulture) |> equal "255.24" @@ -1046,6 +1354,11 @@ let tests = (1).ToString("P2", CultureInfo.InvariantCulture) |> equal "100.00 %" (0.34).ToString("P2", CultureInfo.InvariantCulture) |> equal "34.00 %" + // #2582: zero precision must not leave a trailing dot + (1).ToString("P0", CultureInfo.InvariantCulture) |> equal "100 %" + (-1).ToString("P0", CultureInfo.InvariantCulture) |> equal "-100 %" + (0.5).ToString("P0", CultureInfo.InvariantCulture) |> equal "50 %" + testCase "uint32.ToString 'P' works" <| fun () -> (255u).ToString("P2", CultureInfo.InvariantCulture) |> equal "25,500.00 %" @@ -1090,6 +1403,115 @@ let tests = (-255.2357f).ToString("P", CultureInfo.InvariantCulture) |> equal "-25,523.57 %" + testCase "float.ToString 'R' works" + <| fun () -> + (0.).ToString("R", CultureInfo.InvariantCulture) |> equal "0" + negZero.ToString("R", CultureInfo.InvariantCulture) |> equal "-0" + (1.5).ToString("R") |> equal "1.5" + (0.1 + 0.2).ToString("R", CultureInfo.InvariantCulture) |> equal "0.30000000000000004" + (1e-4).ToString("R", CultureInfo.InvariantCulture) |> equal "0.0001" + (1.5e-5).ToString("R", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1.5e-5).ToString("r", CultureInfo.InvariantCulture) |> equal "1.5e-05" + (1.5e-5).ToString("R3", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1e-7).ToString("R", CultureInfo.InvariantCulture) |> equal "1E-07" + (1e15).ToString("R", CultureInfo.InvariantCulture) |> equal "1000000000000000" + (1e16).ToString("R", CultureInfo.InvariantCulture) |> equal "10000000000000000" + (1e17).ToString("R", CultureInfo.InvariantCulture) |> equal "1E+17" + (12345678901234567.0).ToString("R", CultureInfo.InvariantCulture) |> equal "12345678901234568" + (123456789012345678901.0).ToString("R", CultureInfo.InvariantCulture) |> equal "1.2345678901234568E+20" + (1e21).ToString("r", CultureInfo.InvariantCulture) |> equal "1e+21" + Double.Epsilon.ToString("R", CultureInfo.InvariantCulture) |> equal "5E-324" + Double.MaxValue.ToString("R", CultureInfo.InvariantCulture) |> equal "1.7976931348623157E+308" + Double.MinValue.ToString("R", CultureInfo.InvariantCulture) |> equal "-1.7976931348623157E+308" + nan.ToString("R", CultureInfo.InvariantCulture) |> equal "NaN" + infinity.ToString("R", CultureInfo.InvariantCulture) |> equal "Infinity" + (-infinity).ToString("R", CultureInfo.InvariantCulture) |> equal "-Infinity" + + testCase "float32.ToString 'R' works" + <| fun () -> + (0.1f).ToString("R", CultureInfo.InvariantCulture) |> equal "0.1" + (0.1f + 0.2f).ToString("R", CultureInfo.InvariantCulture) |> equal "0.3" + (float32 negZero).ToString("R", CultureInfo.InvariantCulture) |> equal "-0" + (1.5e-5f).ToString("r", CultureInfo.InvariantCulture) |> equal "1.5e-05" + (1e8f).ToString("R", CultureInfo.InvariantCulture) |> equal "100000000" + (1e9f).ToString("R", CultureInfo.InvariantCulture) |> equal "1E+09" + (123456789.f).ToString("R", CultureInfo.InvariantCulture) |> equal "123456790" + (1234.5678f).ToString("R", CultureInfo.InvariantCulture) |> equal "1234.5677" + (-306242.625f).ToString("R", CultureInfo.InvariantCulture) |> equal "-306242.62" + (1.2621775e-29f).ToString("R", CultureInfo.InvariantCulture) |> equal "1.2621775E-29" + Single.Epsilon.ToString("R", CultureInfo.InvariantCulture) |> equal "1E-45" + Single.MaxValue.ToString("R", CultureInfo.InvariantCulture) |> equal "3.4028235E+38" + nanf.ToString("R", CultureInfo.InvariantCulture) |> equal "NaN" + + testCase "ToString 'R' works for integers and decimals" + <| fun () -> + (5).ToString("R", CultureInfo.InvariantCulture) |> equal "5" + (-2147483648).ToString("r", CultureInfo.InvariantCulture) |> equal "-2147483648" + (5uy).ToString("R", CultureInfo.InvariantCulture) |> equal "5" + Int64.MaxValue.ToString("R", CultureInfo.InvariantCulture) |> equal "9223372036854775807" + UInt64.MaxValue.ToString("R", CultureInfo.InvariantCulture) |> equal "18446744073709551615" + (1.50M).ToString("R", CultureInfo.InvariantCulture) |> equal "1.50" + (1.2345M).ToString("R3", CultureInfo.InvariantCulture) |> equal "1.23" + // JS holds this int as -0 + [ -("0".Length - 1) ] |> List.map (fun x -> x.ToString("R", CultureInfo.InvariantCulture)) |> equal [ "0" ] + + testCase "String.Format and interpolation support 'R'" + <| fun () -> + String.Format("{0:R}", 1.5e-5) |> equal "1.5E-05" + String.Format(CultureInfo.InvariantCulture, "{0:r}", negZero) |> equal "-0" + String.Format(CultureInfo.InvariantCulture, "{0,8:R}", 1.5) |> equal " 1.5" + String.Format("{0:R}", 12345678901234567890I) |> equal "12345678901234567890" + $"{1e17:R}" |> equal "1E+17" + $"{0.1 + 0.2:R}" |> equal "0.30000000000000004" + let format = "R" + (1e-7).ToString(format, CultureInfo.InvariantCulture) |> equal "1E-07" + + testCase "float.ToString() chooses the notation like .NET" + <| fun () -> + let values = [ negZero; 1e-4; 1.5e-5; 1e-7; 1e16; 1e17; 123456789012345678901.0; 1e21; Double.Epsilon ] + + let expected = + [ + "-0" + "0.0001" + "1.5E-05" + "1E-07" + "10000000000000000" + "1E+17" + "1.2345678901234568E+20" + "1E+21" + "5E-324" + ] + + values |> List.map string |> equal expected + values |> List.map (fun x -> x.ToString()) |> equal expected + values |> List.map (fun x -> $"{x}") |> equal expected + values |> List.map (fun x -> String.Format("{0}", x)) |> equal expected + values |> List.map (sprintf "%O") |> equal expected + $"%.1f{2.5} {1e17}" |> equal "2.5 1E+17" + String.Format("{0,6}", negZero) |> equal " -0" + + testCase "float32.ToString() chooses the digits and notation like .NET" + <| fun () -> + let values = [ float32 negZero; 1.5e-5f; 3.14f; 1e8f; 1e15f ] + let expected = [ "-0"; "1.5E-05"; "3.14"; "100000000"; "1E+15" ] + values |> List.map string |> equal expected + values |> List.map (fun x -> x.ToString()) |> equal expected + values |> List.map (fun x -> $"{x}") |> equal expected + + testCase "Negated integer zero is written without a sign" + <| fun () -> + let x = List.head [ 0 ] + String.Format("{0}", -x) |> equal "0" + String.Format("{0}", x * -1) |> equal "0" + String.Format("{0} {1} {2} {3}", -x, x * -1, -0.0, 1.5) |> equal "0 0 -0 1.5" + String.Format("{0} {1}", byte x, -(int16 x)) |> equal "0 0" + String.Format("{0:N0} {0:G}", -x) |> equal "0 0" + sprintf "%O" (-x) |> equal "0" + string (-x) |> equal "0" + (-x).ToString() |> equal "0" + $"{-x}" |> equal "0" + testCase "byte.ToString 'X' works" <| fun () -> (255uy).ToString("X2") |> equal "FF" (255uy).ToString("X15") |> equal "0000000000000FF" @@ -1196,6 +1618,14 @@ let tests = Convert.ToChar("a") |> equal x Convert.ToChar('a') |> equal x + testCase "Negative fractions truncate toward zero when converted to small integers" <| fun () -> + sbyte(-7.9) |> equal -7y + sbyte(-7.9f) |> equal -7y + int16(-7.9) |> equal -7s + int16(-7.9f) |> equal -7s + byte(7.9) |> equal 7uy + uint16(7.9) |> equal 7us + testCase "System.Convert.ToSByte works" <| fun () -> let x = 1y sbyte(1y) |> equal x @@ -1857,19 +2287,49 @@ let tests = let x = "101" Convert.ToString(101.m) |> equal x - // testCase "Convert.ToHexString works" <| fun () -> - // let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] - // Convert.ToHexString(bytes) - // |> equal "FAFBFCFDFE" - - // testCase "Convert.ToHexStringLower works" <| fun () -> - // let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] - // Convert.ToHexStringLower(bytes) - // |> equal "fafbfcfdfe" - - // testCase "Convert.FromHexString works" <| fun () -> - // Convert.FromHexString("fafBFCFDFE") - // |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + testCase "Convert.ToHexString works" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexString(bytes) + |> equal "FAFBFCFDFE" + Convert.ToHexString([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010F10" + Convert.ToHexString([||]) |> equal "" + Convert.ToHexString(bytes, 1, 3) |> equal "FBFCFD" + Convert.ToHexString(bytes, 5, 0) |> equal "" + + testCase "Convert.ToHexStringLower works" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexStringLower(bytes) + |> equal "fafbfcfdfe" + Convert.ToHexStringLower([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010f10" + Convert.ToHexStringLower([||]) |> equal "" + Convert.ToHexStringLower(bytes, 1, 3) |> equal "fbfcfd" + + testCase "Convert.ToHexString checks its arguments" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + raisedExceptionType (fun () -> Convert.ToHexString(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString((null: byte[]), 0, 0)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 1, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 6, 0)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + + testCase "Convert.FromHexString works" <| fun () -> + Convert.FromHexString("fafBFCFDFE") + |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.FromHexString("0123456789abcdefABCDEF") + |> equal [| 1uy; 35uy; 69uy; 103uy; 137uy; 171uy; 205uy; 239uy; 171uy; 205uy; 239uy |] + Convert.FromHexString("") |> equal [||] + + testCase "Convert.FromHexString checks its input" <| fun () -> + raisedExceptionType (fun () -> Convert.FromHexString(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.FromHexString("abc")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("zz")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("0x10")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("00 1")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("+1")) |> equal "FormatException" testCase "Convert.ToBase64String works" <| fun () -> let bytes = [| 2uy; 4uy; 6uy; 8uy; 10uy; 12uy; 14uy; 16uy; 18uy; 20uy |] @@ -2299,6 +2759,12 @@ let tests = let g = Guid [|6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy|] g.ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + testCase "Convert byte array built from a list or seq to Guid works" <| fun () -> + let bytes = id [6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy] + Guid(List.toArray bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(Array.ofList bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(Array.ofSeq (Seq.ofList bytes)).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + testCase "Guid.ToString works with formats" <| fun () -> let g = Guid.Parse("96258006-c4ba-4a7f-80c4-de7f2b2898c5") let g2 = Guid.Parse(id "96258006-c4ba-4a7f-80c4-de7f2b2898c5") @@ -2336,6 +2802,11 @@ let tests = System.Text.Encoding.Unicode.GetString(bytes, 8, 6) |> equal "\u03B2\uD8FF\uDCFF" + testCase "Encoding.Unicode.GetString for range works with non-typed array" <| fun () -> + let bytes = toPlainArray [| 0x7Auy; 0x00uy; 0x61uy; 0x00uy; 0x06uy; 0x03uy; 0xFDuy; 0x01uy; 0xB2uy; 0x03uy; 0xFFuy; 0xD8uy; 0xFFuy; 0xDCuy |] + System.Text.Encoding.Unicode.GetString(bytes, 8, 6) + |> equal "\u03B2\uD8FF\uDCFF" + testCase "Encoding.UTF8.GetBytes works" <| fun () -> System.Text.Encoding.UTF8.GetBytes("za\u0306\u01FD\u03B2\uD8FF\uDCFF") |> equal [| 0x7Auy; 0x61uy; 0xCCuy; 0x86uy; 0xC7uy; 0xBDuy; 0xCEuy; 0xB2uy; 0xF1uy; 0x8Fuy; 0xB3uy; 0xBFuy |] @@ -2354,4 +2825,9 @@ let tests = System.Text.Encoding.UTF8.GetString(bytes, 6, 6) |> equal "\u03B2\uD8FF\uDCFF" + testCase "Encoding.UTF8.GetString for range works with non-typed array" <| fun () -> + let bytes = toPlainArray [| 0x7Auy; 0x61uy; 0xCCuy; 0x86uy; 0xC7uy; 0xBDuy; 0xCEuy; 0xB2uy; 0xF1uy; 0x8Fuy; 0xB3uy; 0xBFuy |] + System.Text.Encoding.UTF8.GetString(bytes, 6, 6) + |> equal "\u03B2\uD8FF\uDCFF" + ] diff --git a/tests/Js/Main/DateTimeOffsetTests.fs b/tests/Js/Main/DateTimeOffsetTests.fs index 44ca837c55..283844a0a6 100644 --- a/tests/Js/Main/DateTimeOffsetTests.fs +++ b/tests/Js/Main/DateTimeOffsetTests.fs @@ -112,6 +112,23 @@ let tests = DateTimeOffset(2014, 9, 1, 16, 37, 2, TimeSpan.Zero).ToString("s", CultureInfo.InvariantCulture) |> equal "2014-09-01T16:37:02" + testCase "DateTimeOffset.ToString uses the offset's date in every local timezone" <| fun _ -> + // At +14 and -12 the offset's date differs from the UTC date, so one of them differs from any local date + let early = DateTimeOffset(2014, 9, 1, 0, 30, 5, TimeSpan.FromHours 14) + let late = DateTimeOffset(2014, 9, 1, 23, 30, 5, TimeSpan.FromHours -12) + for d in [ early; late ] do + let format (f: string) = d.ToString(f, CultureInfo.InvariantCulture) + let time = if d = early then "00:30" else "23:30" + format "D" |> equal "Monday, 01 September 2014" + format "d" |> equal "09/01/2014" + format "F" |> equal ("Monday, 01 September 2014 " + time + ":05") + format "f" |> equal ("Monday, 01 September 2014 " + time) + format "G" |> equal ("09/01/2014 " + time + ":05") + format "g" |> equal ("09/01/2014 " + time) + format "M" |> equal "September 01" + format "Y" |> equal "2014 September" + format "dd/MM/yyyy HH:mm" |> equal ("01/09/2014 " + time) + testCase "DateTimeOffset from Year 1 to 99 works" <| fun () -> let date = DateTimeOffset(1, 1, 2, 0, 0, 0, TimeSpan.Zero) date.Year |> equal 1 @@ -613,10 +630,16 @@ let tests = /// NOTE: local utc offset of `usedDate`, NOT `DateTime.Now` /// -> `usedDate` is in september -> summer time in Europe (`+2`)! let localOffset: TimeSpan = +#if FABLE_COMPILER_JAVASCRIPT_TEMPORAL + // usedDate is a Temporal.PlainDateTime (no getTimezoneOffset); the host offset for + // that wall-clock is what a DateTimeOffset built from an Unspecified DateTime uses. + DateTimeOffset(usedDate).Offset +#else #if FABLE_COMPILER !!(usedDate?getTimezoneOffset() * -60_000) #else TimeZoneInfo.Local.GetUtcOffset(usedDate) +#endif #endif let shouldSucceed = Ok () @@ -905,4 +928,34 @@ let tests = let dto: DateTimeOffset = d dto.UtcDateTime |> equal d ] + +// See the note in DateTimeTests.fs: tick precision only holds on .NET and on the +// Temporal representation. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testList "Tick precision" [ + testCase "DateTimeOffset round-trip format prints all seven tick digits" <| fun () -> + let d = DateTimeOffset(638397614451234567L, TimeSpan.Zero) + d.ToString("O", CultureInfo.InvariantCulture) |> equal "2024-01-02T03:04:05.1234567+00:00" + + testCase "DateTimeOffset boundary values have the exact .NET tick counts" <| fun () -> + DateTimeOffset.MinValue.UtcTicks |> equal 0L + DateTimeOffset.MaxValue.UtcTicks |> equal 3155378975999999999L + + testCase "DateTimeOffset keeps sub-millisecond ticks through construction" <| fun () -> + let d = DateTimeOffset(638397614451234567L, TimeSpan.Zero) + d.Ticks % 10_000L |> equal 4567L + d.ToOffset(TimeSpan.FromHours 2.0).UtcTicks |> equal d.UtcTicks + + testCase "DateTimeOffset.Add* accept fractional values and are tick-precise" <| fun () -> + let d = DateTimeOffset(2024, 1, 2, 3, 4, 5, 6, TimeSpan.Zero) + d.AddDays(1.5).Ticks - d.Ticks |> equal 1_296_000_000_000L + d.AddMilliseconds(0.5).Ticks - d.Ticks |> equal 5_000L + + testCase "DateTimeOffset.Now is representable as a whole number of ticks" <| fun () -> + let n = DateTimeOffset.Now + DateTimeOffset(n.Ticks, n.Offset) |> equal n + let u = DateTimeOffset.UtcNow + DateTimeOffset(u.Ticks, u.Offset) |> equal u + ] +#endif ] diff --git a/tests/Js/Main/DateTimeTests.fs b/tests/Js/Main/DateTimeTests.fs index 83ad64acc1..a0d3f71aa0 100644 --- a/tests/Js/Main/DateTimeTests.fs +++ b/tests/Js/Main/DateTimeTests.fs @@ -1274,4 +1274,58 @@ let tests = let local = DateTime(2020, 2, 20, 20, 20, 20, DateTimeKind.Local) let utc = DateTime(2020, 2, 20, 20, 20, 20, DateTimeKind.Utc) sprintf "%A" local |> equal (sprintf "%A" utc) + +// A .NET tick is 100ns, four digits finer than a JS Date can represent, so these +// only hold where the runtime keeps ticks: on .NET, and on the Temporal +// representation. The default JS-Date representation truncates to milliseconds. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testCase "DateTime keeps sub-millisecond ticks through SpecifyKind" <| fun () -> + let d = DateTime(638397614451234567L, DateTimeKind.Utc) + DateTime.SpecifyKind(d, DateTimeKind.Local).Ticks |> equal d.Ticks + // SpecifyKind must not mutate the value it was handed + d.Kind |> equal DateTimeKind.Utc + + testCase "DateTime keeps sub-millisecond ticks through time zone conversion" <| fun () -> + let utc = DateTime(638397614451234567L, DateTimeKind.Utc) + let local = DateTime(638397614451234567L, DateTimeKind.Local) + // The offset is a whole number of minutes, so the sub-second remainder survives + utc.ToLocalTime().Ticks % 10_000L |> equal 4567L + local.ToUniversalTime().Ticks % 10_000L |> equal 4567L + + testCase "DateTime round-trip format prints all seven tick digits" <| fun () -> + let d = DateTime(638397614451234567L, DateTimeKind.Utc) + d.ToString("O", CultureInfo.InvariantCulture) |> equal "2024-01-02T03:04:05.1234567Z" + d.ToString("fffffff", CultureInfo.InvariantCulture) |> equal "1234567" + d.ToString("FFFFFFF", CultureInfo.InvariantCulture) |> equal "1234567" + // "F" drops trailing zeros, "f" pads them + DateTime(2024, 1, 2, 3, 4, 5, 60).ToString("fffff", CultureInfo.InvariantCulture) |> equal "06000" + DateTime(2024, 1, 2, 3, 4, 5, 60).ToString("FFFFF", CultureInfo.InvariantCulture) |> equal "06" + + testCase "DateTime.Parse recovers sub-millisecond ticks" <| fun () -> + DateTime.Parse("2009-06-15T13:45:30.6175425", CultureInfo.InvariantCulture).Ticks % 10_000_000L + |> equal 6175425L + + testCase "DateTime.Add* accept fractional values and are tick-precise" <| fun () -> + let d = DateTime(2024, 1, 2, 3, 4, 5, 6, DateTimeKind.Utc) + let delta (x: DateTime) = x.Ticks - d.Ticks + d.AddDays(1.5) |> delta |> equal 1_296_000_000_000L + d.AddHours(0.5) |> delta |> equal 18_000_000_000L + d.AddMinutes(1.0 / 3.0) |> delta |> equal 200_000_000L + d.AddSeconds(0.0000001) |> delta |> equal 1L + d.AddMilliseconds(0.5) |> delta |> equal 5_000L + d.AddMilliseconds(0.4) |> delta |> equal 4_000L + d.AddMilliseconds(1.5) |> delta |> equal 15_000L + + testCase "DateTime boundary values have the exact .NET tick counts" <| fun () -> + DateTime.MinValue.Ticks |> equal 0L + DateTime.MaxValue.Ticks |> equal 3155378975999999999L + + testCase "DateTime.Now is representable as a whole number of ticks" <| fun () -> + // Temporal clocks are nanosecond-precise; a value carrying sub-tick + // precision would no longer equal its own tick count round-tripped. + let n = DateTime.Now + DateTime(n.Ticks, n.Kind) |> equal n + let u = DateTime.UtcNow + DateTime(u.Ticks, u.Kind) |> equal u +#endif ] diff --git a/tests/Js/Main/DictionaryTests.fs b/tests/Js/Main/DictionaryTests.fs index 31037ee512..bfa3b37b9c 100644 --- a/tests/Js/Main/DictionaryTests.fs +++ b/tests/Js/Main/DictionaryTests.fs @@ -12,6 +12,10 @@ type MyRecord = { a: int } type R = { i: int; s: string } +type DictKey = + | Int of int + | Str of string + type Table<'K, 'V when 'K: equality and 'K: not null> () = let dic = new Dictionary<'K,'V>() @@ -24,6 +28,14 @@ type Table<'K, 'V when 'K: equality and 'K: not null> () = member _.Dic = dic +type CountingDictionary() = + inherit Dictionary() + + member this.Increment(key: string) = + this.[key] <- (if this.ContainsKey key then this.[key] else 0) + 1 +type ArrayKeyDictionary() = + inherit Dictionary() + let tests = testList "Dictionaries" [ testCase "Dictionary KeyValuePattern works" <| fun () -> // See #509 @@ -194,6 +206,63 @@ let tests = dic.Add("B", "World!") dic.Remove("A") |> equal true dic.Remove("C") |> equal false + dic.Count |> equal 1 + + testCase "Dictionary works when keys share a hash" <| fun () -> + let comparer = + { new IEqualityComparer with + member _.Equals(x, y) = x = y + member _.GetHashCode(_) = 1 } + let dic = Dictionary<_,_>(comparer) + dic.Add("A", 1) + dic.Add("B", 2) + dic.Add("C", 3) + dic.Count |> equal 3 + dic.["C"] |> equal 3 + dic.ContainsKey("B") |> equal true + dic.ContainsKey("D") |> equal false + dic.Remove("B") |> equal true + dic.Remove("B") |> equal false + dic.ContainsKey("C") |> equal true + dic.Count |> equal 2 + throwsAnyError (fun _ -> dic.Add("A", 4)) + + testCase "Dictionary.Count works with indexer insert" <| fun () -> + let dic = Dictionary() + dic.["A"] <- 1 + dic.["B"] <- 2 + dic.Count |> equal 2 + + testCase "Dictionary.Count works with indexer replace" <| fun () -> + let dic = Dictionary() + dic.Add("A", 1) + dic.["A"] <- 2 + dic.Count |> equal 1 + dic.["A"] |> equal 2 + + testCase "Dictionary works with union keys" <| fun () -> + let dic = Dictionary() + dic.Add(Int 1, 1) + dic.Add(Str "a", 2) + dic.[Int 1] |> equal 1 + dic.[Str "a"] |> equal 2 + dic.ContainsKey(Int 1) |> equal true + dic.ContainsKey(Int 2) |> equal false + dic.ContainsKey(Str "b") |> equal false + dic.[Int 1] <- 3 + dic.[Int 1] |> equal 3 + dic.Count |> equal 2 + dic.Remove(Str "a") |> equal true + dic.ContainsKey(Str "a") |> equal false + dic.Count |> equal 1 + + testCase "Dictionary.Remove with complex keys works" <| fun () -> + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 testCase "Dictionary.Remove with records as keys works" <| fun () -> let x1 = { a = 5 } @@ -205,6 +274,7 @@ let tests = dic.Add(x3, "World!") dic.Remove(x2) |> equal true dic.Remove(x4) |> equal false + dic.Count |> equal 1 testCase "Interface IDictionary.Count works" <| fun () -> let dic = dict <| seq { for i in 1. .. 10. -> i.ToString(), i*i } @@ -322,4 +392,52 @@ let tests = coll.Remove(KeyValuePair("D", 3)) |> equal false coll.Remove(KeyValuePair("B", 2)) |> equal true coll.Count |> equal 2 + + testCase "Dictionary subclasses work" <| fun () -> + let dic = CountingDictionary() + dic.Increment "a" + dic.Increment "a" + dic.Increment "b" + dic.["a"] |> equal 2 + dic.Count |> equal 2 + dic.ContainsKey "b" |> equal true + dic.Remove "b" |> equal true + dic |> Seq.map (fun kv -> kv.Key, kv.Value) |> List.ofSeq |> equal [ "a", 2 ] + + testCase "Dictionary compares array keys by reference" <| fun _ -> // See #5165 + let key = [| 1 |] + let dic = Dictionary() + dic.[key] <- 1 + dic.[[| 1 |]] <- 2 + dic.Count |> equal 2 + dic.ContainsKey key |> equal true + dic.ContainsKey [| 1 |] |> equal false + dic.[key] |> equal 1 + let copy = Dictionary(dic) + copy.Count |> equal 2 + copy.ContainsKey key |> equal true + copy.ContainsKey [| 1 |] |> equal false + + testCase "Dictionary subclasses compare array keys by reference" <| fun _ -> + let key = [| 1 |] + let dic = ArrayKeyDictionary() + dic.[key] <- 1 + dic.ContainsKey key |> equal true + dic.ContainsKey [| 1 |] |> equal false + + testCase "Dictionary compares ResizeArray keys by reference" <| fun _ -> + let key = ResizeArray [ 1 ] + let dic = Dictionary, int>() + dic.[key] <- 1 + dic.[ResizeArray [ 1 ]] <- 2 + dic.Count |> equal 2 + dic.ContainsKey key |> equal true + dic.ContainsKey(ResizeArray [ 1 ]) |> equal false + + testCase "Dictionary with a structural comparer compares array keys by value" <| fun _ -> + let dic = Dictionary(HashIdentity.Structural) + dic.[[| 1 |]] <- 1 + dic.ContainsKey [| 1 |] |> equal true + (dict [ [| 1 |], 1 ]).ContainsKey [| 1 |] |> equal true + (readOnlyDict [ [| 1 |], 1 ]).ContainsKey [| 1 |] |> equal true ] diff --git a/tests/Js/Main/EnumerableTests.fs b/tests/Js/Main/EnumerableTests.fs index ff1339c5f7..478e9ad68e 100644 --- a/tests/Js/Main/EnumerableTests.fs +++ b/tests/Js/Main/EnumerableTests.fs @@ -66,4 +66,16 @@ let tests = |> Seq.toList |> List.sum |> equal 55 - ] \ No newline at end of file + + testCase "Array enumerator can be reset" <| fun () -> + let e = ([| 1; 2; 3 |] :> seq).GetEnumerator() + e.MoveNext() |> equal true + e.MoveNext() |> equal true + e.Current |> equal 2 + e.Reset() + e.MoveNext() |> equal true + e.Current |> equal 1 + e.MoveNext() |> equal true + e.MoveNext() |> equal true + e.MoveNext() |> equal false + ] diff --git a/tests/Js/Main/Fable.Tests.fsproj b/tests/Js/Main/Fable.Tests.fsproj index 933d6f4d4f..1fbca90fc9 100644 --- a/tests/Js/Main/Fable.Tests.fsproj +++ b/tests/Js/Main/Fable.Tests.fsproj @@ -11,8 +11,8 @@ - - + + @@ -68,6 +68,7 @@ + diff --git a/tests/Js/Main/HashSetTests.fs b/tests/Js/Main/HashSetTests.fs index 675eb8b970..73542abd3c 100644 --- a/tests/Js/Main/HashSetTests.fs +++ b/tests/Js/Main/HashSetTests.fs @@ -28,10 +28,23 @@ let inline hashSet l = [] type RefEqRecord = { a: int; b: int } +type ArrayHashSet() = + inherit HashSet() + type MyRecord = { a: int } +type NameSet() = + inherit HashSet() + + member this.AddAll(names: string list) = + names |> List.filter this.Add |> List.length + type R = { i: int; s: string } +type SetItem = + | Int of int + | Str of string + type Apa<'t when 't : equality>() = let state = HashSet<'t>() member _.Add t = state.Add t |> ignore @@ -89,6 +102,37 @@ let tests = xs.Remove 3 |> equal false xs.Count |> equal 2 + testCase "HashSet works when items share a hash" <| fun () -> + let comparer = + { new IEqualityComparer with + member _.Equals(x, y) = x = y + member _.GetHashCode(_) = 1 } + let xs = HashSet<_>(comparer) + xs.Add "A" |> equal true + xs.Add "B" |> equal true + xs.Add "C" |> equal true + xs.Add "B" |> equal false + xs.Count |> equal 3 + xs.Contains "C" |> equal true + xs.Contains "D" |> equal false + xs.Remove "B" |> equal true + xs.Remove "B" |> equal false + xs.Contains "C" |> equal true + xs.Count |> equal 2 + + testCase "HashSet works with union items" <| fun () -> + let xs = HashSet() + xs.Add(Int 1) |> equal true + xs.Add(Str "a") |> equal true + xs.Add(Int 1) |> equal false + xs.Contains(Int 1) |> equal true + xs.Contains(Int 2) |> equal false + xs.Contains(Str "a") |> equal true + xs.Count |> equal 2 + xs.Remove(Int 1) |> equal true + xs.Contains(Int 1) |> equal false + xs.Count |> equal 1 + testCase "HashSet.UnionWith works" <| fun () -> let xs = hashSet [1; 2] let ys = hashSet [2; 4] @@ -219,15 +263,15 @@ let tests = hs.Add(x3) |> equal true hs.Count |> equal 2 - // testCase "HashSet.Add with reference records works" <| fun () -> - // let x1 = { a = 5; b = 10 } - // let x2 = { a = 5; b = 10 } - // let x3 = { a = 10; b = 20 } - // let hs = HashSet() - // hs.Add(x1) |> equal true - // hs.Add(x2) |> equal true - // hs.Add(x3) |> equal true - // hs.Count |> equal 3 + testCase "HashSet.Add with reference records works" <| fun () -> + let x1 = { a = 5; b = 10 } + let x2 = { a = 5; b = 10 } + let x3 = { a = 10; b = 20 } + let hs = HashSet() + hs.Add(x1) |> equal true + hs.Add(x2) |> equal true + hs.Add(x3) |> equal true + hs.Count |> equal 3 testCase "HashSet.Clear works" <| fun () -> let hs = HashSet<_>() @@ -381,4 +425,45 @@ let tests = coll.Remove(("D", 3)) |> equal false coll.Remove(("B", 2)) |> equal true coll.Count |> equal 2 - ] \ No newline at end of file + + testCase "HashSet subclasses work" <| fun () -> + let names = NameSet() + names.AddAll [ "a"; "b"; "a" ] |> equal 2 + names.Count |> equal 2 + names.Contains "b" |> equal true + names.Remove "b" |> equal true + names |> List.ofSeq |> equal [ "a" ] + + testCase "HashSet compares arrays by reference" <| fun _ -> // See #5165 + let item = [| 1 |] + let xs = HashSet() + xs.Add item |> equal true + xs.Add item |> equal false + xs.Add [| 1 |] |> equal true + xs.Count |> equal 2 + xs.Contains item |> equal true + xs.Contains [| 1 |] |> equal false + HashSet([ [| 1 |]; [| 1 |] ]).Count |> equal 2 + + testCase "HashSet subclasses compare arrays by reference" <| fun _ -> + let item = [| 1 |] + let xs = ArrayHashSet() + xs.Add item |> equal true + xs.Contains item |> equal true + xs.Contains [| 1 |] |> equal false + + testCase "HashSet compares ResizeArrays by reference" <| fun _ -> + let item = ResizeArray [ 1 ] + let xs = HashSet>() + xs.Add item |> equal true + xs.Add(ResizeArray [ 1 ]) |> equal true + xs.Count |> equal 2 + xs.Contains item |> equal true + xs.Contains(ResizeArray [ 1 ]) |> equal false + + testCase "HashSet with a structural comparer compares arrays by value" <| fun _ -> + let xs = HashSet(HashIdentity.Structural) + xs.Add [| 1 |] |> equal true + xs.Add [| 1 |] |> equal false + xs.Contains [| 1 |] |> equal true + ] diff --git a/tests/Js/Main/ImportTests.fs b/tests/Js/Main/ImportTests.fs index 50b8f46030..0a9ff9545f 100644 --- a/tests/Js/Main/ImportTests.fs +++ b/tests/Js/Main/ImportTests.fs @@ -99,6 +99,17 @@ type Treasure = let myKeyInlineWrapper() = Treasure.keyHolderInline + +[] +let importedLimit: int = jsNative + +let private forLoopBoundThree () = 3 + +let forLoopBoundWithImportedLimit () = + let mutable sum = 0 + for _ = 0 to forLoopBoundThree () do + sum <- sum + importedLimit + sum #endif let tests = @@ -117,6 +128,9 @@ let tests = let fooAll2: IFooImported = JsInterop.importAll "./js/1foo.js" fooAll2.foo |> equal "foo" + testCase "for bound does not shadow an import" <| fun () -> + forLoopBoundWithImportedLimit () |> equal 400 + testCase "Symbols in external projects work" <| fun () -> equal "Fable Rocks!" Spaces.Helper.ConditionalExternalValue diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs index 35c01030c1..360686ffef 100644 --- a/tests/Js/Main/JsInteropTests.fs +++ b/tests/Js/Main/JsInteropTests.fs @@ -247,6 +247,19 @@ type ParseSpeeds = | [] Medium | [] Slow +#if !FABLE_COMPILER_TYPESCRIPT +[] +let globalLimit: int = jsNative + +let private forLoopBoundThree () = 3 + +let forLoopBoundWithGlobalLimit () = + let mutable sum = 0 + for _ = 0 to forLoopBoundThree () do + sum <- sum + globalLimit + sum +#endif + [] #endif type Field = OldPassword | NewPassword | ConfirmPassword @@ -391,6 +404,169 @@ module TaggedUnion = type FooGeneric<'a> = | Bar of baz: 'a + [] + type SharedTag = + | [] Done of value: float + | [] DoneError of error: string + | [] DoneFailed of ok: bool + | Pending + + type Payload(x: int) = + member _.X = x + + type Animal() = + member _.Kind = "animal" + + [] + type Dog() = + inherit Animal() + member _.Kind = "dog" + member _.Bark() = "woof" + + [] + type DerivedTag = + | [] Done of value: float + | [] DoneError of error: string + | [] DoneWithExtra of value: float * extra: string + | [] LogString of message: string + | [] LogNumber of message: float + | [] LogPayload of message: Payload + | [] PetAnimal of pet: Animal + | [] PetDog of pet: Dog + | Pending + + [] + type Options(name: string) = + member val name = name + + [] + type PojoField = + | [] LogString of message: string + | [] LogOptions of message: Options + + [] + type Outcome = + | [] Ok of ok: bool + | [] Failed of ok: bool + + [] + type Job = + | [] Build of kind: string + | [] Test of kind: string + | [] Other of kind: string + | [] Idle + + [] + type NumberTag = + | [] Text of text: string + | [] Count of count: int + | [] On of flag: bool + | [] Off of flag: bool + | [] Idle + + type Take = + abstract ``type``: string + abstract fromIndex: int + abstract toIndex: int + + type Edit = + abstract text: string + + type TakeRange(fromIndex: int, toIndex: int) = + let mutable toIndex = toIndex + member _.Extend(n: int) = toIndex <- toIndex + n + interface Take with + member _.``type`` = "take" + member _.fromIndex = fromIndex + member _.toIndex = toIndex + + [] + type Command = + | Take of Take + | Edit of edit: Edit + | Save + + [] + type AuthState = + | Zero + | One of string + | Two of string * string + + type Circle = + abstract ``type``: string + abstract radius: float + + type Rect = + abstract ``type``: string + abstract width: float + abstract height: float + + [] + type Shape = + | [] Circle of Circle + | [] Rect of Rect + +module ErasedCaseTest = + type Circle = + abstract radius: float + + type Rect = + abstract width: float + + type Animal = + abstract name: string + + type Dog = + inherit Animal + abstract bark: string + + type Kinded = + abstract kind: string + + [] + type Shape = + | Circle of Circle + | Rect of Rect + | Count of int + | Label of string + + [] + type Pet = + | Animal of Animal + | Dog of Dog + + [] + type Tagged = + | [] A of Kinded + | B of Kinded + + [] + type Id = + | [] Name of string + | Slug of string + | Count of int + + let circle r = + { new Circle with + member _.radius = r } + + let rect w = + { new Rect with + member _.width = w } + + let animal n = + { new Animal with + member _.name = n } + + let dog n b = + { new Dog with + member _.name = n + member _.bark = b } + + let kinded k = + { new Kinded with + member _.kind = k } + #if FABLE_COMPILER module PojoDefinedByConsArgs = [] @@ -516,6 +692,14 @@ let tests = instance.regular 3 |> equal 4 #if FABLE_COMPILER +#if !FABLE_COMPILER_TYPESCRIPT + testCase "for bound does not shadow a global" <| fun () -> + emitJsStatement () "Reflect.set(globalThis, 'limit', 21)" + let result = forLoopBoundWithGlobalLimit () + emitJsStatement () "Reflect.deleteProperty(globalThis, 'limit')" + equal 84 result +#endif + testCase "Can type test interfaces decorated with Global" <| fun () -> let ar = ResizeArray [| 1; 2; 3 |] |> box match ar with @@ -946,6 +1130,16 @@ let tests = arr.[0] <- 5uy equal 5uy arr.[0] + testCase "TypedArray can be created from an array literal" <| fun () -> + let floats = JS.Constructors.Float32Array.Create [| 1.5; -2.5 |] + floats.length |> equal 2 + floats.[1] |> equal -2.5f + let bytes = JS.Constructors.Uint8Array.Create [| 1uy; 255uy |] + bytes.[1] |> equal 255uy + // Values are converted the same way as from another typed array + let wrapped = JS.Constructors.Int8Array.Create [| 1; 300 |] + wrapped.[1] |> equal 44y + testCase "jsOptions works" <| fun _ -> let opts = jsOptions(fun o -> o.foo <- "bar" @@ -1090,6 +1284,292 @@ let tests = let fooGeneric = getFooGeneric 10 equal (TaggedUnion.FooGeneric.Bar 10) fooGeneric + +#endif + + testCase "Case testing with TS tagged unions whose cases share a tag works" <| fun () -> + let describe = function + | TaggedUnion.SharedTag.Done v -> sprintf "done: %.1f" v + | TaggedUnion.SharedTag.DoneError e -> sprintf "error: %s" e + | TaggedUnion.SharedTag.DoneFailed _ -> "failed" + | TaggedUnion.SharedTag.Pending -> "pending" + TaggedUnion.SharedTag.Done 1.5 |> describe |> equal "done: 1.5" + TaggedUnion.SharedTag.DoneError "oops" |> describe |> equal "error: oops" + TaggedUnion.SharedTag.DoneFailed false |> describe |> equal "failed" + TaggedUnion.SharedTag.Pending |> describe |> equal "pending" + (TaggedUnion.SharedTag.DoneError "oops").IsDoneError |> equal true + (TaggedUnion.SharedTag.Done 1.5).IsDoneError |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag evaluates the value once" <| fun () -> + let mutable calls = 0 + let next () = + calls <- calls + 1 + TaggedUnion.SharedTag.DoneError "oops" + (next ()).IsDoneError |> equal true + calls |> equal 1 + + testCase "Case testing with TS tagged unions whose cases share a tag is derived from the fields" <| fun () -> + let describe = function + | TaggedUnion.DerivedTag.Done v -> sprintf "done: %.1f" v + | TaggedUnion.DerivedTag.DoneError e -> sprintf "error: %s" e + | TaggedUnion.DerivedTag.DoneWithExtra (v, e) -> sprintf "extra: %.1f %s" v e + | TaggedUnion.DerivedTag.LogString m -> sprintf "string: %s" m + | TaggedUnion.DerivedTag.LogNumber n -> sprintf "number: %.1f" n + | TaggedUnion.DerivedTag.LogPayload p -> sprintf "payload: %i" p.X + | TaggedUnion.DerivedTag.PetAnimal a -> "animal: " + a.Kind + | TaggedUnion.DerivedTag.PetDog d -> "dog: " + d.Kind + | TaggedUnion.DerivedTag.Pending -> "pending" + TaggedUnion.DerivedTag.Done 1.5 |> describe |> equal "done: 1.5" + TaggedUnion.DerivedTag.DoneError "oops" |> describe |> equal "error: oops" + TaggedUnion.DerivedTag.DoneWithExtra (2.5, "e") |> describe |> equal "extra: 2.5 e" + TaggedUnion.DerivedTag.LogString "hi" |> describe |> equal "string: hi" + TaggedUnion.DerivedTag.LogNumber 2.5 |> describe |> equal "number: 2.5" + TaggedUnion.DerivedTag.LogPayload (TaggedUnion.Payload 3) |> describe |> equal "payload: 3" + TaggedUnion.DerivedTag.Pending |> describe |> equal "pending" + (TaggedUnion.DerivedTag.Done 1.5).IsDone |> equal true + (TaggedUnion.DerivedTag.Done 1.5).IsDoneError |> equal false + (TaggedUnion.DerivedTag.Done 1.5).IsDoneWithExtra |> equal false + (TaggedUnion.DerivedTag.DoneWithExtra (2.5, "e")).IsDone |> equal false + (TaggedUnion.DerivedTag.DoneWithExtra (2.5, "e")).IsDoneWithExtra |> equal true + (TaggedUnion.DerivedTag.LogString "hi").IsLogString |> equal true + (TaggedUnion.DerivedTag.LogString "hi").IsLogNumber |> equal false + (TaggedUnion.DerivedTag.LogString "hi").IsLogPayload |> equal false + (TaggedUnion.DerivedTag.LogNumber 2.5).IsLogNumber |> equal true + (TaggedUnion.DerivedTag.LogNumber 2.5).IsLogString |> equal false + (TaggedUnion.DerivedTag.LogPayload (TaggedUnion.Payload 3)).IsLogPayload |> equal true + (TaggedUnion.DerivedTag.LogPayload (TaggedUnion.Payload 3)).IsLogString |> equal false + (TaggedUnion.DerivedTag.LogPayload (TaggedUnion.Payload 3)).IsLogNumber |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag uses a CaseTest to narrow a base class field" <| fun () -> + let describe = function + | TaggedUnion.DerivedTag.PetAnimal a -> "animal: " + a.Kind + | TaggedUnion.DerivedTag.PetDog d -> "dog: " + d.Kind + | _ -> "other" + TaggedUnion.DerivedTag.PetAnimal (TaggedUnion.Animal()) |> describe |> equal "animal: animal" + TaggedUnion.DerivedTag.PetDog (TaggedUnion.Dog()) |> describe |> equal "dog: dog" + (TaggedUnion.DerivedTag.PetAnimal (TaggedUnion.Animal())).IsPetAnimal |> equal true + (TaggedUnion.DerivedTag.PetAnimal (TaggedUnion.Animal())).IsPetDog |> equal false + (TaggedUnion.DerivedTag.PetDog (TaggedUnion.Dog())).IsPetDog |> equal true + (TaggedUnion.DerivedTag.PetDog (TaggedUnion.Dog())).IsPetAnimal |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag uses a CaseTest for a Pojo field" <| fun () -> + let describe = function + | TaggedUnion.PojoField.LogOptions o -> "options: " + o.name + | TaggedUnion.PojoField.LogString s -> "string: " + s + TaggedUnion.PojoField.LogOptions (TaggedUnion.Options "x") |> describe |> equal "options: x" + TaggedUnion.PojoField.LogString "hi" |> describe |> equal "string: hi" + (TaggedUnion.PojoField.LogOptions (TaggedUnion.Options "x")).IsLogOptions |> equal true + (TaggedUnion.PojoField.LogOptions (TaggedUnion.Options "x")).IsLogString |> equal false + (TaggedUnion.PojoField.LogString "hi").IsLogString |> equal true + (TaggedUnion.PojoField.LogString "hi").IsLogOptions |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag uses the CaseTest of the other case" <| fun () -> + let describe = function + | TaggedUnion.Outcome.Failed _ -> "failed" + | TaggedUnion.Outcome.Ok _ -> "ok" + TaggedUnion.Outcome.Ok true |> describe |> equal "ok" + TaggedUnion.Outcome.Failed false |> describe |> equal "failed" + (TaggedUnion.Outcome.Ok true).IsOk |> equal true + (TaggedUnion.Outcome.Ok true).IsFailed |> equal false + (TaggedUnion.Outcome.Failed false).IsFailed |> equal true + (TaggedUnion.Outcome.Failed false).IsOk |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag uses the CaseTest of the other cases" <| fun () -> + let describe = function + | TaggedUnion.Job.Other k -> "other: " + k + | TaggedUnion.Job.Build k -> "build: " + k + | TaggedUnion.Job.Test k -> "test: " + k + | TaggedUnion.Job.Idle -> "idle" + TaggedUnion.Job.Build "build" |> describe |> equal "build: build" + TaggedUnion.Job.Test "test" |> describe |> equal "test: test" + TaggedUnion.Job.Other "lint" |> describe |> equal "other: lint" + TaggedUnion.Job.Idle |> describe |> equal "idle" + (TaggedUnion.Job.Other "lint").IsOther |> equal true + (TaggedUnion.Job.Other "lint").IsBuild |> equal false + (TaggedUnion.Job.Other "lint").IsTest |> equal false + (TaggedUnion.Job.Build "build").IsBuild |> equal true + (TaggedUnion.Job.Build "build").IsOther |> equal false + (TaggedUnion.Job.Test "test").IsTest |> equal true + (TaggedUnion.Job.Test "test").IsOther |> equal false + + testCase "Case testing with TS tagged unions whose cases share a number tag works" <| fun () -> + let describe = function + | TaggedUnion.NumberTag.Text t -> "text: " + t + | TaggedUnion.NumberTag.Count c -> sprintf "count: %i" c + | TaggedUnion.NumberTag.On _ -> "on" + | TaggedUnion.NumberTag.Off _ -> "off" + | TaggedUnion.NumberTag.Idle -> "idle" + TaggedUnion.NumberTag.Text "hi" |> describe |> equal "text: hi" + TaggedUnion.NumberTag.Count 3 |> describe |> equal "count: 3" + TaggedUnion.NumberTag.On true |> describe |> equal "on" + TaggedUnion.NumberTag.Off false |> describe |> equal "off" + TaggedUnion.NumberTag.Idle |> describe |> equal "idle" + (TaggedUnion.NumberTag.Text "hi").IsText |> equal true + (TaggedUnion.NumberTag.Text "hi").IsCount |> equal false + (TaggedUnion.NumberTag.Count 3).IsCount |> equal true + (TaggedUnion.NumberTag.Count 3).IsText |> equal false + (TaggedUnion.NumberTag.On true).IsOn |> equal true + (TaggedUnion.NumberTag.On true).IsOff |> equal false + (TaggedUnion.NumberTag.Off false).IsOff |> equal true + (TaggedUnion.NumberTag.Off false).IsOn |> equal false + + testCase "Erased union cases of interface type are told apart by their members" <| fun () -> + let describe = function + | ErasedCaseTest.Shape.Circle c -> sprintf "circle %.1f" c.radius + | ErasedCaseTest.Shape.Rect r -> sprintf "rect %.1f" r.width + | ErasedCaseTest.Shape.Count n -> sprintf "count %i" n + | ErasedCaseTest.Shape.Label l -> "label " + l + ErasedCaseTest.Shape.Circle (ErasedCaseTest.circle 1.5) |> describe |> equal "circle 1.5" + ErasedCaseTest.Shape.Rect (ErasedCaseTest.rect 2.5) |> describe |> equal "rect 2.5" + ErasedCaseTest.Shape.Count 3 |> describe |> equal "count 3" + ErasedCaseTest.Shape.Label "x" |> describe |> equal "label x" + + testCase "Erased union cases of inherited interface types are told apart by their members" <| fun () -> + let describe = function + | ErasedCaseTest.Pet.Animal a -> "animal " + a.name + | ErasedCaseTest.Pet.Dog d -> "dog " + d.name + " " + d.bark + ErasedCaseTest.Pet.Animal (ErasedCaseTest.animal "cat") |> describe |> equal "animal cat" + ErasedCaseTest.Pet.Dog (ErasedCaseTest.dog "rex" "woof") |> describe |> equal "dog rex woof" + + testCase "Erased union cases of interface type use the CaseTest of the other case" <| fun () -> + let describe = function + | ErasedCaseTest.Tagged.B k -> "b " + k.kind + | ErasedCaseTest.Tagged.A k -> "a " + k.kind + ErasedCaseTest.Tagged.A (ErasedCaseTest.kinded "a") |> describe |> equal "a a" + ErasedCaseTest.Tagged.B (ErasedCaseTest.kinded "b") |> describe |> equal "b b" + + testCase "Erased union cases of the same type use the CaseTest of the other case" <| fun () -> + let describe = function + | ErasedCaseTest.Id.Slug s -> "slug " + s + | ErasedCaseTest.Id.Name n -> "name " + n + | ErasedCaseTest.Id.Count c -> sprintf "count %i" c + ErasedCaseTest.Id.Name "n:x" |> describe |> equal "name n:x" + ErasedCaseTest.Id.Slug "s:y" |> describe |> equal "slug s:y" + ErasedCaseTest.Id.Count 3 |> describe |> equal "count 3" + +#if FABLE_COMPILER + testCase "Case testing with TS tagged unions whose cases share a tag works on raw JS objects" <| fun () -> + let describeShared = function + | TaggedUnion.SharedTag.Done v -> sprintf "done: %.1f" v + | TaggedUnion.SharedTag.DoneError e -> sprintf "error: %s" e + | TaggedUnion.SharedTag.DoneFailed _ -> "failed" + | TaggedUnion.SharedTag.Pending -> "pending" + let describeDerived = function + | TaggedUnion.DerivedTag.Done v -> sprintf "done: %.1f" v + | TaggedUnion.DerivedTag.DoneError e -> sprintf "error: %s" e + | TaggedUnion.DerivedTag.LogString m -> sprintf "string: %s" m + | TaggedUnion.DerivedTag.LogNumber n -> sprintf "number: %.1f" n + | _ -> "other" + let describePojo = function + | TaggedUnion.PojoField.LogOptions o -> "options: " + o.name + | TaggedUnion.PojoField.LogString s -> "string: " + s + let describeOutcome = function + | TaggedUnion.Outcome.Failed _ -> "failed" + | TaggedUnion.Outcome.Ok _ -> "ok" + let describeJob = function + | TaggedUnion.Job.Other k -> "other: " + k + | TaggedUnion.Job.Build k -> "build: " + k + | TaggedUnion.Job.Test k -> "test: " + k + | TaggedUnion.Job.Idle -> "idle" + let describeNumber = function + | TaggedUnion.NumberTag.Text t -> "text: " + t + | TaggedUnion.NumberTag.Count c -> sprintf "count: %i" c + | TaggedUnion.NumberTag.On _ -> "on" + | TaggedUnion.NumberTag.Off _ -> "off" + | TaggedUnion.NumberTag.Idle -> "idle" + !!{| state = "done"; error = "raw" |} |> describeShared |> equal "error: raw" + !!{| state = "done"; error = "raw" |} |> describeDerived |> equal "error: raw" + !!{| state = "log"; message = 1 |} |> describeDerived |> equal "number: 1.0" + !!{| state = "log"; message = "raw" |} |> describeDerived |> equal "string: raw" + !!{| state = "log"; message = {| name = "raw" |} |} |> describePojo |> equal "options: raw" + !!{| state = "log"; message = "raw" |} |> describePojo |> equal "string: raw" + !!{| state = "done"; ok = true |} |> describeOutcome |> equal "ok" + !!{| state = "done"; ok = false |} |> describeOutcome |> equal "failed" + !!{| state = "run"; kind = "deploy" |} |> describeJob |> equal "other: deploy" + !!{| state = "run"; kind = "test" |} |> describeJob |> equal "test: test" + !!{| code = 1; count = 2 |} |> describeNumber |> equal "count: 2" + !!{| code = 1; text = "raw" |} |> describeNumber |> equal "text: raw" + !!{| code = 2; flag = true |} |> describeNumber |> equal "on" + !!{| code = 2; flag = false |} |> describeNumber |> equal "off" + !!{| code = 3 |} |> describeNumber |> equal "idle" + let describeShape = function + | ErasedCaseTest.Shape.Circle c -> sprintf "circle %.1f" c.radius + | ErasedCaseTest.Shape.Rect r -> sprintf "rect %.1f" r.width + | ErasedCaseTest.Shape.Count n -> sprintf "count %i" n + | ErasedCaseTest.Shape.Label l -> "label " + l + let describePet = function + | ErasedCaseTest.Pet.Animal a -> "animal " + a.name + | ErasedCaseTest.Pet.Dog d -> "dog " + d.name + " " + d.bark + let describeTagged = function + | ErasedCaseTest.Tagged.B k -> "b " + k.kind + | ErasedCaseTest.Tagged.A k -> "a " + k.kind + let describeId = function + | ErasedCaseTest.Id.Slug s -> "slug " + s + | ErasedCaseTest.Id.Name n -> "name " + n + | ErasedCaseTest.Id.Count c -> sprintf "count %i" c + !!{| radius = 3.0 |} |> describeShape |> equal "circle 3.0" + !!{| width = 4.0; area = fun () -> 1.0 |} |> describeShape |> equal "rect 4.0" + !!5 |> describeShape |> equal "count 5" + !!"raw" |> describeShape |> equal "label raw" + !!{| name = "cat" |} |> describePet |> equal "animal cat" + !!{| name = "rex"; bark = "woof" |} |> describePet |> equal "dog rex woof" + !!{| kind = "a" |} |> describeTagged |> equal "a a" + !!{| kind = "b" |} |> describeTagged |> equal "b b" + !!"n:raw" |> describeId |> equal "name n:raw" + !!"raw" |> describeId |> equal "slug raw" + !!7 |> describeId |> equal "count 7" + + testCase "TS tagged union case with an interface field compiles to the value itself" <| fun () -> + let range = TaggedUnion.TakeRange(1, 3) + let command = TaggedUnion.Command.Take range + obj.ReferenceEquals(command, range) |> equal true + jsTypeof command?Item |> equal "undefined" + command?``type`` |> equal "take" + command.IsTake |> equal true + command.IsSave |> equal false + range.Extend 2 + match command with + | TaggedUnion.Command.Take t -> t.toIndex |> equal 5 + | _ -> failwith "expected Take" + + testCase "TS tagged union case with an interface field reads raw JS objects" <| fun () -> + let describe = function + | TaggedUnion.Command.Take t -> sprintf "take: %d-%d" t.fromIndex t.toIndex + | TaggedUnion.Command.Edit e -> sprintf "edit: %s" e.text + | TaggedUnion.Command.Save -> "save" + !!{| ``type`` = "take"; fromIndex = 2; toIndex = 5 |} |> describe |> equal "take: 2-5" + !!{| ``type`` = "edit"; edit = {| text = "hi" |} |} |> describe |> equal "edit: hi" + !!{| ``type`` = "save" |} |> describe |> equal "save" + + testCase "TS tagged union case with a named interface field compiles to an object" <| fun () -> + let edit = { new TaggedUnion.Edit with member _.text = "hi" } + let command = TaggedUnion.Command.Edit edit + obj.ReferenceEquals(command, edit) |> equal false + command?``type`` |> equal "edit" + obj.ReferenceEquals(command?edit, edit) |> equal true + match command with + | TaggedUnion.Command.Edit e -> e.text |> equal "hi" + | _ -> failwith "expected Edit" + + // See #3837 + testCase "TS tagged union case with a single primitive field carries the tag" <| fun () -> + let one = TaggedUnion.AuthState.One "a" + one?``type`` |> equal "one" + one?Item |> equal "a" + let describe = function + | TaggedUnion.AuthState.Zero -> "zero" + | TaggedUnion.AuthState.One s -> "one: " + s + | TaggedUnion.AuthState.Two(a, b) -> "two: " + a + b + one |> describe |> equal "one: a" + TaggedUnion.AuthState.Two("a", "b") |> describe |> equal "two: ab" + TaggedUnion.AuthState.Zero |> describe |> equal "zero" + + testCase "TS tagged union cases with interface fields sharing a tag are told apart by the interface members" <| fun () -> + let describe = function + | TaggedUnion.Shape.Circle c -> sprintf "circle: %.1f" c.radius + | TaggedUnion.Shape.Rect r -> sprintf "rect: %.1f x %.1f" r.width r.height + !!{| ``type`` = "shape"; radius = 2.0 |} |> describe |> equal "circle: 2.0" + !!{| ``type`` = "shape"; width = 2.0; height = 3.0 |} |> describe |> equal "rect: 2.0 x 3.0" #endif testCase "Pattern matching with StringEnum works" <| fun () -> diff --git a/tests/Js/Main/ListTests.fs b/tests/Js/Main/ListTests.fs index d5f9a68272..17e8cd3fc5 100644 --- a/tests/Js/Main/ListTests.fs +++ b/tests/Js/Main/ListTests.fs @@ -1075,4 +1075,136 @@ let tests = List.distinct sample |> List.length |> equal 3 throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + + testCase "List random functions give .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + List.randomShuffleWith rng [0 .. 4] |> equal [3; 1; 2; 4; 0] + rng.Next() |> equal 361709742 + let rng = System.Random(42) + List.randomSampleWith rng 10 [0 .. 99] |> equal [66; 14; 12; 52; 16; 26; 72; 51; 17; 76] + rng.Next() |> equal 503774878 + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + + testCase "List random functions give FSharp.Core's error messages" <| fun () -> + throwsError "The index is outside the legal range.\nrandomizer returned NaN, should be in range [0.0, 1.0). (Parameter 'randomizer')" (fun () -> List.randomChoiceBy (fun () -> nan) [1; 2]) + throwsError "The input sequence was empty. (Parameter 'source')" (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + throwsError "The input must be non-negative.\ncount = -1 (Parameter 'count')" (fun () -> List.randomChoicesWith (System.Random(1)) -1 [1]) + + testCase "List functions raise ArgumentException on empty lists" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.tail xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.last xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.maxBy id xs) |> equal "ArgumentException" + + testCase "List Head and Tail properties raise InvalidOperationException on empty lists" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> xs.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> xs.Tail) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + + testCase "List builders leave the empty list untouched" <| fun () -> + let xs = [1; 2; 3] + List.map ((+) 1) xs |> equal [2; 3; 4] + List.filter (fun x -> x > 1) xs |> equal [2; 3] + List.filter (fun _ -> false) xs |> equal [] + List.choose (fun x -> if x > 2 then Some x else None) xs |> equal [3] + List.collect (fun x -> [x; x]) xs |> equal [1; 1; 2; 2; 3; 3] + List.append xs xs |> equal [1; 2; 3; 1; 2; 3] + List.concat [xs; xs] |> equal [1; 2; 3; 1; 2; 3] + List.ofSeq (seq { 1 .. 3 }) |> equal xs + List.partition (fun x -> x > 1) xs |> equal ([2; 3], [1]) + List.scan (+) 0 xs |> equal [0; 1; 3; 6] + List.take 2 xs |> equal [1; 2] + List.init 3 id |> equal [0; 1; 2] + List.unfold (fun s -> if s < 3 then Some (s, s + 1) else None) 0 |> equal [0; 1; 2] + let empty: int list = [] + List.isEmpty empty |> equal true + List.length empty |> equal 0 + List.isEmpty (List.map ((+) 1) empty) |> equal true + empty |> equal [] + + testCase "List functions raise FSharp.Core's exception types for out of range counts" <| fun () -> + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.splitAt 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.splitAt -1 [1]) |> equal "ArgumentException" + + testCase "List functions raise FSharp.Core's exception types for invalid arguments" <| fun () -> + raisedExceptionType (fun () -> List.windowed 0 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) [1] [1; 2] 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[1]; [1; 2]]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun i -> i + 1) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun _ -> 0) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomShuffleBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> 1.0) 2 [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + + testCase "List functions raise ArgumentException for lists of different lengths" <| fun () -> + let xs = [1; 2] + let ys = [1; 2; 3] + let empty: int list = [] + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) empty xs 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.mapi2 (fun i x y -> i + x + y) ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x y z -> x + y + z) xs ys ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x y z -> x + y + z) ys ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip empty xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip xs empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 xs ys ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iter2 (fun _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iteri2 (fun _ _ _ -> ()) ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun x y -> x = y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun x y -> x = y) empty xs) |> equal "ArgumentException" + + testCase "List functions call the function on the common items of lists of different lengths before raising" <| fun () -> + let mutable calls = 0 + raisedExceptionType (fun () -> List.iter2 (fun _ _ -> calls <- calls + 1) [1; 2] [1; 2; 3]) |> equal "ArgumentException" + equal 2 calls + raisedExceptionType (fun () -> List.map3 (fun x _ _ -> calls <- calls + 1; x) [1; 2; 3] [1; 2] [1; 2; 3]) |> equal "ArgumentException" + equal 4 calls + + testCase "List.forall2 returns false before reaching the end of lists of different lengths" <| fun () -> + List.forall2 (fun x y -> x + y < 3) [1; 2] [1; 2; 3] |> equal false + List.forall2 (fun _ _ -> false) [1; 2; 3] [1; 2] |> equal false + + testCase "List.init and List.replicate raise ArgumentException for negative counts" <| fun () -> + raisedExceptionType (fun () -> List.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.replicate -1 "a") |> equal "ArgumentException" + List.init 0 id |> equal [] + + testCase "List.average and List.averageBy raise ArgumentException on empty lists" <| fun () -> + let xs: float list = [] + let ys: decimal list = [] + raisedExceptionType (fun () -> List.average xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.averageBy (fun x -> x * 2.) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.average ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.averageBy id ys) |> equal "ArgumentException" + + testCase "List.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [1; 2; 3] + let empty: int list = [] + raisedExceptionType (fun () -> List.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 2 2 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 3 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 4 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 0 1 empty) |> equal "ArgumentException" + List.removeManyAt 0 0 xs |> equal [1; 2; 3] + List.removeManyAt 1 0 xs |> equal [1; 2; 3] + List.removeManyAt 3 0 xs |> equal [1; 2; 3] + List.removeManyAt 1 -1 xs |> equal [1; 2; 3] + List.removeManyAt 0 0 empty |> equal [] + List.removeManyAt 1 2 xs |> equal [1] ] diff --git a/tests/Js/Main/Main.fs b/tests/Js/Main/Main.fs index f42d889826..f957ec1b76 100644 --- a/tests/Js/Main/Main.fs +++ b/tests/Js/Main/Main.fs @@ -34,6 +34,7 @@ let allTests = Maps.tests Misc.tests Nullness.tests + Nullable.tests NestedAndRecursivePatternTests.tests Observable.tests Option.tests @@ -91,10 +92,15 @@ run() #else +open System.Globalization open Expecto [] let main args = + // Fable formats with the invariant culture, so the .NET run must too + CultureInfo.DefaultThreadCurrentCulture <- CultureInfo.InvariantCulture + CultureInfo.CurrentCulture <- CultureInfo.InvariantCulture + Array.toList allTests |> testList "All" |> runTestsWithCLIArgs [] args diff --git a/tests/Js/Main/MapTests.fs b/tests/Js/Main/MapTests.fs index eee708d352..caa3d043e1 100644 --- a/tests/Js/Main/MapTests.fs +++ b/tests/Js/Main/MapTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Maps open System.Collections.Generic open Util.Testing +open Fable.Tests.Util type R = { i: int; s: string } type R2 = { kv: KeyValuePair } @@ -320,4 +321,19 @@ let tests = let ys = [| ("D", 4); ("E", 5); ("F", 6) |] |> Array.map KeyValuePair coll.CopyTo(ys, 0) ys = (xs |> Array.map KeyValuePair) |> equal true + + testCase "Map enumerator Current raises InvalidOperationException outside the enumeration" <| fun () -> + let m = Map [1, "a"; 2, "b"] + let e = (m :> seq>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + + testCase "Map raises NotSupportedException when mutated through IDictionary or ICollection" <| fun () -> + let dic = Map [1, "a"] :> IDictionary + let coll = Map [1, "a"] :> ICollection> + raisedExceptionType (fun () -> dic.Add(2, "b")) |> equal "NotSupportedException" + raisedExceptionType (fun () -> dic.Remove(1)) |> equal "NotSupportedException" + raisedExceptionType (fun () -> dic.[2] <- "b") |> equal "NotSupportedException" + raisedExceptionType (fun () -> coll.Clear()) |> equal "NotSupportedException" ] diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 41a54fcd46..3cf26f5f4e 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -510,6 +510,64 @@ let buildRecordWithMutableFnWithFor (xs: RecordWithMutableFn list) = let x = xs[i] items <- (x.MutableFn >>* ()) :: items items + +let paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +/// `~` and `^` are *active*/superscript characters, see #5252. +let docCommentWithCommentEnd (c: char) = string c + +/// Ends a block comment */ and opens one /* with a trailing * +type DocCommentRecord = + { + /// Field doc */ + Value: int + } + +/// Class doc */ +type DocCommentClass() = + /// Member doc */ + member _.Get() = 1 + +let mutable private forLoopModuleBound = 3 + +type ForLoopBound() = + let mutable bound = 3 + + member val Bound = 3 with get, set + + member _.FieldBoundIterations() = + let mutable n = 0 + for _ = 0 to bound do + bound <- 1 + n <- n + 1 + n + + member this.PropertyBoundIterations() = + let mutable n = 0 + for _ = 0 to this.Bound do + this.Bound <- 1 + n <- n + 1 + n + +let rec private forLoopBoundTailCall (n: int) acc = + if n = 0 then + acc + else + let xs = ResizeArray [ for _ in 1 .. n -> 1 ] + let mutable c = 0 + for _ = 0 to xs.Count - 1 do + if xs.Count < 20 then + xs.Add 0 + c <- c + 1 + forLoopBoundTailCall (n - 1) (acc + c) + +let mutable forLoopPublicArray = [| 1; 2; 3 |] +let mutable private forLoopPrivateArray = [| 1; 2; 3 |] + let tests = testList "Miscellaneous" [ testCase "Hoisted vars don't conflict with while loop" <| fun () -> // see #4031 @@ -739,6 +797,241 @@ let tests = fns.Add(fun () -> i) fns |> Seq.map (fun f -> f ()) |> Seq.toArray |> equal [| 3; 2; 1 |] + testCase "for bound over a collection is evaluated once" <| fun () -> + let shrinking = ResizeArray [ 1; 2; 3; 4 ] + let mutable n = 0 + for _ in 0 .. shrinking.Count - 1 do + if shrinking.Count > 2 then + shrinking.RemoveAt 0 + n <- n + 1 + equal 4 n + let growing = ResizeArray [ 1; 2 ] + let mutable m = 0 + for _ = 0 to growing.Count - 1 do + if growing.Count < 8 then + growing.Add 9 + m <- m + 1 + equal 2 m + + testCase "for bound with side effects is evaluated once" <| fun () -> + let calls = ref 0 + let hi () = + calls.Value <- calls.Value + 1 + 2 + for _ in 0 .. hi () do + () + equal 1 calls.Value + + testCase "for downto bound is evaluated once" <| fun () -> + let xs = ResizeArray [ 1; 2; 3; 4 ] + let mutable n = 0 + for _ = xs.Count - 1 downto xs.Count - 2 do + xs.RemoveAt 0 + n <- n + 1 + equal 2 n + let ys = ResizeArray [ 1; 2; 3 ] + let mutable m = 0 + for _ in 3 .. -1 .. ys.Count - 2 do + ys.Add 0 + m <- m + 1 + equal 3 m + + testCase "for bound reading a mutable local changed in the body is evaluated once" <| fun () -> + let mutable hi = 3 + let mutable a = 0 + for _ = 0 to hi do + hi <- 10 + a <- a + 1 + equal 4 a + let mutable lo = 3 + let mutable b = 0 + for _ = 5 downto lo do + lo <- 0 + b <- b + 1 + equal 3 b + let mutable x = 2 + let mutable c = 0 + for _ = 0 to x + 1 do + x <- 100 + c <- c + 1 + equal 4 c + let mutable str = "abc" + let mutable d = 0 + for _ = 0 to str.Length - 1 do + str <- "a" + d <- d + 1 + equal 3 d + + testCase "for bound reading a ref cell, a field or a module mutable is evaluated once" <| fun () -> + let r = ref 2 + let mutable a = 0 + for _ = 0 to r.Value do + r.Value <- 7 + a <- a + 1 + equal 3 a + ForLoopBound().FieldBoundIterations() |> equal 4 + ForLoopBound().PropertyBoundIterations() |> equal 4 + forLoopModuleBound <- 3 + let mutable b = 0 + for _ = 0 to forLoopModuleBound do + forLoopModuleBound <- 1 + b <- b + 1 + equal 4 b + + testCase "for start is evaluated before the bound and both only once" <| fun () -> + let log = ResizeArray() + let tick name value = + log.Add name + value + for _ = tick "start" 0 to tick "limit" 1 do + log.Add "body" + for _ = tick "start" 1 downto tick "limit" 0 do + log.Add "body" + String.concat "," log |> equal "start,limit,body,body,start,limit,body,body" + + testCase "Nested for bounds are each evaluated once per entry" <| fun () -> + let log = ResizeArray() + let tick name value = + log.Add name + value + let limit = 0 + for _ = 0 to tick "outer" 1 + limit do + for _ = 0 to tick "inner" 0 + limit do + let limit = 5 + log.Add(string limit) + String.concat "," log |> equal "outer,inner,5,inner,5" + + testCase "Closures in a for body work when the bound is evaluated once" <| fun () -> + let fns = ResizeArray int>() + let xs = ResizeArray [ 1; 2; 3 ] + for i = 0 to xs.Count - 1 do + let doubled = i * 2 + fns.Add(fun () -> doubled + i + xs.Count) + if xs.Count < 8 then + xs.Add 0 + fns |> Seq.map (fun f -> f ()) |> Seq.toArray |> equal [| 6; 9; 12 |] + + testCase "for bound is evaluated once in tail recursive functions" <| fun () -> + forLoopBoundTailCall 3 0 |> equal 6 + + testCase "for bound is evaluated once when the loop is in expression position" <| fun () -> + let growUpTo20 (xs: ResizeArray) = + if xs.Count < 20 then + xs.Add 0 + let xs = ResizeArray [ 1; 2 ] + let unitResult = (for _ = 0 to xs.Count - 1 do growUpTo20 xs) + equal () unitResult + equal 4 xs.Count + let ys = ResizeArray [ 1; 2 ] + let run enabled = + if enabled then + for _ = 0 to ys.Count - 1 do + growUpTo20 ys + run true + equal 4 ys.Count + let zs = ResizeArray [ 1; 2 ] + let count = + try + for _ = 0 to zs.Count - 1 do + growUpTo20 zs + zs.Count + with _ -> -1 + equal 4 count + + testCase "for bound that needs statements is evaluated once" <| fun () -> + let log = ResizeArray() + let tick name value = + log.Add name + value + let xs = ResizeArray [ 1; 2 ] + let mutable a = 0 + for _ = 0 to (match xs.Count with 2 -> tick "match" 3 | _ -> 0) do + if xs.Count < 20 then + xs.Add 0 + a <- a + 1 + equal 4 a + let mutable b = 0 + for _ = 0 to (let x = tick "let" 1 in x + tick "let2" 1) do + b <- b + 1 + equal 3 b + let mutable c = 0 + for _ = 0 to (try tick "try" 1 with _ -> 0) do + c <- c + 1 + equal 2 c + String.concat "," log |> equal "match,let,let2,try" + + testCase "for bound is evaluated once inside a seq expression" <| fun () -> + let count = + seq { + let xs = ResizeArray [ 1; 2 ] + let grow () = + for _ = 0 to xs.Count - 1 do + if xs.Count < 20 then + xs.Add 0 + xs.Count + yield grow () + } + |> Seq.head + equal 4 count + + testCase "for bound reading the length of a mutable array minus one is evaluated each iteration" <| fun () -> + let mutable a = [| 1; 2; 3 |] + let mutable n = 0 + for _ = 0 to a.Length - 1 do + a <- [| 1; 2; 3; 4; 5; 6 |] + n <- n + 1 + equal 6 n + forLoopPublicArray <- [| 1; 2; 3 |] + let mutable m = 0 + for _ = 0 to forLoopPublicArray.Length - 1 do + forLoopPublicArray <- [| 1; 2; 3; 4; 5; 6 |] + m <- m + 1 + equal 6 m + forLoopPrivateArray <- [| 1; 2; 3 |] + let mutable p = 0 + for _ = 0 to forLoopPrivateArray.Length - 1 do + forLoopPrivateArray <- [| 1; 2; 3; 4; 5; 6 |] + p <- p + 1 + equal 6 p + MiscTestsHelper.forLoopCrossFileArray <- [| 1; 2; 3 |] + let mutable q = 0 + for _ = 0 to MiscTestsHelper.forLoopCrossFileArray.Length - 1 do + MiscTestsHelper.forLoopCrossFileArray <- [| 1; 2; 3; 4; 5; 6 |] + q <- q + 1 + equal 6 q + MiscTestsHelper.forLoopCrossFileCalls <- 0 + let mutable r = 0 + for _ = 0 to (MiscTestsHelper.forLoopCrossFileMakeArray ()).Length - 1 do + r <- r + 1 + equal 3 r + equal 1 MiscTestsHelper.forLoopCrossFileCalls + + testCase "for bound reading the length of a mutable array in other shapes is evaluated once" <| fun () -> + let mutable a = [| 1; 2; 3 |] + let mutable n = 0 + for _ = 3 downto a.Length - 1 do + a <- [| 1 |] + n <- n + 1 + equal 2 n + a <- [| 1; 2; 3 |] + let mutable m = 0 + for _ = 0 to a.Length do + a <- [| 1 |] + m <- m + 1 + equal 4 m + a <- [| 1; 2; 3 |] + let mutable p = 0 + for _ = 0 to a.Length - 2 do + a <- [| 1 |] + p <- p + 1 + equal 2 p + a <- [| 1; 2; 3 |] + let mutable q = 0 + for _ = 0 to a.Length do + if q < 10 then a <- Array.append a [| 0 |] + q <- q + 1 + equal 4 q + testCase "Self references in constructors work" <| fun () -> // See #124 let t = Test(5) equal 12 t.Value @@ -1260,6 +1553,58 @@ let tests = invalidArg "arg" "This is invalid" ) + testCase "invalidArg raises ArgumentException" <| fun () -> + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid (Parameter 'arg')" caught + + testCase "ArgumentException.ParamName works" <| fun () -> + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + + testCase "ArgumentException.ParamName of FSharp.Core errors works" <| fun () -> + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + + testCase "invalidOp raises InvalidOperationException" <| fun () -> + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid" caught + + testCase "invalidArg and invalidOp work in expression position" <| fun () -> + let pick b = if b then 1 else invalidArg "b" "This is invalid" + pick true |> equal 1 + throwsError "This is invalid (Parameter 'b')" (fun () -> pick false) + let wrap (x: int) = x + 1 + throwsError "This is invalid" (fun () -> wrap (invalidOp "This is invalid")) + Option.defaultWith (fun () -> invalidOp "This is invalid") (Some 3) |> equal 3 + throwsError "This is invalid" (fun () -> Option.defaultWith (fun () -> invalidOp "This is invalid") None) + + testCase "A read of another file's mutable value is not moved past side effects" <| fun () -> // See #5156 + let before = MiscTestsHelper.trackedCalls + MiscTestsHelper.track 1 |> ignore + MiscTestsHelper.trackedCalls - before |> equal 1 + testCase "Pattern matching optimization works (switch statement)" <| fun () -> let mutable x = "" let i = 4 @@ -1465,4 +1810,65 @@ let tests = let times2 x = x * 2 fn <- fn >> times2 fn 5 |> equal 10 + + testCase "Stopwatch.Frequency and IsHighResolution work" <| fun () -> + Diagnostics.Stopwatch.Frequency > 0L |> equal true + Diagnostics.Stopwatch.IsHighResolution |> equal true + + testCaseAsync "Stopwatch.GetTimestamp and GetElapsedTime work" <| fun () -> + async { + let start = Diagnostics.Stopwatch.GetTimestamp() + do! Async.Sleep 50 + let stop = Diagnostics.Stopwatch.GetTimestamp() + let elapsedMs = float (stop - start) * 1000. / float Diagnostics.Stopwatch.Frequency + elapsedMs >= 40. |> equal true + (Diagnostics.Stopwatch.GetElapsedTime(start, stop)).TotalMilliseconds >= 40. |> equal true + (Diagnostics.Stopwatch.GetElapsedTime start).TotalMilliseconds >= 40. |> equal true + } + + testCaseAsync "Stopwatch.StartNew and Elapsed work" <| fun () -> + async { + let sw = Diagnostics.Stopwatch.StartNew() + box sw :? Diagnostics.Stopwatch |> equal true + sw.IsRunning |> equal true + do! Async.Sleep 50 + sw.ElapsedMilliseconds >= 40L |> equal true + sw.Elapsed.TotalMilliseconds >= 40. |> equal true + sw.ElapsedTicks >= Diagnostics.Stopwatch.Frequency / 25L |> equal true + } + + testCaseAsync "Stopwatch Start, Stop and Reset work" <| fun () -> + async { + let sw = Diagnostics.Stopwatch() + sw.IsRunning |> equal false + sw.ElapsedMilliseconds |> equal 0L + sw.Start() + do! Async.Sleep 50 + sw.Stop() + sw.IsRunning |> equal false + let stopped = sw.ElapsedMilliseconds + stopped >= 40L |> equal true + do! Async.Sleep 50 + sw.ElapsedMilliseconds |> equal stopped + sw.Start() + do! Async.Sleep 50 + sw.ElapsedMilliseconds >= stopped + 40L |> equal true + sw.Reset() + sw.IsRunning |> equal false + sw.ElapsedMilliseconds |> equal 0L + } + + testCaseAsync "Stopwatch.Restart works" <| fun () -> + async { + let sw = Diagnostics.Stopwatch.StartNew() + do! Async.Sleep 100 + sw.Restart() + sw.IsRunning |> equal true + sw.ElapsedMilliseconds < 50L |> equal true + } + + testCase "Doc comments containing */ do not break the output" <| fun () -> // See #5252 + docCommentWithCommentEnd 'x' |> equal "x" + { Value = 2 }.Value |> equal 2 + DocCommentClass().Get() |> equal 1 ] diff --git a/tests/Js/Main/MiscTestsHelper.fs b/tests/Js/Main/MiscTestsHelper.fs index 3157415696..70ecf518dc 100644 --- a/tests/Js/Main/MiscTestsHelper.fs +++ b/tests/Js/Main/MiscTestsHelper.fs @@ -31,3 +31,17 @@ type Vector2<[] 'u> = Vector2 of x: float<'u> * y: float<'u> with static member inline ( + ) (Vector2(ax, ay), Vector2(bx, by)) = Vector2(ax + bx, ay + by) static member inline ( * ) (scalar, Vector2(x, y)) = Vector2(scalar * x, scalar * y) + +let mutable trackedCalls = 0 + +let track x = + trackedCalls <- trackedCalls + 1 + x + +let mutable forLoopCrossFileArray = [| 1; 2; 3 |] + +let mutable forLoopCrossFileCalls = 0 + +let forLoopCrossFileMakeArray () = + forLoopCrossFileCalls <- forLoopCrossFileCalls + 1 + [| 1; 2; 3 |] diff --git a/tests/Js/Main/NullableTests.fs b/tests/Js/Main/NullableTests.fs new file mode 100644 index 0000000000..553e519989 --- /dev/null +++ b/tests/Js/Main/NullableTests.fs @@ -0,0 +1,170 @@ +module Fable.Tests.Nullable + +open Util.Testing +open System +open FSharp.Linq + +let private none<'T when 'T: struct and 'T: (new: unit -> 'T) and 'T :> ValueType> = Nullable<'T>() + +let tests = + testList "Nullable" [ + testCase "Nullable.char" <| fun () -> + Nullable.char (Nullable 49uy) |> equal (Nullable '1') + Nullable.char (Nullable 50y) |> equal (Nullable '2') + Nullable.char (Nullable 51s) |> equal (Nullable '3') + Nullable.char (Nullable 52us) |> equal (Nullable '4') + Nullable.char (Nullable 53) |> equal (Nullable '5') + Nullable.char (Nullable 54u) |> equal (Nullable '6') + Nullable.char (Nullable 55L) |> equal (Nullable '7') + Nullable.char (Nullable 56UL) |> equal (Nullable '8') + Nullable.char (Nullable 57m) |> equal (Nullable '9') + Nullable.char (Nullable 58.0) |> equal (Nullable ':') + Nullable.char (Nullable 59.0f) |> equal (Nullable ';') + Nullable.char (Nullable 'a') |> equal (Nullable 'a') + Nullable.char none |> equal none + + testCase "Nullable.decimal" <| fun () -> + Nullable.decimal (Nullable 1uy) |> equal (Nullable 1m) + Nullable.decimal (Nullable 2y) |> equal (Nullable 2m) + Nullable.decimal (Nullable 3s) |> equal (Nullable 3m) + Nullable.decimal (Nullable 4us) |> equal (Nullable 4m) + Nullable.decimal (Nullable 5) |> equal (Nullable 5m) + Nullable.decimal (Nullable 6u) |> equal (Nullable 6m) + Nullable.decimal (Nullable 7L) |> equal (Nullable 7m) + Nullable.decimal (Nullable 8UL) |> equal (Nullable 8m) + Nullable.decimal (Nullable 9m) |> equal (Nullable 9m) + Nullable.decimal (Nullable 10.5) |> equal (Nullable 10.5m) + Nullable.decimal (Nullable 11.5f) |> equal (Nullable 11.5m) + Nullable.decimal none |> equal none + + testCase "Nullable.double" <| fun () -> + Nullable.double (Nullable 5) |> equal (Nullable 5.0) + Nullable.double (Nullable 7L) |> equal (Nullable 7.0) + Nullable.double (Nullable 9m) |> equal (Nullable 9.0) + Nullable.double (Nullable 11.5f) |> equal (Nullable 11.5) + Nullable.double none |> equal none + + testCase "Nullable.enum" <| fun () -> + (Nullable.enum (Nullable 2): Nullable) |> equal (Nullable DayOfWeek.Tuesday) + (Nullable.enum none: Nullable) |> equal none + + testCase "Nullable.float" <| fun () -> + Nullable.float (Nullable 1uy) |> equal (Nullable 1.0) + Nullable.float (Nullable 2y) |> equal (Nullable 2.0) + Nullable.float (Nullable 3s) |> equal (Nullable 3.0) + Nullable.float (Nullable 4us) |> equal (Nullable 4.0) + Nullable.float (Nullable 5) |> equal (Nullable 5.0) + Nullable.float (Nullable 6u) |> equal (Nullable 6.0) + Nullable.float (Nullable 7L) |> equal (Nullable 7.0) + Nullable.float (Nullable 8UL) |> equal (Nullable 8.0) + Nullable.float (Nullable 9m) |> equal (Nullable 9.0) + Nullable.float (Nullable 10.0) |> equal (Nullable 10.0) + Nullable.float (Nullable 11.0f) |> equal (Nullable 11.0) + Nullable.float (Nullable 'c') |> equal (Nullable 99.0) + Nullable.float none |> equal none + + testCase "Nullable.float32" <| fun () -> + Nullable.float32 (Nullable 1uy) |> equal (Nullable 1.0f) + Nullable.float32 (Nullable 2y) |> equal (Nullable 2.0f) + Nullable.float32 (Nullable 3s) |> equal (Nullable 3.0f) + Nullable.float32 (Nullable 4us) |> equal (Nullable 4.0f) + Nullable.float32 (Nullable 5) |> equal (Nullable 5.0f) + Nullable.float32 (Nullable 6u) |> equal (Nullable 6.0f) + Nullable.float32 (Nullable 7L) |> equal (Nullable 7.0f) + Nullable.float32 (Nullable 8UL) |> equal (Nullable 8.0f) + Nullable.float32 (Nullable 9m) |> equal (Nullable 9.0f) + Nullable.float32 (Nullable 10.0) |> equal (Nullable 10.0f) + Nullable.float32 (Nullable 11.0f) |> equal (Nullable 11.0f) + Nullable.float32 (Nullable 'c') |> equal (Nullable 99.0f) + Nullable.float32 none |> equal none + + testCase "Nullable.int" <| fun () -> + Nullable.int (Nullable 1uy) |> equal (Nullable 1) + Nullable.int (Nullable 2y) |> equal (Nullable 2) + Nullable.int (Nullable 3s) |> equal (Nullable 3) + Nullable.int (Nullable 4us) |> equal (Nullable 4) + Nullable.int (Nullable 5) |> equal (Nullable 5) + Nullable.int (Nullable 6u) |> equal (Nullable 6) + Nullable.int (Nullable 7L) |> equal (Nullable 7) + Nullable.int (Nullable 8UL) |> equal (Nullable 8) + Nullable.int (Nullable 9m) |> equal (Nullable 9) + Nullable.int (Nullable 10.9) |> equal (Nullable 10) + Nullable.int (Nullable -11.9f) |> equal (Nullable -11) + Nullable.int (Nullable 'c') |> equal (Nullable 99) + Nullable.int none |> equal none + + testCase "Nullable.int16" <| fun () -> + Nullable.int16 (Nullable 5) |> equal (Nullable 5s) + Nullable.int16 (Nullable -7L) |> equal (Nullable -7s) + Nullable.int16 (Nullable 9.9) |> equal (Nullable 9s) + Nullable.int16 none |> equal none + + testCase "Nullable.int32" <| fun () -> + Nullable.int32 (Nullable 5L) |> equal (Nullable 5) + Nullable.int32 (Nullable -7.9) |> equal (Nullable -7) + Nullable.int32 (Nullable 9m) |> equal (Nullable 9) + Nullable.int32 none |> equal none + + testCase "Nullable.int64" <| fun () -> + Nullable.int64 (Nullable 5) |> equal (Nullable 5L) + Nullable.int64 (Nullable 7UL) |> equal (Nullable 7L) + Nullable.int64 (Nullable -9.9) |> equal (Nullable -9L) + Nullable.int64 (Nullable 11m) |> equal (Nullable 11L) + Nullable.int64 none |> equal none + + testCase "Nullable.int8" <| fun () -> + Nullable.int8 (Nullable 5) |> equal (Nullable 5y) + Nullable.int8 (Nullable -7L) |> equal (Nullable -7y) + Nullable.int8 (Nullable 9.9) |> equal (Nullable 9y) + Nullable.int8 none |> equal none + + testCase "Nullable.sbyte" <| fun () -> + Nullable.sbyte (Nullable 5) |> equal (Nullable 5y) + Nullable.sbyte (Nullable -7.9) |> equal (Nullable -7y) + Nullable.sbyte none |> equal none + + testCase "Nullable.single" <| fun () -> + Nullable.single (Nullable 5) |> equal (Nullable 5.0f) + Nullable.single (Nullable 7L) |> equal (Nullable 7.0f) + Nullable.single (Nullable 9.5) |> equal (Nullable 9.5f) + Nullable.single none |> equal none + + testCase "Nullable.uint" <| fun () -> + Nullable.uint (Nullable 5) |> equal (Nullable 5u) + Nullable.uint (Nullable 7L) |> equal (Nullable 7u) + Nullable.uint (Nullable 9.9) |> equal (Nullable 9u) + Nullable.uint none |> equal none + + testCase "Nullable.uint16" <| fun () -> + Nullable.uint16 (Nullable 5) |> equal (Nullable 5us) + Nullable.uint16 (Nullable 7L) |> equal (Nullable 7us) + Nullable.uint16 (Nullable 9.9) |> equal (Nullable 9us) + Nullable.uint16 none |> equal none + + testCase "Nullable.uint32" <| fun () -> + Nullable.uint32 (Nullable 5) |> equal (Nullable 5u) + Nullable.uint32 (Nullable 7L) |> equal (Nullable 7u) + Nullable.uint32 (Nullable 9.9) |> equal (Nullable 9u) + Nullable.uint32 none |> equal none + + testCase "Nullable.uint64" <| fun () -> + Nullable.uint64 (Nullable 5) |> equal (Nullable 5UL) + Nullable.uint64 (Nullable 7L) |> equal (Nullable 7UL) + Nullable.uint64 (Nullable 9.9) |> equal (Nullable 9UL) + Nullable.uint64 (Nullable 11m) |> equal (Nullable 11UL) + Nullable.uint64 none |> equal none + + testCase "Nullable.byte and Nullable.uint8" <| fun () -> + Nullable.byte (Nullable 5) |> equal (Nullable 5uy) + Nullable.uint8 (Nullable 7L) |> equal (Nullable 7uy) + Nullable.uint8 (Nullable 9.9) |> equal (Nullable 9uy) + Nullable.byte none |> equal none + + testCase "Nullable conversions evaluate their argument once" <| fun () -> + let mutable calls = 0 + let next () = + calls <- calls + 1 + Nullable 5 + Nullable.float (next ()) |> equal (Nullable 5.0) + calls |> equal 1 + ] diff --git a/tests/Js/Main/QuotationTests.fs b/tests/Js/Main/QuotationTests.fs index 4149b9ba92..ce5dd64b39 100644 --- a/tests/Js/Main/QuotationTests.fs +++ b/tests/Js/Main/QuotationTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Quotation open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Util.Testing #if FABLE_COMPILER @@ -15,10 +16,33 @@ type QuotationTestUnion = let inline quotTestDouble x = x * 2 +type QuotationDefaultValueRecord = { A: int; [] mutable B: int; C: string } + type QuotationIndexerTest(values: int[]) = member _.Item with get (i: int) = values.[i] + +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + let tests = testList "Quotations" [ #if FABLE_COMPILER @@ -272,4 +296,37 @@ let tests = // Thoth Auto format: ["Call", ...] parsed[0] |> unbox |> equal "Call" #endif + testCase "DerivedPatterns AndAlso, OrElse and SpecificCall work" <| fun () -> + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" + + // A local captured by a quotation is spliced in as a Value holding its value, + // the way .NET does. It used to come through as a Var carrying only a name. + testCase "a captured local is a Value, not a Var" <| fun () -> + let captured = "SE" + let describe (e: Expr) = + match e with + | Lambda(_, body) -> + match body with + | Value(v, _) -> "Value:" + unbox v + | Var v -> "Var:" + v.Name + | _ -> "other" + | _ -> "not a lambda" + describe <@ fun (_: int) -> captured @> |> equal "Value:SE" + describe <@ fun (_: int) -> "SE" @> |> equal "Value:SE" + // Checks that a bound variable is still a Var, not its name: JavaScript + // renames the lambda argument (x -> x_10), so the name is not portable. + (match <@ fun (x: string) -> x @> with + | Lambda(_, Var _) -> "Var" + | _ -> "?") + |> equal "Var" + + testCase "NewRecord leaves out DefaultValue fields" <| fun () -> + (match <@ { A = 1; C = "c" }: QuotationDefaultValueRecord @> with + | NewRecord(_, args) -> args |> Seq.map (function Value(v, _) -> string v | _ -> "?") |> String.concat "," + | _ -> "other") + |> equal "1,c" ] diff --git a/tests/Js/Main/RandomTests.fs b/tests/Js/Main/RandomTests.fs index 5a56c1a019..97cb7bc59d 100644 --- a/tests/Js/Main/RandomTests.fs +++ b/tests/Js/Main/RandomTests.fs @@ -84,6 +84,27 @@ let tests = throwsAnyError <| fun () -> rnd.Next(14, 10) throwsAnyError <| fun () -> rnd.NextBytes(null) + testCase "System.Random seeded Next works for ranges above Int32.MaxValue" <| fun () -> + let rnd = Random(42) + rnd.Next(Int32.MinValue, Int32.MaxValue) |> equal 1434747709 + rnd.Next(Int32.MinValue, Int32.MaxValue) |> equal -269548476 + let rnd = Random(0) + [ for _ in 1 .. 3 -> rnd.Next(-1000000000, 2000000000) ] |> equal [-589364906; 1652034034; 809049730] + + testCase "System.Random gives .NET's argument errors" <| fun () -> + for rnd in [Random(42); Random()] do + throwsErrorContaining "maxValue ('-1') must be a non-negative value. (Parameter 'maxValue')" (fun () -> rnd.Next(-1)) + throwsError "'minValue' cannot be greater than maxValue. (Parameter 'minValue')" (fun () -> rnd.Next(5, 3)) + raisedExceptionType (fun () -> rnd.Next(-1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> rnd.Next(5, 3)) |> equal "ArgumentOutOfRangeException" + + testCase "System.Random.Shared works" <| fun () -> + let x = Random.Shared.Next(5) + (x >= 0 && x < 5) |> equal true + let x = Random.Shared.NextDouble() + (x >= 0.0 && x < 1.0) |> equal true + obj.ReferenceEquals(Random.Shared, Random.Shared) |> equal true + testCase "System.Random seeded passes chi-squared test" <| fun () -> let rnd = Random(2026) let statistic = chiSquaredStatistic 16 10000 (fun () -> rnd.Next(16)) diff --git a/tests/Js/Main/RecordTypeTests.fs b/tests/Js/Main/RecordTypeTests.fs index 13d52494f2..6b71bde751 100644 --- a/tests/Js/Main/RecordTypeTests.fs +++ b/tests/Js/Main/RecordTypeTests.fs @@ -56,6 +56,52 @@ type RecordWithStaticMemberVal = { xs: ResizeArray } static member val empty = { xs = ResizeArray() } +[] +type StructRecord = { Size: int; Seed: int; Path: int list } + +type StructRecordHolder = { Tag: int; Data: StructRecord } + +let updateStructRecord (data: StructRecord) (path: int list) = { data with Path = List.rev path } + +let holdStructRecord (data: StructRecord) (path: int list) = + Some + { + Tag = 1 + Data = { data with Path = List.rev path } + } + +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +[] +type StructRecordOrdered = { OrdA: int; OrdB: int; OrdC: int } + +type ByRefClassRecord = { BcP: int; BcQ: int } + +type ByRefClass = + val mutable BkF: int + val BkV: int + new() = { BkF = 0; BkV = 3 } + +let sumStructRecordByRef (r: byref) = r.OrdA + r.OrdC + +let copyStructRecordByRef (r: byref) = { r with OrdB = 99 } + +let sumClassRecordByRef (r: byref) = r.BcP + r.BcQ + +let setClassRecordOutRef (r: outref) = + r <- { BcP = 4; BcQ = 1 } + r.BcP + r.BcQ + +let sumClassByRef (r: byref) = r.BkF + r.BkV + +let sumAnonymousStructRecordByRef (x: byref) = x.AnX + x.AnY + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + let tests = testList "RecordTypes" [ @@ -112,6 +158,17 @@ let tests = record.B |> equal 11 record.C |> equal 6 + testCase "Copy-and-update of a struct record argument works" <| fun () -> + let data: StructRecord = { Size = 1; Seed = 2; Path = [ 3; 4 ] } + let updated = updateStructRecord data [ 5; 6 ] + equal 1 updated.Size + equal 2 updated.Seed + equal [ 6; 5 ] updated.Path + let held = holdStructRecord data [ 7; 8 ] |> Option.get + equal 1 held.Data.Size + equal 2 held.Data.Seed + equal [ 8; 7 ] held.Data.Path + testCase "Recursive record does not cause issues" <| fun () -> let r = { things = [ { things = [] } ] } equal r.things.Length 1 @@ -185,4 +242,52 @@ let tests = equal 1 a.xs.Count equal 1 b.xs.Count + testCase "Copy-and-update of a let mutable struct record keeps the unchanged fields" <| fun () -> + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + + testCase "Copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields" <| fun () -> + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + + testCase "Copy-and-update of a module-level mutable struct record keeps the unchanged fields" <| fun () -> + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + + testCase "Field reads and copy-and-update through a byref struct record parameter work" <| fun () -> + let mutable r = { OrdA = 1; OrdB = 2; OrdC = 3 } + sumStructRecordByRef &r |> equal 4 + let x = copyStructRecordByRef &r + (x.OrdA, x.OrdB, x.OrdC) |> equal (1, 99, 3) + + testCase "Field reads through a byref or outref to a reference type work" <| fun () -> + let mutable c = { BcP = 1; BcQ = 2 } + sumClassRecordByRef &c |> equal 3 + let mutable o = { BcP = 0; BcQ = 0 } + setClassRecordOutRef &o |> equal 5 + let mutable k = ByRefClass() + k.BkF <- 4 + sumClassByRef &k |> equal 7 + + testCase "Copy-and-update of a let mutable anonymous struct record keeps the unchanged fields" <| fun () -> + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) + + testCase "Field reads through a byref to an anonymous struct record work" <| fun () -> + let mutable a = struct {| AnX = 1; AnY = 2 |} + sumAnonymousStructRecordByRef &a |> equal 3 + ] diff --git a/tests/Js/Main/ReflectionTests.fs b/tests/Js/Main/ReflectionTests.fs index 453e15cb43..b23c8716d9 100644 --- a/tests/Js/Main/ReflectionTests.fs +++ b/tests/Js/Main/ReflectionTests.fs @@ -53,6 +53,23 @@ type MyClass<'T1, 'T2>(v1: 'T1, v2: 'T2) = member _.Value1 = v1 member _.Value2 = v2 +[] +type SelfBase<'Inheritor when 'Inheritor :> SelfBase<'Inheritor>>() = + member _.Dummy() = 1 + +type SelfInheritor() = + inherit SelfBase() + +[] +type PairBase<'Other>() = + member _.Dummy() = 2 + +type PairPing() = + inherit PairBase() + +and PairPong() = + inherit PairBase() + let genericTests = [ testCase "typedefof works" <| fun () -> let tdef1 = typedefof @@ -705,6 +722,17 @@ let reflectionTests = [ t2.IsSubclassOf(t1) |> equal true t1.IsSubclassOf(t2) |> equal false + testCase "GetType works when a generic base takes the class itself" <| fun () -> // See #3607 + let t = SelfInheritor().GetType() + t.Name |> equal "SelfInheritor" + t = typeof |> equal true + t.IsSubclassOf(typeof>) |> equal true + SelfInheritor().Dummy() |> equal 1 + let ping = PairPing().GetType() + ping.Name |> equal "PairPing" + ping.IsSubclassOf(typeof>) |> equal true + PairPong().GetType().IsSubclassOf(typeof>) |> equal true + testCase ".GetInterface works when types are know at compile time" <| fun () -> // #2321 let t = typeof>.GetInterface("MyInterface`1") t.GetGenericArguments().[0] = typeof |> equal true diff --git a/tests/Js/Main/RegexTests.fs b/tests/Js/Main/RegexTests.fs index 657f48b715..934d388c14 100644 --- a/tests/Js/Main/RegexTests.fs +++ b/tests/Js/Main/RegexTests.fs @@ -37,6 +37,13 @@ let tests = let r = Regex("[a-z]", options) int r.Options |> equal 257 + testCase "RegexOptions.NonBacktracking is accepted" <| fun _ -> + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + testCase "Regex.IsMatch with IgnoreCase and Multiline works" <| fun _ -> let str = "ab\ncd" let option1 = RegexOptions.IgnoreCase diff --git a/tests/Js/Main/ResizeArrayTests.fs b/tests/Js/Main/ResizeArrayTests.fs index 85a9b0d44e..408699ff97 100644 --- a/tests/Js/Main/ResizeArrayTests.fs +++ b/tests/Js/Main/ResizeArrayTests.fs @@ -6,6 +6,34 @@ open Fable.Tests.Util type Animal = Duck of int | Dog of int +// Fable routes System.Collections.IList members to the ResizeArray replacements +type CustomList() = + let items = ResizeArray() + member _.Items = items + interface System.Collections.IList with + member _.Item + with get i = items.[i] + and set i v = items.[i] <- v + member _.IndexOf(item) = items.IndexOf(item) + member _.Insert(i, item) = items.Insert(i, item) + member _.RemoveAt(i) = items.RemoveAt(i) + member _.Count = items.Count + member _.IsReadOnly = false + member _.IsFixedSize = false + member _.IsSynchronized = false + member _.SyncRoot = box items + member _.Add(item) = + items.Add(item) + items.Count - 1 + member _.Clear() = items.Clear() + member _.Contains(item) = items.Contains(item) + member _.CopyTo(array, arrayIndex) = items.CopyTo(array :?> obj[], arrayIndex) + member _.Remove(item) = items.Remove(item) |> ignore + interface IEnumerable with + member _.GetEnumerator() = (items :> IEnumerable).GetEnumerator() + interface System.Collections.IEnumerable with + member _.GetEnumerator() = (items :> System.Collections.IEnumerable).GetEnumerator() + let tests = testList "ResizeArrays" [ testCase "ResizeArray zero creation works" <| fun () -> @@ -255,6 +283,63 @@ let tests = ns.Sort(comparer) Seq.toList ns |> equal [1;2;3] + testCase "ResizeArray.BinarySearch works" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(7) |> equal 3 + ra.BinarySearch(8) |> equal -5 + let desc = ComparisonIdentity.FromFunction(fun (a: int) b -> compare b a) + (ResizeArray [9; 7; 5; 3; 1]).BinarySearch(3, desc) |> equal 3 + ra.BinarySearch(1, 3, 7, Comparer.Default) |> equal 3 + ra.BinarySearch(1, 2, 8, Comparer.Default) |> equal -4 + ra.BinarySearch(1, 2, 3, null) |> equal 1 + + testCase "ResizeArray.BinarySearch works with an empty list, duplicates and a null range comparer" <| fun () -> + ResizeArray().BinarySearch(1) |> equal -1 + (ResizeArray [2; 2; 2; 2]).BinarySearch(2) |> equal 1 + (ResizeArray [1; 2; 2; 2; 2; 3]).BinarySearch(2) |> equal 2 + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(0, 5, 2, null) |> equal -2 + ra.BinarySearch(2, 3, 9, null) |> equal 4 + + testCase "ResizeArray.BinarySearch with a range passes the element to the comparer first" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + ra.BinarySearch(1, 3, 7, logging) |> equal 3 + List.ofSeq calls |> equal [(5, 7); (7, 7)] + + testCase "ResizeArray.BinarySearch with a range checks index and count together" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(6, 0, 3, null)) |> equal "ArgumentException" + raised (fun () -> ra.BinarySearch(0, 6, 3, null)) |> equal "ArgumentException" + + testCase "ResizeArray.BinarySearch with a range checks its arguments" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, 2, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(3, 3, 3, null)) |> equal "ArgumentException" + testCase "ResizeArray.ToArray works" <| fun () -> let li = ResizeArray<_>() li.Add(3.); li.Add(6.); li.Add(5.); li.Add(4.); li.Add(8.) @@ -350,6 +435,13 @@ let tests = coll.Clear() coll.Count |> equal 0 + testCase "IList.Clear works on a custom collection" <| fun _ -> + let xs = CustomList() + xs.Items.Add(box 1) + xs.Items.Add(box 2) + (xs :> System.Collections.IList).Clear() + xs.Items.Count |> equal 0 + testCase "ResizeArray ICollection.Add works" <| fun _ -> let xs = [| ("A", 1); ("B", 2); ("C", 3) |] let coll = (ResizeArray xs) :> ICollection<_> diff --git a/tests/Js/Main/SeqTests.fs b/tests/Js/Main/SeqTests.fs index 620324d219..4a46414ec6 100644 --- a/tests/Js/Main/SeqTests.fs +++ b/tests/Js/Main/SeqTests.fs @@ -568,6 +568,13 @@ let tests = |> Seq.reduce (+) |> equal 19990000000000002000I + testCase "Ranges with a zero step raise ArgumentException" <| fun () -> + let step = 0 + raisedExceptionType (fun () -> [0 .. step .. 5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [|0L .. 0L .. 5L|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [0. .. 0. .. 1.]) |> equal "ArgumentException" + raisedExceptionType (fun () -> seq {0M .. 0M .. 1M} |> Seq.toList) |> equal "ArgumentException" + testCase "Seq.reduce works" <| fun () -> let xs = [1.; 2.] xs |> Seq.reduce (+) @@ -1000,6 +1007,28 @@ let tests = throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + testCase "Seq random functions give .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + Seq.randomShuffleWith rng [0 .. 4] |> Seq.toList |> equal [3; 1; 2; 4; 0] + rng.Next() |> equal 361709742 + let rng = System.Random(7) + Seq.randomSampleWith rng 5 [0 .. 21] |> Seq.toList |> equal [8; 19; 14; 1; 0] + rng.Next() |> equal 2048632168 + + testCase "Seq.randomChoicesWith draws on enumeration" <| fun () -> + let rng = System.Random(42) + let xs = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal 1434747710 + xs |> Seq.toList |> equal [1; 1; 5] + xs |> Seq.toList |> equal [1; 2; 7] + let mutable calls = 0 + let ys = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + ys |> Seq.toList |> equal [5; 5; 5] + calls |> equal 3 + throwsError "The input must be non-negative.\ncount = -1 (Parameter 'count')" (fun () -> Seq.randomChoicesWith rng -1 [1]) + throwsError "The input sequence was empty. (Parameter 'source')" (fun () -> Seq.randomChoicesWith rng 1 Seq.empty) + testCase "Seq.item throws exception when index is out of range" <| fun () -> let xs = [0] (try Seq.item 1 xs |> ignore; false with | _ -> true) |> equal true @@ -1162,4 +1191,62 @@ let tests = throwsAnyError (fun () -> Seq.removeManyAt 0 2 [] |> Seq.toList |> ignore) throwsAnyError (fun () -> Seq.removeManyAt -1 2 [1] |> Seq.toList |> ignore) throwsAnyError (fun () -> Seq.removeManyAt 2 2 [1] |> Seq.toList |> ignore) + + testCase "Seq functions raise ArgumentException on empty sequences" <| fun () -> + let xs = seq { for x in [1; 2] do if x > 2 then yield x } + raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.tail xs |> Seq.toList) |> equal "ArgumentException" + + testCase "Seq functions raise InvalidOperationException when there are not enough elements" <| fun () -> + raisedExceptionType (fun () -> Seq.take 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + + testCase "Seq functions raise FSharp.Core's exception types for invalid arguments" <| fun () -> + raisedExceptionType (fun () -> Seq.windowed 0 [1] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun i -> i + 1) [1; 2] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun _ -> 0) [1; 2] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + + testCase "Seq functions check their arguments when called, before enumerating" <| fun () -> + let xs = seq { for x in [1; 2; 3] do yield x } + raisedExceptionType (fun () -> Seq.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.replicate -1 "a") |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.take -1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.take -1 [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.chunkBySize 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt -1 0 xs) |> equal "ArgumentException" + Seq.init 0 id |> Seq.toList |> equal [] + Seq.take 0 xs |> Seq.toList |> equal [] + + testCase "Seq.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = seq { for x in [1; 2; 3] do yield x } + let empty: int seq = Seq.empty + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 xs |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 [|1; 2; 3|] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 4 1 xs |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 0 0 empty |> Seq.toList) |> equal "ArgumentException" + Seq.removeManyAt 0 4 xs |> Seq.toList |> equal [] + Seq.removeManyAt 1 3 xs |> Seq.toList |> equal [1] + Seq.removeManyAt 1 0 xs |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 1 -1 [1; 2; 3] |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 2 2 [|1; 2; 3|] |> Seq.toList |> equal [1; 2] + + testCase "Seq.transpose groups the items of jagged sequences by index" <| fun () -> + let transpose (xss: seq>) = Seq.transpose xss |> Seq.map Seq.toList |> Seq.toList + transpose [seq [1; 2]; seq [3]] |> equal [[1; 3]; [2]] + transpose [seq [1]; seq [2; 3]] |> equal [[1; 2]; [3]] + transpose [Seq.empty; seq [1]] |> equal [[1]] + transpose [seq [1]; Seq.empty] |> equal [[1]] + transpose [seq [1; 2; 3]; seq [4]; seq [5; 6]] |> equal [[1; 4; 5]; [2; 6]; [3]] + transpose (seq { for xs in [[1; 2]; [3]] do yield seq { for x in xs do yield x } }) |> equal [[1; 3]; [2]] + Seq.transpose [[1; 2]; [3]] |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 3]; [2]] + Seq.transpose [| [|1|]; [|2; 3|] |] |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 2]; [3]] ] diff --git a/tests/Js/Main/SetTests.fs b/tests/Js/Main/SetTests.fs index 2f752a51e5..88e36cc8d1 100644 --- a/tests/Js/Main/SetTests.fs +++ b/tests/Js/Main/SetTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Sets open System.Collections.Generic open Util.Testing +open Fable.Tests.Util let tests = testList "Sets" [ @@ -320,4 +321,19 @@ let tests = let ys = [| ("D", 4); ("E", 5); ("F", 6) |] coll.CopyTo(ys, 0) ys = xs |> equal true + + testCase "Set functions raise ArgumentException on empty sets" <| fun () -> + let s: Set = Set.empty + raisedExceptionType (fun () -> Set.minElement s) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.maxElement s) |> equal "ArgumentException" + raisedExceptionType (fun () -> s.MinimumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> s.MaximumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.intersectMany ([]: Set list)) |> equal "ArgumentException" + + testCase "Set enumerator Current raises InvalidOperationException outside the enumeration" <| fun () -> + let s = set [1; 2] + let e = (s :> seq).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" ] diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index 8b2f90eb3d..57b1ba0494 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -122,6 +122,12 @@ let tests = testList "Strings" [ sb.Append(8.0f) |> ignore // float32 sb.ToString().Replace(",", ".") |> equal "12345678" + testCase "StringBuilder.Append treats null as empty" <| fun () -> // See #5162 + let n: string = null + let o: obj = null + let sb = System.Text.StringBuilder() + sb.Append("a").Append(n).Append(o).Append("b").ToString() |> equal "ab" + testCase "StringBuilder.AppendFormat works" <| fun () -> let sb = System.Text.StringBuilder() sb.AppendFormat("Hello{0}World{1}", " ", "!") |> ignore @@ -256,6 +262,14 @@ let tests = testList "Strings" [ testCase "sprintf without arguments works" <| fun () -> sprintf "hello" |> equal "hello" + testCase "sprintf \"%s\" prints null as empty" <| fun () -> // See #5162 + let n: string = null + sprintf "%s" n |> equal "" + sprintf "a%sb" n |> equal "ab" + sprintf "[%5s]" n |> equal "[ ]" + sprintf "[%-5s]" n |> equal "[ ]" + (try failwithf "x%sy" n with e -> e.Message) |> equal "xy" + testCase "input of print format can be retrieved" <| fun () -> let pathScan (pf:PrintfFormat<_,_,_,_,'t>) = let formatStr = pf.Value @@ -263,6 +277,17 @@ let tests = testList "Strings" [ equal "/hello/%s" (pathScan "/hello/%s") + testCase "input of interpolated print format can be retrieved" <| fun () -> // See #3969 + let f (fmt: Printf.StringFormat) = fmt.Value, sprintf fmt + let x, s = 42, "z" + f $"wow %d{x}" |> equal ("wow %d%P()", "wow 42") + f $"a %d{x} b %s{s} c {x + 1}" |> equal ("a %d%P() b %s%P() c %P()", "a 42 b z c 43") + + testCase "ksprintf and kprintf work with interpolated strings" <| fun () -> // See #3969 + let x = 3 + Printf.ksprintf (fun s -> s + "!") $"ks %d{x} {x + 1}" |> equal "ks 3 4!" + Printf.kprintf (fun s -> s + "?") $"kp {x}" |> equal "kp 3?" + testCase "sprintf with escaped percent symbols works" <| fun () -> // See #195 let r, r1, r2 = "Ratio", 0.213849, 0.799898 sprintf "%s1: %.2f%% %s2: %.2f%%" r (r1*100.) r (r2*100.) @@ -442,6 +467,30 @@ let tests = testList "Strings" [ containsInOrder ["Hello"; "Hola"] a |> equal true containsInOrder ["Hello"; "Hola"] b |> equal true + testCase "sprintf \"%o\" works" <| fun () -> + sprintf "%o" 8 |> equal "10" + sprintf "%o" 0 |> equal "0" + sprintf "%o" -1 |> equal "37777777777" + sprintf "%o" -8 |> equal "37777777770" + sprintf "%o" (1 <<< 31) |> equal "20000000000" + sprintf "%o" 255uy |> equal "377" + sprintf "%o" 65535us |> equal "177777" + sprintf "%o" 4095L |> equal "7777" + sprintf "%o" -1L |> equal "1777777777777777777777" + sprintf "%o" 18446744073709551615UL |> equal "1777777777777777777777" + + testCase "sprintf \"%o\" padding and flags work" <| fun () -> + sprintf "%5o|%-5o|%05o" 8 8 8 |> equal " 10|10 |00010" + sprintf "%+o|% o|%+05o" 8 8 8 |> equal "+10| 10|+0010" + sprintf "%+o" -1 |> equal "+37777777777" + + testCase "interpolated \"%o\" works" <| fun () -> + $"%o{-1}|%o{8}|%5o{8}" |> equal "37777777777|10| 10" + + testCase "sprintf \"%x\" sign flags work" <| fun () -> + sprintf "%+x|% x|%+X" 8 8 255 |> equal "+8| 8|+FF" + sprintf "%+x" -1 |> equal "+ffffffff" + testCase "sprintf \"%X\" works" <| fun () -> //These should all be the Native JS Versions (except int64 / uint64) //See #1530 for more information. @@ -472,6 +521,13 @@ let tests = testList "Strings" [ testCase "String.Format should fail if there are less arguments than placeholders" <| fun () -> // See #2768 throwsAnyError <| fun () -> String.Format ("Hello {0}", args = Array.empty) + testCase "String.Format treats null arguments as empty" <| fun () -> // See #5162 + let n: string = null + let o: obj = null + String.Format("a{0}b", n) |> equal "ab" + String.Format("a{0}b", o) |> equal "ab" + String.Format("[{0,3}]", n) |> equal "[ ]" + testCase "String.Format combining padding and zeroes pattern works" <| fun () -> String.Format(CultureInfo.InvariantCulture, "{0:++0.00++}", -5000.5657) |> equal "-++5000.57++" String.Format(CultureInfo.InvariantCulture, "{0:000.00}foo", 5) |> equal "005.00foo" @@ -546,6 +602,44 @@ let tests = testList "Strings" [ String.Format(CultureInfo.InvariantCulture, "{0:P2}", 0.1234) |> equal "12.34 %" String.Format(CultureInfo.InvariantCulture, "{0:C2}", 1000) |> equal "¤1,000.00" + testCase "sprintf rounds exact midpoints of floats to even" <| fun () -> // See #5202 + sprintf "%.2f" 0.125 |> equal "0.12" + sprintf "%.0f" 2.5 |> equal "2" + sprintf "%.0f" 0.5 |> equal "0" + sprintf "%.0f" 1.5 |> equal "2" + sprintf "%.0f" -2.5 |> equal "-2" + sprintf "%.0f" -0.5 |> equal "-0" + sprintf "%.0f" 2.5f |> equal "2" + sprintf "%.2g" 0.125 |> equal "0.12" + sprintf "%.2f" 1.005 |> equal "1.00" + sprintf "%.2f" 0.375 |> equal "0.38" + sprintf "%.1f" 0.05 |> equal "0.1" + sprintf "%.0f" 2.5m |> equal "3" + sprintf "%.2f" 0.125m |> equal "0.13" + + testCase "Standard numeric formats round exact midpoints of floats to even" <| fun () -> // See #5202 + (0.125).ToString("F2", CultureInfo.InvariantCulture) |> equal "0.12" + (2.5).ToString("F0", CultureInfo.InvariantCulture) |> equal "2" + (-2.5).ToString("F0", CultureInfo.InvariantCulture) |> equal "-2" + (0.125f).ToString("F2", CultureInfo.InvariantCulture) |> equal "0.12" + (0.125).ToString("E1", CultureInfo.InvariantCulture) |> equal "1.2E-001" + (9.5).ToString("E0", CultureInfo.InvariantCulture) |> equal "1E+001" + (2.5e20).ToString("E0", CultureInfo.InvariantCulture) |> equal "2E+020" + (3.5e20).ToString("E0", CultureInfo.InvariantCulture) |> equal "4E+020" + (0.125).ToString("P0", CultureInfo.InvariantCulture) |> equal "12 %" + (-0.125).ToString("P0", CultureInfo.InvariantCulture) |> equal "-12 %" + (0.00125).ToString("P1", CultureInfo.InvariantCulture) |> equal "0.1 %" + (0.005).ToString("P0", CultureInfo.InvariantCulture) |> equal "1 %" + (2.5).ToString("N0", CultureInfo.InvariantCulture) |> equal "2" + (2.5).ToString("C0", CultureInfo.InvariantCulture) |> equal "¤2" + (0.125).ToString("G2", CultureInfo.InvariantCulture) |> equal "0.12" + String.Format(CultureInfo.InvariantCulture, "{0:F2}", 0.125) |> equal "0.12" + (1.005).ToString("F2", CultureInfo.InvariantCulture) |> equal "1.00" + (0.375).ToString("F2", CultureInfo.InvariantCulture) |> equal "0.38" + (0.125).ToString("0.00", CultureInfo.InvariantCulture) |> equal "0.13" + (2.5m).ToString("F0", CultureInfo.InvariantCulture) |> equal "3" + (0.125m).ToString("F2", CultureInfo.InvariantCulture) |> equal "0.13" + testCase "G format specifier trims trailing zeros without corrupting exponential notation" <| fun () -> (123.45).ToString("G17", CultureInfo.InvariantCulture) |> equal "123.45" (10000.0).ToString("G17", CultureInfo.InvariantCulture) |> equal "10000" @@ -670,6 +764,17 @@ let tests = testList "Strings" [ sprintf "%s" s.[2..] |> equal "t and dog" sprintf "%s" s.[..8] |> equal "cat and d" + testCase "String slices clamp the bounds like .NET" <| fun () -> // See #5164 + let s = "abc" + s.[-1..1] |> equal "ab" + s.[-5..] |> equal "abc" + s.[1..5] |> equal "bc" + s.[.. -2] |> equal "" + s.[-3.. -2] |> equal "" + s.[2..1] |> equal "" + let lower, upper = -1, 1 + s.[lower..upper] |> equal "ab" + testCase "String.Format works" <| fun () -> let arg1, arg2, arg3 = "F#", "Fable", "Babel" String.Format(CultureInfo.InvariantCulture, "{2} is to {1} what {1} is to {0}", arg1, arg2, arg3) @@ -682,6 +787,32 @@ let tests = testList "Strings" [ .Replace(",", ".").Replace(" %", "%") |> equal "0.55 54.67% 2014-09-26 00:19" + testCase "String.Format unescapes doubled braces" <| fun () -> // See #5163 + String.Format(CultureInfo.InvariantCulture, "{{{0}}}", 5) |> equal "{5}" + String.Format(CultureInfo.InvariantCulture, "{{x}} {0}", 1) |> equal "{x} 1" + String.Format(CultureInfo.InvariantCulture, "}}{{", 1) |> equal "}{" + String.Format(CultureInfo.InvariantCulture, "{{{0:N1}}}", 2.26) |> equal "{2.3}" + String.Format(CultureInfo.InvariantCulture, "{{{0,4}}}", 5) |> equal "{ 5}" + +// String.Format has to recognise the value at runtime. The default JS +// representation passes a Date for DateOnly and a number for TimeOnly and +// TimeSpan, which are indistinguishable from a DateTime and a float, so only +// .NET and the Temporal representation can format these as .NET does. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testCase "String.Format uses .NET formatting for DateOnly, TimeOnly and TimeSpan" <| fun () -> + let d = DateOnly(2024, 1, 2) + let t = TimeOnly(1, 2, 3) + let ts = TimeSpan.FromHours(1.) + String.Format(CultureInfo.InvariantCulture, "{0}", d) |> equal "01/02/2024" + String.Format(CultureInfo.InvariantCulture, "{0:O}", d) |> equal "2024-01-02" + String.Format(CultureInfo.InvariantCulture, "{0}", t) |> equal "01:02" + String.Format(CultureInfo.InvariantCulture, "{0:T}", t) |> equal "01:02:03" + String.Format(CultureInfo.InvariantCulture, "{0}", ts) |> equal "01:00:00" + String.Format(CultureInfo.InvariantCulture, "{0:g}", ts) |> equal "1:00:00" + String.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", d, t, ts) + |> equal "01/02/2024 01:02 01:00:00" +#endif + testCase "Padding works" <| fun () -> "3.14".PadLeft(10) |> equal " 3.14" "3.14".PadRight(10) |> equal "3.14 " @@ -1120,6 +1251,40 @@ let tests = testList "Strings" [ input.Chars(2) |> equal 'l' + testCase "String.Chars throws when index is out of bounds" <| fun () -> + let input = "abc" + throwsAnyError <| fun () -> + input.Chars(-1) |> ignore + throwsAnyError <| fun () -> + input.Chars(3) |> ignore + + testCase "String indexer evaluates the string and the index once" <| fun () -> + let mutable calls = 0 + let next () = + calls <- calls + 1 + calls + let pick () = + calls <- calls + 1 + "abc" + "abc".[next ()] |> equal 'b' + calls |> equal 1 + (pick ()).[2] |> equal 'c' + calls |> equal 2 + throwsAnyError (fun () -> "abc".[next () + 2]) + calls |> equal 3 + + testCase "String indexer works" <| fun () -> + let input = "hello" + input.[1] |> equal 'e' + input.[4] |> equal 'o' + + testCase "String indexer throws when index is out of bounds" <| fun () -> + let input = "abc" + throwsAnyError <| fun () -> + input.[-1] |> ignore + throwsAnyError <| fun () -> + input.[3] |> ignore + testCase "String.Substring works" <| fun () -> "abcdefg".Substring(2) |> equal "cdefg" @@ -1203,6 +1368,31 @@ let tests = testList "Strings" [ String.Join(",", [|"abc"|]) |> equal "abc" String.Join(",", ["abc"]) |> equal "abc" + testCase "String.Join treats a null separator as empty" <| fun () -> // See #5167 + String.Join((null: string), [| "a"; "b" |]) |> equal "ab" + String.Join((null: string), seq { yield "a"; yield "b" }) |> equal "ab" + String.Join((null: string), "a", "b") |> equal "ab" + String.Join((null: string), [| "a"; "b"; "c" |], 1, 2) |> equal "bc" + + testCase "String.Join treats null elements as empty" <| fun () -> + String.Join(",", [| "a"; null; "b" |]) |> equal "a,,b" + String.Join(",", [| box "a"; null; box 1 |]) |> equal "a,,1" + + testCase "System.String.Concat treats null arguments as empty" <| fun () -> // See #5167 + String.Concat("a", (null: string), "b") |> equal "ab" + String.Concat((null: string), "b") |> equal "b" + String.Concat([| "a"; null; "b" |]) |> equal "ab" + String.Concat(box "a", null, box 1) |> equal "a1" + + testCase "string treats null as empty" <| fun () -> // See #5162 + let n: string = null + let o: obj = null + string n |> equal "" + string (box n) |> equal "" + string o |> equal "" + string (None: int option) |> equal "" + string "a" |> equal "a" + testCase "System.String.Concat works" <| fun () -> String.Concat("a", "b", "c") |> equal "abc" diff --git a/tests/Js/Main/TaskTests.fs b/tests/Js/Main/TaskTests.fs index 392614cb0a..63e60d4006 100644 --- a/tests/Js/Main/TaskTests.fs +++ b/tests/Js/Main/TaskTests.fs @@ -174,4 +174,64 @@ let tests = let! result = awaitTask tcs.Task equal 42 result } + + testCaseAsync "Task.Delay works" <| fun () -> + async { + let mutable steps = [] + let! _ = awaitTask (task { + steps <- "before" :: steps + do! Task.Delay 20 + do! Task.Delay(TimeSpan.FromMilliseconds 10.) + steps <- "after" :: steps + }) + equal [ "after"; "before" ] steps + } + + testCaseAsync "Task.Delay can be cancelled" <| fun () -> + async { + let cts = new System.Threading.CancellationTokenSource() + let delay = Task.Delay(1000, cts.Token) + cts.Cancel() + let! cancelled = awaitTask (task { + try + do! delay + return false + with _ -> return true + }) + equal true cancelled + } + + testCaseAsync "task can bind an Async" <| fun () -> + async { + let work = async { + do! Async.Sleep 10 + return 41 + } + let! result = awaitTask (task { + let! x = work + return x + 1 + }) + equal 42 result + } + + testCaseAsync "task can return! an Async" <| fun () -> + async { + let work = async { + do! Async.Sleep 10 + return 42 + } + let! result = awaitTask (task { return! work }) + equal 42 result + } + + testCaseAsync "task can do! an Async" <| fun () -> + async { + let mutable ran = false + let work = async { + do! Async.Sleep 10 + ran <- true + } + let! _ = awaitTask (task { do! work }) + equal true ran + } ] diff --git a/tests/Js/Main/TimeOnlyTests.fs b/tests/Js/Main/TimeOnlyTests.fs index 18868da58c..bb097a2324 100644 --- a/tests/Js/Main/TimeOnlyTests.fs +++ b/tests/Js/Main/TimeOnlyTests.fs @@ -194,13 +194,14 @@ let tests = equal (TimeOnly (0, 0, 5)) (TimeOnly.Parse "00:0:5 ") equal TimeOnly.MinValue (TimeOnly.Parse " 0 : 0 : 0 ") equal (TimeOnly (0, 40, 5, 3)) (TimeOnly.Parse "00:40:5.003") - // We're limited to millisecond precision in JS -#if FABLE_COMPILER - equal (TimeOnly (0, 0, 5, 3)) (TimeOnly.Parse "00:0:5.0031") - equal (TimeOnly (0, 0, 5, 3)) (TimeOnly.Parse "00:0:5.00313213213213") -#else + // The JS-Date representation is limited to millisecond precision; + // the Temporal one is tick-precise, like .NET. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL equal (TimeOnly 50031000L) (TimeOnly.Parse "00:0:5.0031") equal (TimeOnly 50031321L) (TimeOnly.Parse "00:0:5.00313213213213") +#else + equal (TimeOnly (0, 0, 5, 3)) (TimeOnly.Parse "00:0:5.0031") + equal (TimeOnly (0, 0, 5, 3)) (TimeOnly.Parse "00:0:5.00313213213213") #endif equal (TimeOnly (0, 0, 59, 30)) (TimeOnly.Parse "00: 0: 59.03") equal (TimeOnly (0, 3, 5, 300)) (TimeOnly.Parse "00 :03:5.3") @@ -268,23 +269,24 @@ let tests = equal true isValid equal (TimeOnly (2, 0, 5, 300)) t - // We're limited to millisecond precision in JS -#if FABLE_COMPILER + // The JS-Date representation is limited to millisecond precision; + // the Temporal one is tick-precise, like .NET. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL let isValid, t = TimeOnly.TryParse "00:0:5.0031" equal true isValid - equal (TimeOnly (0, 0, 5, 3)) t + equal (TimeOnly 50031000L) t let isValid, t = TimeOnly.TryParse "00:0:5.00313213213213" equal true isValid - equal (TimeOnly (0, 0, 5, 3)) t + equal (TimeOnly 50031321L) t #else let isValid, t = TimeOnly.TryParse "00:0:5.0031" equal true isValid - equal (TimeOnly 50031000L) t + equal (TimeOnly (0, 0, 5, 3)) t let isValid, t = TimeOnly.TryParse "00:0:5.00313213213213" equal true isValid - equal (TimeOnly 50031321L) t + equal (TimeOnly (0, 0, 5, 3)) t #endif testCase "Comparison works" <| fun () -> @@ -296,4 +298,29 @@ let tests = let m = [ TimeOnly (20, 5, 1), () ] |> Map.ofList equal true (Map.containsKey (TimeOnly(20, 1, 1).AddMinutes(4.)) m) equal false (Map.containsKey (TimeOnly (20, 1, 1)) m) + +// See the note in DateTimeTests.fs: tick precision only holds on .NET and on the +// Temporal representation. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testCase "TimeOnly.MaxValue is one tick short of a day" <| fun () -> + TimeOnly.MaxValue.Ticks |> equal 863_999_999_999L + + testCase "TimeOnly constructor keeps the microsecond argument" <| fun () -> + TimeOnly(1, 2, 3, 4, 5).Ticks |> equal 37_230_040_050L + + testCase "TimeOnly.Parse keeps all seven fractional digits" <| fun () -> + TimeOnly.Parse("01:02:03.1234567", CultureInfo.InvariantCulture).Ticks % 10_000_000L + |> equal 1234567L + + testCase "TimeOnly.Add wraps a large TimeSpan without losing the low ticks" <| fun () -> + // 100000 whole days plus one tick: the days wrap away and the tick must remain. + // In nanoseconds this amount is past Number.MAX_SAFE_INTEGER. + let big = TimeSpan.FromTicks(864_000_000_000L * 100_000L + 1L) + TimeOnly.MinValue.Add(big).Ticks |> equal 1L + + testCase "TimeOnly.Add* accept fractional values and are tick-precise" <| fun () -> + let t = TimeOnly(1, 2, 3, 4) + t.AddHours(1.0 / 3.0).Ticks - t.Ticks |> equal 12_000_000_000L + t.AddMinutes(0.00001).Ticks - t.Ticks |> equal 6_000L +#endif ] diff --git a/tests/Js/Main/TimeSpanTests.fs b/tests/Js/Main/TimeSpanTests.fs index dd8437b662..78a7a15502 100644 --- a/tests/Js/Main/TimeSpanTests.fs +++ b/tests/Js/Main/TimeSpanTests.fs @@ -139,6 +139,24 @@ let tests = t.Ticks |> equal 20000L + testCase "TimeSpan.Ticks works with big values" <| fun () -> + TimeSpan(10675199, 2, 48, 5, 477).Ticks |> equal 9223372036854770000L + TimeSpan(-10675199, -2, -48, -5, -477).Ticks |> equal -9223372036854770000L + TimeSpan.FromSeconds(922337203685L).Ticks |> equal 9223372036850000000L + TimeSpan.FromMinutes(15372286728.).Ticks |> equal 9223372036800000000L + TimeSpan.FromDays(10675199.).Ticks |> equal 9223371936000000000L + TimeSpan.FromMilliseconds(922337203685477L).Ticks |> equal 9223372036854770000L + + testCase "TimeSpan.Ticks keeps sub-millisecond values" <| fun () -> + TimeSpan.FromMilliseconds(1.0001).Ticks |> equal 10001L + TimeSpan.FromMilliseconds(1.0005).Ticks |> equal 10005L + TimeSpan.FromMilliseconds(-1.0001).Ticks |> equal -10001L + TimeSpan.FromSeconds(0.0010001).Ticks |> equal 10001L + (TimeSpan.FromMilliseconds(1.0) * 1.0001).Ticks |> equal 10001L + TimeSpan.FromMilliseconds(900719925474.1).Ticks |> equal 9007199254741000L + TimeSpan.FromMilliseconds(-900719925474.1).Ticks |> equal -9007199254741000L + TimeSpan.FromMilliseconds(1e12 + 0.3).Ticks |> equal 10000000000003000L + // NOTE: This test fails because of very small fractions, so I cut the fractional part testCase "TimeSpan totals work" <| fun () -> let t = TimeSpan.FromMilliseconds(96441615.) @@ -201,7 +219,9 @@ let tests = let t = TimeSpan.FromMilliseconds(ms) let res1 = t.Multiply(factor).TotalMilliseconds let res2 = (t * factor).TotalMilliseconds + let res3 = (factor * t).TotalMilliseconds equal res1 res2 + equal res1 res3 equal true (res1 = res2) equal expected res1 test 1000. -1. -1000. @@ -672,4 +692,47 @@ let tests = let actual = TimeSpan.Parse("-1.23:45:06.78999").Days let expected = -1 equal actual expected + +// A .NET TimeSpan is an integer count of 100ns ticks. The default JS +// representation is a float64 millisecond count, so only .NET and the Temporal +// representation can hold these exactly. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testCase "TimeSpan holds the full Int64 tick range exactly" <| fun () -> + TimeSpan.FromTicks(1234567890123456789L).Ticks |> equal 1234567890123456789L + TimeSpan.FromTicks(Int64.MaxValue).Ticks |> equal Int64.MaxValue + + testCase "TimeSpan constructor keeps the microsecond argument" <| fun () -> + TimeSpan(0, 0, 0, 0, 0, 5).Ticks |> equal 50L + TimeSpan(1, 2, 3, 4, 5, 6).Ticks |> equal 937_840_050_060L + + testCase "TimeSpan.Parse keeps all seven fractional digits" <| fun () -> + TimeSpan.Parse("00:00:00.1234567").Ticks |> equal 1234567L + + testCase "TimeSpan.ToString keeps all seven fractional digits" <| fun () -> + TimeSpan.FromTicks(1234567L).ToString() |> equal "00:00:00.1234567" + TimeSpan.FromTicks(1L).ToString() |> equal "00:00:00.0000001" + TimeSpan.FromTicks(1234567L).ToString("g", CultureInfo.InvariantCulture) |> equal "0:00:00.1234567" + TimeSpan.FromTicks(1000000L).ToString("g", CultureInfo.InvariantCulture) |> equal "0:00:00.1" + TimeSpan.FromTicks(1234567L).ToString("G", CultureInfo.InvariantCulture) |> equal "0:00:00:00.1234567" + TimeSpan.Parse("-1.02:03:04.0050000").ToString() |> equal "-1.02:03:04.0050000" + + testCase "TimeSpan.FromMilliseconds truncates below a tick" <| fun () -> + // 0.00005ms is half a tick; .NET drops it rather than rounding up + TimeSpan.FromMilliseconds(0.00005).Ticks |> equal 0L + TimeSpan.FromMilliseconds(0.0001).Ticks |> equal 1L + + testCase "TimeSpan multiplication and division are tick-precise" <| fun () -> + let day = TimeSpan.FromDays(1.0) + (day * 1000.0).Ticks |> equal 864_000_000_000_000L + (day / 3.0).Ticks |> equal 288_000_000_000L + day / TimeSpan.FromHours(1.0) |> equal 24.0 + + testCase "TimeSpan arithmetic keeps single ticks" <| fun () -> + let a = TimeSpan.FromTicks(1234567L) + let b = TimeSpan.FromTicks(1L) + (a + b).Ticks |> equal 1234568L + (a - b).Ticks |> equal 1234566L + (-a).Ticks |> equal -1234567L + a.Duration().Ticks |> equal 1234567L +#endif ] diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index f74512a4a2..f646f7f8d5 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -439,6 +439,33 @@ type ClassWithDefaultValueStaticFields() = static member Decimal = ClassWithDefaultValueStaticFields.decimal static member StructTuple2 = ClassWithDefaultValueStaticFields.structTuple2 +type ClassWithDefaultValueInstanceFields = + val X: int + [] val mutable D: int + [] val mutable S: string + val Y: string + new (x, y) = { X = x; Y = y } + +[] +type StructWithDefaultValueInstanceField = + val X: int + [] val mutable D: int + val Y: string + new (x, y) = { X = x; Y = y } + +type ClassWithDefaultValueStaticAndInstanceFields = + [] static val mutable private count: int + val X: int + [] val mutable D: int + val Y: int + new (x, y) = { X = x; Y = y } + static member Count with get () = ClassWithDefaultValueStaticAndInstanceFields.count and set v = ClassWithDefaultValueStaticAndInstanceFields.count <- v + +type ClassWithDefaultValueFunctionField = + val F: int -> int -> int + [] val mutable G: int -> int -> int + new (f) = { F = f } + [] type NestedStruct = @@ -466,6 +493,15 @@ type Distinct1 = static member OfList(_elements: list<'K * 'V>) = () static member OfList(_elements: list) = () +type FlexibleOverloads() = + member _.For(xs: 'a list, f: 'a -> 'b list) : 'b list = xs |> List.collect f + member _.For(xs: #seq<'a>, f: 'a -> unit) : int = + let mutable count = 0 + for x in xs do + f x + count <- count + 1 + count + type InfoA = { Foo: string } @@ -873,6 +909,116 @@ let inline outerGet<'a, 'b when 'a: (static member GetValue: unit -> string) and 'b: (static member GetValue: unit -> string)> () = innerGet<'a>(), innerGet<'b>() +type LazyTree<'a> = LazyNode of 'a * LazyTree<'a> list + +let describeLazy (x: Lazy<'a>) : string = string (x.Force()) + +let walkLazyTree (tree: LazyTree>) : string = + let rec loop (LazyNode(root, xs)) = + match xs with + | [] -> describeLazy root + | x :: _ -> loop x + + loop tree + +type ValClassBase = + val Id: int + val Label: string + new (id: int) = { Id = id * 10; Label = "base" } + member this.Describe() = this.Label + ":" + string this.Id + +type ValClassChild = + inherit ValClassBase + val Extra: float + new (id: int, extra: float) = { inherit ValClassBase(id); Extra = extra } + +type ValClassGrandChild = + inherit ValClassChild + val Flag: bool + new (id: int, extra: float, flag: bool) = + let doubled = id * 2 + { inherit ValClassChild(doubled, extra); Flag = flag } + +let mutable valClassBaseCalls = 0 + +type ValClassCounting = + val N: int + new (n: int) = + valClassBaseCalls <- valClassBaseCalls + 1 + { N = n } + +type ValClassCountingChild = + inherit ValClassCounting + val M: int + new (n: int, m: int) = { inherit ValClassCounting(n); M = m } + +type ValClassDefaultBase = + val Tag: string + new () = { Tag = "default" } + +type ValClassDefaultChild = + inherit ValClassDefaultBase + val Value: int + new (value: int) = { inherit ValClassDefaultBase(); Value = value } + +type ValClassGenericBase<'T> = + val Value: 'T + new (v: 'T) = { Value = v } + +type ValClassIntBox = + inherit ValClassGenericBase + val Tag: string + new (v: int, t: string) = { inherit ValClassGenericBase(v); Tag = t } + +type ValClassPairBox<'U> = + inherit ValClassGenericBase<'U * 'U> + val Count: int + new (a: 'U, b: 'U) = { inherit ValClassGenericBase<'U * 'U>((a, b)); Count = 2 } + +let mutable valClassThenCalls = 0 + +type ValClassThen = + val mutable Value: int + new () = { Value = 1 } then valClassThenCalls <- valClassThenCalls + 1 + +[] +type ValClassSequenceInit = + val Next: ValClassSequenceInit + val Id: int + val Boxed: obj + new (id: int) = { Next = null; Id = id; Boxed = null } + new (a: int, b: int) = { Next = (ValClassSequenceInit(a); ValClassSequenceInit(b)); Id = 0; Boxed = box (do ValClassSequenceInit(a)) } + new (a: int, b: int, c: int) as self = { Next = (ValClassSequenceInit(a); ValClassSequenceInit(b)); Id = c; Boxed = box (do ValClassSequenceInit(a)) } then self.Next.Id |> ignore + +type ValClassSelf = + val mutable Name: string + val mutable Count: int + new (name: string) as self = { Name = name; Count = 0 } then self.Count <- self.Name.Length + new () as self = ValClassSelf("ab") then self.Count <- self.Count * 10 + +type ValClassSelfOutOfOrder = + val Z: int + val mutable W: int + new () as self = { W = 2; Z = 3 } then self.W <- self.W + self.Z + +let valClassSelfSaved = ResizeArray() + +type ValClassSelfSaved = + val Id: int + new (id: int) as self = { Id = id } then valClassSelfSaved.Add(box self) + new () as self = ValClassSelfSaved(1) then valClassSelfSaved.Add(box self) + +type SelfPrimaryBase(x: int) as this = + let mutable touched = 0 + do this.Touch() + new () = SelfPrimaryBase(1) + member _.X = x + member _.Touched = touched + member _.Touch() = touched <- touched + 1 + +type SelfPrimaryChild() = + inherit SelfPrimaryBase(2) + let tests = testList "Types" [ @@ -1109,6 +1255,13 @@ let tests = t.Overload(2) |> equal 4 t.Overload(2, 3) |> equal 5 + testCase "Overloads distinguished by a flexible type work" <| fun () -> + let t = FlexibleOverloads() + t.For([1; 2], fun i -> [i; i * 10]) |> equal [1; 10; 2; 20] + let mutable sum = 0 + t.For([|1; 2; 3|], fun i -> sum <- sum + i) |> equal 3 + sum |> equal 6 + testCase "Type abbreviation works" <| fun () -> let t = T4() t.Value2 |> equal "Bye" @@ -1262,6 +1415,10 @@ let tests = search "b" |> equal (Some "b") search "d" |> equal None + testCase "Local function generic over a Lazy subtype constraint works" <| fun () -> + LazyNode(lazy 5, [LazyNode(lazy 7, [])]) |> walkLazyTree |> equal "7" + LazyNode(lazy 5, []) |> walkLazyTree |> equal "5" + testCase "Secondary constructors work" <| fun () -> let s1 = SecondaryCons(3) let s2 = SecondaryCons() @@ -1762,6 +1919,39 @@ let tests = ClassWithDefaultValueStaticFields.IncrCount() |> equal 1 + testCase "Instance [] fields are skipped by explicit constructors" <| fun () -> + let c = ClassWithDefaultValueInstanceFields(1, "y") + c.X |> equal 1 + c.D |> equal 0 + c.S |> equal null + c.Y |> equal "y" + c.D <- 5 + c.S <- "s" + c.D |> equal 5 + c.S |> equal "s" + + let mutable s = StructWithDefaultValueInstanceField(2, "z") + s.X |> equal 2 + s.D |> equal 0 + s.Y |> equal "z" + s.D <- 6 + s.D |> equal 6 + + testCase "Explicit constructors skip static fields next to DefaultValue fields" <| fun () -> + let c = ClassWithDefaultValueStaticAndInstanceFields(1, 2) + ClassWithDefaultValueStaticAndInstanceFields.Count <- 4 + c.X |> equal 1 + c.D |> equal 0 + c.Y |> equal 2 + ClassWithDefaultValueStaticAndInstanceFields.Count |> equal 4 + + testCase "Function-typed DefaultValue fields are null" <| fun () -> + let c = ClassWithDefaultValueFunctionField(fun a b -> a + b) + isNull (box c.G) |> equal true + c.F 2 3 |> equal 5 + c.G <- fun a b -> a * b + c.G 2 3 |> equal 6 + testCase "Unchecked.defaultof works for fields on structs" <| fun () -> let top = TopLevelStruct() top.A |> equal 0 @@ -1806,4 +1996,79 @@ let tests = testCase "Nested lambda does not shadow an outer generic type parameter" <| fun () -> let box = mapGenericMethodBox 42 box.Get() |> List.ofSeq |> equal [ 42 ] + + testCase "Explicit constructor calls the base constructor" <| fun () -> + let child = ValClassChild(3, 1.5) + child.Id |> equal 30 + child.Label |> equal "base" + child.Extra |> equal 1.5 + child.Describe() |> equal "base:30" + (child :> ValClassBase).Id |> equal 30 + (box child :? ValClassBase) |> equal true + + testCase "Explicit constructors chain through several levels" <| fun () -> + let g = ValClassGrandChild(2, 0.5, true) + g.Id |> equal 40 + g.Label |> equal "base" + g.Extra |> equal 0.5 + g.Flag |> equal true + g.Describe() |> equal "base:40" + + testCase "Explicit constructor runs the base constructor once" <| fun () -> + valClassBaseCalls <- 0 + let c = ValClassCountingChild(5, 6) + valClassBaseCalls |> equal 1 + c.N |> equal 5 + c.M |> equal 6 + + testCase "Explicit constructor calls a parameterless base constructor" <| fun () -> + let c = ValClassDefaultChild(9) + c.Tag |> equal "default" + c.Value |> equal 9 + + testCase "Explicit constructor resolves the generic arguments of the base class" <| fun () -> + let intBox = ValClassIntBox(5, "x") + intBox.Value |> equal 5 + intBox.Tag |> equal "x" + let pairBox = ValClassPairBox("a", "b") + pairBox.Value |> equal ("a", "b") + pairBox.Count |> equal 2 + + testCase "Explicit constructor returns the object after its then block" <| fun () -> + let before = valClassThenCalls + let c = ValClassThen() + c.Value |> equal 1 + valClassThenCalls - before |> equal 1 + + testCase "Explicit constructor keeps sequences of its own constructor calls in field values" <| fun () -> + let c = ValClassSequenceInit(5, 6) + c.Next.Id |> equal 6 + isNull c.Boxed |> equal true + let d = ValClassSequenceInit(5, 6, 7) + (d.Next.Id, d.Id, isNull d.Boxed) |> equal (6, 7, true) + + testCase "Explicit constructor with a self identifier works" <| fun () -> // See #5166 + let a = ValClassSelf("abc") + a.Name |> equal "abc" + a.Count |> equal 3 + let b = ValClassSelf() + b.Name |> equal "ab" + b.Count |> equal 20 + let c = ValClassSelfOutOfOrder() + (c.Z, c.W) |> equal (3, 5) + + testCase "Primary constructor with a self identifier works with a secondary constructor" <| fun () -> + let a = SelfPrimaryBase() + (a.X, a.Touched) |> equal (1, 1) + let b = SelfPrimaryChild() + (b.X, b.Touched) |> equal (2, 1) + + testCase "Explicit constructor self identifier is the created object" <| fun () -> + valClassSelfSaved.Clear() + let a = ValClassSelfSaved(5) + obj.ReferenceEquals(valClassSelfSaved.[0], a) |> equal true + let b = ValClassSelfSaved() + valClassSelfSaved.Count |> equal 3 + obj.ReferenceEquals(valClassSelfSaved.[1], b) |> equal true + obj.ReferenceEquals(valClassSelfSaved.[2], b) |> equal true ] diff --git a/tests/Js/Main/UnionTypeTests.fs b/tests/Js/Main/UnionTypeTests.fs index 3d714753b4..db48e9698c 100644 --- a/tests/Js/Main/UnionTypeTests.fs +++ b/tests/Js/Main/UnionTypeTests.fs @@ -18,7 +18,6 @@ type TestUnion = | Case2 of string * string | Case3 of string * string * string - type TestUnion2 = | Tag of string | NewTag of string @@ -64,9 +63,9 @@ type Wrapper(s: string) = [] type MyUnion = -| Case1 -| Case2 -| Case3 + | Case1 + | Case2 + | Case3 type R = { Name: string @@ -81,6 +80,13 @@ open Fable.Core.JsInterop #endif type DU = Int of int | Str of string +type ErasedPayload = { Description: string; Membership: int } + +[] +type ErasedWrapper = + | ErasedSuccess of ErasedPayload + | ErasedFailure of string + type T1 = T1 type T2 = T2 type T3 = T3 @@ -99,6 +105,114 @@ type LangCode = type S = S of string +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id + +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +[] +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First + +[] +type StructPoint2D = + | Planar2D of float * float + | Infinity + +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + member this.Area = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + member this.Area = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + +type WrappedUnion = + | AString of string + +type DeepRecord = { Value: string } + +type DeepWrappedUnion = + | DeepWrappedA of string * DeepRecord + | DeepWrappedB of string + | DeepWrappedC of int + | DeepWrappedD of DeepRecord + | DeepWrappedE of int * int + | DeepWrappedF of WrappedUnion + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" + let tests = testList "Unions" [ testCase "Union cases matches with no arguments can be generated" <| fun () -> @@ -213,20 +327,35 @@ let tests = "HELLO" |> Wrapper |> U3.Case3 |> toString |> equal "OLLEH" #endif + testCase "Erased union payload fields can be read" <| fun () -> + let payload = { Description = "a"; Membership = 2 } + + match ErasedSuccess payload with + | ErasedSuccess details -> details.Membership + | ErasedFailure _ -> -1 + |> equal 2 + testCase "Equality works in filter" <| fun () -> - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> r.Case = MyUnion.Case1) |> Array.length |> equal 2 testCase "Pattern matching works in filter" <| fun () -> - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) |> Array.length |> equal 2 + testCase "Union field can have the same name as its type" <| fun () -> + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + #if FABLE_COMPILER testCase "Case testing with generic erased unions works" <| fun () -> let strify (x: U2) = @@ -267,12 +396,23 @@ let tests = let x : U9> = !^42 x |> equal (U9.Case4 42) + + testCase "Implicit conversion to erased unions works" <| fun () -> + let strify (x: U3) = + match x with + | U3.Case1 i -> "i: " + string i + | U3.Case2 s -> "s: " + s + | U3.Case3 b -> if b then "b: yes" else "b: no" + let s: U2 = "foo" + s |> equal (U2.Case2 "foo") + strify 42 |> equal "i: 42" + strify "foo" |> equal "s: foo" + strify true |> equal "b: yes" #endif testCase "Verify that union cases can have `-` in their names" <| fun () -> // See https://github.com/fable-compiler/Fable/issues/4071 let x = LangCode.``pt-Br`` - equal x LangCode.``pt-Br`` testCase "sprintf formats strings cases correctly" <| fun () -> @@ -285,4 +425,116 @@ let tests = obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false obj.ReferenceEquals(T1, T1) |> equal true + + testCase "Named union case fields work when a field is called name" <| fun () -> + let value = NamedCase("v1", "v2") + match value with + | NamedCase(name, optional) -> + name |> equal "v1" + optional |> equal "v2" + + testCase "Static union member returning the union type works" <| fun () -> + Example.All |> Array.length |> equal 2 + Example.All.[0] |> equal Example.First + Example.All.[1] |> equal Example.Second + + testCase "Static union member with nested self-reference works" <| fun () -> + Example.Head |> equal (Some Example.First) + Example.Nested |> List.head |> Array.head |> equal Example.First + Example.Pair |> fst |> equal Example.First + Example.Pair |> snd |> equal Example.Second + Example.Factory() |> equal Example.First + + testCase "Struct unions work" <| fun () -> + let distance p = + match p with + | Planar2D (x, y) -> sqrt (x * x + y * y) + | Infinity -> infinity + let p = Planar2D (3, 4) + let res = distance p + res |> equal 5. + + testCase "Struct unions with fields work" <| fun () -> + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + + testCase "Struct unions without fields work" <| fun () -> + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + + testCase "Nested union match keeps case-specific field access" <| fun () -> + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + + testCase "Union case matching works" <| fun () -> + let x = IntCase1 5 + let res = + match x with + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + res |> equal 5 + + // testCase "Union case equality works" <| fun () -> + // IntCase1 5 = IntCase1 5 |> equal true + // IntCase1 5 = IntCase2 5 |> equal false + // IntCase3 2 = IntCase3 3 |> equal false + // IntCase2 1 = IntCase2 1 |> equal true + // IntCase3 1 = IntCase3 1 |> equal true + + testCase "Union fn call works" <| fun () -> + let x = IntCase1 3 + let res = unionFnAlways1 x + let res2 = unionFnAlways1 x //deliberately force clone/borrow break + let res3 = unionFnRetNum x + let res4 = unionFnRetNum (IntCase2 24) + res |> equal 3 + res2 |> equal 3 + res3 |> equal 3 + res4 |> equal 24 + + testCase "Union with wrapped type works" <| fun () -> + let a = AString "hello" + let b = match a with AString s -> s + " world" + b |> equal "hello world" + + testCase "Deep union with wrapped type works" <| fun () -> + let a = DeepWrappedA (" world", {Value = "hello"}) + let b = DeepWrappedB "world" + let c = DeepWrappedC 42 + let d = DeepWrappedD { Value = "hello" } + let f = DeepWrappedF (AString "doublewrapped") + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + a |> matchStrings |> equal "hello world" + b |> matchStrings |> equal "world" + c |> matchStrings |> equal "nothing" + d |> matchStrings |> equal "hello" + f |> matchStrings |> equal "doublewrapped" + g |> matchStrings |> equal "G" + + testCase "Deep union with tuped prim type works" <| fun () -> + let e = DeepWrappedE (3, 2) + let c = DeepWrappedC 42 //todo to string! + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + e |> matchNumbers |> equal 5 + c |> matchNumbers |> equal 42 + g |> matchNumbers |> equal 365 + + testCase "Multi-case Union with wrapped type works" <| fun () -> + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" + + testCase "Match with condition works" <| fun () -> + let b1 = DeepWrappedB "hello" + let b2 = DeepWrappedB "not" + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" + ] diff --git a/tests/Js/Main/Util/Util.fs b/tests/Js/Main/Util/Util.fs index da64962240..954d89af5f 100644 --- a/tests/Js/Main/Util/Util.fs +++ b/tests/Js/Main/Util/Util.fs @@ -108,6 +108,23 @@ let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentNullException -> "ArgumentNullException" + | :? ArgumentException -> "ArgumentException" + | :? FormatException -> "FormatException" + | :? OverflowException -> "OverflowException" + | :? InvalidOperationException -> "InvalidOperationException" + | :? Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | :? NotSupportedException -> "NotSupportedException" + | :? IndexOutOfRangeException -> "IndexOutOfRangeException" + | e -> "Unexpected exception: " + e.Message + let rec sumFirstSeq (zs: seq) (n: int): float = match n with | 0 -> 0. diff --git a/tests/Js/Main/js/1foo.js b/tests/Js/Main/js/1foo.js index 3a953f1b85..01fb60150f 100644 --- a/tests/Js/Main/js/1foo.js +++ b/tests/Js/Main/js/1foo.js @@ -2,6 +2,8 @@ export const foo = "foo"; +export const limit = 100; + export default { myKey: "a secret" } diff --git a/tests/Js/Main/js/async-deferred.js b/tests/Js/Main/js/async-deferred.js new file mode 100644 index 0000000000..07cb2ad500 --- /dev/null +++ b/tests/Js/Main/js/async-deferred.js @@ -0,0 +1,15 @@ +export function startDeferred(computation, token, onSuccess, onError, onCancel) { + let body; + computation({ + onSuccess, + onError, + onCancel, + cancelToken: token, + trampoline: { completed: false, incrementAndCheck: () => true, hijack: f => { body = f; } }, + }); + return () => body(); +} + +export function handlerCount(event) { + return event.delegates.length; +} diff --git a/tests/Js/Main/js/async-runtime.js b/tests/Js/Main/js/async-runtime.js new file mode 100644 index 0000000000..871ac9224d --- /dev/null +++ b/tests/Js/Main/js/async-runtime.js @@ -0,0 +1,36 @@ +// Controlled native fixtures; test cases and assertions live in AsyncTests.fs. +export function listenerCount(token) { + return token._listeners.size; +} + +export function cancellationErrorCount(error) { + if (error.constructor.name !== "CancellationCallbackError" || !Array.isArray(error.errors)) { + return -1; + } + return error.errors.length; +} + +export function withTimers(action) { + const originalSetTimeout = globalThis.setTimeout; + const originalClearTimeout = globalThis.clearTimeout; + let callback; + const timer = { + retired: 0, + invalidHandles: 0, + fire() { + // decision: fires retired callbacks to model a timeout already queued at cancellation + callback(); + }, + }; + globalThis.setTimeout = f => { callback = f; return 123; }; + globalThis.clearTimeout = id => { + if (id !== 123) { timer.invalidHandles++; } + timer.retired++; + }; + try { + return action(timer); + } finally { + globalThis.setTimeout = originalSetTimeout; + globalThis.clearTimeout = originalClearTimeout; + } +} diff --git a/tests/Php/Fable.Tests.Php.fsproj b/tests/Php/Fable.Tests.Php.fsproj index 509b2813e4..b670ce39b8 100644 --- a/tests/Php/Fable.Tests.Php.fsproj +++ b/tests/Php/Fable.Tests.Php.fsproj @@ -1,6 +1,10 @@ Exe + + false + false + false net10.0 Major false @@ -9,8 +13,9 @@ - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -31,7 +36,6 @@ - diff --git a/tests/Php/Main.fs b/tests/Php/Main.fs deleted file mode 100644 index ae5f6f6959..0000000000 --- a/tests/Php/Main.fs +++ /dev/null @@ -1,6 +0,0 @@ -#if FABLE_COMPILER -module Program -() -#else -module Program = let [] main _ = 0 -#endif \ No newline at end of file diff --git a/tests/Php/TestUnionType.fs b/tests/Php/TestUnionType.fs index 6d462a81de..e2e66ba784 100644 --- a/tests/Php/TestUnionType.fs +++ b/tests/Php/TestUnionType.fs @@ -18,7 +18,6 @@ type Either<'TL,'TR> = // | Case2 of string * string // | Case3 of string * string * string - // type TestUnion2 = // | Tag of string // | NewTag of string diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index e2374f7e18..1feeccc0dd 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -1,6 +1,10 @@ Exe + + false + false + false net10.0 Major false @@ -9,19 +13,24 @@ Fable.Tests - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + + + + + @@ -55,6 +64,7 @@ + @@ -63,6 +73,7 @@ + @@ -87,6 +98,5 @@ - diff --git a/tests/Python/Main.fs b/tests/Python/Main.fs deleted file mode 100644 index ae5f6f6959..0000000000 --- a/tests/Python/Main.fs +++ /dev/null @@ -1,6 +0,0 @@ -#if FABLE_COMPILER -module Program -() -#else -module Program = let [] main _ = 0 -#endif \ No newline at end of file diff --git a/tests/Python/MiscTestsHelper.fs b/tests/Python/MiscTestsHelper.fs index 8f4352e620..ae7f2ab643 100644 --- a/tests/Python/MiscTestsHelper.fs +++ b/tests/Python/MiscTestsHelper.fs @@ -42,3 +42,9 @@ type CrossModuleDemo = static member propDefault = A "prop" static member methDefault() = A "meth" + +let mutable trackedCalls = 0 + +let track x = + trackedCalls <- trackedCalls + 1 + x diff --git a/tests/Python/TestApplicative.fs b/tests/Python/TestApplicative.fs index af0164b7c2..a3d636ca8b 100644 --- a/tests/Python/TestApplicative.fs +++ b/tests/Python/TestApplicative.fs @@ -1659,6 +1659,53 @@ module MultipleInlines = let mapMyList (xs: NonEmptyList) : NonEmptyList = map (fun s -> s + "_") xs + let mutable codecCalls = 0 + + type Codec<'a> = Codec of (unit -> int) + + type Codecs = + static member inline Codecs(_output: int, _mthd: Codecs) : Codec = Codec(fun () -> 0) + static member inline Codecs(_output: bool, _mthd: Codecs) : Codec = Codec(fun () -> 0) + + static member inline Invoke() = + let inline call_2 (a: ^a, b: ^b) = + ((^a or ^b): (static member Codecs: _ * _ -> _) b, a) + + let inline call (a: 'a) = + call_2 (a, Unchecked.defaultof<'r>): Codec<'r> + + call Unchecked.defaultof + + type Codecs with + static member inline Codecs< ^a, ^b + when (^a): (static member codecInner: Codec< ^b > -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + (_output: ^a, _mthd: Codecs) + : Codec< ^a > = + codecCalls <- codecCalls + 1 + let x = codecCalls * 10 + let (Codec inner) = 'a.codecInner (Codecs.Invoke()) + Codec(fun () -> x + inner ()) + + let inline getCodecs<'a, 'b + when (Codecs or ^a): (static member Codecs: ^a * Codecs -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + () + : Codec< ^a > * Codec< ^b > = + let a: Codec<'a> = Codecs.Invoke() + let b: Codec<'b> = Codecs.Invoke() + (a, b) + + type CodecContainer<'T> = + | CodecContainer + + static member codecInner(Codec f: Codec<'T>) : Codec> = Codec(fun () -> f () + 1) + + let getContainerCodecs () = + codecCalls <- 0 + let (Codec a), (Codec b) = (getCodecs () : Codec> * Codec>) + a (), b () + open MultipleInlines [] @@ -1685,6 +1732,10 @@ let ``test Trait call works with multiple inlined functions II`` () = // See #28 let ``test Identifiers from witnesses don't get duplicated when resolving inline expressions`` () = // See #2855 NonEmptyList("a", ["b"; "c"]) |> mapMyList |> equal (NonEmptyList("a_", ["b_"; "c_"])) +[] +let ``test Identifiers from a witness inlined twice in the same scope don't get duplicated`` () = // See #3921 + getContainerCodecs () |> equal (11, 21) + module AccessorFunctionShorthand = type User = diff --git a/tests/Python/TestArithmetic.fs b/tests/Python/TestArithmetic.fs index 2cc90d4e6c..2cba8c54c2 100644 --- a/tests/Python/TestArithmetic.fs +++ b/tests/Python/TestArithmetic.fs @@ -17,6 +17,10 @@ let notALiteral = 5 let checkTo3dp (expected: float) actual = floor (actual * 1000.) |> equal expected +let moduleSinglePositiveInfinity = Single.PositiveInfinity +let moduleSingleNegativeInfinity = Single.NegativeInfinity +let moduleSingleNaN = Single.NaN + [] let ``test Adding floats works`` () = 3.141 + 2.85 |> equal 5.991 @@ -317,8 +321,8 @@ let ``test Decimal literals can be generated`` () = 0M |> equal Decimal.Zero 1M |> equal Decimal.One -1M |> equal Decimal.MinusOne - // FIXME: 79228162514264337593543950335M |> equal Decimal.MaxValue - // FIXME: -79228162514264337593543950335M |> equal Decimal.MinValue + 79228162514264337593543950335M |> equal Decimal.MaxValue + -79228162514264337593543950335M |> equal Decimal.MinValue [] let ``test Decimal.ToString works`` () = @@ -415,23 +419,51 @@ let ``test Decimal.Negate works`` () = let ``test Decimal Evaluation order is preserved by generated code`` () = (4.4567M - 2.2234M) * 2.6492M + 1.2493M |> equal 7.16575836M -// [] -// let ``test Decimal constructors work`` () = -// let d = 1.2493M -// let bits = Decimal.GetBits(d) -// let d2 = Decimal(bits) -// let d3 = Decimal(bits[0], bits[1], bits[2], false, 4uy) -// d2 |> equal d -// d3 |> equal d +[] +let ``test Decimal constructors work`` () = + let d = 1.2493M + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits[0], bits[1], bits[2], false, 4uy) + d2 |> equal d + d3 |> equal d -// [] -// let ``test Decimal GetBits works`` () = -// let d = Decimal([| -1; -1; -2; 0 |]) -// let bits = Decimal.GetBits(d) -// let d2 = Decimal(bits) -// let d3 = Decimal(bits[0], bits[1], bits[2], true, 0uy) -// d2 |> equal d -// d3 |> equal -d +[] +let ``test Decimal GetBits works`` () = + let d = Decimal([| -1; -1; -2; 0 |]) + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits[0], bits[1], bits[2], true, 0uy) + d2 |> equal d + d3 |> equal -d + +[] +let ``test Decimal GetBits works with arithmetic operations`` () = + let check (d: decimal) = + let bits = Decimal.GetBits(d) + Decimal(bits) |> equal d + + check (10M + 10M) + check (100.5M - 0.5M) + check (5M * 200M) + check (-0.1M * 100M) + check (2000M / 10M) + check (1.5M + 0.5M) + +[] +let ``test Decimal from a float rounds like .NET`` () = + string (decimal 0.1) |> equal "0.1" + string (decimal -0.1) |> equal "-0.1" + string (decimal 0.3) |> equal "0.3" + string (decimal 1.1) |> equal "1.1" + string (decimal 1e20) |> equal "100000000000000000000" + string (decimal (1.0 / 3.0)) |> equal "0.333333333333333" + +[] +let ``test Decimal from a float32 rounds like .NET`` () = + string (decimal 0.1f) |> equal "0.1" + string (decimal -0.1f) |> equal "-0.1" + string (decimal (1.0f / 3.0f)) |> equal "0.3333333" [] let ``test Decimal abs works`` () = @@ -750,6 +782,33 @@ let ``test sqrt matches .NET core implementation`` () = // let result = Single.Parse("10.5", culture) // float result |> equal 10.5 +[] +let ``test Single special values work`` () = + Single.IsPositiveInfinity Single.PositiveInfinity |> equal true + Single.IsNegativeInfinity Single.NegativeInfinity |> equal true + Single.IsInfinity Single.PositiveInfinity |> equal true + Single.IsInfinity Single.NegativeInfinity |> equal true + Single.IsNaN Single.NaN |> equal true + Single.PositiveInfinity > 0.0f |> equal true + Single.NegativeInfinity < 0.0f |> equal true + Single.PositiveInfinity |> equal infinityf + Single.NegativeInfinity |> equal -infinityf + Single.IsNaN nanf |> equal true + +[] +let ``test Single special values work as module bindings`` () = + Single.IsPositiveInfinity moduleSinglePositiveInfinity |> equal true + Single.IsNegativeInfinity moduleSingleNegativeInfinity |> equal true + Single.IsNaN moduleSingleNaN |> equal true + +[] +let ``test Double special values work`` () = + Double.IsPositiveInfinity Double.PositiveInfinity |> equal true + Double.IsNegativeInfinity Double.NegativeInfinity |> equal true + Double.IsNaN Double.NaN |> equal true + Double.PositiveInfinity |> equal infinity + Double.NegativeInfinity |> equal -infinity + [] let ``test acos works`` () = acos 0.25 |> checkTo3dp 1318. diff --git a/tests/Python/TestArray.fs b/tests/Python/TestArray.fs index 260343908c..1d85ae5af3 100644 --- a/tests/Python/TestArray.fs +++ b/tests/Python/TestArray.fs @@ -248,6 +248,22 @@ let ``test Array slice with non-numeric arrays work`` () = equal xs.[2] "X" equal xs.[3] "D" +[] +let ``test Array slices clamp the bounds like .NET`` () = + let xs = [| 1; 2; 3 |] + xs.[-1..1] |> equal [| 1; 2 |] + xs.[-5..] |> equal [| 1; 2; 3 |] + xs.[1..5] |> equal [| 2; 3 |] + xs.[..5] |> equal [| 1; 2; 3 |] + xs.[.. -2] |> equal [||] + xs.[-5.. -1] |> equal [||] + xs.[5..6] |> equal [||] + xs.[2..1] |> equal [||] + let lower, upper = -1, 1 + xs.[lower..upper] |> equal [| 1; 2 |] + let strings = [| "a"; "b"; "c" |] + strings.[-1..1] |> equal [| "a"; "b" |] + [] let ``test Array literals work`` () = let x = [| 1; 2; 3; 4; 5 |] @@ -516,6 +532,147 @@ let ``test System.Array.IndexOf works with non-primitive types`` () = System.Array.IndexOf(myArray, Dog 3, 0, 1) |> equal -1 System.Array.IndexOf(myArray, Duck 5, 1) |> equal -1 +[] +let ``test System.Array.BinarySearch works`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 0) |> equal -1 + System.Array.BinarySearch(xs, 1) |> equal 0 + System.Array.BinarySearch(xs, 2) |> equal -2 + System.Array.BinarySearch(xs, 7) |> equal 3 + System.Array.BinarySearch(xs, 8) |> equal -5 + System.Array.BinarySearch(xs, 10) |> equal -6 + System.Array.BinarySearch(([||]: int[]), 3) |> equal -1 + +[] +let ``test System.Array.BinarySearch works with duplicates`` () = + System.Array.BinarySearch([|2; 2; 2; 2; 2; 2; 2; 2|], 2) |> equal 3 + System.Array.BinarySearch([|1; 2; 2; 2; 2; 3; 4; 5; 6|], 2) |> equal 4 + +[] +let ``test System.Array.BinarySearch works with a range`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2) |> equal -2 + System.Array.BinarySearch(xs, 1, 3, 9) |> equal -5 + System.Array.BinarySearch(xs, 1, 2, 9) |> equal -4 + System.Array.BinarySearch(xs, 2, 0, 5) |> equal -3 + System.Array.BinarySearch(xs, 5, 0, 5) |> equal -6 + +[] +let ``test System.Array.BinarySearch works with a comparer`` () = + let desc = ComparisonIdentity.FromFunction(fun (a: int) b -> compare b a) + let ys = [|9; 7; 5; 3; 1|] + System.Array.BinarySearch(ys, 3, desc) |> equal 3 + System.Array.BinarySearch(ys, 4, desc) |> equal -4 + System.Array.BinarySearch(ys, 1, 3, 3, desc) |> equal 3 + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 7, (null: System.Collections.Generic.IComparer)) |> equal 3 + +[] +let ``test System.Array.BinarySearch passes the array element to the comparer first`` () = + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 9, logging) |> equal 4 + List.ofSeq calls |> equal [(5, 9); (7, 9); (9, 9)] + calls.Clear() + System.Array.BinarySearch(xs, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (1, 4); (3, 4)] + +[] +let ``test System.Array.BinarySearch works with other element types`` () = + let strings = [|"apple"; "banana"; "cherry"|] + System.Array.BinarySearch(strings, "banana") |> equal 1 + System.Array.BinarySearch(strings, "blue") |> equal -3 + let floats = [|1.5; 2.5; 3.5|] + System.Array.BinarySearch(floats, 2.5) |> equal 1 + System.Array.BinarySearch(floats, 3.0) |> equal -3 + let longs = [|1L; 5L; 10L|] + System.Array.BinarySearch(longs, 10L) |> equal 2 + System.Array.BinarySearch(longs, 6L) |> equal -3 + let records = + [|{ MainThing = 1; OtherThing = "x" }; { MainThing = 2; OtherThing = "a" }; { MainThing = 2; OtherThing = "b" }|] + System.Array.BinarySearch(records, { MainThing = 2; OtherThing = "b" }) |> equal 2 + System.Array.BinarySearch(records, { MainThing = 1; OtherThing = "z" }) |> equal -2 + let unions = [|Duck 1; Duck 4; Dog 0|] + System.Array.BinarySearch(unions, Dog 0) |> equal 2 + System.Array.BinarySearch(unions, Duck 2) |> equal -2 + +[] +let ``test System.Array.BinarySearch works with a range and a null comparer`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7, null) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2, null) |> equal -2 + +[] +let ``test System.Array.BinarySearch works with a single element and a large array`` () = + System.Array.BinarySearch([|5|], 5) |> equal 0 + System.Array.BinarySearch([|5|], 3) |> equal -1 + System.Array.BinarySearch([|5|], 7) |> equal -2 + let big = Array.init 1000 (fun i -> i * 2) + System.Array.BinarySearch(big, 998) |> equal 499 + System.Array.BinarySearch(big, 999) |> equal -501 + +[] +let ``test System.Array.BinarySearch works with more element types`` () = + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 3uy) |> equal 1 + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 4uy) |> equal -3 + System.Array.BinarySearch([|-5s; 0s; 5s|], 0s) |> equal 1 + System.Array.BinarySearch([|-5s; 0s; 5s|], -6s) |> equal -1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'c') |> equal 1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'd') |> equal -3 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "b")) |> equal 1 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "c")) |> equal -3 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 2.5M) |> equal 1 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 3.0M) |> equal -3 + let dates = [|System.DateTime(2020, 1, 1); System.DateTime(2021, 1, 1)|] + System.Array.BinarySearch(dates, System.DateTime(2021, 1, 1)) |> equal 1 + System.Array.BinarySearch(dates, System.DateTime(2020, 6, 1)) |> equal -2 + System.Array.BinarySearch([|1I; 5I; 10I|], 5I) |> equal 1 + System.Array.BinarySearch([|1I; 5I; 10I|], 11I) |> equal -4 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 1) |> equal 1 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 2) |> equal -3 + +[] +let ``test System.Array.BinarySearch with a range passes the array element to the comparer first`` () = + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 1, 3, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (3, 4)] + +[] +let ``test System.Array.BinarySearch non-generic overloads work`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs :> System.Array, box 7) |> equal 3 + System.Array.BinarySearch(xs :> System.Array, box 4) |> equal -3 + System.Array.BinarySearch(xs :> System.Array, 1, 3, box 5) |> equal 2 + +[] +let ``test System.Array.BinarySearch with a range checks its arguments`` () = + let xs = [|1; 3; 5; 7; 9|] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> System.Array.BinarySearch(xs, -1, 2, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, -1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 3, 3, 5)) |> equal "ArgumentException" + raised (fun () -> System.Array.BinarySearch(xs, 6, 0, 5)) |> equal "ArgumentException" + [] let ``test Array.findIndex works`` () = let xs = [|1.f; 2.f; 3.f; 4.f|] @@ -1462,6 +1619,59 @@ let ``test Array.randomSampleBy works`` () = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) +[] +let ``test Array.randomShuffleBy matches FSharp.Core`` () = + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + float ((calls * 37) % 100) / 100.0 + Array.randomShuffleBy next [|0 .. 9|] |> equal [|3; 7; 2; 6; 9; 0; 8; 4; 5; 1|] + calls |> equal 9 + +[] +let ``test Array.randomSampleBy matches FSharp.Core`` () = + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + Array.randomSampleBy (fun () -> 0.0) 0 ([||]: int[]) |> equal [||] + // Above the max set size it draws again on a repeated index + let values = [|0.0; 0.0; 0.5|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + values[calls - 1] + Array.randomSampleBy next 2 [|0 .. 21|] |> equal [|0; 11|] + calls |> equal 3 + +[] +let ``test Array random functions draw from the given Random`` () = + let rng = System.Random(42) + let shuffled = Array.randomShuffleWith rng [|0 .. 9|] + Array.randomShuffleWith (System.Random(42)) [|0 .. 9|] |> equal shuffled + let reference = System.Random(42) + for _ in 1 .. 9 do reference.NextDouble() |> ignore + rng.Next() |> equal (reference.Next()) + let rng = System.Random(42) + let sample = Array.randomSampleWith rng 3 [|0 .. 9|] + Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|] |> equal sample + let reference = System.Random(42) + for _ in 1 .. 3 do reference.NextDouble() |> ignore + rng.Next() |> equal (reference.Next()) + +[] +let ``test Array random functions give FSharp.Core's error messages`` () = + let message = "randomizer returned NaN, should be in range [0.0, 1.0)." + throwsErrorContaining message (fun () -> Array.randomShuffleBy (fun () -> nan) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoiceBy (fun () -> nan) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoicesBy (fun () -> nan) 2 [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomSampleBy (fun () -> nan) 1 [|1; 2|]) + throwsErrorContaining "The input array was empty." (fun () -> Array.randomChoiceWith (System.Random(1)) ([||]: int[])) + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + [] let ``test Int64 arrays roundtrip through their backing storage`` () = let xs = [| 1L; -2L; 3L |] diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index 6db0fdf8c3..6d8cd7002d 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -1,8 +1,171 @@ module Fable.Tests.Async open System +open System.Threading +open Util open Util.Testing +let private runOffThreadPool action = +#if FABLE_COMPILER + action () +#else + // .NET reschedules deferred FromContinuations callbacks invoked on a pool thread. + // Use a dedicated thread so the probe can observe completion and thrown callbacks. + let mutable failure = None + let thread = Thread(ThreadStart(fun () -> + try action () + with error -> failure <- Some error)) + thread.Start() + thread.Join() + failure |> Option.iter raise +#endif + +let private observeTryCancelled token (work: Async) = + let calls = ResizeArray() + Async.StartWithContinuations( + work, + (fun value -> calls.Add(string value)), + (fun error -> calls.Add("error: " + error.Message)), + (fun error -> calls.Add("cancel: " + error.Message)), + token) + calls + +module private FinalizerRegressions = + open System + open System.Threading + + let private check expected actual = + if expected <> actual then failwithf "Expected %A, got %A" expected actual + + let private observe (token: CancellationToken) work = + let terminal = ResizeArray() + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun e -> terminal.Add e.Message), (fun _ -> terminal.Add "cancel"), token) + terminal + + let preCancelled () = + for throws in [false; true] do + use source = new CancellationTokenSource() + source.Cancel() + let mutable cleanups = 0 + let mutable entered = false + let work = async.TryFinally(async { entered <- true }, fun () -> + cleanups <- cleanups + 1 + if throws then failwith "cleanup") + let terminal = observe source.Token work + check false entered + check 1 cleanups + check [|"cancel"|] (terminal.ToArray()) + + let nestedPreCancelled () = + use source = new CancellationTokenSource() + source.Cancel() + let cleanups = ResizeArray() + let inner = async.TryFinally(async.Return 42, fun () -> cleanups.Add "inner") + let outer = async.TryFinally(inner, fun () -> cleanups.Add "outer") + let terminal = observe source.Token outer + check [|"outer"|] (cleanups.ToArray()) + check [|"cancel"|] (terminal.ToArray()) + + let cancelledAcquisition () = + for acquisition in [0; 1; 2] do + use source = new CancellationTokenSource() + let mutable disposals = 0 + let mutable entered = false + let resource = { new IDisposable with member _.Dispose() = disposals <- disposals + 1 } + let acquire = Async.FromContinuations(fun (ok, _, _) -> + source.Cancel() + ok resource) + let work = + if acquisition = 1 then async { + use! owned = acquire + entered <- true + } + elif acquisition = 0 then async { + let! acquired = acquire + use owned = acquired + entered <- true + } + else async { + use owned = + source.Cancel() + resource + entered <- true + } + let terminal = observe source.Token work + check false entered + check 1 disposals + check [|"cancel"|] (terminal.ToArray()) + + let normalAndFailure () = + for fails in [false; true] do + let mutable disposals = 0 + let resource = { new IDisposable with member _.Dispose() = disposals <- disposals + 1 } + let work = async { + use owned = resource + if fails then failwith "body" + return 42 + } + let terminal = observe CancellationToken.None work + check 1 disposals + check [|if fails then "body" else "success"|] (terminal.ToArray()) + + let trampoline () = async { + let mutable cleanups = 0 + for _ in 1 .. 10000 do + do! async.TryFinally(async { return () }, fun () -> cleanups <- cleanups + 1) + check 10000 cleanups + } + + let downstreamExceptions () = + for channel in ["success"; "error"; "cancel"] do + let mutable cleanups = 0 + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (ok, error, cancelled) -> + match channel with + | "success" -> ok () + | "error" -> error (Exception "body") + | _ -> cancelled (OperationCanceledException())) + let terminal name = calls.Add name; failwith "downstream" + let caught = + try + Async.StartWithContinuations( + async.TryFinally(body, fun () -> cleanups <- cleanups + 1), + (fun () -> terminal "success"), (fun _ -> terminal "error"), (fun _ -> terminal "cancel")) + "no exception" + with e -> e.Message + check "downstream" caught + check 1 cleanups + check [|channel|] (calls.ToArray()) + +#if FABLE_COMPILER +module private Runtime = + open Fable.Core.PyInterop + + type Observation = + abstract terminal: string array + abstract errors: string array + abstract before: string array + abstract outcome: string array + abstract listeners: int + abstract scheduled: int + abstract cancel_count: int + abstract retired: bool + abstract owner_loop: bool + abstract threads_finished: bool + abstract pending_before: bool + abstract pending_after: bool + + type ObserveSleep = delegate of Async * System.Threading.CancellationToken * string -> Observation + type ObserveMailbox = delegate of Async * MailboxProcessor * System.Threading.CancellationToken -> Observation + type ObserveOwnerLoop = delegate of Async * System.Threading.CancellationToken -> Observation + type ObserveSettlement = delegate of ((unit -> unit) -> Async) * System.Threading.CancellationToken * string -> Observation + + let observeSleep: ObserveSleep = import "observe_sleep" "./py/async_runtime.py" + let observeMailbox: ObserveMailbox = import "observe_mailbox" "./py/async_runtime.py" + let observeOwnerLoop: ObserveOwnerLoop = import "observe_owner_loop" "./py/async_runtime.py" + let observeSettlement: ObserveSettlement = import "observe_settlement" "./py/async_runtime.py" +#endif + type DisposableAction(f) = interface IDisposable with member _.Dispose() = f() @@ -27,11 +190,163 @@ let errorWork: Async = Async.FromContinuations(fun (_,onError,_) -> onEr let cancelWork: Async = Async.FromContinuations(fun (_,_,onCancel) -> System.OperationCanceledException("cancelled") |> onCancel) +[] +let ``test TryCancelled preserves success and ordinary errors`` () = + let mutable compensations = 0 + let wrap work = Async.TryCancelled(work, fun _ -> compensations <- compensations + 1) + equal [|"42"|] ((observeTryCancelled CancellationToken.None (wrap (async.Return 42))).ToArray()) + for error in [Exception "body"; OperationCanceledException "thrown"] do + let work: Async = async { return raise error } + equal [|"error: " + error.Message|] ((observeTryCancelled CancellationToken.None (wrap work)).ToArray()) + let mutable completed = false + Async.StartWithContinuations( + Async.TryCancelled(async.Return (), fun _ -> compensations <- compensations + 1), + (fun () -> completed <- true), raise, raise) + equal true completed + equal 0 compensations + +[] +let ``test TryCancelled preserves cancellation identity and supports reuse`` () = + for throws in [false; true] do + let original = OperationCanceledException "original" + let calls = ResizeArray() + let mutable handlerSawOriginal = true + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel original) + let work = Async.TryCancelled(body, fun error -> + handlerSawOriginal <- handlerSawOriginal && obj.ReferenceEquals(original, error) + calls.Add "compensation" + if throws then failwith "compensation defect") + for _ in 1 .. 2 do + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), + (fun _ -> failwith "unexpected error"), + (fun error -> + equal true (obj.ReferenceEquals(original, error)) + calls.Add "cancel")) + equal [|"compensation"; "cancel"; "compensation"; "cancel"|] (calls.ToArray()) + equal true handlerSawOriginal + +[] +let ``test TryCancelled installs nested handlers before cancellation`` () = + use source = new CancellationTokenSource() + source.Cancel() + let mutable entered = false + let calls = ResizeArray() + let body = async { entered <- true; return 42 } + let inner = Async.TryCancelled(body, fun _ -> calls.Add "inner") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer") + Async.StartWithContinuations(outer, + (fun _ -> failwith "unexpected success"), raise, + (fun _ -> calls.Add "cancel"), source.Token) + equal false entered + equal [|"inner"; "outer"; "cancel"|] (calls.ToArray()) + +[] +let ``test TryCancelled handles deferred cancellation`` () = + runOffThreadPool (fun () -> + let original = OperationCanceledException "deferred" + let mutable cancel = fun (_: OperationCanceledException) -> failwith "not started" + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, continuation) -> cancel <- continuation) + let work = Async.TryCancelled(body, fun _ -> calls.Add "compensation") + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), raise, + (fun error -> equal true (obj.ReferenceEquals(original, error)); calls.Add "cancel")) + equal 0 calls.Count + cancel original + equal [|"compensation"; "cancel"|] (calls.ToArray())) + +[] +let ``test TryCancelled preserves finalizer order when handlers throw`` () = + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel (OperationCanceledException "original")) + let finalized = async.TryFinally(body, fun () -> calls.Add "finally") + let inner = Async.TryCancelled(finalized, fun _ -> calls.Add "inner"; failwith "inner defect") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer"; failwith "outer defect") + equal [|"cancel: original"|] ((observeTryCancelled CancellationToken.None outer).ToArray()) + equal [|"finally"; "inner"; "outer"|] (calls.ToArray()) + +[] +let ``test TryCancelled propagates downstream errors once`` () = + runOffThreadPool (fun () -> + for channel in ["success"; "error"; "cancel"] do + let mutable compensations = 0 + let calls = ResizeArray() + let mutable complete = fun () -> failwith "not started" + let body: Async = Async.FromContinuations(fun (ok, error, cancel) -> + complete <- fun () -> + match channel with + | "success" -> ok 42 + | "error" -> error (Exception "body") + | _ -> cancel (OperationCanceledException "original")) + let work = Async.TryCancelled(body, fun _ -> compensations <- compensations + 1) + let terminal name = calls.Add name; failwith "downstream" + Async.StartWithContinuations(work, + (fun _ -> terminal "success"), + (fun _ -> terminal "error"), + (fun _ -> terminal "cancel")) + let caught = + try + complete () + "no exception" + with error -> error.Message + equal "downstream" caught + equal [|channel|] (calls.ToArray()) + equal (if channel = "cancel" then 1 else 0) compensations) + +[] +let ``test TryCancelled preserves bounded trampoline execution`` () = + async { + let original = OperationCanceledException "deep cancellation" + let mutable compensations = 0 + let rec loop remaining = + if remaining = 0 then + Async.FromContinuations(fun (_, _, cancel) -> cancel original) + else + Async.TryCancelled(async { + do! async.Return () + return! loop (remaining - 1) + }, fun _ -> compensations <- compensations + 1) + do! Async.FromContinuations(fun (ok, error, _) -> + Async.StartWithContinuations(loop 10000, + (fun () -> error (Exception "unexpected success")), error, + (fun cancelled -> + if obj.ReferenceEquals(original, cancelled) then ok () + else error (Exception "cancellation identity changed")))) + equal 10000 compensations + } + |> Async.RunSynchronously + [] let ``test Simple async translates without exception`` () = async { return () } |> Async.StartImmediate +[] +let ``test Cancellation callback errors expose the first InnerException`` () = + for count in [1; 2] do + use cts = new System.Threading.CancellationTokenSource() + let errors = Array.init count (fun i -> exn (string i)) + let mutable calls = 0 + errors |> Array.iter (fun error -> + cts.Token.Register(fun () -> + calls <- calls + 1 + raise error) |> ignore) + let caught = + try + cts.Cancel() + None + with ex -> Some ex + match caught with + | Some ex -> + equal errors[count - 1].Message ex.InnerException.Message + equal true (obj.ReferenceEquals(errors[count - 1], ex.InnerException)) + | None -> failwith "Expected cancellation callback errors" + equal count calls + equal true cts.IsCancellationRequested + cts.Cancel() + equal count calls + [] let ``test Async while binding works correctly`` () = @@ -291,26 +606,34 @@ let ``test Interaction between Async and Task works`` () = equal true res } |> Async.RunSynchronously -#if FABLE_COMPILER [] let ``test Tasks can be cancelled`` () = async { - let mutable res = 0 - let tcs = new System.Threading.CancellationTokenSource(50) - let work = - let work = async { - do! Async.Sleep 75 - res <- -1 - } - Async.StartAsTask(work, cancellationToken=tcs.Token) |> Async.AwaitTask - // behavior change: a cancelled task is now triggering the exception continuation instead of - // the cancellation continuation, see: https://github.com/Microsoft/visualfsharp/issues/1416 - // also, System.OperationCanceledException will be changed to TaskCanceledException (not yet) - Async.StartWithContinuations(work, ignore, ignore, (fun _ -> res <- 1)) - do! Async.Sleep 100 - equal 1 res - } |> Async.StartImmediate -#endif + use source = new System.Threading.CancellationTokenSource() + let entered = System.Threading.Tasks.TaskCompletionSource() + let work = async { + entered.SetResult 1 + do! Async.Sleep Int32.MaxValue + failwith "Cancelled task resumed" + } + let task = Async.StartAsTask(work, cancellationToken=source.Token) + let! _ = Async.AwaitTask entered.Task + source.Cancel() + let! result = Async.Catch(Async.AwaitTask task) + match result with + | Choice2Of2 (:? System.Threading.Tasks.TaskCanceledException) -> () + | _ -> failwith "Expected TaskCanceledException through the error continuation" + } |> Async.RunSynchronously + +[] +let ``test TaskCanceledException preserves message and inner exception`` () = + let inner = Exception("inner") + let error: exn = System.Threading.Tasks.TaskCanceledException("task failure", inner) + match error with + | :? System.Threading.Tasks.TaskCanceledException -> + equal "task failure" error.Message + equal true (obj.ReferenceEquals(inner, error.InnerException)) + | _ -> failwith "Expected TaskCanceledException" [] let ``test Deep recursion with async doesn't cause stack overflow`` () = @@ -467,7 +790,7 @@ let ``test Async.StartChild applies timeout`` () = let mutable x = "" let task = async { - x <- x + "A" + // A busy .NET runner can time out before this child is scheduled. do! Async.Sleep 1_000 x <- x + "X" // Never hit } @@ -482,8 +805,8 @@ let ``test Async.StartChild applies timeout`` () = x <- x + "C" - equal x "ABC" - } |> Async.StartImmediate + equal "BC" x + } |> Async.RunSynchronously [] let ``test Async.StartChild with timeout completes when computation finishes before timeout`` () = // See #4481 @@ -496,7 +819,74 @@ let ``test Async.StartChild with timeout completes when computation finishes bef with | :? TimeoutException -> failwith "should not time out" - } |> Async.StartImmediate + } |> Async.RunSynchronously + +let private assertChildFailure startChild (error: exn) = + async { + let failing: Async = async { + do! Async.Sleep 10 + return raise error + } + let! child = startChild(failing, 60_000) + let! result = Async.Catch child + match result with + | Choice2Of2 caught -> obj.ReferenceEquals(error, caught) |> equal true + | _ -> failwith "Expected child failure" + } |> Async.RunSynchronously + +[] +let ``test Async.StartChild preserves child TimeoutException`` () = + assertChildFailure + (fun (computation, timeout) -> Async.StartChild(computation, timeout)) + (TimeoutException("child timeout")) + +let private assertChildTimeout startChild assertError = + async { + let pending: Async = Async.FromContinuations(ignore) + let! child = startChild(pending, 0) + let! result = Async.Catch child + match result with + | Choice2Of2 ex -> assertError ex + | _ -> failwith "Expected child timeout" + } |> Async.RunSynchronously + +[] +let ``test Async.StartChild zero timeout raises TimeoutException`` () = + assertChildTimeout + (fun (computation, timeout) -> Async.StartChild(computation, timeout)) + (fun ex -> ex :? TimeoutException |> equal true) + +#if FABLE_COMPILER +module private NativeTimeout = + open Fable.Core + open Fable.Core.PyInterop + + let startChild: Func, int, Async>> = import "start_child" "fable_library.async_" + let timeoutError: obj = import "TimeoutError" "builtins" + + [] + let createTimeoutError (message: string) : exn = nativeOnly + +[] +let ``test Python start_child deadline matches both timeout types`` () = + assertChildTimeout + (fun (computation, timeout) -> NativeTimeout.startChild.Invoke(computation, timeout)) + (fun ex -> + ex :? TimeoutException |> equal true + Fable.Core.PyInterop.pyInstanceof ex NativeTimeout.timeoutError |> equal true) + +[] +let ``test Async.StartChild preserves child Python TimeoutError`` () = + assertChildFailure + (fun (computation, timeout) -> Async.StartChild(computation, timeout)) + (NativeTimeout.createTimeoutError "child timeout") + +[] +let ``test Python start_child preserves child TimeoutError`` () = + assertChildFailure + (fun (computation, timeout) -> NativeTimeout.startChild.Invoke(computation, timeout)) + (NativeTimeout.createTimeoutError "child timeout") +#endif [] let ``test Unit arguments are erased`` () = // See #1832 @@ -586,3 +976,378 @@ let ``test Async.AwaitEvent with cancelAction invokes it on cancellation`` () = }, cts.Token) cts.Cancel() equal true cancelCalled + +[] +let ``test FromContinuations unit and generic values`` () = + let fromValue value = Async.FromContinuations(fun (ok, _, _) -> ok value) + let mutable completed = 0 + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok ()) + Async.StartWithContinuations(unitWork, (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 3 completed + +[] +let ``test unit continuations preserve captured callbacks`` () = + let mutable calls = 0 + let rec genericDeadline callback value remaining = + if remaining > 0 then genericDeadline callback value (remaining - 1) + else Async.StartWithContinuations(async { return value }, (fun result -> callback result), raise, raise) + let rec unitDeadline (callback: unit -> unit) remaining = + if remaining > 0 then unitDeadline callback (remaining - 1) + else Async.StartWithContinuations(async { return () }, (fun () -> callback ()), raise, raise) + genericDeadline (fun () -> calls <- calls + 1) () 2 + unitDeadline (fun () -> calls <- calls + 1) 2 + let rec namedUnitCapture (__unit: int) remaining = + if remaining > 0 then namedUnitCapture __unit (remaining - 1) + else Async.StartWithContinuations(async { return () }, (fun () -> calls <- calls + __unit), raise, raise) + namedUnitCapture 3 2 + equal 5 calls + +[] +let ``test cooperative cancellation stops at the next async boundary`` () = + use source = new System.Threading.CancellationTokenSource() + let mutable finalizers = 0 + let mutable terminal = "" + let mutable reached = false + let work = async { + try + source.Cancel() + do! async { return () } + reached <- true + finally + finalizers <- finalizers + 1 + } + Async.StartWithContinuations(work, (fun () -> terminal <- "success"), (fun _ -> terminal <- "error"), (fun _ -> terminal <- "cancel"), source.Token) + equal false reached + equal 1 finalizers + equal "cancel" terminal + + +[] +let ``test finalizer failure overrides success once`` () = + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! successWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun error -> terminal.Add error.Message), (fun _ -> terminal.Add "cancel")) + equal [| "finalizer" |] (terminal.ToArray()) + equal 1 finalizers + +[] +let ``test finalizer failure overrides body failure once`` () = + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! errorWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun error -> terminal.Add error.Message), (fun _ -> terminal.Add "cancel")) + equal [| "finalizer" |] (terminal.ToArray()) + equal 1 finalizers + +[] +let ``test finalizer failure preserves cancellation once`` () = + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! cancelWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun _ -> terminal.Add "error"), (fun _ -> terminal.Add "cancel")) + equal [| "cancel" |] (terminal.ToArray()) + equal 1 finalizers + +#if FABLE_COMPILER +let private checkSleep cancelFirst throws = + use source = new System.Threading.CancellationTokenSource() + let mutable finalizers = 0 + let work = async { + try do! Async.Sleep 100 + finally + finalizers <- finalizers + 1 + if throws then failwith "finalizer" + } + let result = Runtime.observeSleep.Invoke(work, source.Token, if cancelFirst then "cancel" else "timeout") + let expected = if cancelFirst then "cancel" elif throws then "error" else "success" + equal [| expected |] result.terminal + equal 1 finalizers + equal 0 result.listeners + equal 1 result.scheduled + equal true result.retired + equal 1 result.cancel_count + if throws && not cancelFirst then equal [| "finalizer" |] result.errors + else equal [||] result.errors + +[] +let ``test cancelled Sleep ignores stale timers and cleans up`` () = + checkSleep true false + +[] +let ``test completed Sleep ignores cancellation and stale timers`` () = + checkSleep false false + +[] +let ``test cancelled Sleep ignores a throwing finalizer and stale timers`` () = + checkSleep true true + +[] +let ``test precancelled Sleep does not execute or schedule`` () = + use source = new System.Threading.CancellationTokenSource() + let mutable reached = false + let work = async { + reached <- true + do! Async.Sleep 100 + } + let result = Runtime.observeSleep.Invoke(work, source.Token, "precancel") + equal [| "cancel" |] result.terminal + equal false reached + equal 0 result.scheduled + equal 0 result.listeners + +[] +let ``test idle Receive is cancelled by its token and keeps later posts queued`` () = + use source = new System.Threading.CancellationTokenSource() + let mailbox = new MailboxProcessor((fun _ -> async { return () }), source.Token) + let result = Runtime.observeMailbox.Invoke(mailbox.Receive(), mailbox, source.Token) + equal [| "cancel" |] result.before + equal false result.pending_before + equal [| "cancel" |] result.terminal + equal false result.pending_after + equal 0 result.listeners + equal [| "42" |] result.outcome + +[] +let ``test Sleep cancellation from another thread runs on the owner loop`` () = + use source = new System.Threading.CancellationTokenSource() + let result = Runtime.observeOwnerLoop.Invoke(Async.Sleep 100000, source.Token) + equal [| "cancel" |] result.terminal + equal true result.owner_loop + equal true result.threads_finished + equal 0 result.listeners + +let private checkSettlement winner = + use source = new System.Threading.CancellationTokenSource() + let mutable finalizers = 0 + let makeWork timeout = async { + try do! Async.Sleep 100 + finally + finalizers <- finalizers + 1 + timeout () + } + let result = Runtime.observeSettlement.Invoke(makeWork, source.Token, winner) + equal [| winner |] result.outcome + equal [| if winner = "timeout" then "success" else "cancel" |] result.terminal + equal 1 finalizers + equal true result.threads_finished + equal 0 result.listeners + equal true result.retired + equal 1 result.cancel_count + +[] +let ``test reply wins concurrent reply cancellation and timeout settlement`` () = + checkSettlement "reply" + +[] +let ``test cancellation wins concurrent reply cancellation and timeout settlement`` () = + checkSettlement "cancel" + +[] +let ``test timeout wins concurrent reply cancellation and timeout settlement`` () = + checkSettlement "timeout" + +[] +let ``test cancellation during Sleep installation releases resources`` () = + use source = new System.Threading.CancellationTokenSource() + let result = Runtime.observeSleep.Invoke(Async.Sleep 100, source.Token, "install_cancel") + equal [| "cancel" |] result.terminal + equal 0 result.listeners + equal 1 result.scheduled + equal true result.retired + equal 1 result.cancel_count + +[] +let ``test failed Sleep installation releases its registration`` () = + use source = new System.Threading.CancellationTokenSource() + let result = Runtime.observeSleep.Invoke(Async.Sleep 100, source.Token, "install_error") + equal [| "error" |] result.terminal + equal [| "scheduler" |] result.errors + equal 0 result.listeners + equal 0 result.scheduled +#endif + +[] +let ``test cancelling a waiting Receive reports cancel and keeps later posts queued`` () = + async { + use source = new System.Threading.CancellationTokenSource() + let mailbox = new MailboxProcessor((fun _ -> async { return () }), source.Token) + let! outcome = + Async.FromContinuations(fun (ok, _, _) -> + Async.StartWithContinuations(mailbox.Receive(), (fun value -> ok $"value {value}"), (fun _ -> ok "error"), (fun _ -> ok "cancel"), source.Token) + source.Cancel()) + equal "cancel" outcome + mailbox.Post 42 + let! next = mailbox.Receive() + equal 42 next + } |> Async.RunSynchronously + +[] +let ``test cancelling the mailbox token cancels a body waiting in Receive`` () = + async { + use source = new System.Threading.CancellationTokenSource() + let! outcome = + Async.FromContinuations(fun (ok, _, _) -> + MailboxProcessor.Start((fun inbox -> async { + let received = ref None + try + source.CancelAfter 50 + let! message = inbox.Receive() + received.Value <- Some message + finally + match received.Value with + | Some message -> ok $"received {message}" + | None -> ok "cancelled" }), source.Token) + |> ignore) + equal "cancelled" outcome + } |> Async.RunSynchronously + +[] +let ``test TryFinally installs cleanup before pre-cancellation`` () = + FinalizerRegressions.preCancelled () + +[] +let ``test pre-cancelled nested TryFinally only runs outer cleanup`` () = + FinalizerRegressions.nestedPreCancelled () + +[] +let ``test cancelled acquisition disposes use and use bang resources`` () = + FinalizerRegressions.cancelledAcquisition () + +[] +let ``test resource cleanup runs once on success and failure`` () = + FinalizerRegressions.normalAndFailure () + +[] +let ``test TryFinally preserves bounded trampoline execution`` () = + FinalizerRegressions.trampoline () |> Async.RunSynchronously + +[] +let ``test TryFinally propagates downstream continuation errors once`` () = + FinalizerRegressions.downstreamExceptions () + +let private fromGenericCallback (callback: ('T -> unit) -> (exn -> unit) -> (OperationCanceledException -> unit) -> unit) : Async<'T> = + Async.FromContinuations(fun (ok, error, cancelled) -> callback ok error cancelled) + +let private forwardGenericValue (continuation: 'T -> unit) (value: 'T) = + continuation value + +[] +let ``test unit FromContinuations accepts a forwarded generic value`` () = + let work: Async = Async.FromContinuations(fun (ok, _, _) -> forwardGenericValue ok ()) + let mutable completed = false + Async.StartWithContinuations(work, (fun () -> completed <- true), raise, raise) + equal true completed + +[] +let ``test generic FromContinuations accepts erased unit callbacks`` () = + let calls = ResizeArray() + let start work = Async.StartWithContinuations(work, (fun _ -> calls.Add "success"), (fun e -> calls.Add e.Message), (fun e -> calls.Add e.Message)) + let unitWork: Async = fromGenericCallback (fun ok _ _ -> ok ()) + start unitWork + let valueWork = fromGenericCallback (fun ok _ _ -> ok 42) + Async.StartWithContinuations(valueWork, (fun value -> equal 42 value; calls.Add "value"), raise, raise) + let errorWork: Async = fromGenericCallback (fun _ error _ -> error (exn "error")) + start errorWork + let cancelWork: Async = fromGenericCallback (fun _ _ cancelled -> cancelled (OperationCanceledException("cancel"))) + start cancelWork + equal [|"success"; "value"; "error"; "cancel"|] (calls.ToArray()) + +let private invokeGenericCallback (callback: ('T -> unit) -> 'Result) (value: 'T) : 'Result = + callback (fun received -> equal value received) + +[] +let ``test generic unit callback adaptation preserves return types`` () = + equal 42 (invokeGenericCallback (fun ok -> ok (); 42) ()) + equal "unit" (invokeGenericCallback (fun ok -> ok (); "unit") ()) + equal "value" (invokeGenericCallback (fun ok -> ok 7; "value") 7) + +type private CallbackRunner = + static member Run(callback: ('T -> unit) -> 'Result, value: 'T, [] extra: int[]) : 'Result = + callback (fun received -> equal value received) + +[] +let ``test generic unit callback adaptation covers local functions and param arrays`` () = + let run (callback: ('T -> unit) -> 'Result) (value: 'T) : 'Result = + callback (fun received -> equal value received) + equal 42 (run (fun ok -> ok (); 42) ()) + equal "value" (run (fun ok -> ok 7; "value") 7) + equal 42 (CallbackRunner.Run((fun ok -> ok (); 42), (), 1, 2)) + equal "value" (CallbackRunner.Run((fun ok -> ok 7; "value"), 7)) + +[] +let ``test Async try with does not run with branch when body succeeds`` () = + let work = async { + try + return 1 + with _ -> + return 99 } + let mutable result = 0 + Async.StartWithContinuations(work, (fun r -> result <- r), ignore, ignore) + equal 1 result + +[] +let ``test Exception thrown by the continuation is not redirected to the with branch`` () = + let work = async { + try + return 1 + with _ -> + return 99 } + let mutable calls = 0 + throwsAnyError (fun () -> + Async.StartWithContinuations( + work, + (fun _ -> calls <- calls + 1; failwith "boom from continuation"), + ignore, + ignore)) + equal 1 calls + +[] +let ``test Exception thrown by the continuation is not redirected through a bind`` () = + let work = async { + let! x = async { return 1 } + return x + 1 } + let calls = ResizeArray() + throwsAnyError (fun () -> + Async.StartWithContinuations( + work, + (fun value -> calls.Add (string value); failwith "boom from continuation"), + (fun _ -> calls.Add "error"), + (fun _ -> calls.Add "cancel"))) + equal [|"2"|] (calls.ToArray()) + +[] +let ``test FromContinuations rejects a second completion`` () = + let unitCalls = ResizeArray() + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok (); ok ()) + throwsAnyError (fun () -> + Async.StartWithContinuations(unitWork, (fun () -> unitCalls.Add "success"), (fun _ -> unitCalls.Add "error"), (fun _ -> unitCalls.Add "cancel"))) + equal true (unitCalls.Count <= 1) + equal false (unitCalls.Contains "error") + equal false (unitCalls.Contains "cancel") + + let valueCalls = ResizeArray() + let valueWork = Async.FromContinuations(fun (ok, _, _) -> ok 42; ok 43) + throwsAnyError (fun () -> + Async.StartWithContinuations(valueWork, (fun v -> valueCalls.Add (string v)), (fun _ -> valueCalls.Add "error"), (fun _ -> valueCalls.Add "cancel"))) + equal true (valueCalls.Count <= 1) + equal false (valueCalls.Contains "43") + equal false (valueCalls.Contains "error") + equal false (valueCalls.Contains "cancel") diff --git a/tests/Python/TestComparison.fs b/tests/Python/TestComparison.fs index d8374f46be..9e29ebcb7f 100644 --- a/tests/Python/TestComparison.fs +++ b/tests/Python/TestComparison.fs @@ -38,6 +38,34 @@ type UTest2 = exception Ex of int +[] +type RefEqRecord = { a2: int; b2: int } + +[] +type RefEqUnion = RefC of int | RefD of int + +[] +type NoEqRecord = { a3: int } + +[] +type NoEqUnion = NoEqCase of int + +#nowarn "386" +[] +type NoEqRecordWithOverride = + { a4: int } + override _.Equals(other) = true + override _.GetHashCode() = 4 + +[] +type NoEqStruct = { s2: int } + +let genericEquals (a: 'T) (b: 'T) : bool = + EqualityComparer<'T>.Default.Equals(a, b) + +let genericHash (x: 'T) : int = + EqualityComparer<'T>.Default.GetHashCode(x) + type MyTest(i: int) = member x.Value = i override x.GetHashCode() = i @@ -95,6 +123,69 @@ let ``test PhysicalEquality works`` () = // See #3998 LanguagePrimitives.PhysicalEquality r2 r2 |> equal true LanguagePrimitives.PhysicalEquality r3 r1 |> equal true +[] +let ``test ReferenceEquality record equality works`` () = + let x = { a2 = 1; b2 = 2 } + let y = { a2 = 1; b2 = 2 } + equal true (x = x) + equal false (x = y) + equal true (x <> y) + equal false (x.Equals y) + equal true (genericEquals x x) + equal false (genericEquals x y) + equal false ([ x ] = [ y ]) + equal false ((x, 1) = (y, 1)) + equal false (Some x = Some y) + equal true ([ x ] |> List.contains x) + equal false ([ x ] |> List.contains y) + +[] +let ``test ReferenceEquality record hash works`` () = + let x = { a2 = 1; b2 = 2 } + let y = { a2 = 1; b2 = 2 } + equal (hash x) (hash x) + equal (genericHash x) (genericHash x) + let d = Dictionary() + d[x] <- 1 + equal true (d.ContainsKey x) + equal false (d.ContainsKey y) + equal 2 (HashSet([ x; y ]).Count) + +[] +let ``test ReferenceEquality union equality works`` () = + let x = RefC 1 + let y = RefC 1 + equal true (x = x) + equal false (x = y) + equal false (x.Equals y) + equal true (genericEquals x x) + equal false (genericEquals x y) + equal false ([ x ] |> List.contains y) + equal 2 (HashSet([ x; y ]).Count) + +[] +let ``test NoEquality record and union compare by reference`` () = + let x = { a3 = 1 } + equal true ((box x).Equals(box x)) + equal false ((box x).Equals(box { a3 = 1 })) + equal true (genericEquals x x) + equal false (genericEquals x { a3 = 1 }) + let u = NoEqCase 1 + equal true ((box u).Equals(box u)) + equal false ((box u).Equals(box (NoEqCase 1))) + equal true (genericEquals u u) + equal false (genericEquals u (NoEqCase 1)) + +[] +let ``test NoEquality record keeps an explicit Equals override`` () = + equal true ((box { a4 = 1 }).Equals(box { a4 = 2 })) + equal 4 ((box { a4 = 1 }).GetHashCode()) + +[] +let ``test NoEquality struct record compares structurally when boxed`` () = + equal true ((box { s2 = 1 }).Equals(box { s2 = 1 })) + equal false ((box { s2 = 1 }).Equals(box { s2 = 2 })) + [] let ``test Typed array equality works`` () = let xs1 = [| 1; 2; 3 |] diff --git a/tests/Python/TestConvert.fs b/tests/Python/TestConvert.fs index 0ab2b74a89..ea04995e4f 100644 --- a/tests/Python/TestConvert.fs +++ b/tests/Python/TestConvert.fs @@ -197,6 +197,13 @@ let ``test System.UInt64.TryParse works`` () = tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) +[] +let ``test System.UInt64.TryParse works above Int64.MaxValue`` () = + tryParse UInt64.TryParse 0UL "9223372036854775808" |> equal (true, 9223372036854775808UL) + tryParse UInt64.TryParse 0UL "18446744073709551615" |> equal (true, UInt64.MaxValue) + tryParse UInt64.TryParse 0UL "18446744073709551616" |> equal (false, 0UL) + tryParse UInt64.TryParse 0UL "-1" |> equal (false, 0UL) + [] let ``test Parsing integers with different radices works`` () = equal 11 (int "11") @@ -411,6 +418,26 @@ let private toIntFromInt16 (x: int16) = int x let private toFloatFromSingle (x: float32) = float x let private toFloatFromByte (x: byte) = float x let private toFloatFromInt (x: int) = float x +let private toInt16FromSByte (x: sbyte) = int16 x +let private toUInt16FromByte (x: byte) = uint16 x +let private toUInt32FromByte (x: byte) = uint32 x +let private toUInt32FromUInt16 (x: uint16) = uint32 x + +[] +let ``test Widening between integer wrappers uses the target width`` () = + // Compare as int so wrapper equality cannot truncate the expected value to the source width. + toInt16FromSByte 100y <<< 1 |> int |> equal 200 + toInt16FromSByte -100y <<< 1 |> int |> equal -200 + toUInt16FromByte 200uy <<< 1 |> int |> equal 400 + toUInt32FromByte 200uy <<< 1 |> int |> equal 400 + toUInt32FromUInt16 60000us <<< 1 |> int |> equal 120000 + +[] +let ``test Widening between integer wrappers has the target runtime type`` () = + box (toInt16FromSByte 5y) :? int16 |> equal true + box (toUInt16FromByte 5uy) :? uint16 |> equal true + box (toUInt32FromByte 5uy) :? uint32 |> equal true + box (toUInt32FromUInt16 5us) :? uint32 |> equal true [] let ``test Widening to int does not keep the source width`` () = @@ -1033,22 +1060,54 @@ let ``test System.Convert.ToString Decimal works`` () = let x = "101" Convert.ToString(101.m) |> equal x -// [] -// let ``test Convert.ToHexString works`` () = -// let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] -// Convert.ToHexString(bytes) -// |> equal "FAFBFCFDFE" +[] +let ``test Convert.ToHexString works`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexString(bytes) + |> equal "FAFBFCFDFE" + Convert.ToHexString([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010F10" + Convert.ToHexString([||]) |> equal "" + Convert.ToHexString(bytes, 1, 3) |> equal "FBFCFD" + Convert.ToHexString(bytes, 5, 0) |> equal "" -// [] -// let ``test Convert.ToHexStringLower works`` () = -// let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] -// Convert.ToHexStringLower(bytes) -// |> equal "fafbfcfdfe" +[] +let ``test Convert.ToHexStringLower works`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexStringLower(bytes) + |> equal "fafbfcfdfe" + Convert.ToHexStringLower([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010f10" + Convert.ToHexStringLower([||]) |> equal "" + Convert.ToHexStringLower(bytes, 1, 3) |> equal "fbfcfd" -// [] -// let ``test Convert.FromHexString works`` () = -// Convert.FromHexString("fafBFCFDFE") -// |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] +[] +let ``test Convert.ToHexString checks its arguments`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + raisedExceptionType (fun () -> Convert.ToHexString(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString((null: byte[]), 0, 0)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 1, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 6, 0)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Convert.FromHexString works`` () = + Convert.FromHexString("fafBFCFDFE") + |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.FromHexString("0123456789abcdefABCDEF") + |> equal [| 1uy; 35uy; 69uy; 103uy; 137uy; 171uy; 205uy; 239uy; 171uy; 205uy; 239uy |] + Convert.FromHexString("") |> equal [||] + +[] +let ``test Convert.FromHexString checks its input`` () = + raisedExceptionType (fun () -> Convert.FromHexString(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.FromHexString("abc")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("zz")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("0x10")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("00 1")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("+1")) |> equal "FormatException" [] let ``test Convert.ToBase64String works`` () = @@ -1056,6 +1115,14 @@ let ``test Convert.ToBase64String works`` () = Convert.ToBase64String(bytes) |> equal "AgQGCAoMDhASFA==" +let private genericOfList<'T> (xs: 'T list) : 'T[] = Array.ofList xs + +[] +let ``test Convert.ToBase64String works on an array built by a generic function`` () = + let bytes = genericOfList [ 2uy; 4uy; 6uy; 8uy; 10uy; 12uy; 14uy; 16uy; 18uy; 20uy ] + Convert.ToBase64String(bytes) + |> equal "AgQGCAoMDhASFA==" + [] let ``test Convert.FromBase64String works`` () = Convert.FromBase64String("AgQGCAoMDhASFA==") diff --git a/tests/Python/TestDateTime.fs b/tests/Python/TestDateTime.fs index a1d06ff962..76e9af4218 100644 --- a/tests/Python/TestDateTime.fs +++ b/tests/Python/TestDateTime.fs @@ -925,7 +925,8 @@ let ``test DateTime.Now works`` () = let ``test DateTime.UtcNow works`` () = let d = DateTime.UtcNow d > DateTime.MinValue |> equal true - // d.Kind |> equal DateTimeKind.Utc // currently we're not storing Kind + d.Kind |> equal DateTimeKind.Utc + d.Date.Kind |> equal DateTimeKind.Utc [] let ``test DateTime.AddYears works`` () = diff --git a/tests/Python/TestDateTimeOffset.fs b/tests/Python/TestDateTimeOffset.fs index c4ea6ac15c..af806354cc 100644 --- a/tests/Python/TestDateTimeOffset.fs +++ b/tests/Python/TestDateTimeOffset.fs @@ -569,3 +569,16 @@ let ``test DateTimeOffset constructor from DateOnly and TimeOnly works`` () = dt.Minute |> equal dt_exp.Minute dt.Second |> equal dt_exp.Second dt.Millisecond |> equal dt_exp.Millisecond + +[] +let ``test Implicit conversion from DateTime preserves value`` () = + let d = DateTime(2024, 1, 15, 13, 23, 30, DateTimeKind.Utc) + let dto: DateTimeOffset = d + dto |> equal (DateTimeOffset(d)) + dto.Offset |> equal TimeSpan.Zero + dto.UtcDateTime.Year |> equal 2024 + dto.UtcDateTime.Month |> equal 1 + dto.UtcDateTime.Day |> equal 15 + dto.UtcDateTime.Hour |> equal 13 + dto.UtcDateTime.Minute |> equal 23 + dto.UtcDateTime.Second |> equal 30 diff --git a/tests/Python/TestDictionary.fs b/tests/Python/TestDictionary.fs index 878f652fb8..8f28123c47 100644 --- a/tests/Python/TestDictionary.fs +++ b/tests/Python/TestDictionary.fs @@ -226,16 +226,39 @@ let ``test Dictionary.ContainsValue works`` () = dic.ContainsValue("Everybody!") |> equal false [] -let ``test Dictionary.Delete works`` () = +let ``test Dictionary.Remove works`` () = let dic = Dictionary<_, _>() dic.Add("A", "Hello") dic.Add("B", "World!") dic.ContainsValue("Hello") |> equal true dic.Remove("A") |> equal true dic.ContainsValue("Hello") |> equal false + dic.Count |> equal 1 [] -let ``test Dictionary.Delete works with tuples as keys`` () = +let ``test Dictionary.Remove with complex keys works`` () = + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 + +[] +let ``test Dictionary.Remove with records as keys works`` () = + let x1 = { a = 5 } + let x2 = { a = 5 } + let x3 = { a = 10 } + let x4 = { a = 15 } + let dict = Dictionary<_, _>() + dict.Add(x1, "Hello") + dict.Add(x3, "World!") + dict.Remove(x2) |> equal true + dict.Remove(x4) |> equal false + dict.Count |> equal 1 + +[] +let ``test Dictionary.Remove works with tuples as keys`` () = let my_dict = Dictionary((Map [for i in 0..10 do yield (i,i), sprintf "Number: %i" i])) my_dict.ContainsKey((0,0)) |> equal true my_dict.Remove((0,0)) |> equal true diff --git a/tests/Python/TestGuid.fs b/tests/Python/TestGuid.fs index 04cf7aad89..16cfc2ca5e 100644 --- a/tests/Python/TestGuid.fs +++ b/tests/Python/TestGuid.fs @@ -157,6 +157,23 @@ let ``test Convert byte array to Guid works`` () = let g = Guid [|6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy|] g.ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" +[] +let ``test Convert byte array built from a list or seq to Guid works`` () = + let bytes = id [6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy] + Guid(List.toArray bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(Array.ofList bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(Array.ofSeq (Seq.ofList bytes)).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + +let private genericOfList<'T> (xs: 'T list) : 'T[] = Array.ofList xs + +let private genericOfSeq<'T> (xs: 'T seq) : 'T[] = Array.ofSeq xs + +[] +let ``test Convert byte array built by a generic function to Guid works`` () = + let bytes = id [6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy] + Guid(genericOfList bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(genericOfSeq (Seq.ofList bytes)).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + // [] // let ``Guid.ToString works with formats`` () = // let g = Guid.Parse("96258006-c4ba-4a7f-80c4-de7f2b2898c5") diff --git a/tests/Python/TestHashSet.fs b/tests/Python/TestHashSet.fs index 4304248e50..537b02b26e 100644 --- a/tests/Python/TestHashSet.fs +++ b/tests/Python/TestHashSet.fs @@ -243,16 +243,16 @@ let ``test HashSet.Add with records works`` () = hs.Add(x3) |> equal true hs.Count |> equal 2 -// [] -// let ``test HashSet.Add with reference records works`` () = -// let x1 = { a = 5; b = 10 } -// let x2 = { a = 5; b = 10 } -// let x3 = { a = 10; b = 20 } -// let hs = HashSet() -// hs.Add(x1) |> equal true -// hs.Add(x2) |> equal true -// hs.Add(x3) |> equal true -// hs.Count |> equal 3 +[] +let ``test HashSet.Add with reference records works`` () = + let x1 = { a = 5; b = 10 } + let x2 = { a = 5; b = 10 } + let x3 = { a = 10; b = 20 } + let hs = HashSet() + hs.Add(x1) |> equal true + hs.Add(x2) |> equal true + hs.Add(x3) |> equal true + hs.Count |> equal 3 [] let ``test HashSet.Clear works`` () = diff --git a/tests/Python/TestList.fs b/tests/Python/TestList.fs index 05badb8fa9..3a82d99dd4 100644 --- a/tests/Python/TestList.fs +++ b/tests/Python/TestList.fs @@ -1240,9 +1240,126 @@ let ``test List.randomSampleBy works`` () = throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) +[] +let ``test List random functions match FSharp.Core`` () = + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + throwsErrorContaining "randomizer returned NaN, should be in range [0.0, 1.0)." (fun () -> List.randomChoiceBy (fun () -> nan) [1; 2]) + throwsErrorContaining "The input sequence was empty." (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + [] let ``test List.collect works II`` () = let xs = ["a"; "fable"; "bar" ] xs |> List.collect (fun a -> [a.Length]) |> equal [1; 5; 3] + +[] +let ``test List.reduce and List.reduceBack raise ArgumentException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduceBack (+) xs) |> equal "ArgumentException" + +[] +let ``test List.min, List.max, List.minBy and List.maxBy raise ArgumentException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> List.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.maxBy id xs) |> equal "ArgumentException" + +[] +let ``test List.last, List.head and List.tail raise ArgumentException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> List.last xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.tail xs) |> equal "ArgumentException" + +[] +let ``test list.Head and list.Tail raise InvalidOperationException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> xs.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> xs.Tail) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + +[] +let ``test List.take and List.splitAt raise InvalidOperationException when the list is too short`` () = + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.splitAt 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitAt -1 [1]) |> equal "ArgumentException" + +[] +let ``test List.skip raises ArgumentOutOfRangeException when the list is too short`` () = + raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" + +[] +let ``test List functions walking two lists raise ArgumentException when the lengths differ`` () = + let xs = [1; 2] + let ys = [1; 2; 3] + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) [1] [1; 2] 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.mapi2 (fun i x y -> i + x + y) ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iter2 (fun _ _ -> ()) [] [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iteri2 (fun _ _ _ -> ()) [1] []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip ([]: int list) [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> true) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> false) [1] []) |> equal "ArgumentException" + +[] +let ``test List.map3 and List.zip3 raise ArgumentException when the lengths differ`` () = + let add3 x y z = x + y + z + raisedExceptionType (fun () -> List.map3 add3 [1; 2] [1; 2; 3] [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 add3 [1; 2; 3] [1; 2; 3] [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 [1] [] [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 [1; 2] [1; 2] [1; 2; 3]) |> equal "ArgumentException" + +[] +let ``test List functions walking lists in step call back on the common prefix before raising`` () = + let calls = ResizeArray() + raisedExceptionType (fun () -> List.iter2 (fun x _ -> calls.Add x) [1; 2] [1; 2; 3]) + |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x _ _ -> calls.Add x) [3; 4; 5] [1; 2] [1; 2; 3]) + |> equal "ArgumentException" + List.ofSeq calls |> equal [1; 2; 3; 4] + +[] +let ``test List.forall2 stops at the first false before checking the lengths`` () = + List.forall2 (fun _ _ -> false) [1; 2] [1; 2; 3] |> equal false + List.forall2 (fun x _ -> x < 2) [1; 2; 3] [1; 2] |> equal false + List.forall2 (fun x y -> x = y) [1; 2] [1; 2] |> equal true + +[] +let ``test List.init and List.replicate raise ArgumentException for a negative count`` () = + raisedExceptionType (fun () -> List.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.replicate -1 "a") |> equal "ArgumentException" + List.init 0 id |> equal ([]: int list) + List.replicate 0 "a" |> equal [] + +[] +let ``test List.average and List.averageBy raise ArgumentException on empty input`` () = + let xs: float list = [] + raisedExceptionType (fun () -> List.average xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.averageBy (fun x -> x * 2.) xs) |> equal "ArgumentException" + +[] +let ``test List.removeManyAt raises ArgumentException where FSharp.Core does`` () = + let xs = [1; 2; 3] + raisedExceptionType (fun () -> List.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 2 2 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 4 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 0 1 ([]: int list)) |> equal "ArgumentException" + +[] +let ``test List.removeManyAt works and removes nothing for a count that is not positive`` () = + let xs = [1; 2; 3] + List.removeManyAt 1 2 xs |> equal [1] + List.removeManyAt 1 0 xs |> equal [1; 2; 3] + List.removeManyAt 1 -1 xs |> equal [1; 2; 3] + List.removeManyAt 3 0 xs |> equal [1; 2; 3] + List.removeManyAt 0 0 ([]: int list) |> equal [] diff --git a/tests/Python/TestMailboxProcessor.fs b/tests/Python/TestMailboxProcessor.fs index 6e6ce289b1..9eeac160b2 100644 --- a/tests/Python/TestMailboxProcessor.fs +++ b/tests/Python/TestMailboxProcessor.fs @@ -317,3 +317,33 @@ let ``test MailboxProcessor postAndAsyncReply can compute reply from state`` () let! sum = agent.PostAndAsyncReply(GetSum) equal 65 sum } |> Async.StartImmediate + +[] +let ``test MailboxProcessor second Reply is ignored`` () = + async { + let log = ResizeArray() + let completed = new MailboxProcessor(fun _ -> async { return () }) + let agent = MailboxProcessor>.Start(fun inbox -> async { + let! channel = inbox.Receive() + channel.Reply 1 + channel.Reply 2 + log.Add "body done" + completed.Post() }) + let! reply = agent.PostAndAsyncReply(id) + equal 1 reply + do! completed.Receive() + equal [|"body done"|] (log.ToArray()) + } |> Async.RunSynchronously + +[] +let ``test MailboxProcessor reply computation can be awaited twice`` () = + async { + let agent = MailboxProcessor>.Start(fun inbox -> async { + let! channel = inbox.Receive() + channel.Reply 42 }) + let work = agent.PostAndAsyncReply(id) + let! first = work + let! second = work + equal 42 first + equal 42 second + } |> Async.RunSynchronously diff --git a/tests/Python/TestMap.fs b/tests/Python/TestMap.fs index 4c33e1d710..bee7396796 100644 --- a/tests/Python/TestMap.fs +++ b/tests/Python/TestMap.fs @@ -1,6 +1,7 @@ module Fable.Tests.Maps open System.Collections.Generic +open Fable.Tests.Util open Util.Testing type R = { i: int; s: string } @@ -351,3 +352,10 @@ let ``test Map keys and values work correctly`` () = let myMap = Map [ "a", 10; "b", 20 ] :> IDictionary<_,_> myMap.Keys |> Seq.toList |> equal ["a"; "b"] myMap.Values |> Seq.toList |> equal [10; 20] + +[] +let ``test Map enumerator Current raises InvalidOperationException outside the enumeration`` () = + let e = (Map [1, "a"] :> seq<_>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index 973873b61e..2e7d545f41 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1122,6 +1122,94 @@ let ``test Exception InnerException is null when not provided`` () = let ex = System.ArgumentException("no inner") isNull (box ex.InnerException) |> equal true +[] +let ``test TimeoutException preserves message and empty InnerException`` () = + let ex = TimeoutException("timeout message") + ex.Message |> equal "timeout message" + isNull (box ex.InnerException) |> equal true + let caught = + try + raise ex + false + with + | :? TimeoutException as caught -> obj.ReferenceEquals(ex, caught) + caught |> equal true + +[] +let ``test exceptions sharing a Python base retain distinct dotnet types`` () = + let caught = + try + raise (FormatException("bad format")) + "not caught" + with + | :? ArgumentException -> "argument" + | :? FormatException as ex -> ex.Message + caught |> equal "bad format" + +#if FABLE_COMPILER +module private PythonExceptionBases = + open Fable.Core.PyInterop + + let valueError: obj = import "ValueError" "builtins" + let indexError: obj = import "IndexError" "builtins" + let runtimeError: obj = import "RuntimeError" "builtins" + let zeroDivisionError: obj = import "ZeroDivisionError" "builtins" + let overflowError: obj = import "OverflowError" "builtins" + let notImplementedError: obj = import "NotImplementedError" "builtins" + let memoryError: obj = import "MemoryError" "builtins" + let timeoutError: obj = import "TimeoutError" "builtins" + let exceptionBase: obj = import "ExceptionBase" "fable_library.exception_bases" + let legacyExceptionBase: obj = import "ExceptionBase" "fable_library.types" + let timeoutReflection: Func = import "TimeoutException_reflection" "fable_library.system" + let argumentNullReflection: Func = import "ArgumentNullException_reflection" "fable_library.system" + + [] + type TimeoutError() = inherit Exception() + +[] +let ``test Python ExceptionBase compatibility import preserves class identity`` () = + obj.ReferenceEquals(PythonExceptionBases.exceptionBase, PythonExceptionBases.legacyExceptionBase) |> equal true + +[] +let ``test dotnet exceptions also match their Python exception bases`` () = + let mappings: (exn * obj) list = [ + ArgumentException("message"), PythonExceptionBases.valueError + ArgumentNullException("parameter", "message"), PythonExceptionBases.valueError + ArgumentOutOfRangeException("parameter", "message"), PythonExceptionBases.valueError + FormatException("message"), PythonExceptionBases.valueError + IndexOutOfRangeException("message"), PythonExceptionBases.indexError + InvalidOperationException("message"), PythonExceptionBases.runtimeError + DivideByZeroException("message"), PythonExceptionBases.zeroDivisionError + OverflowException("message"), PythonExceptionBases.overflowError + NotImplementedException("message"), PythonExceptionBases.notImplementedError + OutOfMemoryException("message"), PythonExceptionBases.memoryError + TimeoutException("message"), PythonExceptionBases.timeoutError + ] + for ex, pythonBase in mappings do + Fable.Core.PyInterop.pyInstanceof ex pythonBase |> equal true + Fable.Core.PyInterop.pyInstanceof ex PythonExceptionBases.exceptionBase |> equal true + ex.Message.Contains("message") |> equal true + +[] +let ``test Python TimeoutError handler catches dotnet TimeoutException`` () = + let ex = TimeoutException("timeout message") + let caught = + try + raise ex + false + with + | :? PythonExceptionBases.TimeoutError as caught -> obj.ReferenceEquals(ex, caught) + caught |> equal true + +[] +let ``test Python exception reflection preserves dotnet ancestry`` () = + let timeoutType = PythonExceptionBases.timeoutReflection.Invoke() + timeoutType.IsSubclassOf(typeof) |> equal true + let argumentNullType = PythonExceptionBases.argumentNullReflection.Invoke() + argumentNullType.IsSubclassOf(typeof) |> equal true + argumentNullType.IsSubclassOf(typeof) |> equal true +#endif + [] let ``test use doesn't return on finally clause`` () = // See #211 let foo() = @@ -1189,6 +1277,54 @@ let ``test invalidArg formats the message like .NET`` () = invalidArg "arg" "This is invalid" ) +[] +let ``test invalidArg raises ArgumentException`` () = + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid (Parameter 'arg')" + +let private paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +[] +let ``test ArgumentException.ParamName works`` () = + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + +[] +let ``test ArgumentException.ParamName of FSharp.Core errors works`` () = + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + +[] +let ``test invalidOp raises InvalidOperationException`` () = + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid" + # nowarn "26" // This rule will never be matched (code 26) [] @@ -1589,3 +1725,117 @@ let ``test exception variable captured in deferred single-arg closure`` () = with ex -> (fun (_: int) -> ex.Message) // lambda-with-arg captures ex sub 42 |> equal "boom" + +[] +let ``test lock is reentrant and returns the body value`` () = + let key = obj() + lock key (fun () -> lock key (fun () -> 42)) |> equal 42 + +[] +let ``test lock releases after a body exception`` () = + let key = obj() + let mutable error = "" + try + lock key (fun () -> failwith "body") + with ex -> + error <- ex.Message + equal "body" error + lock key (fun () -> 42) |> equal 42 + +#if FABLE_COMPILER +module private LockRuntime = + open Fable.Core.PyInterop + + type Threads = + abstract trace: string array + abstract errors: string array + abstract first_entered: bool + abstract second_attempted: bool + abstract second_blocked: bool + abstract threads_finished: bool + abstract record: string -> unit + abstract wait_release: unit -> bool + abstract run: Action * Action -> unit + + type Storage = + abstract stable: bool + abstract count: int + + let makeThreads: Func = import "make_threads" "./py/lock_runtime.py" + let makeStorage: Func = import "make_storage" "./py/lock_runtime.py" + let makeKey: Func = import "make_key" "./py/lock_runtime.py" + let makeCollidingKeys: Func = import "make_colliding_keys" "./py/lock_runtime.py" + +[] +let ``test real threads share a lock and release after an exception`` () = + let key = LockRuntime.makeKey.Invoke() + let threads = LockRuntime.makeThreads.Invoke() + let mutable error = "" + let first = Action(fun () -> + try + lock key (fun () -> + threads.record "first-enter" + threads.wait_release() |> equal true + threads.record "first-exit" + failwith "body") + with ex -> + error <- ex.Message) + let second = Action(fun () -> + lock key (fun () -> threads.record "second-enter") + threads.record "second-exit") + threads.run(first, second) + equal true threads.first_entered + equal true threads.second_attempted + equal true threads.second_blocked + equal true threads.threads_finished + equal [||] threads.errors + equal [| "first-enter"; "first-exit"; "second-enter"; "second-exit" |] threads.trace + equal "body" error + lock key (fun () -> 42) |> equal 42 + +[] +let ``test independent objects do not share a lock`` () = + let keys = LockRuntime.makeCollidingKeys.Invoke() + let threads = LockRuntime.makeThreads.Invoke() + let first = Action(fun () -> + lock keys.[0] (fun () -> + threads.record "first-enter" + threads.wait_release() |> equal true + threads.record "first-exit")) + let second = Action(fun () -> + lock keys.[1] (fun () -> threads.record "second-enter") + threads.record "second-exit") + threads.run(first, second) + equal true threads.first_entered + equal true threads.second_attempted + equal false threads.second_blocked + equal true threads.threads_finished + equal [||] threads.errors + equal 4 threads.trace.Length + +[] +let ``test lock storage releases idle entries across new objects`` () = + let iterations = 2048 + let storage = LockRuntime.makeStorage.Invoke() + let before = storage.count + lock (LockRuntime.makeKey.Invoke()) (fun () -> equal (before + 1) storage.count) + for _ in 1 .. iterations do + lock (LockRuntime.makeKey.Invoke()) (fun () -> 1) |> equal 1 + equal true storage.stable + equal before storage.count + +[] +let ``test lock storage releases idle entries after exceptions`` () = + let storage = LockRuntime.makeStorage.Invoke() + let before = storage.count + try + lock (LockRuntime.makeKey.Invoke()) (fun () -> failwith "body") + with _ -> () + equal before storage.count +#endif + +[] +let ``test A read of another file's mutable value is not moved past side effects`` () = // See #5156 + let before = MiscTestsHelper.trackedCalls + MiscTestsHelper.track 1 |> ignore + MiscTestsHelper.trackedCalls - before |> equal 1 diff --git a/tests/Python/TestNullable.fs b/tests/Python/TestNullable.fs new file mode 100644 index 0000000000..0370bddd71 --- /dev/null +++ b/tests/Python/TestNullable.fs @@ -0,0 +1,72 @@ +module Fable.Tests.Nullable + +open System +open Util.Testing +open FSharp.Linq + +[] +let ``test Nullable.char works`` () = + Nullable.char (Nullable 49uy) |> equal (Nullable '1') + Nullable.char (Nullable 53) |> equal (Nullable '5') + Nullable.char (Nullable 55L) |> equal (Nullable '7') + Nullable.char (Nullable 57m) |> equal (Nullable '9') + Nullable.char (Nullable 58.0) |> equal (Nullable ':') + Nullable.char (Nullable 'a') |> equal (Nullable 'a') + Nullable.char (Nullable()) |> equal (Nullable()) + +[] +let ``test Nullable.decimal works`` () = + Nullable.decimal (Nullable 5) |> equal (Nullable 5m) + Nullable.decimal (Nullable 7L) |> equal (Nullable 7m) + Nullable.decimal (Nullable 10.5) |> equal (Nullable 10.5m) + Nullable.decimal (Nullable()) |> equal (Nullable()) + +[] +let ``test Nullable.enum works`` () = + (Nullable.enum (Nullable 2): Nullable) |> equal (Nullable DayOfWeek.Tuesday) + (Nullable.enum (Nullable()): Nullable) |> equal (Nullable()) + +[] +let ``test Nullable.float and Nullable.float32 work`` () = + Nullable.float (Nullable 1uy) |> equal (Nullable 1.0) + Nullable.float (Nullable 5) |> equal (Nullable 5.0) + Nullable.float (Nullable 7L) |> equal (Nullable 7.0) + Nullable.float (Nullable 9m) |> equal (Nullable 9.0) + Nullable.float (Nullable 'c') |> equal (Nullable 99.0) + Nullable.float (Nullable()) |> equal (Nullable()) + Nullable.float32 (Nullable 5) |> equal (Nullable 5.0f) + Nullable.float32 (Nullable 10.0) |> equal (Nullable 10.0f) + Nullable.float32 (Nullable()) |> equal (Nullable()) + Nullable.double (Nullable 5) |> equal (Nullable 5.0) + Nullable.single (Nullable 9.5) |> equal (Nullable 9.5f) + +[] +let ``test Nullable integer conversions work`` () = + Nullable.int (Nullable 1uy) |> equal (Nullable 1) + Nullable.int (Nullable 7L) |> equal (Nullable 7) + Nullable.int (Nullable 9m) |> equal (Nullable 9) + Nullable.int (Nullable 10.9) |> equal (Nullable 10) + Nullable.int (Nullable -11.9) |> equal (Nullable -11) + Nullable.int (Nullable 'c') |> equal (Nullable 99) + Nullable.int (Nullable()) |> equal (Nullable()) + Nullable.int16 (Nullable -7L) |> equal (Nullable -7s) + Nullable.int32 (Nullable 5L) |> equal (Nullable 5) + Nullable.int64 (Nullable -9.9) |> equal (Nullable -9L) + Nullable.int64 (Nullable()) |> equal (Nullable()) + Nullable.int8 (Nullable 9.9) |> equal (Nullable 9y) + Nullable.sbyte (Nullable -7.9) |> equal (Nullable -7y) + Nullable.byte (Nullable 5) |> equal (Nullable 5uy) + Nullable.uint8 (Nullable 7L) |> equal (Nullable 7uy) + Nullable.uint16 (Nullable 9.9) |> equal (Nullable 9us) + Nullable.uint (Nullable 5) |> equal (Nullable 5u) + Nullable.uint32 (Nullable 7L) |> equal (Nullable 7u) + Nullable.uint64 (Nullable()) |> equal (Nullable()) + +[] +let ``test Nullable conversions evaluate their argument once`` () = + let mutable calls = 0 + let next () = + calls <- calls + 1 + Nullable 5 + Nullable.float (next ()) |> equal (Nullable 5.0) + calls |> equal 1 diff --git a/tests/Python/TestOption.fs b/tests/Python/TestOption.fs index f776bdb992..f15d8b3745 100644 --- a/tests/Python/TestOption.fs +++ b/tests/Python/TestOption.fs @@ -191,6 +191,38 @@ let ``test Option.map/map2/map3/bind/filter with nested options work`` () = Some(Some 5) |> Option.bind (fun inner -> Some inner) |> equal (Some(Some 5)) Some(Some 5) |> Option.filter Option.isSome |> equal (Some(Some 5)) +[] +let ``test equality on nested options of different depth works`` () = + let a: int option option option = Some None + let b: int option option option = Some(Some None) + (a = b) |> equal false + (b = a) |> equal false + (a = a) |> equal true + (b = b) |> equal true + notEqual a b + let ra: Result = Ok a + let rb: Result = Ok b + (ra = rb) |> equal false + notEqual ra rb + let c: int option option = Some None + let d: int option option = Some(Some 42) + (c = d) |> equal false + (d = c) |> equal false + +[] +let ``test comparison on nested options of different depth works`` () = + let a: int option option option = Some None + let b: int option option option = Some(Some None) + compare a b |> equal -1 + compare b a |> equal 1 + compare a a |> equal 0 + compare (None: int option option option) a |> equal -1 + compare a (None: int option option option) |> equal 1 + let c: int option option = Some None + let d: int option option = Some(Some 42) + compare c d |> equal -1 + compare d c |> equal 1 + [] let ``test ValueOption.map/map2/map3/bind/filter/defaultWith/orElseWith work`` () = ValueSome 5 |> ValueOption.map (fun x -> x + 1) |> equal (ValueSome 6) diff --git a/tests/Python/TestPatternMatch.fs b/tests/Python/TestPatternMatch.fs index 932d0eb987..0095333d90 100644 --- a/tests/Python/TestPatternMatch.fs +++ b/tests/Python/TestPatternMatch.fs @@ -311,6 +311,56 @@ let ``test nested match complex`` () = nestedComplex 2 2 |> equal "x=2, y=other" nestedComplex 3 1 |> equal "x=other" +// https://github.com/fable-compiler/Fable/issues/5026 +module NestedMatchNames = + type Shape = + | Circle of radius: int + | Square + + let describe (shapes: Shape list) n = + match List.head shapes with + | Circle radius -> + match n + 1, radius with + | 2, 1 -> "unit circle" + | _ -> "other" + | Square -> "square" + + let describeInClosure (shapes: Shape list) = + match List.head shapes with + | Circle radius -> + fun xs -> + match List.tryHead xs, radius with + | Some 2, 1 -> "unit circle" + | _ -> "other" + | Square -> fun _ -> "square" + + let describeWithUserBinding (match_value: string) n = + match n + 1, n + 2 with + | 2, 3 -> match_value + | _ -> "other" + +[] +let ``test nested tuple match preserves outer union fields`` () = + NestedMatchNames.describe [ NestedMatchNames.Circle 1 ] 1 |> equal "unit circle" + NestedMatchNames.describe [ NestedMatchNames.Circle 2 ] 1 |> equal "other" + NestedMatchNames.describe [ NestedMatchNames.Circle 1 ] 2 |> equal "other" + NestedMatchNames.describe [ NestedMatchNames.Square ] 1 |> equal "square" + +[] +let ``test tuple match in closure preserves captured union fields`` () = + let describe = NestedMatchNames.describeInClosure [ NestedMatchNames.Circle 1 ] + describe [ 2 ] |> equal "unit circle" + describe [ 3 ] |> equal "other" + describe [] |> equal "other" + describe [ 2 ] |> equal "unit circle" + NestedMatchNames.describeInClosure [ NestedMatchNames.Circle 2 ] [ 2 ] |> equal "other" + NestedMatchNames.describeInClosure [ NestedMatchNames.Square ] [ 2 ] |> equal "square" + +[] +let ``test tuple match temporaries preserve user bindings`` () = + NestedMatchNames.describeWithUserBinding "matched" 1 |> equal "matched" + NestedMatchNames.describeWithUserBinding "matched" 2 |> equal "other" + // ---------------------------------------------------------------------------- // 7. Option Matching // ---------------------------------------------------------------------------- diff --git a/tests/Python/TestQuotation.fs b/tests/Python/TestQuotation.fs index f0a5d41a22..775cfff491 100644 --- a/tests/Python/TestQuotation.fs +++ b/tests/Python/TestQuotation.fs @@ -4,6 +4,7 @@ open Fable.Tests.Util open Util.Testing open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Microsoft.FSharp.Linq.RuntimeHelpers type QuotationTestUnion = @@ -12,6 +13,26 @@ type QuotationTestUnion = let inline quotTestDouble x = x * 2 +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + [] let ``test Simple integer value quotation`` () = let q = <@ 42 @> @@ -283,3 +304,35 @@ let ``test Call on an instance whose value is null keeps a Some instance`` () = if not (hasSomeInstancePropertyGet q) then failwith "Expected a PropertyGet with a Some instance, even though its value is null" + +[] +let ``test DerivedPatterns AndAlso, OrElse and SpecificCall work`` () = + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" + +[] +let ``test a captured local is a Value, not a Var`` () = + let captured = "SE" + + let describe (e: Expr) = + match e with + | Lambda(_, body) -> + match body with + | Value(v, _) -> "Value:" + unbox v + | Var v -> "Var:" + v.Name + | _ -> "other" + | _ -> "not a lambda" + + describe <@ fun (_: int) -> captured @> |> equal "Value:SE" + describe <@ fun (_: int) -> "SE" @> |> equal "Value:SE" + + // Checks that a bound variable remains a Var, rather than being spliced in. + (match <@ fun (x: string) -> x @> with + | Lambda(_, Var _) -> "Var" + | _ -> "?") + |> equal "Var" diff --git a/tests/Python/TestRandom.fs b/tests/Python/TestRandom.fs index 06eeacfe95..0f5a3091ef 100644 --- a/tests/Python/TestRandom.fs +++ b/tests/Python/TestRandom.fs @@ -112,3 +112,20 @@ let ``test System.Random.NextBytes seeded works`` () = Random(5432).NextBytes(buffer1) Random(5432).NextBytes(buffer2) buffer1 |> equal buffer2 + +[] +let ``test System.Random gives .NET's argument errors`` () = + for rnd in [Random(42); Random()] do + throwsErrorContaining "maxValue ('-1') must be a non-negative value. (Parameter 'maxValue')" (fun () -> rnd.Next(-1)) + throwsErrorContaining "'minValue' cannot be greater than maxValue. (Parameter 'minValue')" (fun () -> rnd.Next(5, 3)) + raisedExceptionType (fun () -> rnd.Next(-1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> rnd.Next(5, 3)) |> equal "ArgumentOutOfRangeException" + rnd.Next(5, 5) |> equal 5 + +[] +let ``test System.Random.Shared works`` () = + let x = Random.Shared.Next(5) + (x >= 0 && x < 5) |> equal true + let x = Random.Shared.NextDouble() + (x >= 0.0 && x < 1.0) |> equal true + obj.ReferenceEquals(Random.Shared, Random.Shared) |> equal true diff --git a/tests/Python/TestRecordType.fs b/tests/Python/TestRecordType.fs index df1c867157..08a158aafb 100644 --- a/tests/Python/TestRecordType.fs +++ b/tests/Python/TestRecordType.fs @@ -56,6 +56,57 @@ type RecordWithStaticMemberVal = type RecordA = { OptionalField : string option } +[] +type StructRecord = { Size: int; Seed: int; Path: int list } + +type StructRecordHolder = { Tag: int; Data: StructRecord } + +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +[] +type StructRecordOrdered = { OrdA: int; OrdB: int; OrdC: int } + +type ByRefClassRecord = { BcP: int; BcQ: int } + +type ByRefClass = + val mutable BkF: int + val BkV: int + new() = { BkF = 0; BkV = 3 } + +let sumStructRecordByRef (r: byref) = r.OrdA + r.OrdC + +let copyStructRecordByRef (r: byref) = { r with OrdB = 99 } + +let sumClassRecordByRef (r: byref) = r.BcP + r.BcQ + +let setClassRecordOutRef (r: outref) = + r <- { BcP = 4; BcQ = 1 } + r.BcP + r.BcQ + +let sumClassByRef (r: byref) = r.BkF + r.BkV + +let sumAnonymousStructRecordByRef (x: byref) = x.AnX + x.AnY + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + +[] +let ``test sprintf formats record fields with their F# names`` () = + sprintf "%A" { Size = 1; Seed = 2; Path = [] } + |> equal "{ Size = 1\n Seed = 2\n Path = [] }" + +let updateStructRecord (data: StructRecord) (path: int list) = { data with Path = List.rev path } + +let holdStructRecord (data: StructRecord) (path: int list) = + Some + { + Tag = 1 + Data = { data with Path = List.rev path } + } + [] let ``test Anonymous records work`` () = let r = makeAnonRec() @@ -226,4 +277,71 @@ let ``test Record with static member val does not share state across instances`` let b = RecordWithStaticMemberVal.empty a.xs.Add 1 equal 1 a.xs.Count - equal 1 b.xs.Count \ No newline at end of file + equal 1 b.xs.Count + +[] +let ``test Copy-and-update of a struct record argument works`` () = + let data: StructRecord = { Size = 1; Seed = 2; Path = [ 3; 4 ] } + let updated = updateStructRecord data [ 5; 6 ] + equal 1 updated.Size + equal 2 updated.Seed + equal [ 6; 5 ] updated.Path + let held = holdStructRecord data [ 7; 8 ] |> Option.get + equal 1 held.Data.Size + equal 2 held.Data.Seed + equal [ 8; 7 ] held.Data.Path + +[] +let ``test copy-and-update of a let mutable struct record keeps the unchanged fields`` () = + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + +[] +let ``test copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields`` () = + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + +[] +let ``test copy-and-update of a module-level mutable struct record keeps the unchanged fields`` () = + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + +[] +let ``test field reads and copy-and-update through a byref struct record parameter work`` () = + let mutable r = { OrdA = 1; OrdB = 2; OrdC = 3 } + sumStructRecordByRef &r |> equal 4 + let x = copyStructRecordByRef &r + (x.OrdA, x.OrdB, x.OrdC) |> equal (1, 99, 3) + +[] +let ``test field reads through a byref or outref to a reference type work`` () = + let mutable c = { BcP = 1; BcQ = 2 } + sumClassRecordByRef &c |> equal 3 + let mutable o = { BcP = 0; BcQ = 0 } + setClassRecordOutRef &o |> equal 5 + let mutable k = ByRefClass() + k.BkF <- 4 + sumClassByRef &k |> equal 7 + +[] +let ``test copy-and-update of a let mutable anonymous struct record keeps the unchanged fields`` () = + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) + +[] +let ``test field reads through a byref to an anonymous struct record work`` () = + let mutable a = struct {| AnX = 1; AnY = 2 |} + sumAnonymousStructRecordByRef &a |> equal 3 diff --git a/tests/Python/TestReflection.fs b/tests/Python/TestReflection.fs index 3ce9051c29..43c34cda20 100644 --- a/tests/Python/TestReflection.fs +++ b/tests/Python/TestReflection.fs @@ -664,5 +664,39 @@ let ``test Can get union values without type`` () = getCaseFields x |> equal [||] getCaseFields y |> equal [|5; "foo"|] +type ReactiveNode<'T>(value: 'T) = + member _.Value = value + +[] +type ErasedView<'T> = + | ErasedView of node: ReactiveNode<'T> + +type ModelWithErasedField = { Item: ErasedView } + +type DuWithErasedField = + | Wrapped of ErasedView + | Empty + +[] +let ``test An erased union reflects as the type it erases to`` () = + let erasedTyp = typeof> + let nodeTyp = typeof> + erasedTyp.FullName |> equal nodeTyp.FullName + FSharpType.IsUnion erasedTyp |> equal false + +[] +let ``test Reflection works for a record with an erased generic union field`` () = + let typ = typeof + FSharpType.IsRecord typ |> equal true + let fields = FSharpType.GetRecordFields typ + fields.Length |> equal 1 + fields.[0].PropertyType.GetGenericArguments().[0] |> equal typeof + +[] +let ``test Reflection works for a union with an erased generic union field`` () = + let typ = typeof + let case = (FSharpType.GetUnionCases typ).[0] + case.GetFields().[0].PropertyType.GetGenericArguments().[0] |> equal typeof + #endif #endif diff --git a/tests/Python/TestReflectionUnionImports.fs b/tests/Python/TestReflectionUnionImports.fs new file mode 100644 index 0000000000..1934a4a7e0 --- /dev/null +++ b/tests/Python/TestReflectionUnionImports.fs @@ -0,0 +1,19 @@ +module Fable.Tests.ReflectionUnionImports + +open FSharp.Reflection +open Util.Testing + +type RecordWithResult = { Value: Result } + +let getGenericTypeArguments (typ: System.Type) = typ.GenericTypeArguments + +[] +let ``test FSharp.Reflection: record field Result imports its case constructors`` () = + let field = FSharpType.GetRecordFields(typeof).[0] + field.PropertyType |> equal typeof> + +[] +let ``test FSharp.Reflection: Choice imports its case constructors`` () = + getGenericTypeArguments typeof> + |> Array.length + |> equal 2 diff --git a/tests/Python/TestRegex.fs b/tests/Python/TestRegex.fs index de19016043..faa854f1e3 100644 --- a/tests/Python/TestRegex.fs +++ b/tests/Python/TestRegex.fs @@ -46,6 +46,14 @@ let ``test Literal regex works with whitespace characters`` () = // See #2635 Regex("\\t").IsMatch("foo\tbar") |> equal true Regex(@"\\t").IsMatch("foo\tbar") |> equal false +[] +let ``test RegexOptions.NonBacktracking is accepted`` () = + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + [] let ``test Regex.Options works`` () = let option1 = int RegexOptions.IgnoreCase diff --git a/tests/Python/TestResizeArray.fs b/tests/Python/TestResizeArray.fs index dd98f6bf9d..fe07fc5cb2 100644 --- a/tests/Python/TestResizeArray.fs +++ b/tests/Python/TestResizeArray.fs @@ -193,6 +193,68 @@ let ``test ResizeArray.IndexOf works`` () = li.IndexOf("ab") |> equal 1 li.IndexOf("cd") |> equal -1 +[] +let ``test ResizeArray.BinarySearch works`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(7) |> equal 3 + ra.BinarySearch(8) |> equal -5 + let desc = ComparisonIdentity.FromFunction(fun (a: int) b -> compare b a) + (ResizeArray [9; 7; 5; 3; 1]).BinarySearch(3, desc) |> equal 3 + ra.BinarySearch(1, 3, 7, System.Collections.Generic.Comparer.Default) |> equal 3 + ra.BinarySearch(1, 2, 8, System.Collections.Generic.Comparer.Default) |> equal -4 + ra.BinarySearch(1, 2, 3, null) |> equal 1 + +[] +let ``test ResizeArray.BinarySearch works with an empty list, duplicates and a null range comparer`` () = + ResizeArray().BinarySearch(1) |> equal -1 + (ResizeArray [2; 2; 2; 2]).BinarySearch(2) |> equal 1 + (ResizeArray [1; 2; 2; 2; 2; 3]).BinarySearch(2) |> equal 2 + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(0, 5, 2, null) |> equal -2 + ra.BinarySearch(2, 3, 9, null) |> equal 4 + +[] +let ``test ResizeArray.BinarySearch with a range passes the element to the comparer first`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + ra.BinarySearch(1, 3, 7, logging) |> equal 3 + List.ofSeq calls |> equal [(5, 7); (7, 7)] + +[] +let ``test ResizeArray.BinarySearch with a range checks index and count together`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(6, 0, 3, null)) |> equal "ArgumentException" + raised (fun () -> ra.BinarySearch(0, 6, 3, null)) |> equal "ArgumentException" + +[] +let ``test ResizeArray.BinarySearch with a range checks its arguments`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, 2, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(3, 3, 3, null)) |> equal "ArgumentException" + [] let ``test ResizeArray.Remove works`` () = let li = ResizeArray<_>() diff --git a/tests/Python/TestSeq.fs b/tests/Python/TestSeq.fs index 35a4ff6f8f..cc5c435a83 100644 --- a/tests/Python/TestSeq.fs +++ b/tests/Python/TestSeq.fs @@ -1319,3 +1319,108 @@ let ``test Seq.randomSampleBy works`` () = Array.distinct sample |> Array.length |> equal 3 throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + +[] +let ``test Seq.randomChoicesBy draws on enumeration`` () = + let mutable calls = 0 + let xs = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + xs |> Seq.toList |> equal [5; 5; 5] + xs |> Seq.toList |> equal [5; 5; 5] + calls |> equal 6 + let rng = System.Random(42) + let ys = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal (System.Random(42).Next()) + ys |> Seq.length |> equal 3 + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Seq.randomChoicesWith rng -1 [1]) + +[] +let ``test Seq.reduce and Seq.reduceBack raise ArgumentException on empty input`` () = + let xs = Seq.empty + raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) xs) |> equal "ArgumentException" + +[] +let ``test Seq.min, Seq.max, Seq.minBy and Seq.maxBy raise ArgumentException on empty input`` () = + let xs = Seq.empty + raisedExceptionType (fun () -> Seq.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id xs) |> equal "ArgumentException" + +[] +let ``test Seq.take and Seq.skip raise InvalidOperationException when the sequence is too short`` () = + raisedExceptionType (fun () -> Seq.take 5 (seq { 1 }) |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 (seq { 1 }) |> Seq.toList) |> equal "InvalidOperationException" + +[] +let ``test Seq.tail raises ArgumentException on empty input`` () = + raisedExceptionType (fun () -> Seq.tail Seq.empty |> Seq.toList) |> equal "ArgumentException" + +[] +let ``test range with a zero step raises ArgumentException`` () = + raisedExceptionType (fun () -> [0..0..5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [0L..0L..5L]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [0.0..0.0..5.0]) |> equal "ArgumentException" + +[] +let ``test Seq.head and Seq.item raise ArgumentException when the sequence is too short`` () = + raisedExceptionType (fun () -> Seq.head Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.head ([]: int list)) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.head ([||]: int[])) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item 5 (seq { 1 })) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item 5 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item 5 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item -1 (seq { 1 })) |> equal "ArgumentException" + +[] +let ``test Seq.init, Seq.replicate and Seq.take check for a negative count when called`` () = + raisedExceptionType (fun () -> Seq.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.replicate -1 "a") |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.take -1 (seq { 1; 2 })) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.take -1 [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.take -1 [|1; 2|]) |> equal "ArgumentException" + Seq.init 0 id |> Seq.toList |> equal [] + Seq.replicate 0 "a" |> Seq.toList |> equal [] + Seq.take 0 (seq { 1; 2 }) |> Seq.toList |> equal [] + +[] +let ``test Seq.chunkBySize, Seq.windowed and Seq.splitInto check the size when called`` () = + let xs = seq { 1; 2; 3 } + raisedExceptionType (fun () -> Seq.chunkBySize 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.chunkBySize -1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed -1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto -1 xs) |> equal "ArgumentException" + +[] +let ``test Seq.transpose groups jagged input by index`` () = + let toLists (xss: seq>) = xss |> Seq.map Seq.toList |> Seq.toList + Seq.transpose [[1; 2]; [3]] |> toLists |> equal [[1; 3]; [2]] + Seq.transpose [[1]; [2; 3]] |> toLists |> equal [[1; 2]; [3]] + Seq.transpose [[]; [1]] |> toLists |> equal [[1]] + Seq.transpose [[1]; []] |> toLists |> equal [[1]] + Seq.transpose [[1; 2; 3]; [4]; [5; 6]] |> toLists |> equal [[1; 4; 5]; [2; 6]; [3]] + Seq.transpose (seq [seq { 1; 2 }; seq { 3 }]) |> toLists |> equal [[1; 3]; [2]] + Seq.transpose [|[|1; 2|]; [|3|]|] |> toLists |> equal [[1; 3]; [2]] + +[] +let ``test Seq.removeManyAt raises ArgumentException where FSharp.Core does`` () = + let xs = seq { 1; 2; 3 } + raisedExceptionType (fun () -> Seq.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 0 0 Seq.empty |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 [1; 2; 3] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 [|1; 2; 3|] |> Seq.toList) |> equal "ArgumentException" + let yielded = ResizeArray() + raisedExceptionType (fun () -> Seq.removeManyAt 4 1 xs |> Seq.iter yielded.Add) |> equal "ArgumentException" + List.ofSeq yielded |> equal [1; 2; 3] + +[] +let ``test Seq.removeManyAt does not raise when the count runs past the end or is not positive`` () = + let xs = seq { 1; 2; 3 } + Seq.removeManyAt 1 3 xs |> Seq.toList |> equal [1] + Seq.removeManyAt 0 4 xs |> Seq.toList |> equal [] + Seq.removeManyAt 2 2 [|1; 2; 3|] |> Seq.toList |> equal [1; 2] + Seq.removeManyAt 1 0 xs |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 1 -1 [1; 2; 3] |> Seq.toList |> equal [1; 2; 3] diff --git a/tests/Python/TestSet.fs b/tests/Python/TestSet.fs index df67710256..fe5e9956d1 100644 --- a/tests/Python/TestSet.fs +++ b/tests/Python/TestSet.fs @@ -1,5 +1,6 @@ module Fable.Tests.Set +open Fable.Tests.Util open Util.Testing [] @@ -333,3 +334,22 @@ let ``test Comparing large sets works`` () = largeSetB.IsSupersetOf(largeSetA) |> equal true largeSetA.IsProperSubsetOf(largeSetB) |> equal false largeSetA.IsSubsetOf(largeSetB) |> equal true + +[] +let ``test Set.minElement and Set.maxElement raise ArgumentException on empty input`` () = + let xs = Set.empty + raisedExceptionType (fun () -> Set.minElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.maxElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MinimumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MaximumElement) |> equal "ArgumentException" + +[] +let ``test Set.intersectMany raises ArgumentException on empty input`` () = + raisedExceptionType (fun () -> Set.intersectMany ([]: Set list)) |> equal "ArgumentException" + +[] +let ``test Set enumerator Current raises InvalidOperationException outside the enumeration`` () = + let e = (set [1] :> seq<_>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Python/TestString.fs b/tests/Python/TestString.fs index 6bc2e2c975..95ea6e9ed3 100644 --- a/tests/Python/TestString.fs +++ b/tests/Python/TestString.fs @@ -1215,3 +1215,15 @@ let ``test unicode categories`` () = Char.IsLetterOrDigit c |> ignore ) #endif + +[] +let ``test String slices clamp the bounds like .NET`` () = + let s = "abc" + s.[-1..1] |> equal "ab" + s.[-5..] |> equal "abc" + s.[1..5] |> equal "bc" + s.[.. -2] |> equal "" + s.[-3.. -2] |> equal "" + s.[2..1] |> equal "" + let lower, upper = -1, 1 + s.[lower..upper] |> equal "ab" diff --git a/tests/Python/TestType.fs b/tests/Python/TestType.fs index 29e5416dcb..ab9fc9476c 100644 --- a/tests/Python/TestType.fs +++ b/tests/Python/TestType.fs @@ -369,6 +369,35 @@ type TypeWithDefaultValueTest() = [] val mutable ObjValue: System.Collections.Generic.Dictionary +type ClassWithDefaultValueInstanceFields = + val X: int + [] + val mutable D: int + [] + val mutable S: string + val Y: string + new(x, y) = { X = x; Y = y } + +[] +type StructWithDefaultValueInstanceField = + val X: int + [] + val mutable D: int + val Y: string + new(x, y) = { X = x; Y = y } + +type ClassWithDefaultValueStaticAndInstanceFields = + [] + static val mutable private count: int + val X: int + [] + val mutable D: int + val Y: int + new(x, y) = { X = x; Y = y } + static member Count + with get () = ClassWithDefaultValueStaticAndInstanceFields.count + and set v = ClassWithDefaultValueStaticAndInstanceFields.count <- v + [] type NestedStruct = val A: float @@ -772,6 +801,14 @@ let ``test Optional named params work`` () = StaticClass.NamedParams(foo="14") |> equal 11 StaticClass.NamedParams() |> equal "John is 30 years old" +[] +let ``test Mutable function bindings accept replacement callbacks`` () = + let mutable callback = fun (initial: int) -> initial + 1 + let replace (replacement: int -> int) = callback <- replacement + callback 3 |> equal 4 + replace (fun differentName -> differentName * 2) + callback 3 |> equal 6 + [] let ``test F# optional param works`` () = let mutable f1 = fun (v: bool) -> @@ -1832,6 +1869,34 @@ let ``test Different type parameter combinations work correctly`` () = let result3 = getTwoValues() result3 |> equal ("B", "C") +[] +let ``test Instance DefaultValue fields are skipped by explicit constructors`` () = + let c = ClassWithDefaultValueInstanceFields(1, "y") + c.X |> equal 1 + c.D |> equal 0 + c.S |> equal null + c.Y |> equal "y" + c.D <- 5 + c.S <- "s" + c.D |> equal 5 + c.S |> equal "s" + + let mutable s = StructWithDefaultValueInstanceField(2, "z") + s.X |> equal 2 + s.D |> equal 0 + s.Y |> equal "z" + s.D <- 6 + s.D |> equal 6 + +[] +let ``test Explicit constructors skip static fields next to DefaultValue fields`` () = + let c = ClassWithDefaultValueStaticAndInstanceFields(1, 2) + ClassWithDefaultValueStaticAndInstanceFields.Count <- 4 + c.X |> equal 1 + c.D |> equal 0 + c.Y |> equal 2 + ClassWithDefaultValueStaticAndInstanceFields.Count |> equal 4 + [] let ``test Unchecked.defaultof works for fields on structs`` () = let top = TopLevelStruct() @@ -1869,3 +1934,186 @@ let ``test Super call works correctly in multi-level generic class hierarchy`` ( obj.Attach("hello") // Each override delegates to base before logging, so the chain unwinds Base -> Mid -> Leaf log |> List.ofSeq |> equal [ "Base"; "Mid"; "Leaf" ] + +type ValClassBase = + val Id: int + val Label: string + new (id: int) = { Id = id * 10; Label = "base" } + member this.Describe() = this.Label + ":" + string this.Id + +type ValClassChild = + inherit ValClassBase + val Extra: float + new (id: int, extra: float) = { inherit ValClassBase(id); Extra = extra } + +type ValClassGrandChild = + inherit ValClassChild + val Flag: bool + new (id: int, extra: float, flag: bool) = + let doubled = id * 2 + { inherit ValClassChild(doubled, extra); Flag = flag } + +let mutable valClassBaseCalls = 0 + +type ValClassCounting = + val N: int + new (n: int) = + valClassBaseCalls <- valClassBaseCalls + 1 + { N = n } + +type ValClassCountingChild = + inherit ValClassCounting + val M: int + new (n: int, m: int) = { inherit ValClassCounting(n); M = m } + +type ValClassDefaultBase = + val Tag: string + new () = { Tag = "default" } + +type ValClassDefaultChild = + inherit ValClassDefaultBase + val Value: int + new (value: int) = { inherit ValClassDefaultBase(); Value = value } + +[] +let ``test explicit constructor calls the base constructor`` () = + let child = ValClassChild(3, 1.5) + child.Id |> equal 30 + child.Label |> equal "base" + child.Extra |> equal 1.5 + child.Describe() |> equal "base:30" + (child :> ValClassBase).Id |> equal 30 + (box child :? ValClassBase) |> equal true + +[] +let ``test explicit constructors chain through several levels`` () = + let g = ValClassGrandChild(2, 0.5, true) + g.Id |> equal 40 + g.Label |> equal "base" + g.Extra |> equal 0.5 + g.Flag |> equal true + g.Describe() |> equal "base:40" + +[] +let ``test explicit constructor runs the base constructor once`` () = + valClassBaseCalls <- 0 + let c = ValClassCountingChild(5, 6) + valClassBaseCalls |> equal 1 + c.N |> equal 5 + c.M |> equal 6 + +[] +let ``test explicit constructor calls a parameterless base constructor`` () = + let c = ValClassDefaultChild(9) + c.Tag |> equal "default" + c.Value |> equal 9 + +type ValClassGenericBase<'T> = + val Value: 'T + new (v: 'T) = { Value = v } + +type ValClassIntBox = + inherit ValClassGenericBase + val Tag: string + new (v: int, t: string) = { inherit ValClassGenericBase(v); Tag = t } + +type ValClassPairBox<'U> = + inherit ValClassGenericBase<'U * 'U> + val Count: int + new (a: 'U, b: 'U) = { inherit ValClassGenericBase<'U * 'U>((a, b)); Count = 2 } + +[] +let ``test explicit constructor resolves the generic arguments of the base class`` () = + let intBox = ValClassIntBox(5, "x") + intBox.Value |> equal 5 + intBox.Tag |> equal "x" + let pairBox = ValClassPairBox("a", "b") + pairBox.Value |> equal ("a", "b") + pairBox.Count |> equal 2 + +let mutable valClassThenCalls = 0 + +type ValClassThen = + val mutable Value: int + new () = { Value = 1 } then valClassThenCalls <- valClassThenCalls + 1 + +[] +type ValClassSequenceInit = + val Next: ValClassSequenceInit + val Id: int + val Boxed: obj + new (id: int) = { Next = null; Id = id; Boxed = null } + new (a: int, b: int) = { Next = (ValClassSequenceInit(a); ValClassSequenceInit(b)); Id = 0; Boxed = box (do ValClassSequenceInit(a)) } + new (a: int, b: int, c: int) as self = { Next = (ValClassSequenceInit(a); ValClassSequenceInit(b)); Id = c; Boxed = box (do ValClassSequenceInit(a)) } then self.Next.Id |> ignore + +type ValClassSelf = + val mutable Name: string + val mutable Count: int + new (name: string) as self = { Name = name; Count = 0 } then self.Count <- self.Name.Length + new () as self = ValClassSelf("ab") then self.Count <- self.Count * 10 + +type ValClassSelfOutOfOrder = + val Z: int + val mutable W: int + new () as self = { W = 2; Z = 3 } then self.W <- self.W + self.Z + +let valClassSelfSaved = ResizeArray() + +type ValClassSelfSaved = + val Id: int + new (id: int) as self = { Id = id } then valClassSelfSaved.Add(box self) + new () as self = ValClassSelfSaved(1) then valClassSelfSaved.Add(box self) + +type SelfPrimaryBase(x: int) as this = + let mutable touched = 0 + do this.Touch() + new () = SelfPrimaryBase(1) + member _.X = x + member _.Touched = touched + member _.Touch() = touched <- touched + 1 + +type SelfPrimaryChild() = + inherit SelfPrimaryBase(2) + +[] +let ``test explicit constructor returns the object after its then block`` () = + let before = valClassThenCalls + let c = ValClassThen() + c.Value |> equal 1 + valClassThenCalls - before |> equal 1 + +[] +let ``test explicit constructor keeps sequences of its own constructor calls in field values`` () = + let c = ValClassSequenceInit(5, 6) + c.Next.Id |> equal 6 + isNull c.Boxed |> equal true + let d = ValClassSequenceInit(5, 6, 7) + (d.Next.Id, d.Id, isNull d.Boxed) |> equal (6, 7, true) + +[] +let ``test explicit constructor with a self identifier works`` () = // See #5166 + let a = ValClassSelf("abc") + a.Name |> equal "abc" + a.Count |> equal 3 + let b = ValClassSelf() + b.Name |> equal "ab" + b.Count |> equal 20 + let c = ValClassSelfOutOfOrder() + (c.Z, c.W) |> equal (3, 5) + +[] +let ``test primary constructor with a self identifier works with a secondary constructor`` () = + let a = SelfPrimaryBase() + (a.X, a.Touched) |> equal (1, 1) + let b = SelfPrimaryChild() + (b.X, b.Touched) |> equal (2, 1) + +[] +let ``test explicit constructor self identifier is the created object`` () = + valClassSelfSaved.Clear() + let a = ValClassSelfSaved(5) + obj.ReferenceEquals(valClassSelfSaved.[0], a) |> equal true + let b = ValClassSelfSaved() + valClassSelfSaved.Count |> equal 3 + obj.ReferenceEquals(valClassSelfSaved.[1], b) |> equal true + obj.ReferenceEquals(valClassSelfSaved.[2], b) |> equal true diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs index f9b3d3f7bc..bda4c2a22c 100644 --- a/tests/Python/TestUnionType.fs +++ b/tests/Python/TestUnionType.fs @@ -4,8 +4,6 @@ open Util.Testing type Gender = Male | Female -type T1 = T1 - type Either<'TL,'TR> = | Left of 'TL | Right of 'TR @@ -14,14 +12,13 @@ type Either<'TL,'TR> = | Left y -> y.ToString() | Right y -> y.ToString() -type MyUnion = +type TestUnion = | Case0 | Case1 of string | Case2 of string * string | Case3 of string * string * string - -type MyUnion2 = +type TestUnion2 = | Tag of string | NewTag of string @@ -65,16 +62,151 @@ type Wrapper(s: string) = member x.Value = s |> Seq.rev |> Seq.map string |> String.concat "" [] -type MyUnion3 = +type MyUnion = | Case1 | Case2 | Case3 type R = { Name: string - Case: MyUnion3 + Case: MyUnion } +type T1 = T1 +type T2 = T2 +type T3 = T3 +type T4 = T4 +type T5 = T5 +type T6 = T6 +type T7 = T7 +type T8 = T8 +type T9 = T9 +type 'a more = More of 'a + +// [] +// type LangCode = +// | EN +// | ``pt-Br`` + +type S = S of string + +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id + +// See https://github.com/fable-compiler/Fable/issues/4645 +// A named union case field called `name` collided with the inherited `Union.name` +// property, which Python's @dataclass treated as a default value. +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +// See https://github.com/fable-compiler/Fable/issues/4736 +// A static member whose type references the enclosing union used to emit a class-body +// `StaticLazyProperty[...]` subscript that evaluated the union type before its name was bound, +// raising `NameError` at import time. The reference can be nested in any type shape, so each +// member below exercises a different one: array, option (rendered with `|`), nested list/array, +// tuple, and a function type (`unit -> Example`). +[] +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First + +[] +type StructPoint2D = + | Planar2D of float * float + | Infinity + +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + member this.Area = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + member this.Area = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + +type WrappedUnion = + | AString of string + +type DeepRecord = { Value: string } + +type DeepWrappedUnion = + | DeepWrappedA of string * DeepRecord + | DeepWrappedB of string + | DeepWrappedC of int + | DeepWrappedD of DeepRecord + | DeepWrappedE of int * int + | DeepWrappedF of WrappedUnion + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" + #if FABLE_COMPILER open Fable.Core @@ -82,6 +214,13 @@ open Fable.Core #endif type DU = Int of int | Str of string +type ErasedPayload = { Description: string; Membership: int } + +[] +type ErasedWrapper = + | ErasedSuccess of ErasedPayload + | ErasedFailure of string + [] let ``test Union cases matches with no arguments can be generated`` () = let x = Male @@ -125,12 +264,12 @@ let ``test Union cases matches with many arguments can be generated`` () = [] let ``test Pattern matching with common targets works`` () = - let x = MyUnion.Case2("a", "b") + let x = TestUnion.Case2("a", "b") match x with - | MyUnion.Case0 -> failwith "unexpected" - | MyUnion.Case1 _ - | MyUnion.Case2 _ -> "a" - | MyUnion.Case3(a, b, c) -> a + b + c + | TestUnion.Case0 -> failwith "unexpected" + | TestUnion.Case1 _ + | TestUnion.Case2 _ -> "a" + | TestUnion.Case3(a, b, c) -> a + b + c |> equal "a" [] @@ -198,27 +337,72 @@ let ``test Erased union type testing works`` () = U3.Case1 "HELLO" |> toString |> equal "HELLO" U3.Case2 3 |> toString |> equal "6" "HELLO" |> Wrapper |> U3.Case3 |> toString |> equal "OLLEH" + +[] +let ``test Implicit conversion to erased unions works`` () = + let strify (x: U3) = + match x with + | U3.Case1 i -> "i: " + string i + | U3.Case2 s -> "s: " + s + | U3.Case3 b -> if b then "b: yes" else "b: no" + let s: U2 = "foo" + s |> equal (U2.Case2 "foo") + strify 42 |> equal "i: 42" + strify "foo" |> equal "s: foo" + strify true |> equal "b: yes" #endif +[] +let ``test Erased union payload fields can be read`` () = + let payload = { Description = "a"; Membership = 2 } + + match ErasedSuccess payload with + | ErasedSuccess details -> details.Membership + | ErasedFailure _ -> -1 + |> equal 2 + [] let ``test Equality works in filter`` () = - let original = [| { Name = "1"; Case = MyUnion3.Case1 } ; { Name = "2"; Case = MyUnion3.Case1 }; { Name = "3"; Case = MyUnion3.Case2 }; { Name = "4"; Case = MyUnion3.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original - |> Array.filter (fun r -> r.Case = MyUnion3.Case1) + |> Array.filter (fun r -> r.Case = MyUnion.Case1) + |> Array.length + |> equal 2 + +[] +let ``test Pattern matching works in filter`` () = + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + original + |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) |> Array.length |> equal 2 -type S = S of string +[] +let ``test Union field can have the same name as its type`` () = + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + +// [] +// let ``test Verify that union cases can have `-` in their names`` () = +// // See https://github.com/fable-compiler/Fable/issues/4071 +// let x = LangCode.``pt-Br`` +// equal x LangCode.``pt-Br`` + [] let ``test sprintf formats strings cases correctly`` () = let s = sprintf "%A" (S "1") s |> equal "S \"1\"" -// See https://github.com/fable-compiler/Fable/issues/4645 -// A named union case field called `name` collided with the inherited `Union.name` -// property, which Python's @dataclass treated as a default value. -type NamedFieldUnion = - | NamedCase of name: string * optional: string +[] +let ``test Union cases with no fields are physically equal`` () = + obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true + obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false + obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true + obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false + obj.ReferenceEquals(T1, T1) |> equal true [] let ``test Named union case fields work when a field is called name`` () = @@ -228,33 +412,6 @@ let ``test Named union case fields work when a field is called name`` () = name |> equal "v1" optional |> equal "v2" -[] -let ``test Union cases with no fields are physically equal`` () = - obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true - obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false - obj.ReferenceEquals(MyUnion.Case0, MyUnion.Case0) |> equal true - obj.ReferenceEquals(MyUnion.Case1 "a", MyUnion.Case1 "a") |> equal false - obj.ReferenceEquals(T1, T1) |> equal true - -// See https://github.com/fable-compiler/Fable/issues/4736 -// A static member whose type references the enclosing union used to emit a class-body -// `StaticLazyProperty[...]` subscript that evaluated the union type before its name was bound, -// raising `NameError` at import time. The reference can be nested in any type shape, so each -// member below exercises a different one: array, option (rendered with `|`), nested list/array, -// tuple, and a function type (`unit -> Example`). -[] -[] -type Example = - | First - | Second - | Other of string - - static member All = [| Example.First; Example.Second |] - static member Head = Some Example.First - static member Nested = [ [| Example.First |] ] - static member Pair = (Example.First, Example.Second) - static member Factory: unit -> Example = fun () -> Example.First - [] let ``test Static union member returning the union type works`` () = Example.All |> Array.length |> equal 2 @@ -272,3 +429,101 @@ let ``test Static union member with nested self-reference works`` () = Example.Pair |> snd |> equal Example.Second // Function type (`Callable[[], _Example]`) — missed by the original narrower detection. Example.Factory() |> equal Example.First + +[] +let ``test Struct unions work`` () = + let distance p = + match p with + | Planar2D (x, y) -> sqrt (x * x + y * y) + | Infinity -> infinity + let p = Planar2D (3, 4) + let res = distance p + res |> equal 5. + +[] +let ``test Struct unions with fields work`` () = + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + +[] +let ``test Struct unions without fields work`` () = + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + +[] +let ``test Nested union match keeps case-specific field access`` () = + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + +[] +let ``test Union case matching works`` () = + let x = IntCase1 5 + let res = + match x with + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + res |> equal 5 + +[] +let ``test Union case equality works`` () = + IntCase1 5 = IntCase1 5 |> equal true + IntCase1 5 = IntCase2 5 |> equal false + IntCase3 2 = IntCase3 3 |> equal false + IntCase2 1 = IntCase2 1 |> equal true + IntCase3 1 = IntCase3 1 |> equal true + +[] +let ``test Union fn call works`` () = + let x = IntCase1 3 + let res = unionFnAlways1 x + let res2 = unionFnAlways1 x //deliberately force clone/borrow break + let res3 = unionFnRetNum x + let res4 = unionFnRetNum (IntCase2 24) + res |> equal 3 + res2 |> equal 3 + res3 |> equal 3 + res4 |> equal 24 + +[] +let ``test Deep union with wrapped type works`` () = + let a = DeepWrappedA (" world", {Value = "hello"}) + let b = DeepWrappedB "world" + let c = DeepWrappedC 42 + let d = DeepWrappedD { Value = "hello" } + let f = DeepWrappedF (AString "doublewrapped") + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + a |> matchStrings |> equal "hello world" + b |> matchStrings |> equal "world" + c |> matchStrings |> equal "nothing" + d |> matchStrings |> equal "hello" + f |> matchStrings |> equal "doublewrapped" + g |> matchStrings |> equal "G" + +[] +let ``test Deep union with tuped prim type works`` () = + let e = DeepWrappedE (3, 2) + let c = DeepWrappedC 42 //todo to string! + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + e |> matchNumbers |> equal 5 + c |> matchNumbers |> equal 42 + g |> matchNumbers |> equal 365 + +[] +let ``test Multi-case Union with wrapped type works`` () = + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" + +[] +let ``test Match with condition works`` () = + let b1 = DeepWrappedB "hello" + let b2 = DeepWrappedB "not" + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" diff --git a/tests/Python/Util.fs b/tests/Python/Util.fs index e80a7e9561..9eeb542b5a 100644 --- a/tests/Python/Util.fs +++ b/tests/Python/Util.fs @@ -106,6 +106,19 @@ let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentNullException -> "ArgumentNullException" + | :? ArgumentException -> "ArgumentException" + | :? InvalidOperationException -> "InvalidOperationException" + | :? FormatException -> "FormatException" + | e -> "Unexpected exception: " + e.Message + let Int32Array = [|1;2|] module Float64Array = diff --git a/tests/Python/py/async_runtime.py b/tests/Python/py/async_runtime.py new file mode 100644 index 0000000000..52dd18d55d --- /dev/null +++ b/tests/Python/py/async_runtime.py @@ -0,0 +1,248 @@ +"""Controlled scheduler and thread fixtures for the generated F# async tests.""" + +import asyncio +from collections.abc import Callable +from dataclasses import dataclass, field +from threading import Barrier, Event, Thread + +from fable_library.array_ import Array +from fable_library.async_builder import Async, CancellationToken, IAsyncContext, Trampoline +from fable_library.mailbox_processor import MailboxProcessor +from fable_library.util import lock + + +@dataclass +class Observation: + _terminal: list[str] = field(default_factory=list) + _errors: list[str] = field(default_factory=list) + _before: list[str] = field(default_factory=list) + _outcome: list[str] = field(default_factory=list) + listeners: int = 0 + scheduled: int = 0 + cancel_count: int = 0 + retired: bool = False + owner_loop: bool = False + threads_finished: bool = False + pending_before: bool = False + pending_after: bool = False + + @property + def terminal(self) -> Array[str]: + return Array(self._terminal) + + @property + def errors(self) -> Array[str]: + return Array(self._errors) + + @property + def before(self) -> Array[str]: + return Array(self._before) + + @property + def outcome(self) -> Array[str]: + return Array(self._outcome) + + +class Timer(asyncio.TimerHandle): + def __init__(self, callback: Callable[[], None]): + super().__init__(0, callback, (), asyncio.get_running_loop()) + self.callback = callback + self.retired = False + self.cancel_count = 0 + + def cancel(self): + self.retired = True + self.cancel_count += 1 + + def fire(self): + # decision: fires retired callbacks to model a timeout already queued when cancellation wins + self.callback() + + +class Scheduler(Trampoline): + def __init__(self, token: CancellationToken, mode: str = ""): + super().__init__() + self.token = token + self.mode = mode + self.timers: list[Timer] = [] + + def run_later(self, action: Callable[[], None], due_time: float = 0.0): + if self.mode == "install_error": + raise ValueError("scheduler") + timer = Timer(action) + self.timers.append(timer) + if self.mode == "install_cancel": + self.token.cancel() + return timer + + +def context(scheduler: Trampoline, token: CancellationToken, result: Observation) -> IAsyncContext[None]: + def failed(error: Exception): + result._terminal.append("error") + result._errors.append(str(error)) + + return IAsyncContext.create( + scheduler, + token, + lambda _: result._terminal.append("success"), + failed, + lambda _: result._terminal.append("cancel"), + ) + + +def observe_sleep(work: Async[None], token: CancellationToken, mode: str) -> Observation: + async def run(): + result = Observation() + scheduler = Scheduler(token, mode) + if mode == "precancel": + token.cancel() + work(context(scheduler, token, result)) + if scheduler.timers: + timer = scheduler.timers[0] + if mode == "cancel": + token.cancel() + timer.fire() + elif mode == "timeout": + timer.fire() + token.cancel() + timer.fire() + result.retired = timer.retired + result.cancel_count = timer.cancel_count + result.scheduled = len(scheduler.timers) + result.listeners = len(token.listeners) + return result + + return asyncio.run(run()) + + +def observe_mailbox(work: Async[int], mailbox: MailboxProcessor[int], token: CancellationToken) -> Observation: + async def run(): + result = Observation() + ctx = IAsyncContext.create( + Trampoline(), + token, + lambda value: result._terminal.append(str(value)), + lambda _: result._terminal.append("error"), + lambda _: result._terminal.append("cancel"), + ) + work(ctx) + token.cancel() + result._before = result._terminal.copy() + result.pending_before = mailbox.continuation is not None + mailbox.post(42) + result.pending_after = mailbox.continuation is not None + result.listeners = len(token.listeners) + next_ctx = IAsyncContext.create( + Trampoline(), + CancellationToken(), + lambda value: result._outcome.append(str(value)), + lambda _: result._outcome.append("error"), + lambda _: result._outcome.append("cancel"), + ) + mailbox.receive()(next_ctx) + return result + + return asyncio.run(run()) + + +def observe_owner_loop(work: Async[None], token: CancellationToken) -> Observation: + async def run(): + result = Observation() + loop = asyncio.get_running_loop() + finished = loop.create_future() + + def cancelled(_): + result.owner_loop = asyncio.get_running_loop() is loop + result._terminal.append("cancel") + finished.set_result(None) + + ctx = IAsyncContext.create( + Trampoline(), + token, + lambda _: result._terminal.append("success"), + lambda _: result._terminal.append("error"), + cancelled, + ) + work(ctx) + thread = Thread(target=token.cancel) + thread.start() + try: + await asyncio.wait_for(finished, 5) + finally: + thread.join(5) + result.threads_finished = not thread.is_alive() + result.listeners = len(token.listeners) + return result + + return asyncio.run(run()) + + +def observe_settlement( + make_work: Callable[[Callable[[], None]], Async[None]], + token: CancellationToken, + winner: str, +) -> Observation: + async def run(): + result = Observation() + scheduler = Scheduler(token) + key = object() + gate = Barrier(4) + winner_done = Event() + loop = asyncio.get_running_loop() + finished = loop.create_future() + thread_errors: list[Exception] = [] + + def choose(tag: str): + def settle(): + if result._outcome: + return + result._outcome.append(tag) + if tag == "reply": + # invariant: the winning reply retires its deadline before competitors proceed + token.cancel() + winner_done.set() + loop.call_soon_threadsafe(finished.set_result, None) + + lock(key, settle) + + registration = token.register(lambda: choose("cancel")) + make_work(lambda: choose("timeout"))(context(scheduler, token, result)) + timer = scheduler.timers[0] + + def compete(tag: str): + try: + gate.wait(5) + if tag != winner and not winner_done.wait(5): + raise TimeoutError("settlement winner did not signal") + if tag == "reply": + choose("reply") + elif tag == "cancel": + token.cancel() + else: + loop.call_soon_threadsafe(timer.fire) + except Exception as error: + thread_errors.append(error) + + threads = [Thread(target=compete, args=(tag,)) for tag in ("reply", "cancel", "timeout")] + for thread in threads: + thread.start() + gate.wait(5) + try: + await asyncio.wait_for(finished, 5) + finally: + for thread in threads: + thread.join(5) + registration.Dispose() + token.cancel() + if thread_errors: + raise ExceptionGroup("fixture worker failed", thread_errors) + # Exercise a stale callback after every winner, including cancellation's finalizer. + timer.fire() + await asyncio.sleep(0) + result.threads_finished = all(not thread.is_alive() for thread in threads) + result.listeners = len(token.listeners) + result.cancel_count = timer.cancel_count + result.retired = timer.retired + return result + + return asyncio.run(run()) diff --git a/tests/Python/py/lock_runtime.py b/tests/Python/py/lock_runtime.py new file mode 100644 index 0000000000..22f29ffd71 --- /dev/null +++ b/tests/Python/py/lock_runtime.py @@ -0,0 +1,141 @@ +"""Thread and acquisition controls for the generated F# lock tests.""" + +import gc +from collections.abc import Callable +from threading import Event, RLock, Thread +from types import TracebackType +from unittest.mock import patch +from weakref import WeakValueDictionary + +from fable_library import util +from fable_library.array_ import Array + + +LEGACY_STRIPE_COUNT = 1024 + + +class Acquisition: + def __init__(self) -> None: + self.attempted = Event() + self.blocked = False + + +class ObservedLock: + def __init__(self, acquisition: Acquisition) -> None: + self.lock = RLock() + self.acquisition = acquisition + + def __enter__(self) -> "ObservedLock": + acquired = self.lock.acquire(blocking=False) + self.acquisition.blocked = not acquired + # invariant: the contender's observation follows an actual acquisition attempt + self.acquisition.attempted.set() + if not acquired: + self.lock.acquire() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.lock.release() + + +class Threads: + def __init__(self) -> None: + self.entered = Event() + self.release = Event() + self._trace: list[str] = [] + self._errors: list[str] = [] + self.first_entered = False + self.second_attempted = False + self.second_blocked = False + self.threads_finished = False + + @property + def trace(self) -> Array[str]: + return Array(self._trace) + + @property + def errors(self) -> Array[str]: + return Array(self._errors) + + def record(self, entry: str) -> None: + self._trace.append(entry) + if entry == "first-enter": + self.entered.set() + + def wait_release(self) -> bool: + return self.release.wait(5) + + def run(self, first: Callable[[], None], second: Callable[[], None]) -> None: + acquisition = Acquisition() + + def invoke(action: Callable[[], None]) -> None: + try: + action() + except Exception as error: + self._errors.append(str(error)) + + first_thread = Thread(target=invoke, args=(first,), daemon=True) + second_thread = Thread(target=invoke, args=(second,), daemon=True) + with ( + patch.object(util, "RLock", lambda: ObservedLock(acquisition)), + patch.object(util, "_locks", WeakValueDictionary()), + ): + first_thread.start() + try: + self.first_entered = self.entered.wait(5) + if self.first_entered: + acquisition.attempted.clear() + second_thread.start() + self.second_attempted = acquisition.attempted.wait(5) + self.second_blocked = acquisition.blocked + finally: + self.release.set() + first_thread.join(5) + if second_thread.ident is not None: + second_thread.join(5) + self.threads_finished = not first_thread.is_alive() and not second_thread.is_alive() + + +class Storage: + def __init__(self) -> None: + self.before = getattr(util, "_locks", ()) + + @property + def stable(self) -> bool: + return getattr(util, "_locks", None) is self.before + + @property + def count(self) -> int: + gc.collect() + return len(getattr(util, "_locks", ())) + + +def make_key() -> object: + # decision: uses an unhashable, non-weak-referenceable key to cover arbitrary lock objects + return [] + + +def make_colliding_keys() -> Array[object]: + # decision: reproduces the former stripe collision using distinct, unhashable objects + keys: dict[int, object] = {} + for _ in range(LEGACY_STRIPE_COUNT + 1): + key: object = [] + stripe = (id(key) >> 4) % LEGACY_STRIPE_COUNT + previous = keys.get(stripe) + if previous is not None: + return Array([previous, key]) + keys[stripe] = key + raise AssertionError("Expected a collision in the former fixed stripes") + + +def make_threads() -> Threads: + return Threads() + + +def make_storage() -> Storage: + return Storage() diff --git a/tests/Python/test_async_runtime.py b/tests/Python/test_async_runtime.py new file mode 100644 index 0000000000..72ad4a3ccb --- /dev/null +++ b/tests/Python/test_async_runtime.py @@ -0,0 +1,617 @@ +import asyncio +from collections.abc import Callable +from threading import Barrier, Condition, Event, Thread + +import pytest +from fable_library.async_ import await_event, await_task_with_cancellation, sleep, start_with_continuations +from fable_library.async_builder import ( + Async, + CancellationCallbackError, + CancellationToken, + IAsyncContext, + OperationCanceledError, + TaskCanceledError, + Trampoline, +) +from fable_library.event import Event as FableEvent + + +def test_registration_handle_and_disposal(): + token = CancellationToken() + calls: list[str] = [] + first = token.register(lambda: calls.append("first")) + second = token.register(lambda: calls.append("second")) + assert first is not None + first.Dispose() + first.Dispose() + token.cancel() + second.Dispose() + assert calls == ["second"] + assert not token.listeners + + +def test_late_registration_and_falsy_state(): + token = CancellationToken(True) + calls: list[object] = [] + handle = token.register(lambda state: calls.append(state), 0) + assert calls == [0] + token.register(lambda state: calls.append(state), None).Dispose() + assert calls == [0, None] + handle.Dispose() + assert not token.listeners + + +def test_self_disposal_and_disposal_of_pending_callback(): + token = CancellationToken() + calls: list[str] = [] + first = token.register(lambda: calls.append("first")) + + def callback(): + second.Dispose() + first.Dispose() + calls.append("second") + + second = token.register(callback) + token.cancel() + token.cancel() + assert calls == ["second"] + assert not token.listeners + + +def test_callback_failures_do_not_skip_other_callbacks(): + token = CancellationToken() + calls: list[str] = [] + token.register(lambda: calls.append("other")) + + def failing(): + raise ValueError("callback") + + token.register(failing) + with pytest.raises(ExceptionGroup) as errors: + token.cancel() + assert len(errors.value.exceptions) == 1 + assert calls == ["other"] + assert not token.listeners + + +@pytest.mark.parametrize("count", [1, 2]) +def test_callback_errors_preserve_all_errors_and_first_inner_exception(count: int) -> None: + token = CancellationToken() + errors = [ValueError(str(i)) for i in range(count)] + + def failing(error: Exception) -> None: + raise error + + for error in errors: + token.register(failing, error) + + with pytest.raises(CancellationCallbackError) as caught: + token.cancel() + assert isinstance(caught.value, ExceptionGroup) + assert caught.value.exceptions == tuple(reversed(errors)) + assert caught.value.inner_exception is errors[-1] + assert not token.listeners + token.cancel() + + +def test_dispose_waits_for_running_callback_and_late_registration_runs_outside_lock(): + token = CancellationToken() + entered, release, disposed, late = Event(), Event(), Event(), Event() + + def callback(): + entered.set() + assert release.wait(5) + + waiting = Event() + + class ObservedCondition(Condition): + def wait(self, timeout=None): + waiting.set() + return super().wait(timeout) + + token._condition = ObservedCondition(token.lock) + handle = token.register(callback) + cancel_thread = Thread(target=token.cancel) + cancel_thread.start() + assert entered.wait(5) + + # Registration after cancellation must run immediately, even while an earlier callback is blocked. + register_thread = Thread(target=lambda: token.register(late.set).Dispose()) + register_thread.start() + assert late.wait(5) + register_thread.join(5) + + def dispose(): + handle.Dispose() + disposed.set() + + dispose_thread = Thread(target=dispose) + dispose_thread.start() + assert waiting.wait(5) + assert not disposed.is_set() + release.set() + cancel_thread.join(5) + dispose_thread.join(5) + assert disposed.is_set() + assert not token.listeners + + +def test_concurrent_registration_and_cancellation(): + token = CancellationToken() + gate = Barrier(17) + calls = [0] * 16 + + def register(index: int): + gate.wait(5) + + def callback(): + calls[index] += 1 + + token.register(callback).Dispose() + + threads = [Thread(target=register, args=(i,)) for i in range(16)] + for thread in threads: + thread.start() + gate.wait(5) + token.cancel() + for thread in threads: + thread.join(5) + assert not thread.is_alive() + assert all(count in (0, 1) for count in calls) + assert not token.listeners + assert not token._running + + +def test_sleep_on_cancelled_token_settles_once(): + calls: list[str] = [] + start_with_continuations( + sleep(10), + lambda _: calls.append("success"), + lambda _: calls.append("error"), + lambda _: calls.append("cancel"), + CancellationToken(True), + ) + assert calls == ["cancel"] + + +@pytest.mark.parametrize("already_cancelled", [False, True]) +def test_await_task_cancellation_uses_error_continuation(already_cancelled: bool) -> None: + async def run() -> None: + loop = asyncio.get_running_loop() + unhandled: list[object] = [] + loop.set_exception_handler(lambda _, context: unhandled.append(context.get("exception"))) + task: asyncio.Future[int] = loop.create_future() + token = CancellationToken() + calls: list[str] = [] + errors: list[Exception] = [] + if already_cancelled: + task.cancel() + + def on_error(error: Exception) -> None: + calls.append("error") + errors.append(error) + + start_with_continuations( + await_task_with_cancellation(task), + lambda _: calls.append("success"), + on_error, + lambda _: calls.append("cancel"), + token, + ) + await asyncio.sleep(0) + if not already_cancelled: + task.cancel() + await asyncio.sleep(0) + await asyncio.sleep(0) + assert calls == ["error"] + assert len(errors) == 1 + assert isinstance(errors[0], TaskCanceledError) + assert isinstance(errors[0], OperationCanceledError) + assert errors[0].inner_exception is None + assert not token.is_cancelled + assert not unhandled + + asyncio.run(run()) + + +class _DeferringTrampoline(Trampoline): + def increment_and_check(self) -> bool: + return True + + +def test_await_event_cancelled_before_its_body_runs_does_not_subscribe(): + event: FableEvent[int] = FableEvent() + token = CancellationToken() + calls: list[str] = [] + + async def run() -> None: + ctx = IAsyncContext.create( + _DeferringTrampoline(), + token, + lambda value: calls.append(f"success {value}"), + lambda _: calls.append("error"), + lambda _: calls.append("cancel"), + ) + await_event(event)(ctx) + token.cancel() + await asyncio.sleep(0) + + asyncio.run(run()) + event.Trigger(42) + assert calls == ["cancel"] + assert not event.delegates + + +def test_sleep_cancelled_while_its_loop_is_stopped_settles_once(): + loop = asyncio.new_event_loop() + token = CancellationToken() + calls: list[str] = [] + + async def start() -> None: + start_with_continuations( + sleep(100_000), + lambda _: calls.append("success"), + lambda _: calls.append("error"), + lambda _: calls.append("cancel"), + token, + ) + + try: + loop.run_until_complete(start()) + cancel_thread = Thread(target=token.cancel) + cancel_thread.start() + cancel_thread.join(5) + assert calls == ["cancel"] + assert not token.listeners + finally: + loop.close() + + +@pytest.mark.parametrize("channel", ["success", "error", "cancel"]) +def test_try_finally_propagates_downstream_errors_once(channel: str) -> None: + from fable_library.async_builder import OperationCanceledError, singleton + + async def run() -> None: + cleanups = 0 + calls: list[str] = [] + trampoline = Trampoline() + token = CancellationToken() + defect = ValueError("downstream") + + def cleanup() -> None: + nonlocal cleanups + cleanups += 1 + + def terminal(value: object) -> None: + calls.append(channel) + raise defect + + def body(ctx: IAsyncContext[int]) -> None: + assert ctx.trampoline is trampoline + assert ctx.cancel_token is token + if channel == "success": + ctx.on_success(42) + elif channel == "error": + ctx.on_error(ValueError("body")) + else: + ctx.on_cancel(OperationCanceledError()) + + ctx = IAsyncContext.create(trampoline, token, terminal, terminal, terminal) + with pytest.raises(ValueError) as caught: + singleton.TryFinally(body, cleanup)(ctx) + assert caught.value is defect + assert cleanups == 1 + assert calls == [channel] + + asyncio.run(run()) + + +@pytest.mark.parametrize("channel", ["success", "error", "cancel"]) +def test_generic_from_continuations_propagates_terminal_exceptions(channel: str) -> None: + from fable_library.async_ import from_continuations + from fable_library.async_builder import Continuations, OperationCanceledError + + async def run() -> None: + calls: list[str] = [] + defect = ValueError("downstream") + trampoline = Trampoline() + token = CancellationToken() + + def terminal(value: object) -> None: + calls.append(channel) + raise defect + + def callback(conts: Continuations[int]) -> None: + ok, error, cancelled = conts + if channel == "success": + ok(42) + elif channel == "error": + error(ValueError("body")) + else: + cancelled(OperationCanceledError()) + + ctx = IAsyncContext.create(trampoline, token, terminal, terminal, terminal) + with pytest.raises(ValueError) as caught: + from_continuations(callback)(ctx) + assert caught.value is defect + assert calls == [channel] + assert ctx.trampoline is trampoline + + asyncio.run(run()) + + +def test_generic_from_continuations_rejects_duplicate_completion() -> None: + from fable_library.async_ import from_continuations + from fable_library.async_builder import Continuations + + calls: list[int] = [] + + def callback(conts: Continuations[int]) -> None: + conts[0](42) + conts[0](43) + + with pytest.raises(RuntimeError, match="invoked more than once"): + start_with_continuations(from_continuations(callback), calls.append) + assert calls == [42] + + +def test_generic_from_continuations_claims_concurrent_completion_once() -> None: + from fable_library.async_ import from_continuations + from fable_library.async_builder import Continuations + + callbacks: Continuations[int] | None = None + calls: list[str] = [] + rejected: list[Exception] = [] + barrier = Barrier(2) + + def callback(conts: Continuations[int]) -> None: + nonlocal callbacks + callbacks = conts + + start_with_continuations( + from_continuations(callback), + lambda _: calls.append("success"), + lambda _: calls.append("error"), + ) + assert callbacks is not None + ok, error, _ = callbacks + + def complete(success: bool) -> None: + barrier.wait(5) + try: + if success: + ok(42) + else: + error(ValueError("body")) + except RuntimeError as duplicate: + rejected.append(duplicate) + + threads = [Thread(target=complete, args=(success,)) for success in (True, False)] + for thread in threads: + thread.start() + for thread in threads: + thread.join(5) + assert all(not thread.is_alive() for thread in threads) + assert len(calls) == 1 + assert len(rejected) == 1 + + +@pytest.mark.parametrize("combinator", ["try_with", "bind"]) +def test_completed_continuation_exceptions_are_not_redirected(combinator: str) -> None: + from fable_library.async_builder import singleton + + calls: list[str] = [] + defect = ValueError("downstream") + + if combinator == "try_with": + work = singleton.TryWith(singleton.Return(1), lambda _: singleton.Return(99)) + else: + work = singleton.Bind(singleton.Return(1), lambda x: singleton.Return(x + 1)) + + def success(value: int) -> None: + calls.append(f"success:{value}") + raise defect + + with pytest.raises(ValueError) as caught: + start_with_continuations(work, success, lambda _: calls.append("error"), lambda _: calls.append("cancel")) + assert caught.value is defect + assert len(calls) == 1 + + +def test_workflow_errors_still_reach_the_handler_before_completion() -> None: + from fable_library.async_builder import singleton + + def failing_binder(_: int) -> Async[str]: + raise ValueError("binder") + + def handler(error: Exception) -> Async[str]: + return singleton.Return(f"handled:{error}") + + results: list[object] = [] + work = singleton.TryWith(singleton.Bind(singleton.Return(1), failing_binder), handler) + start_with_continuations(work, results.append, lambda e: results.append(("error", e))) + assert results == ["handled:binder"] + + +@pytest.mark.parametrize("channel", ["success", "error", "cancel"]) +def test_unit_from_continuations_propagates_terminal_exceptions(channel: str) -> None: + from fable_library.async_ import from_continuations_unit + from fable_library.async_builder import OperationCanceledError + + calls: list[str] = [] + defect = ValueError("downstream") + + def terminal(value: object = None) -> None: + calls.append(channel) + raise defect + + def callback( + conts: tuple[Callable[[], None], Callable[[Exception], None], Callable[[OperationCanceledError], None]], + ) -> None: + ok, error, cancelled = conts + if channel == "success": + ok() + elif channel == "error": + error(ValueError("body")) + else: + cancelled(OperationCanceledError()) + + with pytest.raises(ValueError) as caught: + start_with_continuations(from_continuations_unit(callback), terminal, terminal, terminal) + assert caught.value is defect + assert calls == [channel] + + +@pytest.mark.parametrize("explicit_unit", [False, True]) +def test_unit_from_continuations_accepts_erased_and_explicit_unit(explicit_unit: bool) -> None: + from fable_library.async_ import from_continuations_unit + + calls: list[object] = [] + + def callback(conts: tuple[Callable[..., None], object, object]) -> None: + if explicit_unit: + conts[0](None) + else: + conts[0]() + + start_with_continuations(from_continuations_unit(callback), calls.append, lambda e: calls.append(("error", e))) + assert calls == [None] + + +def test_unit_from_continuations_rejects_duplicate_completion() -> None: + from fable_library.async_ import from_continuations_unit + + calls: list[object] = [] + + def callback(conts: tuple[Callable[[], None], object, object]) -> None: + conts[0]() + conts[0]() + + with pytest.raises(RuntimeError, match="invoked more than once"): + start_with_continuations(from_continuations_unit(callback), calls.append) + assert calls == [None] + + +def test_try_finally_claims_concurrent_completion_once() -> None: + from fable_library.async_builder import OperationCanceledError, singleton + + captured: list[IAsyncContext[int]] = [] + calls: list[str] = [] + cleanups: list[int] = [] + barrier = Barrier(2) + + def body(ctx: IAsyncContext[int]) -> None: + captured.append(ctx) + + start_with_continuations( + singleton.TryFinally(body, lambda: cleanups.append(1)), + lambda _: calls.append("success"), + lambda _: calls.append("error"), + lambda _: calls.append("cancel"), + ) + assert len(captured) == 1 + ctx = captured[0] + + def complete(success: bool) -> None: + barrier.wait(5) + if success: + ctx.on_success(42) + else: + ctx.on_cancel(OperationCanceledError()) + + threads = [Thread(target=complete, args=(success,)) for success in (True, False)] + for thread in threads: + thread.start() + for thread in threads: + thread.join(5) + assert all(not thread.is_alive() for thread in threads) + assert len(cleanups) == 1 + assert len(calls) == 1 + + +def test_post_and_async_reply_delivers_once_when_reply_races_with_start() -> None: + from fable_library.mailbox_processor import AsyncReplyChannel, MailboxProcessor + + for _ in range(20): + channels: list[AsyncReplyChannel[int]] = [] + calls: list[object] = [] + failures: list[BaseException] = [] + barrier = Barrier(2) + mailbox: MailboxProcessor[AsyncReplyChannel[int]] = MailboxProcessor(lambda _: lambda _: None) + + def build(channel: AsyncReplyChannel[int]) -> AsyncReplyChannel[int]: + channels.append(channel) + return channel + + work = mailbox.post_and_async_reply(build) + assert len(channels) == 1 + + def start() -> None: + barrier.wait(5) + try: + start_with_continuations(work, calls.append, lambda e: failures.append(e), lambda e: failures.append(e)) + except BaseException as error: + failures.append(error) + + def reply() -> None: + barrier.wait(5) + try: + channels[0].reply(42) + except BaseException as error: + failures.append(error) + + threads = [Thread(target=start), Thread(target=reply)] + for thread in threads: + thread.start() + for thread in threads: + thread.join(5) + assert all(not thread.is_alive() for thread in threads) + assert failures == [] + assert calls == [42] + start_with_continuations(work, calls.append, lambda e: failures.append(e), lambda e: failures.append(e)) + assert failures == [] + assert calls == [42, 42] + + +@pytest.mark.parametrize("value", [None, 42]) +@pytest.mark.parametrize("reply_before_start", [False, True]) +def test_post_and_async_reply_can_be_reused(value: int | None, reply_before_start: bool) -> None: + from fable_library.mailbox_processor import AsyncReplyChannel, MailboxProcessor + + channels: list[AsyncReplyChannel[int | None]] = [] + calls: list[object] = [] + mailbox: MailboxProcessor[AsyncReplyChannel[int | None]] = MailboxProcessor(lambda _: lambda _: None) + + def build(channel: AsyncReplyChannel[int | None]) -> AsyncReplyChannel[int | None]: + channels.append(channel) + return channel + + work = mailbox.post_and_async_reply(build) + if reply_before_start: + channels[0].reply(value) + start_with_continuations(work, calls.append, lambda e: calls.append(("error", e))) + if not reply_before_start: + channels[0].reply(value) + assert calls == [value] + + start_with_continuations(work, calls.append, lambda e: calls.append(("error", e))) + assert calls == [value, value] + assert len(channels) == 1 + + +def test_post_and_async_reply_ignores_a_second_reply() -> None: + from fable_library.mailbox_processor import AsyncReplyChannel, MailboxProcessor + + channels: list[AsyncReplyChannel[int]] = [] + calls: list[object] = [] + mailbox: MailboxProcessor[AsyncReplyChannel[int]] = MailboxProcessor(lambda _: lambda _: None) + + def build(channel: AsyncReplyChannel[int]) -> AsyncReplyChannel[int]: + channels.append(channel) + return channel + + work = mailbox.post_and_async_reply(build) + start_with_continuations(work, calls.append, lambda e: calls.append(("error", e))) + channels[0].reply(1) + channels[0].reply(2) + assert calls == [1] diff --git a/tests/Python/xunit.runner.json b/tests/Python/xunit.runner.json new file mode 100644 index 0000000000..8641732346 --- /dev/null +++ b/tests/Python/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeTestCollections": false, + "maxParallelThreads": 1 +} diff --git a/tests/React/Fable.Tests.React.fsproj b/tests/React/Fable.Tests.React.fsproj index 6d63e41769..ed954b9a0c 100644 --- a/tests/React/Fable.Tests.React.fsproj +++ b/tests/React/Fable.Tests.React.fsproj @@ -28,11 +28,13 @@ - + + + diff --git a/tests/React/__tests__/JSX_API.fs b/tests/React/__tests__/JSX_API.fs index ff157d31ed..9a0b268b11 100644 --- a/tests/React/__tests__/JSX_API.fs +++ b/tests/React/__tests__/JSX_API.fs @@ -93,4 +93,36 @@ Jest.describe("JSX API tests (using React)", fun () -> Jest.test("Test that properties can be defined using `unbox`", fun () -> matchSnapshot Components.propsCanUseUnbox ) + + Jest.test("Spread props are applied to the element", fun () -> + let elem = RTL.render Components.divWithSpread + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveAttribute("title", "from-spread") + ) + + Jest.test("Props after a spread override the spread props", fun () -> + let elem = RTL.render Components.divWithSpreadBetweenProps + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveClass("before") + Jest.expect(target).toHaveAttribute("title", "after") + ) + + Jest.test("Spread props override the props before the spread", fun () -> + let elem = RTL.render Components.divWithSpreadOverridingProps + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveAttribute("title", "from-spread") + ) + + Jest.test("Several spreads are applied in order", fun () -> + let elem = RTL.render Components.divWithTwoSpreads + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveAttribute("title", "second") + ) + + Jest.test("Spread props can be combined with children", fun () -> + let elem = RTL.render Components.divWithSpreadAndChildren + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveAttribute("title", "from-spread") + Jest.expect(target).toHaveTextContent("Child") + ) ) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index e5df4a5130..f1d1c1f123 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -7,10 +7,10 @@ "dependencies": { "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-react-jsx": "^7.29.7", - "@fluentui/react": "^8.125.7", + "@fluentui/react": "^8.125.10", "@testing-library/jest-dom": "^7.0.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.4", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.8", "jest": "^30", "jest-environment-jsdom": "^30", "react": "^18", @@ -57,29 +57,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -124,13 +124,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -206,22 +206,22 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -294,12 +294,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -450,12 +450,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -721,27 +721,27 @@ } }, "node_modules/@fluentui/font-icons-mdl2": { - "version": "8.5.74", - "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.74.tgz", - "integrity": "sha512-q3QiQ2CuFdMjtiP9U5+Qi8LMhxKqWEi7C8R+O6SuaGbGS+3YOO/1iuenhKtt961OHn5ev/0M4j4okUZ1112GSw==", + "version": "8.5.76", + "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.76.tgz", + "integrity": "sha512-lC5WfGQq2qUu8tGUyu0TK84N+K4qxjdF6jJgocJni4UhFMr7xBCljl/AhSFY+hUlbnMF8qw0SY/FhFYlF/f4fA==", "license": "MIT", "dependencies": { "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/style-utilities": "^8.15.4", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" } }, "node_modules/@fluentui/foundation-legacy": { - "version": "8.6.7", - "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.6.7.tgz", - "integrity": "sha512-mu1wHlkom+XT+mlaGe7GW4eIZeIT9OjgdXg2Az/P670N7JA1OKcjHd8F/9ktMb5FGrXJ287AjQDNCDfWnjnCYA==", + "version": "8.6.9", + "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.6.9.tgz", + "integrity": "sha512-1haga7Idbz59IbB1Pq+FNXVm0CcH/jZiuVQ8vYWjcOfUdDApHrBYk20gUmBjOVzyviQVqm9Ts4sh8k36FwtUxQ==", "license": "MIT", "dependencies": { - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/style-utilities": "^8.15.4", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" }, "peerDependencies": { @@ -759,9 +759,9 @@ } }, "node_modules/@fluentui/merge-styles": { - "version": "8.6.14", - "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.6.14.tgz", - "integrity": "sha512-vghuHFAfQgS9WLIIs4kgDOCh/DHd5vGIddP4/bzposhlAVLZR6wUBqldm9AuCdY88r5LyCRMavVJLV+Up3xdvA==", + "version": "8.6.16", + "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.6.16.tgz", + "integrity": "sha512-HeqTdVPpSElxzONf6jRbL6CvYACey/Lj/aI5yNVzGrz/68krZKiUaBI1IE+Q+AnliSNUoPKeqti4Lyyify0Eow==", "license": "MIT", "dependencies": { "@fluentui/set-version": "^8.2.24", @@ -769,23 +769,23 @@ } }, "node_modules/@fluentui/react": { - "version": "8.125.7", - "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.125.7.tgz", - "integrity": "sha512-MsputVjgAZEcSzfr6nXBghfZnqu+GKT4NCBwLeSL8yyQEugUxyZJNp4TVZblGdMognyRDbobNn+/zIhEK4sxNQ==", + "version": "8.125.10", + "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.125.10.tgz", + "integrity": "sha512-V40Au1Pj5g8mnsCnBDd7yTDGmGlIEeJsYixkY0NA6gV6P5IIctzSSWjAJWHWEnwidcdXUiANNOJzvlWZA03Pew==", "license": "MIT", "dependencies": { "@fluentui/date-time-utilities": "^8.6.11", - "@fluentui/font-icons-mdl2": "^8.5.74", - "@fluentui/foundation-legacy": "^8.6.7", - "@fluentui/merge-styles": "^8.6.14", - "@fluentui/react-focus": "^8.10.7", - "@fluentui/react-hooks": "^8.10.2", - "@fluentui/react-portal-compat-context": "^9.0.15", + "@fluentui/font-icons-mdl2": "^8.5.76", + "@fluentui/foundation-legacy": "^8.6.9", + "@fluentui/merge-styles": "^8.6.16", + "@fluentui/react-focus": "^8.10.9", + "@fluentui/react-hooks": "^8.10.4", + "@fluentui/react-portal-compat-context": "^9.0.17", "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.2", - "@fluentui/theme": "^2.7.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/style-utilities": "^8.15.4", + "@fluentui/theme": "^2.7.4", + "@fluentui/utilities": "^8.17.4", "@microsoft/load-themed-styles": "^1.10.26", "tslib": "^2.1.0" }, @@ -797,16 +797,16 @@ } }, "node_modules/@fluentui/react-focus": { - "version": "8.10.7", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.10.7.tgz", - "integrity": "sha512-Uf0wliddeVPEYylNvbBqYu2V2ofCQ/YhC5JHIFT4RwBkZil1Ta3X+9EFhJMO1LejyLXTKnMMimPxdPS1XzLTGQ==", + "version": "8.10.9", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.10.9.tgz", + "integrity": "sha512-yi/GhJCogcY8lLLkWoSmzH1nVdmHxlokuRw3GOr3kcv1IX4mmFZ0fFwcVVp5ik0BTc6SeDD4xBUXdNQIXgSyKA==", "license": "MIT", "dependencies": { "@fluentui/keyboard-key": "^0.4.23", - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/style-utilities": "^8.15.4", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" }, "peerDependencies": { @@ -815,14 +815,14 @@ } }, "node_modules/@fluentui/react-hooks": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.10.2.tgz", - "integrity": "sha512-HAd5cX50yKW/LljWlwt+FpSpdS/pNJutk9kMb7FyzxfoGBulL7sj6vX2HvxhSKyJMRKuTstXTdfJmsh22+3W3w==", + "version": "8.10.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.10.4.tgz", + "integrity": "sha512-nwJfVHccX7wcPaY2JGLyzzZBlg5DSAPxNpET0t1d2dduQsRJMo/fbSpblpgTwF0z3bP0kPAxAXgb1vz34Sud2Q==", "license": "MIT", "dependencies": { "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", - "@fluentui/utilities": "^8.17.2", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" }, "peerDependencies": { @@ -831,9 +831,9 @@ } }, "node_modules/@fluentui/react-portal-compat-context": { - "version": "9.0.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.15.tgz", - "integrity": "sha512-DpV+qtFvM3dmH1j8ZD+YcM5vaTvmQPHUAx6tQnnmIoYJWs2R0wU/L5p2EajXy7zSg74jrDbDRxzaziamoOaJdg==", + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.17.tgz", + "integrity": "sha512-zfRwL3EeOXCNELCd4C+Yz2LsdVVnyvh0+pyXLDPDxN+r7Hj434dFdS5D4HWv/k0EYzkfoBbACfilmB6h9kviJg==", "license": "MIT", "dependencies": { "@swc/helpers": "^0.5.1" @@ -841,6 +841,11 @@ "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "react": ">=16.14.0 <20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@fluentui/react-window-provider": { @@ -867,28 +872,28 @@ } }, "node_modules/@fluentui/style-utilities": { - "version": "8.15.2", - "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.15.2.tgz", - "integrity": "sha512-rss7pkgiyNQuo1OpS7R2X6IJXIwDzQ0fxQxIeBB7Xp7lM+AwcKn5V7fzgIgCyE7BI6TWSy2TeQJ9Jtr/4fYEaw==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.15.4.tgz", + "integrity": "sha512-x9gieJHscmrBByc2qQgmkNfz19Ghyp7RHHTiqJEDYVHnLoRsVneuhvDqJGDCgmd3JJOxeoTPoqwF5sj1Q0ZghA==", "license": "MIT", "dependencies": { - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/set-version": "^8.2.24", - "@fluentui/theme": "^2.7.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/theme": "^2.7.4", + "@fluentui/utilities": "^8.17.4", "@microsoft/load-themed-styles": "^1.10.26", "tslib": "^2.1.0" } }, "node_modules/@fluentui/theme": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.7.2.tgz", - "integrity": "sha512-UXGNfGa/1bLmYrOpmHXdvyc7CzlNSKUQAADweTncbNoMF1DvscWEjPj5kxFgCmOU8wVtvvn4GraNNUSWtNxeeA==", + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.7.4.tgz", + "integrity": "sha512-EDEyMyxG6ygMN5XRHaM7ImVwdfQyxJe6jvig018N7HUv580mRsIxP7uV5P80Y4no1cJF6kJVp2PlLFQug0rEUg==", "license": "MIT", "dependencies": { - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/set-version": "^8.2.24", - "@fluentui/utilities": "^8.17.2", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" }, "peerDependencies": { @@ -897,13 +902,13 @@ } }, "node_modules/@fluentui/utilities": { - "version": "8.17.2", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.17.2.tgz", - "integrity": "sha512-TmeWVtGN+Lk0mch7tuRcbkeMdrBwltI68fvQbPwcNLo4igFtTInMmjEnVJGa7pBQN5lQAmHYqB9IJI6RZU/t6w==", + "version": "8.17.4", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.17.4.tgz", + "integrity": "sha512-qoteV6ZrV0MPv/g/Qb+KE+OeIelJOxvI2dFyoV9RXut3WjP+PAaQqheaWPU+zeQvnzgTfs52I+PbiI7qZgdLBg==", "license": "MIT", "dependencies": { "@fluentui/dom-utilities": "^2.3.10", - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", "tslib": "^2.1.0" @@ -930,6 +935,85 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -956,16 +1040,16 @@ } }, "node_modules/@jest/console": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", - "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.2.tgz", + "integrity": "sha512-e8sQC4pCMHPBrPX7Hk8TJr+DM5dkUnImSTNag8SEWFAFAF6xIz4AJvCKapnrdHnZxep4bxw4PhG+ZbimYBMyKw==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", + "jest-message-util": "30.5.1", + "jest-util": "30.5.1", "slash": "^3.0.0" }, "engines": { @@ -973,17 +1057,17 @@ } }, "node_modules/@jest/core": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", - "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.2.tgz", + "integrity": "sha512-/jqvFWoJF7LV1a6AUpYMbMm+2yIYsHJfR474H0SCzr9k8tnO/jl9wRg6zSG6lxgf6EozNlCG4amFzWsbZSvvZA==", "license": "MIT", "dependencies": { - "@jest/console": "30.4.1", - "@jest/pattern": "30.4.0", - "@jest/reporters": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", + "@jest/console": "30.5.2", + "@jest/pattern": "30.5.0", + "@jest/reporters": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", @@ -991,20 +1075,20 @@ "exit-x": "^0.2.2", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.4.1", - "jest-config": "30.4.2", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-resolve-dependencies": "30.4.2", - "jest-runner": "30.4.2", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "jest-watcher": "30.4.1", - "pretty-format": "30.4.1", + "jest-changed-files": "30.5.1", + "jest-config": "30.5.2", + "jest-haste-map": "30.5.1", + "jest-message-util": "30.5.1", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.1", + "jest-resolve-dependencies": "30.5.2", + "jest-runner": "30.5.2", + "jest-runtime": "30.5.2", + "jest-snapshot": "30.5.2", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", + "jest-watcher": "30.5.2", + "pretty-format": "30.5.1", "slash": "^3.0.0" }, "engines": { @@ -1032,62 +1116,62 @@ } }, "node_modules/@jest/core/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/diff-sequences": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", - "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz", + "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==", "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", - "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.2.tgz", + "integrity": "sha512-nwFPOUMbmsjNNRCpnJpB9HbSZu4C07wDDds++5j+j1MezzPLxMPDdbdUVjr2o4RULDOValnDd70taPFI1EtxAg==", "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", + "@jest/fake-timers": "30.5.2", + "@jest/types": "30.5.1", "@types/node": "*", - "jest-mock": "30.4.1" + "jest-mock": "30.5.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.4.1.tgz", - "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.5.2.tgz", + "integrity": "sha512-8/P+8hKv7L4mhFoB6gpYLj/fuzXk8PWrde7TDHh8tJz5pE1ktlMbg0y6hDY/V8MfAnispFlPl/vPifowLy3JGQ==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/jsdom": "^21.1.7", + "@jest/environment": "30.5.2", + "@jest/fake-timers": "30.5.2", + "@jest/types": "30.5.1", "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" + "jest-mock": "30.5.2", + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { + "@types/jsdom": "*", "canvas": "^3.0.0", "jsdom": "*" }, @@ -1098,110 +1182,124 @@ } }, "node_modules/@jest/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.2.tgz", + "integrity": "sha512-2E+1Pg6jJxbRjLNLpciC71iri/3qsKJL222/aXJEFwC0VdUSvfS+JZQQLEVjUYYc8t8pQu2MGE3cF2+bNTYRsg==", "license": "MIT", "dependencies": { - "expect": "30.4.1", - "jest-snapshot": "30.4.1" + "expect": "30.5.2", + "jest-snapshot": "30.5.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", - "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.2.tgz", + "integrity": "sha512-d/HOLSrJUlBIl6n3LzOa7cjW3xdePm5H5gQw2lixZ/0h157l9sw3mLblzFqnPcTbhDzhRQdBOX3A3KcG73nm5Q==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0" + "@jest/get-type": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", - "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.2.tgz", + "integrity": "sha512-GKk0h0tKT5SpDPxDhPg3r9mm5s8/YCBzVNMOGdRiQXwUiMsjU10mVPlMaQstH6/cu50vOb8N9oQUMETHuwwxKw==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.1", "@sinonjs/fake-timers": "^15.4.0", "@types/node": "*", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" + "jest-message-util": "30.5.1", + "jest-mock": "30.5.2", + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.5.0.tgz", + "integrity": "sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==", "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/globals": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", - "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.2.tgz", + "integrity": "sha512-62zfS+dL+M5aTNHXCNLyqD+j4oqxTIzDYrZbpqMP8Yn+gvlAGyzC6BSPNb1ZmFOakV1RtFYX/O3FCQXSWmQdgg==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/types": "30.4.1", - "jest-mock": "30.4.1" + "@jest/environment": "30.5.2", + "@jest/expect": "30.5.2", + "@jest/types": "30.5.1", + "jest-mock": "30.5.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/pattern": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", - "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.5.0.tgz", + "integrity": "sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==", "license": "MIT", "dependencies": { "@types/node": "*", - "jest-regex-util": "30.4.0" + "jest-regex-util": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/@jest/react-is-19": { + "name": "react-is", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, "node_modules/@jest/reporters": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", - "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.2.tgz", + "integrity": "sha512-ev6E9iG73twQcfAfoTnviuYkr3yOrYbzAtMFCimv2fxHXPIp9PBt2u1wNMt0aYWgl8FPwIcS8oqRJg6alnht4Q==", "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", + "@jest/console": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", + "@jridgewell/trace-mapping": "^0.3.31", "@types/node": "*", "chalk": "^4.1.2", "collect-v8-coverage": "^1.0.2", "exit-x": "^0.2.2", - "glob": "^10.5.0", + "glob": "^13.0.6", "graceful-fs": "^4.2.11", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", + "jest-message-util": "30.5.1", + "jest-util": "30.5.1", + "jest-worker": "30.5.1", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -1219,9 +1317,9 @@ } }, "node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" @@ -1231,12 +1329,12 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", - "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.5.1.tgz", + "integrity": "sha512-V3wnxNtiVmw5PPVg433Cn3VdXnsOeu/ofLw3KC04Bn2y1wIlU5kozXQn48rhrgj/02LrCVtntTEF1yBha0XSUw==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.1", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -1246,13 +1344,14 @@ } }, "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.5.2.tgz", + "integrity": "sha512-c5CehvkbfHX6yyNg7bMn2ylqlp9AHXuZ/3hPH/Lh5bxD/vefd/lV9HBjMqXef9yQNqkdwyZvkLSVSwB38hy8Qw==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", + "@jridgewell/trace-mapping": "^0.3.31", "callsites": "^3.1.0", + "convert-source-map": "^2.0.0", "graceful-fs": "^4.2.11" }, "engines": { @@ -1260,13 +1359,13 @@ } }, "node_modules/@jest/test-result": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", - "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.2.tgz", + "integrity": "sha512-eO6YU5rsLfs60ne694e0IAmRgeBnoA8mu0nlxXDbl/1j5km0o2vO9/VbbUIKIH+WRKKEI+ELgwraRGn2Lhep8g==", "license": "MIT", "dependencies": { - "@jest/console": "30.4.1", - "@jest/types": "30.4.1", + "@jest/console": "30.5.2", + "@jest/types": "30.5.1", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -1275,14 +1374,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", - "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.2.tgz", + "integrity": "sha512-bmij8jZyYAC47ExT2GeP7PcrlwSNS+Bp3KeRuBH88gpcxqDv6fljN8PemS4J/lLZ79xb6mpaennQTTq0zUAVVg==", "license": "MIT", "dependencies": { - "@jest/test-result": "30.4.1", + "@jest/test-result": "30.5.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", + "jest-haste-map": "30.5.1", "slash": "^3.0.0" }, "engines": { @@ -1290,22 +1389,22 @@ } }, "node_modules/@jest/transform": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", - "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.2.tgz", + "integrity": "sha512-LBGwmaE886C41jDZ65eqR1YbXM7642MK3ZhTowZd5+xDlrDLuf4ePuDdDmVSvcr8NTVJ3MqxLkMbUCzuigUaoQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", + "@jest/types": "30.5.1", + "@jridgewell/trace-mapping": "^0.3.31", + "babel-plugin-istanbul": "^8.0.0", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", + "jest-haste-map": "30.5.1", + "jest-regex-util": "30.5.0", + "jest-util": "30.5.1", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" @@ -1315,13 +1414,13 @@ } }, "node_modules/@jest/types": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", - "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz", + "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==", "license": "MIT", "dependencies": { - "@jest/pattern": "30.4.0", - "@jest/schemas": "30.4.1", + "@jest/pattern": "30.5.0", + "@jest/schemas": "30.5.0", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", @@ -1384,140 +1483,435 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], "license": "MIT", "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=14" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", - "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", - "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/jest-dom": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", - "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=22", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@testing-library/dom": ">=10 <11", - "vitest": ">= 0.32" + "node": ">= 10.0.0" }, - "peerDependenciesMeta": { - "vitest": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, "engines": { "node": ">=18" }, @@ -1538,9 +1932,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.4", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz", - "integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==", + "version": "14.6.8", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.8.tgz", + "integrity": "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==", "license": "MIT", "engines": { "node": ">=12", @@ -1551,9 +1945,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", "license": "MIT", "optional": true, "dependencies": { @@ -1700,9 +2094,9 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", - "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { @@ -1803,6 +2197,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1816,6 +2213,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1829,6 +2229,9 @@ "cpu": [ "loong64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1842,6 +2245,9 @@ "cpu": [ "loong64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1855,6 +2261,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1868,6 +2277,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1881,6 +2293,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1894,6 +2309,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1907,6 +2325,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1920,6 +2341,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2088,15 +2512,15 @@ } }, "node_modules/babel-jest": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", - "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.2.tgz", + "integrity": "sha512-ES8WeJ2fNWPEDunyAtUfJ12nHyaWUloCHdkfK2oW5uSoQSUmjNKdWiGaXJITJwvFEWoD/evHEg/QoCXAvLaQqQ==", "license": "MIT", "dependencies": { - "@jest/transform": "30.4.1", + "@jest/transform": "30.5.2", "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.4.0", + "babel-plugin-istanbul": "^8.0.0", + "babel-preset-jest": "30.5.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -2109,9 +2533,9 @@ } }, "node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-8.0.0.tgz", + "integrity": "sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==", "license": "BSD-3-Clause", "workspaces": [ "test/babel-8" @@ -2121,16 +2545,16 @@ "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.3", "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" + "test-exclude": "^7.0.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", - "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.5.0.tgz", + "integrity": "sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==", "license": "MIT", "dependencies": { "@types/babel__core": "^7.20.5" @@ -2166,31 +2590,34 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", - "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.5.0.tgz", + "integrity": "sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w==", "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.4.0", + "babel-plugin-jest-hoist": "30.5.0", "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + "@babel/core": "^7.11.0 || ^8.0.0-beta.1 || ^8.0.0" } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.11.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz", + "integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -2200,18 +2627,21 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", "funding": [ { "type": "opencollective", @@ -2228,11 +2658,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -2250,12 +2680,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2275,9 +2699,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001777", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", - "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "funding": [ { "type": "opencollective", @@ -2335,9 +2759,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", + "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", "license": "MIT" }, "node_modules/cliui": { @@ -2354,55 +2778,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2437,12 +2812,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2557,6 +2926,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2580,9 +2958,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "version": "1.5.430", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", + "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==", "license": "ISC" }, "node_modules/emittery": { @@ -2598,9 +2976,9 @@ } }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/entities": { @@ -2624,6 +3002,12 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2678,12 +3062,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/exit-x": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", @@ -2694,17 +3072,17 @@ } }, "node_modules/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.2.tgz", + "integrity": "sha512-0LNuMvs8/5mRYhnCR4k+lGV7fqPMs6Bnksda4S3zsCUmdxBpAn4zU4NNzXdq1pMKe/H4W5t/zVIDSJ/H3e0vDA==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" + "@jest/expect-utils": "30.5.2", + "@jest/get-type": "30.5.0", + "jest-matcher-utils": "30.5.2", + "jest-message-util": "30.5.1", + "jest-mock": "30.5.2", + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2725,6 +3103,23 @@ "bser": "2.1.1" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2754,24 +3149,16 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/gensync": { @@ -2814,21 +3201,17 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2951,29 +3334,21 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2992,6 +3367,18 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3042,9 +3429,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3110,15 +3497,15 @@ } }, "node_modules/jest": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", - "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.2.tgz", + "integrity": "sha512-3gnpdBIza8ijCl3iMV9ChE3hSt1+46865qqod863gQtJr1DixOkAU5DoGbi3u+XlRcw++BhZQMu/y4xBdHBvSQ==", "license": "MIT", "dependencies": { - "@jest/core": "30.4.2", - "@jest/types": "30.4.1", + "@jest/core": "30.5.2", + "@jest/types": "30.5.1", "import-local": "^3.2.0", - "jest-cli": "30.4.2" + "jest-cli": "30.5.2" }, "bin": { "jest": "bin/jest.js" @@ -3136,13 +3523,13 @@ } }, "node_modules/jest-changed-files": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", - "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.1.tgz", + "integrity": "sha512-0+bvMM/ENhDI29Z8q1r4HxiDIi4G5tnBmSw4esfPQoj9q8Nik7KIyuxHkTVnnniJQf05SxpGaKDQ+4h28ynGPg==", "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.4.1", + "jest-util": "30.5.1", "p-limit": "^3.1.0" }, "engines": { @@ -3150,28 +3537,28 @@ } }, "node_modules/jest-circus": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", - "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.2.tgz", + "integrity": "sha512-vffFgjs5JSJ9q4ds1vNW3klKYPJfYfIY61LT/zaZgvTeASJOWZUt8LBfHgIcV4rwxlPBLl/ePBGccHEclS4PlQ==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", + "@jest/environment": "30.5.2", + "@jest/expect": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", + "jest-each": "30.5.2", + "jest-matcher-utils": "30.5.2", + "jest-message-util": "30.5.1", + "jest-runtime": "30.5.2", + "jest-snapshot": "30.5.2", + "jest-util": "30.5.1", "p-limit": "^3.1.0", - "pretty-format": "30.4.1", + "pretty-format": "30.5.1", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -3193,35 +3580,35 @@ } }, "node_modules/jest-circus/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-cli": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", - "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.2.tgz", + "integrity": "sha512-YYYxUUVjFgPvgEleKNKMaYGIFgR3l3832Cl0kRL/oCiDZ03v7wImUtquHl2OOBCCNgprjZgBN5bczS/JklPBDg==", "license": "MIT", "dependencies": { - "@jest/core": "30.4.2", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", + "@jest/core": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/types": "30.5.1", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", + "jest-config": "30.5.2", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", "yargs": "^17.7.2" }, "bin": { @@ -3240,32 +3627,32 @@ } }, "node_modules/jest-config": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", - "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.2.tgz", + "integrity": "sha512-U6221OZekabePvGcGPf4raIBKMvYQWFHvhobDJRojOYaFMj2HaGytAdknjDhYc6JTz2fdHW9+6k0wWUDaNeOFQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.4.0", - "@jest/test-sequencer": "30.4.1", - "@jest/types": "30.4.1", - "babel-jest": "30.4.1", + "@jest/get-type": "30.5.0", + "@jest/pattern": "30.5.0", + "@jest/test-sequencer": "30.5.2", + "@jest/types": "30.5.1", + "babel-jest": "30.5.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", - "glob": "^10.5.0", + "glob": "^13.0.6", "graceful-fs": "^4.2.11", - "jest-circus": "30.4.2", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-runner": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", + "jest-circus": "30.5.2", + "jest-docblock": "30.5.0", + "jest-environment-node": "30.5.2", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.1", + "jest-runner": "30.5.2", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", "parse-json": "^5.2.0", - "pretty-format": "30.4.1", + "pretty-format": "30.5.1", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -3302,30 +3689,30 @@ } }, "node_modules/jest-config/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-diff": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", - "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.2.tgz", + "integrity": "sha512-rBtHnI6BWTiWj3lM7fOLVfChx7R9B0u9VV3PRHawKs79x6ZjW1QoSrKenaigNGWlvhtoi2BqvDbABDie+Os3xQ==", "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.4.0", - "@jest/get-type": "30.1.0", + "@jest/diff-sequences": "30.5.0", + "@jest/get-type": "30.5.0", "chalk": "^4.1.2", - "pretty-format": "30.4.1" + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3344,24 +3731,24 @@ } }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", - "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.5.0.tgz", + "integrity": "sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==", "license": "MIT", "dependencies": { "detect-newline": "^3.1.0" @@ -3371,16 +3758,17 @@ } }, "node_modules/jest-each": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", - "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.2.tgz", + "integrity": "sha512-GRrW+7fmmgmkvyfEBqc1QFdWnF1Ex5yG6Y6AWL4TTxnkWlKIT98dsKh3P3UuWm6v4XsQwVE6DEzXIPMzWJ3ZbA==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", + "@jest/get-type": "30.5.0", + "@jest/types": "30.5.1", "chalk": "^4.1.2", - "jest-util": "30.4.1", - "pretty-format": "30.4.1" + "jest-regex-util": "30.5.0", + "jest-util": "30.5.1", + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3399,28 +3787,29 @@ } }, "node_modules/jest-each/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-jsdom": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.4.1.tgz", - "integrity": "sha512-o3nfaN4zej7qgk2X0j8Jhq/S9nAVKs2xK3QeQxeHVvpkEPxaA1yxDGydR+iVI7zPy7Cp62Aq2h3Ja46QvfWHGA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.5.2.tgz", + "integrity": "sha512-6NOZjEP+WNi4WXjbxb/1ROnfaMxn6Dhqu6uSC22o0H28z+uppejs/jZ9KCVoLepqqoCjcNZujtzQqC9jSV15jg==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/environment-jsdom-abstract": "30.4.1", + "@jest/environment": "30.5.2", + "@jest/environment-jsdom-abstract": "30.5.2", + "@types/jsdom": "^21.1.7", "jsdom": "^26.1.0" }, "engines": { @@ -3436,55 +3825,53 @@ } }, "node_modules/jest-environment-node": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", - "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.2.tgz", + "integrity": "sha512-Ciz4KgTGIbojxSKpuFImgWJj5EyqfE8xMq9NxWmOXnQywHHS03H9Mv+vaCB29J5pr3JBrY27WbRV18rBdjh8Dg==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", + "@jest/environment": "30.5.2", + "@jest/fake-timers": "30.5.2", + "@jest/types": "30.5.1", "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1" + "jest-mock": "30.5.2", + "jest-util": "30.5.1", + "jest-validate": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", - "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.1.tgz", + "integrity": "sha512-VIFgt67jW480YDxKfEv9IYQKrFpYt7bOCMn3VsnjK7AK9qo7p6acpnA1DHwsVOULE3dTaYew/6JaTD/d0VDHoQ==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.1", + "@parcel/watcher": "^2.6.0", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", + "fdir": "^6.5.0", "graceful-fs": "^4.2.11", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "picomatch": "^4.0.3", - "walker": "^1.0.8" + "jest-regex-util": "30.5.0", + "jest-util": "30.5.1", + "jest-worker": "30.5.1", + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" } }, "node_modules/jest-leak-detector": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", - "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.1.tgz", + "integrity": "sha512-gt4GT2aWgEoCNTcBe4rqS74xTIUJxi+UD9SNSu9aOk5LmTEd6fS5KSTmAKAfitCCktQHNN+upUuL6EkBfFbMDQ==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "pretty-format": "30.4.1" + "@jest/get-type": "30.5.0", + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3503,30 +3890,30 @@ } }, "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", - "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.2.tgz", + "integrity": "sha512-lFkB365PTIHOhPzwrb9T6gvlDnPpOZ7/c3ewOKKB7bzztqjVrtlyRL2MkyfJXpyY2u2SKw001JKrTx14fBUFcQ==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", + "@jest/get-type": "30.5.0", "chalk": "^4.1.2", - "jest-diff": "30.4.1", - "pretty-format": "30.4.1" + "jest-diff": "30.5.2", + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3545,34 +3932,34 @@ } }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", - "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz", + "integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.4.1", + "@jest/types": "30.5.1", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-util": "30.4.1", + "jest-util": "30.5.1", "picomatch": "^4.0.3", - "pretty-format": "30.4.1", + "pretty-format": "30.5.1", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -3593,151 +3980,135 @@ } }, "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-mock": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", - "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.2.tgz", + "integrity": "sha512-KTHb37Fhj8xY8qPFvTFaFJHcdsumQp+ExsUlgoX232Agmc706gagBs53+CYvulV7MVQo9AgXOUxsqzzVBoJlrA==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/expect-utils": "30.5.2", + "@jest/types": "30.5.1", "@types/node": "*", - "jest-util": "30.4.1" + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, "node_modules/jest-regex-util": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", - "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz", + "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==", "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", - "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.1.tgz", + "integrity": "sha512-wprhLejRtwN6h8ZgaqC0eYjGJ1uMdGPT/+b3eFncbG4NWuuP9QL+vWwRinu9waw7hkOLcpMJGVJAMgBwsPssMQ==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", + "jest-haste-map": "30.5.1", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" + "unrs-resolver": "^1.12.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", - "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.2.tgz", + "integrity": "sha512-GMI0DP5enX9Z2qW2zFy1bOkrCfWAOPRJQ7qHt0pdfrxBPsgWxglMPrRSK2TX/wRWYxcPRSLFg3Z88qPOzHx7FQ==", "license": "MIT", "dependencies": { - "jest-regex-util": "30.4.0", - "jest-snapshot": "30.4.1" + "jest-regex-util": "30.5.0", + "jest-snapshot": "30.5.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", - "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.2.tgz", + "integrity": "sha512-/B6px7hiiNhVSRwuv9AGn0nawYvwsHeQZ3ItTd5Gp2diFd0EOKaWVeWqcbw3L88vx1GfpCKjQkxWItZCTl11Rw==", "license": "MIT", "dependencies": { - "@jest/console": "30.4.1", - "@jest/environment": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", + "@jest/console": "30.5.2", + "@jest/environment": "30.5.2", + "@jest/source-map": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-haste-map": "30.4.1", - "jest-leak-detector": "30.4.1", - "jest-message-util": "30.4.1", - "jest-resolve": "30.4.1", - "jest-runtime": "30.4.2", - "jest-util": "30.4.1", - "jest-watcher": "30.4.1", - "jest-worker": "30.4.1", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" + "jest-docblock": "30.5.0", + "jest-environment-node": "30.5.2", + "jest-haste-map": "30.5.1", + "jest-leak-detector": "30.5.1", + "jest-message-util": "30.5.1", + "jest-resolve": "30.5.1", + "jest-runtime": "30.5.2", + "jest-util": "30.5.1", + "jest-watcher": "30.5.2", + "jest-worker": "30.5.1", + "p-limit": "^3.1.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runtime": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", - "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/globals": "30.4.1", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.2.tgz", + "integrity": "sha512-0paUAHBlheai10A3XSy9Xh+YBG0IROy+/qzcDPzQ/nbbXrznQzeWy5qniICWK6d3W3scaRQCVAbXdcK0ibBq4w==", + "license": "MIT", + "dependencies": { + "@jest/environment": "30.5.2", + "@jest/fake-timers": "30.5.2", + "@jest/globals": "30.5.2", + "@jest/source-map": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", + "cjs-module-lexer": "^2.2.0", "collect-v8-coverage": "^1.0.2", - "glob": "^10.5.0", + "es-module-lexer": "^2.1.0", + "glob": "^13.0.6", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", + "jest-haste-map": "30.5.1", + "jest-message-util": "30.5.1", + "jest-mock": "30.5.2", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.1", + "jest-snapshot": "30.5.2", + "jest-util": "30.5.1", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -3746,9 +4117,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", - "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.2.tgz", + "integrity": "sha512-jKIdes25AnAH73dOCi2qE6J6vHJ6L2vyBS11M89kT50DVJcLTf2AOjvcc+2vVaN8lUbY5v2R83aqXo3Ro9fqYA==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", @@ -3756,20 +4127,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", + "@jest/expect-utils": "30.5.2", + "@jest/get-type": "30.5.0", + "@jest/snapshot-utils": "30.5.1", + "@jest/transform": "30.5.2", + "@jest/types": "30.5.1", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.4.1", + "expect": "30.5.2", "graceful-fs": "^4.2.11", - "jest-diff": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "pretty-format": "30.4.1", + "jest-diff": "30.5.2", + "jest-matcher-utils": "30.5.2", + "jest-message-util": "30.5.1", + "jest-util": "30.5.1", + "pretty-format": "30.5.1", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -3790,24 +4161,24 @@ } }, "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3817,12 +4188,12 @@ } }, "node_modules/jest-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", - "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz", + "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -3834,17 +4205,17 @@ } }, "node_modules/jest-validate": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", - "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.1.tgz", + "integrity": "sha512-i/buJ56wTpxihE93hQYNMfdOThS87+HGvLZzZJmU4xggPcdTYQq051iwALLCHp3q+SkCGH+EFejQZz5EbBSkkg==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", + "@jest/get-type": "30.5.0", + "@jest/types": "30.5.1", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.4.1" + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3875,33 +4246,33 @@ } }, "node_modules/jest-validate/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-watcher": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", - "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.2.tgz", + "integrity": "sha512-rqRgg4R11GA9m1UsEZwQeixHsgZReCn6TOHAHtIH33yXrIvoX0OGPA20Yzzjkz7X40fZvcRSxEpQppYPcWzmcQ==", "license": "MIT", "dependencies": { - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", + "@jest/test-result": "30.5.2", + "@jest/types": "30.5.1", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.4.1", + "jest-util": "30.5.1", "string-length": "^4.0.2" }, "engines": { @@ -3909,14 +4280,14 @@ } }, "node_modules/jest-worker": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", - "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.1.tgz", + "integrity": "sha512-Cbxh5v7AoLuFRmFJSM4/aHdQ68rjXvUWr716EE0Dh3I7T+T/3FgFKhOERGXHcU2Meftq9+9zxPM3TSNyI9D+HA==", "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.4.1", + "jest-util": "30.5.1", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -3946,9 +4317,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -4101,9 +4472,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4112,15 +4483,6 @@ "node": ">=10" } }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4146,15 +4508,15 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4196,6 +4558,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4203,10 +4571,13 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "license": "MIT" + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -4230,20 +4601,11 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", "license": "MIT" }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -4355,15 +4717,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4374,26 +4727,29 @@ } }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/picocolors": { "version": "1.1.1", @@ -4519,20 +4875,6 @@ "license": "MIT", "peer": true }, - "node_modules/react-is-18": { - "name": "react-is", - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "node_modules/react-is-19": { - "name": "react-is", - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", - "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", - "license": "MIT" - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4640,16 +4982,10 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", @@ -4660,25 +4996,6 @@ "node": ">=8" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4710,33 +5027,18 @@ "node": ">=10" } }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { @@ -4754,13 +5056,7 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -4772,21 +5068,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", @@ -4800,18 +5081,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -4873,12 +5142,12 @@ "license": "MIT" }, "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.9" + "@pkgr/core": "^0.3.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -4888,60 +5157,90 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^10.2.2" }, "engines": { - "node": ">=8" + "node": ">=18" } }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/tldts": { @@ -4962,12 +5261,6 @@ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "license": "MIT" }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "license": "BSD-3-Clause" - }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -5063,9 +5356,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "funding": [ { "type": "opencollective", @@ -5118,15 +5411,6 @@ "node": ">=18" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -5187,17 +5471,17 @@ } }, "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -5221,56 +5505,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/write-file-atomic": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", @@ -5284,10 +5518,22 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -5336,9 +5582,9 @@ "license": "ISC" }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -5362,38 +5608,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/tests/React/package.json b/tests/React/package.json index 1099726075..e6c36afa4c 100644 --- a/tests/React/package.json +++ b/tests/React/package.json @@ -9,10 +9,10 @@ "dependencies": { "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-react-jsx": "^7.29.7", - "@fluentui/react": "^8.125.7", + "@fluentui/react": "^8.125.10", "@testing-library/jest-dom": "^7.0.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.4", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.8", "jest": "^30", "jest-environment-jsdom": "^30", "react": "^18", diff --git a/tests/Rust/Cargo.toml b/tests/Rust/Cargo.toml index 3487151d1d..519c99b520 100644 --- a/tests/Rust/Cargo.toml +++ b/tests/Rust/Cargo.toml @@ -3,11 +3,18 @@ name = "fable_tests_rust" version = "0.1.0" edition = "2024" +[lib] +name = "fable_tests_rust" +path = "tests/src/lib.rs" + [features] +lrc_ptr = ["fable_library_rust/lrc_ptr"] no_std = ["fable_library_rust/no_std"] +reflection = ["fable_library_rust/reflection"] static_do_bindings = ["fable_library_rust/static_do_bindings"] threaded = ["fable_library_rust/threaded"] # default = ["no_std"] # Uncomment to default to "no_std" mode +# default = ["reflection"] # Uncomment to default to "reflection" mode # default = ["threaded"] # Uncomment to default to "threaded" mode [dependencies] diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 7264061dca..74b5b52f70 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -1,5 +1,10 @@ + Exe + + false + false + false net10.0 Major false @@ -7,9 +12,10 @@ Preview - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -69,6 +75,7 @@ + @@ -76,14 +83,14 @@ - + - - + + diff --git a/tests/Rust/tests/src/ArithmeticTests.fs b/tests/Rust/tests/src/ArithmeticTests.fs index 3acf821fe8..c4a867b786 100644 --- a/tests/Rust/tests/src/ArithmeticTests.fs +++ b/tests/Rust/tests/src/ArithmeticTests.fs @@ -1,6 +1,7 @@ module Fable.Tests.ArithmeticTests open System +open System.Globalization open Util.Testing let [] posLiteral = 5 @@ -173,17 +174,6 @@ let ``Decimal literals can be generated`` () = 79228162514264337593543950335M |> equal Decimal.MaxValue -79228162514264337593543950335M |> equal Decimal.MinValue -[] -let ``Decimal integer predicates work`` () = - System.Decimal.IsInteger(3M) |> equal true - System.Decimal.IsInteger(3.5M) |> equal false - System.Decimal.IsEvenInteger(4M) |> equal true - System.Decimal.IsEvenInteger(3M) |> equal false - System.Decimal.IsOddInteger(3M) |> equal true - System.Decimal.IsOddInteger(4M) |> equal false - System.Decimal.IsOddInteger(-3M) |> equal true - System.Decimal.IsOddInteger(3.5M) |> equal false - [] let ``Decimal.IsCanonical works`` () = // Canonical means the value's stored scale is already minimal (no representable @@ -197,6 +187,108 @@ let ``Decimal.IsCanonical works`` () = System.Decimal.IsCanonical(1.20M) |> equal false System.Decimal.IsCanonical(0.0M) |> equal false +[] +let ``Decimal.IsInteger works`` () = + System.Decimal.IsInteger(3M) |> equal true + System.Decimal.IsInteger(3.5M) |> equal false + System.Decimal.IsInteger(-3M) |> equal true + System.Decimal.IsInteger(-3.5M) |> equal false + +[] +let ``Decimal.IsEvenInteger works`` () = + System.Decimal.IsEvenInteger(3M) |> equal false + System.Decimal.IsEvenInteger(4M) |> equal true + System.Decimal.IsEvenInteger(-3M) |> equal false + System.Decimal.IsEvenInteger(-4M) |> equal true + System.Decimal.IsEvenInteger(3.5M) |> equal false + System.Decimal.IsEvenInteger(-3.5M) |> equal false + System.Decimal.IsEvenInteger(4.00M) |> equal true + System.Decimal.IsEvenInteger(-3.00M) |> equal false + System.Decimal.IsEvenInteger(Decimal.MaxValue) |> equal false + +[] +let ``Decimal.IsOddInteger works`` () = + System.Decimal.IsOddInteger(3M) |> equal true + System.Decimal.IsOddInteger(4M) |> equal false + System.Decimal.IsOddInteger(-3M) |> equal true + System.Decimal.IsOddInteger(-4M) |> equal false + System.Decimal.IsOddInteger(3.5M) |> equal false + System.Decimal.IsOddInteger(-3.5M) |> equal false + System.Decimal.IsOddInteger(-3.00M) |> equal true + System.Decimal.IsOddInteger(4.00M) |> equal false + System.Decimal.IsOddInteger(Decimal.MinValue) |> equal true + +[] +let ``Decimal.IsNegative works`` () = + System.Decimal.IsNegative(-1M) |> equal true + System.Decimal.IsNegative(1M) |> equal false + +[] +let ``Decimal.IsPositive works`` () = + System.Decimal.IsPositive(1M) |> equal true + System.Decimal.IsPositive(-1M) |> equal false + +[] +let ``Decimal.Scale works`` () = + 1M.Scale |> equal 0uy + 1.20M.Scale |> equal 2uy + +[] +let ``Decimal.CopySign works`` () = + Decimal.CopySign(1M, 2M) |> equal 1M + Decimal.CopySign(-1M, 2M) |> equal 1M + Decimal.CopySign(1M, -2M) |> equal -1M + Decimal.CopySign(-1M, -2M) |> equal -1M + +[] +let ``Decimal.FromOACurrency works`` () = + Decimal.FromOACurrency(0L) |> equal 0M + Decimal.FromOACurrency(1L) |> equal 0.0001M + Decimal.FromOACurrency(100000L) |> equal 10M + Decimal.FromOACurrency(100000000000L) |> equal 10000000M + Decimal.FromOACurrency(1000000000000000000L) |> equal 100000000000000M + Decimal.FromOACurrency(1000000000000000001L) |> equal 100000000000000.0001M + Decimal.FromOACurrency(Int64.MaxValue) |> equal 922337203685477.5807M + Decimal.FromOACurrency(Int64.MinValue) |> equal -922337203685477.5808M + Decimal.FromOACurrency(123456L) |> equal 12.3456M + Decimal.FromOACurrency(123456789L) |> equal 12345.6789M + Decimal.FromOACurrency(1234567890000L) |> equal 123456789M + Decimal.FromOACurrency(1234567890987654321L) |> equal 123456789098765.4321M + Decimal.FromOACurrency(4294967295L) |> equal 429496.7295M + +[] +let ``Decimal.ToOACurrency works`` () = + Decimal.ToOACurrency(0M) |> equal 0L + Decimal.ToOACurrency(1M) |> equal 10000L + Decimal.ToOACurrency(1.0000000000000000000000000000M) |> equal 10000L + Decimal.ToOACurrency(100000000000000M) |> equal 1000000000000000000L + Decimal.ToOACurrency(100000000000000.00000000000000M) |> equal 1000000000000000000L + throwsAnyError (fun () -> Decimal.ToOACurrency(10000000000000000000000000000M) |> ignore) + Decimal.ToOACurrency(0.000000000123456789M) |> equal 0L + Decimal.ToOACurrency(0.123456789M) |> equal 1235L + Decimal.ToOACurrency(12.3456M) |> equal 123456L + Decimal.ToOACurrency(123456789M) |> equal 1234567890000L + throwsAnyError (fun () -> Decimal.ToOACurrency(123456789000000000M) |> ignore) + Decimal.ToOACurrency(4294967295M) |> equal 42949672950000L + throwsAnyError (fun () -> Decimal.ToOACurrency(18446744073709551615M) |> ignore) + Decimal.ToOACurrency(-79.228162514264337593543950335M) |> equal -792282L + Decimal.ToOACurrency(-79228162514264.337593543950335M) |> equal -792281625142643376L + +[] +let ``Decimal.GetTypeCode works`` () = + 1M.GetTypeCode() |> equal TypeCode.Decimal + +[] +let ``Decimal.Abs works`` () = + Decimal.Abs(4M) |> equal 4M + Decimal.Abs(-4M) |> equal 4M + +[] +let ``Decimal.Sign works`` () = + Decimal.Sign(-1M) |> equal -1 + Decimal.Sign(0M) |> equal 0 + Decimal.Sign(1M) |> equal 1 + [] let ``Decimal.ToString works`` () = string 001.23456M |> equal "1.23456" @@ -309,6 +401,40 @@ let ``Decimal GetBits works`` () = d2 |> equal d d3 |> equal -d +[] +let ``Decimal bits preserve negative sign and scale`` () = + for scale in [0uy; 1uy; 28uy] do + let value = Decimal(123, 0, 0, true, scale) + let bits = Decimal.GetBits(value) + bits[3] |> equal (Int32.MinValue ||| (int scale <<< 16)) + Decimal.GetBits(Decimal(bits)) |> equal bits + throwsAnyError (fun () -> Decimal([| 0; 0; 0; 1 |]) |> ignore) + throwsAnyError (fun () -> Decimal(0, 0, 0, false, 29uy) |> ignore) + Decimal.GetBits(Decimal.Negate(0.00M))[3] |> equal (Int32.MinValue ||| (2 <<< 16)) + Decimal.GetBits(Decimal(0, 0, 0, true, 2uy))[3] |> equal (Int32.MinValue ||| (2 <<< 16)) + Decimal.GetBits(Decimal([| 0; 0; 0; Int32.MinValue |]))[3] |> equal Int32.MinValue + throwsAnyError (fun () -> Decimal.Round(1M, -1) |> ignore) + throwsAnyError (fun () -> Decimal.Round(1M, 29, MidpointRounding.ToEven) |> ignore) + throwsAnyError (fun () -> Decimal.Round(1M, 0, enum 5) |> ignore) + Decimal.Round(2.5M, MidpointRounding.AwayFromZero) |> equal 3M + Math.Round(2.5M, MidpointRounding.AwayFromZero) |> equal 3M + Decimal.Round(-2.5M, 0, MidpointRounding.ToNegativeInfinity) |> equal -3M + +[] +let ``Decimal formatting rounds significant digits`` () = + let inv = System.Globalization.CultureInfo.InvariantCulture + (2M).ToString("F0", inv) |> equal "2" + (2M).ToString("E0", inv) |> equal "2E+000" + (2.5M).ToString("F0", inv) |> equal "3" + (2.5M).ToString("E0", inv) |> equal "3E+000" + (-2.5M).ToString("E0", inv) |> equal "-3E+000" + (9.99M).ToString("G1", inv) |> equal "1E+01" + (999.9M).ToString("G3", inv) |> equal "1E+03" + (0.00009999M).ToString("G3", inv) |> equal "0.0001" + (100000M).ToString("G3", inv) |> equal "1E+05" + (2500M).ToString("G1", inv) |> equal "3E+03" + Decimal.MaxValue.ToString("E0", inv) |> equal "8E+028" + [] let ``Decimal abs works`` () = abs -4M |> equal 4M @@ -396,6 +522,344 @@ let ``Decimal floor works`` () = Math.Floor 11.25M |> equal 11.M Math.Floor -11.25M |> equal -12.M +[] +let ``Decimal rounding works for values below 0.1`` () = + Math.Floor (decimal -0.0836) |> equal -1.M + Math.Floor (decimal (sin (-5. * Math.PI / 180.))) |> equal -1.M + Math.Floor (decimal "-0.0836") |> equal -1.M + Math.Floor (decimal "-0.0000123") |> equal -1.M + Math.Floor -0.0836M |> equal -1.M + Math.Floor (decimal 0.0836) |> equal 0.M + Math.Ceiling (decimal 0.0836) |> equal 1.M + Math.Ceiling (decimal "0.0000123") |> equal 1.M + Math.Ceiling (decimal -0.0836) |> equal 0.M + Math.Round (decimal 0.06) |> equal 0.M + Math.Round (decimal 0.0836, 2) |> equal 0.08M + Math.Round (decimal -0.0836, 3) |> equal -0.084M + Math.Truncate (decimal -0.0836) |> equal 0.M + Math.Floor (0.M - 0.0836M) |> equal -1.M + +[] +let ``Decimal values below 0.1 hash the same however they are built`` () = + (decimal -0.0836).GetHashCode() |> equal ((0.M - 0.0836M).GetHashCode()) + (decimal "0.05").GetHashCode() |> equal ((0.1M / 2.M).GetHashCode()) + let set = Collections.Generic.HashSet() + set.Add(decimal -0.0836) |> equal true + set.Add(0.M - 0.0836M) |> equal false + let dict = Collections.Generic.Dictionary() + dict.[decimal "0.05"] <- "a" + dict.ContainsKey(0.1M / 2.M) |> equal true + +[] +let ``Decimal hash ignores trailing zeros and the sign of zero`` () = + (decimal "1.0").GetHashCode() |> equal ((decimal "1").GetHashCode()) + (decimal "0.050").GetHashCode() |> equal ((0.1M / 2.M).GetHashCode()) + (decimal "0.00").GetHashCode() |> equal ((decimal "0").GetHashCode()) + (-(0.M)).GetHashCode() |> equal ((0.M).GetHashCode()) + let set = Collections.Generic.HashSet([ 1.M ]) + set.Contains 1.0M |> equal true + set.Add 0.M |> equal true + set.Add(Math.Truncate -0.0836M) |> equal false + let dict = Collections.Generic.Dictionary() + dict.[0.050M] <- "a" + dict.ContainsKey(0.1M / 2.M) |> equal true + +[] +let ``Decimal parsing rounds to 28 places and keeps the scale of zero`` () = + string (decimal "0.0000000000000000000000000000004") |> equal "0.0000000000000000000000000000" + string (decimal "0.00000000000000000000000000005") |> equal "0.0000000000000000000000000000" + string (decimal "0.00000000000000000000000000015") |> equal "0.0000000000000000000000000002" + string (decimal "0.000000000000000000000000000012") |> equal "0.0000000000000000000000000000" + Decimal.GetBits(decimal "0.0000000000000000000000000000004") |> equal [| 0; 0; 0; 1835008 |] + Decimal.GetBits(decimal "0.00") |> equal [| 0; 0; 0; 131072 |] + string (-(0.00M)) |> equal "0.00" + string (abs 0.00M) |> equal "0.00" + string (Math.Round(0.004M, 2)) |> equal "0.00" + (0.00M).ToString("E3", CultureInfo.InvariantCulture) |> equal "0.000E+000" + (decimal "0.00").GetHashCode() |> equal (Decimal(0, 0, 0, false, 2uy).GetHashCode()) + +[] +let ``Decimal parsing handles precision limits and resets failed results`` () = + string (decimal "1.12345678901234567890123456789") |> equal "1.1234567890123456789012345679" + string (decimal "79228162514264337593543950335.4") |> equal "79228162514264337593543950335" + let mutable result = 1M + Decimal.TryParse("invalid", &result) |> equal false + result |> equal 0M + +[] +let ``Decimal parsing rounds long fractions with carry and at the mantissa limit`` () = + string (decimal ("0." + "9999999" + "9999999" + "9999999" + "9999999" + "5")) + |> equal ("1." + "0000000" + "0000000" + "0000000" + "0000000") + string (decimal ("7922816251426433759354395033.5" + "0000000" + "0000000" + "0000000" + "0000000")) + |> equal "7922816251426433759354395033.5" + Decimal.TryParse("79228162514264337593543950335.5" + "0000000" + "0000000" + "0000000" + "0000000") + |> fst + |> equal false + string (decimal ("000000000000000000000000000000001.25")) |> equal "1.25" + string (decimal ("0.00000000000000000000000000005" + String.replicate 1000 "0" + "1")) + |> equal "0.0000000000000000000000000001" + +[] +let ``Decimal addition and subtraction keep the larger scale`` () = + string (1.50M + 0.5M) |> equal "2.00" + string (1.5M + 0.50M) |> equal "2.00" + string (0.1M + 0.2M) |> equal "0.3" + string (1M + 1M) |> equal "2" + string (0.00M + 0M) |> equal "0.00" + string (0.00M + 1.5M) |> equal "1.50" + string (1.0M + (-1.0M)) |> equal "0.0" + string (0.05M - 0.05M) |> equal "0.00" + string (1.00M - 0.50M) |> equal "0.50" + string (100.5M - 0.5M) |> equal "100.0" + string (1.5M - 0.00M) |> equal "1.50" + +[] +let ``Decimal multiplication adds the scales up to 28 places`` () = + string (2.0M * 3.0M) |> equal "6.00" + string (1.5M * 2M) |> equal "3.0" + string (0.5M * 0.5M) |> equal "0.25" + string (1.50M * 1.0M) |> equal "1.500" + string (10M * 0.1M) |> equal "1.0" + string (1.10M * 1.10M) |> equal "1.2100" + string (0.0M * 5M) |> equal "0.0" + string (0M * 1.50M) |> equal "0.00" + string (pown 1.50M 2) |> equal "2.2500" + string (0.1234567890123456789012345678M * 0.1M) |> equal "0.0123456789012345678901234568" + string (0.0000000000000000000000000025M * 0.1M) |> equal "0.0000000000000000000000000002" + string (0.0000000000000000000000000035M * 0.1M) |> equal "0.0000000000000000000000000004" + string (123456789012345678901234567.89M * 1.1M) |> equal "135802467913580246791358024.68" + +[] +let ``Decimal division keeps the scale of an exact quotient`` () = + string (1.00M / 2M) |> equal "0.50" + string (10M / 2M) |> equal "5" + string (1M / 4M) |> equal "0.25" + string (1.0M / 0.5M) |> equal "2" + string (2M / 1.0M) |> equal "2" + string (5.0M / 2.0M) |> equal "2.5" + string (1.000M / 8M) |> equal "0.125" + string (1.23M / 0.1M) |> equal "12.3" + string (0.0M / 5M) |> equal "0.0" + +[] +let ``Decimal division rounds an inexact quotient half to even to fit a decimal`` () = + string (1M / 3M) |> equal "0.3333333333333333333333333333" + string (2M / 3M) |> equal "0.6666666666666666666666666667" + string (10M / 3M) |> equal "3.3333333333333333333333333333" + string (100M / 3M) |> equal "33.333333333333333333333333333" + string (1M / 30000M) |> equal "0.0000333333333333333333333333" + string (1M / 7M) |> equal "0.1428571428571428571428571429" + string (80M / 9M) |> equal "8.888888888888888888888888889" + string (1000000000000000000000000000M / 3M) |> equal "333333333333333333333333333.33" + string (10000000000000000000000000001M / 2M) |> equal "5000000000000000000000000000.5" + string (0.0000000000000000000000000005M / 2M) |> equal "0.0000000000000000000000000002" + string (0.0000000000000000000000000007M / 2M) |> equal "0.0000000000000000000000000004" + string (0.0000000000000000000000000001M / 2M) |> equal "0" + +[] +let ``Decimal remainder keeps the larger scale`` () = + string (5.0M % 2M) |> equal "1.0" + string (5M % 2.0M) |> equal "1.0" + string (5.5M % 2M) |> equal "1.5" + string (7.00M % 2.5M) |> equal "2.00" + string (-5.0M % 2M) |> equal "-1.0" + string (1.5M % 2.00M) |> equal "1.5" + +[] +let ``Decimal arithmetic and parsing throw on overflow`` () = + (fun () -> Decimal.MaxValue + 1M) |> throwsAnyError + (fun () -> Decimal.MaxValue + 0.5M) |> throwsAnyError + string (Decimal.MaxValue + 0.4M) |> equal "79228162514264337593543950335" + string ((Decimal.MaxValue - 1M) + 1M) |> equal "79228162514264337593543950335" + (fun () -> Decimal.MinValue - 1M) |> throwsAnyError + (fun () -> Decimal.MaxValue * 1.1M) |> throwsAnyError + string (Decimal.MaxValue * 1.0M) |> equal "79228162514264337593543950335" + (fun () -> Decimal.MaxValue / 0.5M) |> throwsAnyError + (fun () -> decimal "1e30") |> throwsAnyError + (fun () -> decimal "79228162514264337593543950336") |> throwsAnyError + (fun () -> decimal "79228162514264337593543950335.5") |> throwsAnyError + string (decimal "79228162514264337593543950335.4") |> equal "79228162514264337593543950335" + (fun () -> decimal 1e30) |> throwsAnyError + +[] +let ``Decimal from float rounds to 15 significant digits`` () = + let inv = CultureInfo.InvariantCulture + string (decimal 0.1) |> equal "0.1" + string (decimal (0.1 + 0.2)) |> equal "0.3" + string (decimal 123456789.123456789) |> equal "123456789.123457" + string (decimal (1.0 / 3.0)) |> equal "0.333333333333333" + string (decimal 123456789012345.6789) |> equal "123456789012346" + string (decimal 123456789012345678.0) |> equal "123456789012346000" + string (decimal 1e20) |> equal "100000000000000000000" + string (decimal 1e28) |> equal "10000000000000000000000000000" + string (decimal 1e-10) |> equal "0.0000000001" + string (decimal 1.5e-28) |> equal "0.0000000000000000000000000002" + string (decimal 5e-29) |> equal "0" + string (decimal 1e-30) |> equal "0" + string (decimal -0.0) |> equal "0" + string (decimal 100000000000001.5) |> equal "100000000000002" + string (decimal 100000000000002.5) |> equal "100000000000002" + (fun () -> decimal 1e29) |> throwsAnyError + (fun () -> decimal nan) |> throwsAnyError + (fun () -> decimal infinity) |> throwsAnyError + +[] +let ``Decimal from float32 rounds to 7 significant digits`` () = + let inv = CultureInfo.InvariantCulture + string (decimal 1.1f) |> equal "1.1" + string (decimal (0.1f + 0.2f)) |> equal "0.3" + string (decimal 16777216.f) |> equal "16777220" + string (decimal 1.23456789f) |> equal "1.234568" + string (decimal 123456.789f) |> equal "123456.8" + string (decimal 1000001.5f) |> equal "1000002" + string (decimal 1000002.5f) |> equal "1000002" + string (decimal 1e-30f) |> equal "0" + (fun () -> decimal 3.4e38f) |> throwsAnyError + +[] +let ``Decimal.Parse follows NumberStyles`` () = + let inv = CultureInfo.InvariantCulture + Decimal.Parse("+5", inv) |> equal 5M + Decimal.Parse(" 5 ", inv) |> equal 5M + Decimal.Parse("5-", inv) |> equal -5M + Decimal.Parse("1,234.5", inv) |> equal 1234.5M + Decimal.Parse("1,23,4", inv) |> equal 1234M + Decimal.Parse(".5", inv) |> equal 0.5M + Decimal.Parse("5.", inv) |> equal 5M + string (Decimal.Parse("-0.00", inv)) |> equal "0.00" + (fun () -> Decimal.Parse("1e3", inv)) |> throwsAnyError + (fun () -> Decimal.Parse(",5", inv)) |> throwsAnyError + (fun () -> Decimal.Parse("1,234.5,6", inv)) |> throwsAnyError + (fun () -> Decimal.Parse("(5)", inv)) |> throwsAnyError + (fun () -> Decimal.Parse("", inv)) |> throwsAnyError + Decimal.Parse("1.5E+2", NumberStyles.Float, inv) |> equal 150M + string (Decimal.Parse("1.50e1", NumberStyles.Float, inv)) |> equal "15.0" + Decimal.Parse("1e3", NumberStyles.Any, inv) |> equal 1000M + Decimal.Parse("(5)", NumberStyles.Any, inv) |> equal -5M + (fun () -> Decimal.Parse("1,234", NumberStyles.Float, inv)) |> throwsAnyError + (fun () -> Decimal.Parse("1e30", NumberStyles.Float, inv)) |> throwsAnyError + let ok1, v1 = Decimal.TryParse("1,5", NumberStyles.Number, inv) + ok1 |> equal true + v1 |> equal 15M + let ok2, _ = Decimal.TryParse("1e3", NumberStyles.Number, inv) + ok2 |> equal false + +[] +let ``Decimal parsing handles grouping currency and extreme exponents`` () = + let inv = CultureInfo.InvariantCulture + Decimal.Parse("1,,2", inv) |> equal 12M + Decimal.Parse("1,", inv) |> equal 1M + Decimal.Parse("1,.2", inv) |> equal 1.2M + string (Decimal.Parse("1e-1001", NumberStyles.Float, inv)) |> equal "0.0000000000000000000000000000" + Decimal.Parse("0e1001", NumberStyles.Float, inv) |> equal 0M + let hugeExponent = String.replicate 1000 "9" + string (Decimal.Parse("1e-" + hugeExponent, NumberStyles.Float, inv)) + |> equal "0.0000000000000000000000000000" + Decimal.TryParse("1e" + hugeExponent, NumberStyles.Float, inv) |> fst |> equal false + Decimal.TryParse("0e" + hugeExponent + "x", NumberStyles.Float, inv) |> fst |> equal false + Decimal.TryParse("0e-" + hugeExponent + "x", NumberStyles.Float, inv) |> fst |> equal false + Decimal.Parse("1" + String.replicate 1001 "0" + "e-1001", NumberStyles.Float, inv) |> equal 1M + Decimal.Parse("0." + String.replicate 1000 "0" + "1e1001", NumberStyles.Float, inv) |> equal 1M + Decimal.Parse("\u00A41", NumberStyles.Any, inv) |> equal 1M + Decimal.Parse("-\u00A41", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("\u00A4-1", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("1\u00A4-", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("\u00A4 1", NumberStyles.Any, inv) |> equal 1M + Decimal.Parse("1 \u00A4", NumberStyles.Any, inv) |> equal 1M + Decimal.Parse("(1 )", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("\u00A4 (1)", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("(1)\u00A4", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("\u00A4 ( 1 )", NumberStyles.Any, inv) |> equal -1M + Decimal.TryParse("( 1)", NumberStyles.Any, inv) |> fst |> equal false + Decimal.TryParse("( \u00A41)", NumberStyles.Any, inv) |> fst |> equal false + Decimal.TryParse("(1-)", NumberStyles.Any, inv) |> fst |> equal false + Decimal.TryParse("\u00A41\u00A4", NumberStyles.Any, inv) |> fst |> equal false + (fun () -> Decimal.Parse("$1", NumberStyles.Any, inv)) |> throwsAnyError + (fun () -> Decimal.Parse("1", NumberStyles.AllowHexSpecifier, inv)) |> throwsAnyError + (fun () -> Decimal.TryParse("1", enum 0x4000, inv) |> ignore) |> throwsAnyError + +[] +let ``Decimal parsing preserves zero sign and validates whitespace`` () = + let inv = CultureInfo.InvariantCulture + for text in [ "-0"; "-0.00"; "-0.00000000000000000000000000001"; "-0e-99"; "(0)"; "0-" ] do + let value = Decimal.Parse(text, NumberStyles.Any, inv) + Decimal.IsNegative(value) |> equal true + Decimal.GetBits(Decimal.Parse("-0.00", inv))[3] |> equal (Int32.MinValue ||| (2 <<< 16)) + Decimal.TryParse("\u00a01\u00a0", NumberStyles.Number, inv) |> fst |> equal false + Decimal.TryParse("\u20031\u2003", NumberStyles.Number, inv) |> fst |> equal false + Decimal.Parse("1 \000\000", NumberStyles.Number, inv) |> equal 1M + Decimal.TryParse("1\000 ", NumberStyles.Number, inv) |> fst |> equal false + +[] +let ``F# decimal on strings follows NumberStyles.Float`` () = + decimal "1e3" |> equal 1000M + string (decimal "1.50e1") |> equal "15.0" + string (decimal "0.0e1") |> equal "0" + decimal "+.5" |> equal 0.5M + decimal "5." |> equal 5M + decimal "5\000" |> equal 5M + string (decimal "1.50") |> equal "1.50" + (fun () -> decimal "1,000") |> throwsAnyError + (fun () -> decimal "5-") |> throwsAnyError + (fun () -> decimal "(5)") |> throwsAnyError + (fun () -> decimal "1e") |> throwsAnyError + (fun () -> decimal (nan: float)) |> throwsAnyError + (fun () -> decimal (infinity: float)) |> throwsAnyError + +[] +let ``Decimal G format keeps the scale without a precision`` () = + let inv = CultureInfo.InvariantCulture + (1.10M).ToString("G", inv) |> equal "1.10" + (0.00M).ToString("G", inv) |> equal "0.00" + (1.10M).ToString("G5", inv) |> equal "1.1" + (123456M).ToString("G3", inv) |> equal "1.23E+05" + (0.0001234M).ToString("G3", inv) |> equal "0.000123" + (2.5M).ToString("G1", inv) |> equal "3" + (1.5M).ToString("G1", inv) |> equal "2" + +[] +let ``Decimal max returns the first of two equal values`` () = + let inv = CultureInfo.InvariantCulture + string (max 1.0M 1.00M) |> equal "1.0" + string (min 1.0M 1.00M) |> equal "1.00" + string (Math.Max(1.0M, 1.00M)) |> equal "1.0" + string (Math.Min(1.0M, 1.00M)) |> equal "1.00" + +[] +let ``Decimal static conversions truncate and check the range`` () = + Decimal.ToInt32 2.9M |> equal 2 + Decimal.ToInt32 -2.9M |> equal -2 + Decimal.ToInt64 -2.9M |> equal -2L + Decimal.ToInt64 9223372036854775807.5M |> equal 9223372036854775807L + Decimal.ToUInt64 18446744073709551615M |> equal 18446744073709551615UL + Decimal.ToInt16 32767.5M |> equal 32767s + Decimal.ToUInt16 65535.9M |> equal 65535us + Decimal.ToByte 255.9M |> equal 255uy + Decimal.ToSByte -128.9M |> equal -128y + Decimal.ToUInt32 4294967295.9M |> equal 4294967295u + Decimal.ToDouble 0.1M |> equal 0.1 + Decimal.ToSingle 1.5M |> equal 1.5f + (fun () -> Decimal.ToInt32 2147483648M) |> throwsAnyError + (fun () -> Decimal.ToByte -1M) |> throwsAnyError + Decimal.ToUInt64 -0.5M |> equal 0UL + +[] +let ``Decimal rounding keeps the requested scale`` () = + string (Math.Round(1.20001M, 2)) |> equal "1.20" + string (Math.Round(1.2M, 3)) |> equal "1.2" + string (Math.Round(1.99M, 1)) |> equal "2.0" + string (Math.Floor 1.50M) |> equal "1" + string (Math.Ceiling 1.50M) |> equal "2" + string (Math.Truncate 1.50M) |> equal "1" + +[] +let ``Decimal rounding is half to even when a trailing zero follows the 5`` () = + Math.Round 2.50M |> equal 2.M + Math.Round 0.50M |> equal 0.M + Math.Round(1.250M, 1) |> equal 1.2M + Math.Round(0.050M, 1) |> equal 0.0M + Math.Round(0.0050M, 2) |> equal 0.00M + [] let ``Decimal pown works`` () = pown 2.2M 3 |> equal 10.648M @@ -615,14 +1079,14 @@ let ``Double comparison works`` () = // [] // let ``Double.Parse works with IFormatProvider`` () = // // culture compiles to { } for now and it is ignored on the call-site -// let culture = Globalization.CultureInfo.InvariantCulture +// let culture = CultureInfo.InvariantCulture // let result = Double.Parse("10.5", culture) // result |> equal 10.5 // [] // let ``Single.Parse works with IFormatProvider`` () = // // culture compiles to { } for now and it is ignored on the call-site -// let culture = Globalization.CultureInfo.InvariantCulture +// let culture = CultureInfo.InvariantCulture // let result = Single.Parse("10.5", culture) // float result |> equal 10.5 diff --git a/tests/Rust/tests/src/ArrayTests.fs b/tests/Rust/tests/src/ArrayTests.fs index ed8d8830b4..ec2404f8c0 100644 --- a/tests/Rust/tests/src/ArrayTests.fs +++ b/tests/Rust/tests/src/ArrayTests.fs @@ -989,6 +989,14 @@ let ``Array.toSeq works`` () = ys |> Seq.head |> equal 1. +[] +let ``Arrays can be enumerated`` () = + let xs = [|1.; 2.; 3.; 4.|] + let mutable total = 0 + for x in (xs :> seq<_>) do + total <- total + int x + total |> equal 10 + [] let ``Array.truncate works`` () = let xs = [|1.; 2.; 3.; 4.; 5.|] @@ -1444,6 +1452,59 @@ let ``Array.randomSampleBy works`` () = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) +[] +let ``Array.randomShuffleBy matches FSharp.Core`` () = + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + float ((calls * 37) % 100) / 100.0 + Array.randomShuffleBy next [|0 .. 9|] |> equal [|3; 7; 2; 6; 9; 0; 8; 4; 5; 1|] + calls |> equal 9 + +[] +let ``Array.randomSampleBy matches FSharp.Core`` () = + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + Array.randomSampleBy (fun () -> 0.0) 0 ([||]: int[]) |> equal [||] + // Above the max set size it draws again on a repeated index + let values = [|0.0; 0.0; 0.5|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + values[calls - 1] + Array.randomSampleBy next 2 [|0 .. 21|] |> equal [|0; 11|] + calls |> equal 3 + +[] +let ``Array random functions draw from the given Random like FSharp.Core`` () = + let rng = System.Random(42) + let shuffled = Array.randomShuffleWith rng [|0 .. 9|] + Array.randomShuffleWith (System.Random(42)) [|0 .. 9|] |> equal shuffled + let reference = System.Random(42) + for i in 0 .. 8 do reference.Next(i, 10) |> ignore + rng.Next() |> equal (reference.Next()) + let rng = System.Random(42) + let sample = Array.randomSampleWith rng 3 [|0 .. 9|] + Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|] |> equal sample + let reference = System.Random(42) + for i in 0 .. 2 do reference.Next(0, 10 - i) |> ignore + rng.Next() |> equal (reference.Next()) + +[] +let ``Array random functions give FSharp.Core's error messages`` () = + let message = "randomizer returned NaN, should be in range [0.0, 1.0)." + throwsErrorContaining message (fun () -> Array.randomShuffleBy (fun () -> nan) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoiceBy (fun () -> nan) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoicesBy (fun () -> nan) 2 [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomSampleBy (fun () -> nan) 1 [|1; 2|]) + throwsErrorContaining "The input array was empty." (fun () -> Array.randomChoiceWith (System.Random(1)) ([||]: int[])) + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + [] let ``Array.transpose works`` () = // integer array diff --git a/tests/Rust/tests/src/AsyncTests.fs b/tests/Rust/tests/src/AsyncTests.fs index 8cfb295020..2d12073243 100644 --- a/tests/Rust/tests/src/AsyncTests.fs +++ b/tests/Rust/tests/src/AsyncTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.AsyncTests open Util.Testing +open System open System.Threading open System.Threading.Tasks @@ -82,24 +83,44 @@ let ``return! propagates error Result from inner async`` () = Async.RunSynchronously (outer (Ok 99)) |> equal (Ok 99) Async.RunSynchronously (outer (Error "oops")) |> equal (Error "oops") -// [] -// let shouldExecAsParallelStructurallyCorrect () = -// let t = Async.Parallel [ -// async { return 1 } -// async { return 2 } -// ] -// t |> Async.RunSynchronously |> Array.sum |> equal 3 +[] +let shouldExecAsParallelStructurallyCorrect () = + let t = Async.Parallel [ + async { return 1 } + async { return 2 } + ] + t |> Async.RunSynchronously |> Array.sum |> equal 3 -// [] -// let shouldMarshalMutOverAsyncClosureCorrectly () = -// let mutable x = 1 -// let comp = async { -// let! z = async { return 3 } -// x <- x + 1 -// return x + z -// } -// let t = Async.StartAsTask comp -// t.Result |> equal 5 +[] +let shouldExecAsParallelInOrder () = + Async.Parallel [ + async { return 1 } + async { return 2 } + async { return 3 } + ] + |> Async.RunSynchronously + |> Array.toList + |> equal [ 1; 2; 3 ] + +[] +let shouldExecStartImmediateAsTask () = + // Runs on the calling thread instead of queueing onto the thread pool. + async { return 7 } + |> Async.StartImmediateAsTask + |> Async.AwaitTask + |> Async.RunSynchronously + |> equal 7 + +[] +let shouldMarshalMutOverAsyncClosureCorrectly () = + let mutable x = 1 + let comp = async { + let! z = async { return 3 } + x <- x + 1 + return x + z + } + let t = Async.StartAsTask comp + t.Result |> equal 5 [] type ArcRecord = { @@ -131,17 +152,16 @@ let shouldExecuteTask () = } comp.Result |> equal 3 -// TODO: Fix ResumableStateMachine issue on .NET 8.0 -// [] -// let shouldExecuteMutationOnTask () = -// let a = Task.FromResult 0 -// let mutable x = 0 -// let comp = task { -// let! _ = a -// x <- x + 1 -// } -// do comp.Result -// x |> equal 1 +[] +let shouldExecuteMutationOnTask () = + let a = Task.FromResult 0 + let mutable x = 0 + let comp = task { + let! _ = a + x <- x + 1 + } + do comp.Result + x |> equal 1 // [] // let ``should execute mutation on thread unsafe`` () = @@ -210,18 +230,18 @@ let ``Lock should return result`` () = let res = lock o (fun () -> { x = "42"}) res.x |> equal "42" -//[] -// let testShouldMutateAndLock () = -// let o = new System.Object() -// let mutable x = 1 -// let lazyGet = async { return 1 } -// let comp = async { -// let! _ = lazyGet -// lock o (fun () -> x <- x + 1) -// } -// let t = Async.StartAsTask comp -// do t.Result -// x |> equal 2 +[] +let testShouldMutateAndLock () = + let o = { x = "test" } + let mutable x = 1 + let lazyGet = async { return 1 } + let comp = async { + let! _ = lazyGet + lock o (fun () -> x <- x + 1) + } + let t = Async.StartAsTask comp + do t.Result + x |> equal 2 [] let ``Async.Sleep works`` () = @@ -267,35 +287,35 @@ let ``Async.Sleep works`` () = // res := true // }, token) -// let asyncMap f a = async { -// let! a = a -// return f a -// } +let asyncMap (f: int -> unit) (a: Async) : Async = async { + let! a = a + return f a +} -// [] -// let ``Simple async translates without exception`` () = -// async { return () } -// |> Async.StartImmediate +[] +let ``Simple async translates without exception`` () = + async { return () } + |> Async.StartImmediate -// [] -// let ``Async while binding works correctly`` () = -// let mutable result = 0 -// async { -// while result < 10 do -// result <- result + 1 -// } |> Async.StartImmediate -// equal result 10 +[] +let ``Async while binding works correctly`` () = + let mutable result = 0 + async { + while result < 10 do + result <- result + 1 + } |> Async.StartImmediate + equal result 10 -// [] -// let ``Async for binding works correctly`` () = -// let inputs = [|1; 2; 3|] -// let result = ref 0 -// async { -// for inp in inputs do -// result := !result + inp -// } |> Async.StartImmediate -// equal !result 6 +[] +let ``Async for binding works correctly`` () = + let inputs = [|1; 2; 3|] + let result = ref 0 + async { + for inp in inputs do + result := !result + inp + } |> Async.StartImmediate + equal !result 6 // [] // let ``Async exceptions are handled correctly`` () = @@ -310,18 +330,18 @@ let ``Async.Sleep works`` () = // !result // f true + f false |> equal 22 -// [] -// let ``Simple async is executed correctly`` () = -// let result = ref false -// let x = async { return 99 } -// async { -// let! x = x -// let y = 99 -// result := x = y -// } -// //TODO: RunSynchronously would make more sense here but in JS I think this will be ok. -// |> Async.StartImmediate -// equal !result true +[] +let ``Simple async is executed correctly`` () = + let result = ref false + let x = async { return 99 } + async { + let! x = x + let y = 99 + result := x = y + } + //TODO: RunSynchronously would make more sense here but in JS I think this will be ok. + |> Async.StartImmediate + equal !result true // [] // let ``async use statements should dispose of resources when they go out of scope`` () = @@ -340,29 +360,29 @@ let ``Async.Sleep works`` () = // step2ok := !isDisposed // (!step1ok && !step2ok) |> equal true -// [] -// let ``Try _ with _ expressions inside async expressions work the same`` () = -// let result = ref "" -// let throw() : unit = -// raise(exn "Boo!") -// let append(x) = -// result := !result + x -// let innerAsync() = -// async { -// append "b" -// try append "c" -// throw() -// append "1" -// with _ -> append "d" -// append "e" -// } -// async { -// append "a" -// try do! innerAsync() -// with _ -> append "2" -// append "f" -// } |> Async.StartImmediate -// equal !result "abcdef" +[] +let ``Try _ with _ expressions inside async expressions work the same`` () = + let result = ref "" + let throw() : unit = + raise(exn "Boo!") + let append(x) = + result := !result + x + let innerAsync() = + async { + append "b" + try append "c" + throw() + append "1" + with _ -> append "d" + append "e" + } + async { + append "a" + try do! innerAsync() + with _ -> append "2" + append "f" + } |> Async.StartImmediate + equal !result "abcdef" // // Disable this test for dotnet as it's failing too many times in Appveyor // #if FABLE_COMPILER @@ -451,89 +471,89 @@ let ``Async.Sleep works`` () = // } // |> Async.StartImmediate -// [] -// let ``Async.Parallel works`` () = -// async { -// let makeWork i = -// async { -// do! Async.Sleep 200 -// return i -// } -// let res: int[] ref = ref [||] -// let works = [makeWork 1; makeWork 2; makeWork 3] -// async { -// let! x = Async.Parallel works -// res := x -// } |> Async.StartImmediate -// do! Async.Sleep 500 -// !res |> Array.sum |> equal 6 -// } -// |> Async.StartImmediate - -// [] -// let ``Async.Parallel is lazy`` () = -// async { -// let mutable x = 0 -// let add i = -// #if FABLE_COMPILER -// x <- x + i -// #else -// Interlocked.Add(&x, i) |> ignore -// #endif -// let a = Async.Parallel [ -// async { add 1 } -// async { add 2 } -// ] -// do! Async.Sleep 100 -// equal 0 x -// let! _ = a -// equal 3 x -// } -// |> Async.StartImmediate +[] +let ``Async.Parallel works`` () = + async { + let makeWork (i: int): Async = + async { + do! Async.Sleep 200 + return i + } + let res: int[] ref = ref [||] + let works = [makeWork 1; makeWork 2; makeWork 3] + async { + let! x = Async.Parallel works + res := x + } |> Async.StartImmediate + do! Async.Sleep 500 + !res |> Array.sum |> equal 6 + } + |> Async.StartImmediate -// [] -// let ``Async.Sequential works`` () = -// async { -// let mutable _aggregate = 0 +[] +let ``Async.Parallel is lazy`` () = + async { + let mutable x = 0 + let add i = +#if FABLE_COMPILER + x <- x + i +#else + Interlocked.Add(&x, i) |> ignore +#endif + let a = Async.Parallel [ + async { add 1 } + async { add 2 } + ] + do! Async.Sleep 100 + equal 0 x + let! _ = a + equal 3 x + } + |> Async.StartImmediate -// let makeWork i = -// async { -// // check that the individual work items run sequentially and not interleaved -// _aggregate <- _aggregate + i -// let copyOfI = _aggregate -// do! Async.Sleep 100 -// equal copyOfI _aggregate -// do! Async.Sleep 100 -// equal copyOfI _aggregate -// return i -// } -// let works = [ for i in 1 .. 5 -> makeWork i ] -// let now = DateTimeOffset.Now -// let! result = Async.Sequential works -// let ``then`` = DateTimeOffset.Now -// let d = ``then`` - now -// // For some reason this sometimes fail in CI if d is exactly 1s so give it some breadth -// if d < TimeSpan.FromSeconds 0.9 then -// failwithf "expected sequential operations to take longer than 1 second, but took %0.00f" d.TotalSeconds -// result |> equal [| 1 .. 5 |] -// result |> Seq.sum |> equal _aggregate -// } -// |> Async.StartImmediate +[] +let ``Async.Sequential works`` () = + async { + let mutable _aggregate = 0 + + let makeWork i = + async { + // check that the individual work items run sequentially and not interleaved + _aggregate <- _aggregate + i + let copyOfI = _aggregate + do! Async.Sleep 100 + equal copyOfI _aggregate + do! Async.Sleep 100 + equal copyOfI _aggregate + return i + } + let works = [ for i in 1 .. 5 -> makeWork i ] + let now = DateTimeOffset.Now + let! result = Async.Sequential works + let ``then`` = DateTimeOffset.Now + let d = ``then`` - now + // For some reason this sometimes fail in CI if d is exactly 1s so give it some breadth + if d < TimeSpan.FromSeconds 0.9 then + failwithf "expected sequential operations to take longer than 1 second, but took %0.00f" d.TotalSeconds + result |> equal [| 1 .. 5 |] + result |> Seq.sum |> equal _aggregate + } + |> Async.StartImmediate -// [] -// let ``Async.Sequential is lazy`` () = -// async { -// let mutable x = 0 -// let a = Async.Sequential [ -// async { x <- x + 1 } -// async { x <- x + 2 } -// ] -// do! Async.Sleep 100 -// equal 0 x -// let! _ = a -// equal 3 x -// } -// |> Async.StartImmediate +[] +let ``Async.Sequential is lazy`` () = + async { + let mutable x = 0 + let a = Async.Sequential [ + async { x <- x + 1 } + async { x <- x + 2 } + ] + do! Async.Sleep 100 + equal 0 x + let! _ = a + equal 3 x + } + |> Async.StartImmediate // #if FABLE_COMPILER // [] @@ -668,58 +688,58 @@ let ``Async.Sleep works`` () = // } // |> Async.StartImmediate -// [] -// let ``Nested failure propagates in async expressions`` () = -// async { -// let data = ref "" -// let f1 x = -// async { -// try -// failwith "1" -// return x -// with -// | e -> return! failwith ("2 " + e.Message) -// } -// let f2 x = -// async { -// try -// return! f1 x -// with -// | e -> return! failwith ("3 " + e.Message) -// } -// let f() = -// async { -// try -// let! y = f2 4 -// return () -// with -// | e -> data := e.Message -// } -// |> Async.StartImmediate -// f() -// do! Async.Sleep 100 -// equal "3 2 1" !data -// } -// |> Async.StartImmediate +[] +let ``Nested failure propagates in async expressions`` () = + async { + let data = ref "" + let f1 x = + async { + try + failwith "1" + return x + with + | e -> return! failwith ("2 " + e.Message) + } + let f2 x = + async { + try + return! f1 x + with + | e -> return! failwith ("3 " + e.Message) + } + let f() = + async { + try + let! y = f2 4 + return () + with + | e -> data := e.Message + } + |> Async.StartImmediate + f() + do! Async.Sleep 100 + equal "3 2 1" !data + } + |> Async.StartImmediate -// [] -// let ``Try _ finally _ expressions inside async expressions work`` () = -// async { -// let data = ref "" -// async { -// try data := !data + "1 " -// finally data := !data + "2 " -// } |> Async.StartImmediate -// async { -// try -// try failwith "boom!" -// finally data := !data + "3" -// with _ -> () -// } |> Async.StartImmediate -// do! Async.Sleep 100 -// equal "1 2 3" !data -// } -// |> Async.StartImmediate +[] +let ``Try _ finally _ expressions inside async expressions work`` () = + async { + let data = ref "" + async { + try data := !data + "1 " + finally data := !data + "2 " + } |> Async.StartImmediate + async { + try + try failwith "boom!" + finally data := !data + "3" + with _ -> () + } |> Async.StartImmediate + do! Async.Sleep 100 + equal "1 2 3" !data + } + |> Async.StartImmediate // [] // let ``Final statement inside async expressions can throw`` () = @@ -828,17 +848,17 @@ let ``Async.Sleep works`` () = // } // |> Async.StartImmediate -// [] -// let ``Unit arguments are erased`` () = // See #1832 -// let mutable token = 0 -// async { -// let! res = -// async.Return 5 -// |> asyncMap (fun x -> token <- x) -// equal 5 token -// res -// } -// |> Async.StartImmediate +[] +let ``Unit arguments are erased`` () = // See #1832 + let mutable token = 0 + async { + let! res = + async.Return 5 + |> asyncMap (fun x -> token <- x) + equal 5 token + res + } + |> Async.StartImmediate // [] // let ``Can use custom exceptions in async workflows #2396`` () = @@ -934,3 +954,198 @@ let ``Async.Sleep works`` () = // acc > 2 |> equal true // } // |> Async.StartImmediate + +// The async builder's Combine, While, For, TryWith, TryFinally and Using used +// to be missing from the Rust runtime, so any async block with a computation in +// statement position failed to compile with "unresolved import +// AsyncBuilder_::singleton". + +[] +let ``if without else in an async block combines`` () = + let comp = async { + if 1 < 2 then + do! async { return () } + return 42 + } + comp |> Async.RunSynchronously |> equal 42 + +[] +let ``while in an async block loops`` () = + let comp = async { + let mutable i = 0 + while i < 3 do + let! step = async { return 1 } + i <- i + step + return i + } + comp |> Async.RunSynchronously |> equal 3 + +[] +let ``while in an async block can run zero times`` () = + let comp = async { + let mutable i = 10 + while i < 3 do + let! step = async { return 1 } + i <- i + step + return i + } + comp |> Async.RunSynchronously |> equal 10 + +[] +let ``for in an async block iterates`` () = + let comp = async { + let mutable sum = 0 + for x in [1; 2; 3] do + let! y = async { return x * 10 } + sum <- sum + y + return sum + } + comp |> Async.RunSynchronously |> equal 60 + +// Delay used to run its builder immediately, so the `return` half of the block +// was built -- and its value read -- before the loop above it had run. Only +// Combine makes that visible, which is why it went unnoticed. +[] +let ``state written by a loop is visible after it`` () = + let comp = async { + let mutable acc = "" + for x in ["a"; "b"] do + let! y = async { return x } + acc <- acc + y + return acc + } + comp |> Async.RunSynchronously |> equal "ab" + +[] +let ``try with in an async block catches`` () = + let comp = async { + try + let! _ = async { return 1 } + failwith "boom" + return 0 + with _ -> + return -1 + } + comp |> Async.RunSynchronously |> equal -1 + +[] +let ``try with in an async block leaves a success alone`` () = + let comp = async { + try + let! x = async { return 1 } + return x + with _ -> + return -1 + } + comp |> Async.RunSynchronously |> equal 1 + +[] +let ``try finally in an async block runs the finalizer`` () = + let mutable ran = 0 + let comp = async { + try + let! x = async { return 1 } + return x + finally + ran <- ran + 1 + } + comp |> Async.RunSynchronously |> equal 1 + ran |> equal 1 + +[] +let ``try finally in an async block runs the finalizer on failure`` () = + let mutable ran = 0 + let comp = async { + try + let! _ = async { return 1 } + failwith "boom" + return 0 + finally + ran <- ran + 1 + } + throwsAnyError (fun () -> comp |> Async.RunSynchronously) + ran |> equal 1 + +type AsyncDisposable(onDispose: unit -> unit) = + interface System.IDisposable with + member _.Dispose() = onDispose() + +[] +let ``use in an async block disposes the resource`` () = + let mutable isDisposed = false + let comp = async { + use res = new AsyncDisposable(fun () -> isDisposed <- true) + let! x = async { return 1 } + return (if isNull (box res) then 0 else x) + } + comp |> Async.RunSynchronously |> equal 1 + isDisposed |> equal true + +[] +let ``use in an async block disposes the resource when the body fails`` () = + let mutable isDisposed = false + let comp = async { + use res = new AsyncDisposable(fun () -> isDisposed <- true) + let! _ = async { return 1 } + failwith "boom" + return 0 + } + throwsAnyError (fun () -> comp |> Async.RunSynchronously |> ignore) + isDisposed |> equal true + +[] +let ``nested loops in an async block`` () = + let comp = async { + let mutable total = 0 + for i in 1 .. 3 do + let mutable j = 0 + while j < 2 do + let! v = async { return i } + total <- total + v + j <- j + 1 + return total + } + comp |> Async.RunSynchronously |> equal 12 + +[] +let ``a loop body that awaits nothing still runs`` () = + let comp = async { + let mutable i = 0 + while i < 3 do + i <- i + 1 + return i + } + comp |> Async.RunSynchronously |> equal 3 + +[] +let ``try with inside a loop catches each time`` () = + let comp = async { + let mutable caught = 0 + for _ in 1 .. 3 do + try + let! _ = async { return () } + failwith "boom" + with _ -> + caught <- caught + 1 + return caught + } + comp |> Async.RunSynchronously |> equal 3 + +// The inner try/finally makes F# pass Delay a `unit -> Async` lambda whose +// unit parameter is not erased, so the builder arrived as Func1 rather than +// Func0 and this shape did not compile. +[] +let ``try finally nested inside try with`` () = + let comp = async { + let mutable order = "" + try + try + let! _ = async { return () } + failwith "boom" + finally + order <- order + "inner" + with _ -> + order <- order + "-caught" + return order + } + comp |> Async.RunSynchronously |> equal "inner-caught" diff --git a/tests/Rust/tests/src/CharTests.fs b/tests/Rust/tests/src/CharTests.fs index b8de96f1a0..8f9d26a65a 100644 --- a/tests/Rust/tests/src/CharTests.fs +++ b/tests/Rust/tests/src/CharTests.fs @@ -23,6 +23,14 @@ let ``Char.ToUpper with 1-to-many mapping returns original char`` () = let ``Char.ToLower with non-ASCII single mapping works`` () = Char.ToLower('É') |> equal 'é' +[] +let ``Char.ToLower with invariant culture works`` () = + Char.ToLower('É', CultureInfo.InvariantCulture) |> equal 'é' + +[] +let ``Char.ToUpper with invariant culture works`` () = + Char.ToUpper('é', CultureInfo.InvariantCulture) |> equal 'É' + [] let ``Char.ToUpperInvariant works`` () = Char.ToUpperInvariant('b') |> equal 'B' @@ -35,6 +43,18 @@ let ``Char.ToLowerInvariant works`` () = let ``Char.ToString works`` () = Char.ToString('b') |> equal "b" +[] +let ``Char.ToString with format provider works`` () = + 'b'.ToString(CultureInfo.InvariantCulture) |> equal "b" + +[] +let ``Char.GetTypeCode works`` () = + 'b'.GetTypeCode() |> equal TypeCode.Char + +[] +let ``Char.ConvertToUtf32 rejects non-surrogate pairs`` () = + throwsAnyError (fun () -> Char.ConvertToUtf32('a', 'b') |> ignore) + [] let ``Char.GetUnicodeCategory works`` () = Char.GetUnicodeCategory('a') |> equal UnicodeCategory.LowercaseLetter @@ -182,6 +202,20 @@ let ``Char.IsWhitespace works`` () = Char.IsWhiteSpace('\160') |> equal true Char.IsWhiteSpace('-') |> equal false +[] +let ``Char Unicode category predicates work`` () = + Char.IsControl('\u0085') |> equal true + Char.IsDigit('\u0661') |> equal true + Char.IsLetter('Ж') |> equal true + Char.IsLetterOrDigit('\u0661') |> equal true + Char.IsLower('я') |> equal true + Char.IsNumber('\u2167') |> equal true + Char.IsPunctuation('\u201C') |> equal true + Char.IsSeparator('\u2028') |> equal true + Char.IsSymbol('\u20AC') |> equal true + Char.IsUpper('Ж') |> equal true + Char.IsWhiteSpace('\u2007') |> equal true + [] let ``Char.IsWhitespace works with two args`` () = let input = " \r" diff --git a/tests/Rust/tests/src/ControlFlowTests.fs b/tests/Rust/tests/src/ControlFlowTests.fs index a66a6bce17..b44f1ed7ff 100644 --- a/tests/Rust/tests/src/ControlFlowTests.fs +++ b/tests/Rust/tests/src/ControlFlowTests.fs @@ -183,3 +183,145 @@ let ``two-switch or-pattern with obj binding works`` () = // exercises the getZeroObj placeholder for a `dyn Any` binding on the two-switch path orPatObj (OA(box 9)) :? int |> equal true orPatObj (OB(box 11)) :? string |> equal false + +// An active pattern in the first clause of a match F# cannot prove exhaustive. +// F# emits `raise (MatchFailureException(...))` for the fallthrough, and the +// rewrite that normally removes it only fires when the decision expression +// starts with a UnionCaseTest — an active pattern there is not one, so the +// construction survived into codegen and crashed the compiler with +// "The lists had different lengths" and no source location. +type private ApExpr = + | ApColumn of string * string + | ApConst of string + +let private (|IsApConst|_|) e = + match e with + | ApConst c -> Some c + | _ -> None + +let private (|IsApColumn|_|) e = + match e with + | ApColumn(t, n) -> Some(t, n) + | _ -> None + +let private renderUnionAfterAp e = + match e with + | IsApConst c -> c + | ApColumn(t, n) -> t + "." + n + +let private renderApOnly e = + match e with + | IsApConst c -> c + | IsApColumn(t, n) -> t + "." + n + +[] +let ``non-exhaustive match starting with an active pattern works`` () = + renderUnionAfterAp (ApColumn("c", "x")) |> equal "c.x" + renderUnionAfterAp (ApConst "lit") |> equal "lit" + renderApOnly (ApColumn("c", "x")) |> equal "c.x" + renderApOnly (ApConst "lit") |> equal "lit" + +// A fieldless union reached through a record field rather than a plain ident. +// The switch conversion recovers the union type from the ident; without one it +// used to fall back to the tag's int32 and emit a bare integer literal pattern +// against the union value, which does not compile. +type JoinKind = + | InnerJoin + | LeftJoin + +type Join = { Kind: JoinKind; Table: string } + +let private renderJoinKind (j: Join) = + let mutable out = "" + + match j.Kind with + | InnerJoin -> out <- "INNER" + | LeftJoin -> out <- "LEFT" + + out + +[] +let ``fieldless union matched through a record field works`` () = + renderJoinKind { Kind = InnerJoin; Table = "t" } |> equal "INNER" + renderJoinKind { Kind = LeftJoin; Table = "t" } |> equal "LEFT" + +// A guard on a nested pattern makes the whole match take the decision-tree +// path, which pre-declares a mutable binding for every variable in the match +// and initialises it before testing anything. Strings, options, arrays and +// lists are heap-backed, and zero-initialising one is a runtime panic rather +// than a compile error. +type Payload = + | Number of int + | Numbers of int[] + | Names of string list + | Maybe of int option + | Label of string + +let private describePayload (p: Payload) (flag: bool) = + match p with + | Maybe(Some v) when flag -> "guarded " + string v + | Maybe(Some v) -> "some " + string v + | Maybe None -> "none" + | Number n -> "number " + string n + | Numbers xs -> "numbers " + string xs.Length + | Names ns -> "names " + string (List.length ns) + | Label s -> "label " + s + +[] +let ``a guarded nested pattern does not zero-initialise the other bindings`` () = + describePayload (Maybe(Some 1)) true |> equal "guarded 1" + describePayload (Maybe(Some 2)) false |> equal "some 2" + describePayload (Maybe None) false |> equal "none" + describePayload (Number 3) false |> equal "number 3" + describePayload (Numbers [| 1; 2; 3 |]) false |> equal "numbers 3" + describePayload (Names [ "a"; "b" ]) false |> equal "names 2" + describePayload (Label "hi") false |> equal "label hi" + +// A try/with/finally lowers to closures on this target, but the Fable AST has no +// Lambda there, so the capture walk did not treat those bodies as closure +// contexts. A `let mutable` assigned in a finally block was emitted as a bare +// MutCell and the closure mutated a clone, so the write was silently lost. +// +// The exception-free case runs everywhere; the ones below need a `with` that +// actually catches, which no_std's try_catch does not do. +[] +let ``a mutable assigned in a finally block keeps its value`` () = + let mutable ran = false + + let message = + try + "body" + finally + ran <- true + + ran |> equal true + message |> equal "body" + +#if !NO_STD_NO_EXCEPTIONS +[] +let ``a mutable assigned in a finally block survives an exception`` () = + let mutable ran = false + + let message = + try + try + failwith "inner" + finally + ran <- true + with ex -> + ex.Message + + ran |> equal true + message |> equal "inner" + +[] +let ``a mutable assigned in a with handler keeps its value`` () = + let mutable ran = false + + try + failwith "x" + with _ -> + ran <- true + + ran |> equal true +#endif diff --git a/tests/Rust/tests/src/ConvertTests.fs b/tests/Rust/tests/src/ConvertTests.fs index 33f69d37de..2c7fd42ae6 100644 --- a/tests/Rust/tests/src/ConvertTests.fs +++ b/tests/Rust/tests/src/ConvertTests.fs @@ -45,6 +45,28 @@ let ``System.Decimal.TryParse works`` () = Decimal.TryParse "9SayWhat12Huh" |> equal (false, 0.0M) Decimal.TryParse "-1.5" |> equal (true, -1.5M) +[] +let ``System.Decimal.TryParse handles null strings`` () = + let text: string = null + let mutable result = 42M + Decimal.TryParse(text, &result) |> equal false + result |> equal 0M + result <- 42M + Decimal.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, &result) |> equal false + result |> equal 0M + result <- 42M + (fun () -> Decimal.TryParse(text, enum 0x4000, CultureInfo.InvariantCulture, &result) |> ignore) + |> throwsAnyError + result |> equal 42M + +[] +let ``System.Decimal.Parse handles null strings`` () = + let text: string = null + (fun () -> Decimal.Parse(text)) |> throwsAnyError + (fun () -> Decimal.Parse(text, CultureInfo.InvariantCulture)) |> throwsAnyError + (fun () -> decimal text) |> throwsAnyError + decimal "1.50e1" |> equal 15M + [] let ``System.Single.Parse works`` () = Single.Parse "1.5" |> equal 1.5f @@ -767,6 +789,24 @@ let ``System.Convert.ToDecimal works`` () = Convert.ToDecimal(1.m) |> equal x Convert.ToDecimal("1.") |> equal x +[] +let ``System.Convert.ToDecimal string uses NumberStyles.Number`` () = + Convert.ToDecimal("1,000") |> equal 1000M + Convert.ToDecimal("1-") |> equal (-1M) + Convert.ToDecimal("1000,") |> equal 1000M + Convert.ToDecimal("1,,000") |> equal 1000M + Convert.ToDecimal("1,000", CultureInfo.InvariantCulture) |> equal 1000M + (fun () -> Convert.ToDecimal("1e3")) |> throwsAnyError + (fun () -> Convert.ToDecimal(",1")) |> throwsAnyError + +[] +let ``System.Convert.ToDecimal handles null strings`` () = + let text: string = null + Convert.ToDecimal(text) |> equal 0M + Convert.ToDecimal(text, CultureInfo.InvariantCulture) |> equal 0M + Convert.ToDecimal("1,234.50", CultureInfo.InvariantCulture) |> equal 1234.50M + (fun () -> Convert.ToDecimal("invalid")) |> throwsAnyError + //------------------------------------- // Convert.ToString //------------------------------------- @@ -1018,6 +1058,11 @@ let ``Convert.FromHexString works`` () = Convert.FromHexString("fafBFCFDFE") |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] +[] +let ``Convert.FromHexString checks its input`` () = + throwsAnyError (fun () -> Convert.FromHexString("abc")) + throwsAnyError (fun () -> Convert.FromHexString("zz")) + [] let ``Convert.ToBase64String works`` () = let bytes = [| 2uy; 4uy; 6uy; 8uy; 10uy; 12uy; 14uy; 16uy; 18uy; 20uy |] @@ -1584,3 +1629,29 @@ let ``System.Convert.ToFloat from bool works`` () = Convert.ToSingle(false) |> equal 0.f Convert.ToDouble(true) |> equal 1. Convert.ToDouble(false) |> equal 0. + +// The culture-taking overloads used to fail to compile: the provider was passed +// on to runtime functions that have no parameter for it, so Decimal.Parse(s, +// style, culture) reached a one-argument function and d.ToString(culture) put a +// provider where the decimal belongs. Rust formatting and parsing are invariant, +// which is what InvariantCulture asks for, so the argument is dropped. +[] +let ``culture-taking overloads work`` () = + let d = 1234.5678M + d.ToString(System.Globalization.CultureInfo.InvariantCulture) |> equal "1234.5678" + d.ToString() |> equal "1234.5678" + + System.Decimal.Parse( + "1234.5678", + System.Globalization.NumberStyles.Number, + System.Globalization.CultureInfo.InvariantCulture + ) + |> equal 1234.5678M + + System.Decimal.Parse("12.34", System.Globalization.CultureInfo.InvariantCulture) + |> equal 12.34M + + System.DateTime + .Parse("2026-08-27T10:30:00.0000000", System.Globalization.CultureInfo.InvariantCulture) + .ToString("o") + |> equal "2026-08-27T10:30:00.0000000" diff --git a/tests/Rust/tests/src/DateTimeOffsetTests.fs b/tests/Rust/tests/src/DateTimeOffsetTests.fs index e0f13dd684..d95c8c5b67 100644 --- a/tests/Rust/tests/src/DateTimeOffsetTests.fs +++ b/tests/Rust/tests/src/DateTimeOffsetTests.fs @@ -1031,3 +1031,16 @@ let ``-10s offset throws exception`` () = let source = DateTimeOffset(2007, 9, 1, 14, 30, 0, TimeSpan.Zero) (fun () -> source.ToOffset(TimeSpan(0, 0, -10))) |> throwsErrorContaining "Offset must be specified in whole minutes" + +[] +let ``Implicit conversion from DateTime preserves value`` () = + let d = DateTime(2024, 1, 15, 13, 23, 30, DateTimeKind.Utc) + let dto: DateTimeOffset = d + dto |> equal (DateTimeOffset(d)) + dto.Offset |> equal TimeSpan.Zero + dto.Year |> equal 2024 + dto.Month |> equal 1 + dto.Day |> equal 15 + dto.Hour |> equal 13 + dto.Minute |> equal 23 + dto.Second |> equal 30 diff --git a/tests/Rust/tests/src/DictionaryTests.fs b/tests/Rust/tests/src/DictionaryTests.fs index 11458d3126..ccd124d79f 100644 --- a/tests/Rust/tests/src/DictionaryTests.fs +++ b/tests/Rust/tests/src/DictionaryTests.fs @@ -271,6 +271,15 @@ let ``Dictionary.Remove works`` () = dict.Remove(3) |> equal false dict.Count |> equal 1 +[] +let ``Dictionary.Remove with complex keys works`` () = + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 + [] let ``Dictionary.Remove with records as keys works`` () = let x1 = { a = 5 } diff --git a/tests/Rust/tests/src/EventTests.fs b/tests/Rust/tests/src/EventTests.fs index dde1561419..c5ced0c106 100644 --- a/tests/Rust/tests/src/EventTests.fs +++ b/tests/Rust/tests/src/EventTests.fs @@ -2,44 +2,30 @@ module Fable.Tests.EventTests open Util.Testing -// NOTE (Rust): these class-hosted event types are disabled on Rust. They trigger -// with `event.Trigger(this, arg)`, i.e. they pass the enclosing instance (`this`) -// as an event value. The Rust backend cannot recover the reference-counted `this` -// from a `&self` method body (it emits a by-value struct clone of the wrong type), -// and `member Event = event.Publish` exposes the event as `IEvent<_>`, which the -// backend does not coerce to `IObservable<_>` for `.Add`/`.Subscribe`. The plain -// `Event<'T>` module-function tests below (the core of this port) are unaffected. -// These pass on the JS/TS/Python targets. -// type ClassWithCLIEvent() = -// let event = new Event<_>() -// [] -// member _.Event = event.Publish -// member this.TestEvent(arg) = -// event.Trigger(this, arg) -// -// type ClassWithNonCLIEvent() = -// let event = new Event<_>() -// member _.Event = event.Publish -// member this.TestEvent(arg) = -// event.Trigger(this, arg) - -// NOTE (Rust): the two-generic Event<'Delegate,'Args> (FSharpEvent`2, with a -// sender-carrying 2-arg Trigger) and CLI events declared on an *interface* are -// not supported by the Rust runtime port yet. The `Microsoft.FSharp.Control` -// runtime only implements FSharpEvent`1 (single generic, 1-arg Trigger). These -// types and the two tests that use them (below) are therefore disabled on Rust; -// they pass on the JS/TS/Python targets. -// type InterfaceWithCLIEvent<'t> = -// [] -// abstract Event : IEvent,'t> -// -// type ClassWithInterfaceWithCLIEvent<'t>() = -// let event = new Event<_,_>() -// member this.TestEvent(arg) = -// event.Trigger(this, arg) -// interface InterfaceWithCLIEvent<'t> with -// [] -// member _.Event = event.Publish +type ClassWithCLIEvent() = + let event = new Event<_>() + [] + member _.Event = event.Publish + member this.TestEvent(arg) = + event.Trigger(this, arg) + +type ClassWithNonCLIEvent() = + let event = new Event<_>() + member _.Event = event.Publish + member this.TestEvent(arg) = + event.Trigger(this, arg) + +type InterfaceWithCLIEvent<'t> = + [] + abstract Event : IEvent,'t> + +type ClassWithInterfaceWithCLIEvent<'t>() = + let event = new Event<_,_>() + member this.TestEvent(arg) = + event.Trigger(this, arg) + interface InterfaceWithCLIEvent<'t> with + [] + member _.Event = event.Publish module tests = @@ -197,6 +183,60 @@ module tests = source.Trigger 6 equal 6 result + [] + let ``IEvent handlers receive a null sender`` () = + let mutable senderWasNull = false + let source = Event<_>() + source.Publish.AddHandler(new Handler<_>(fun s _ -> senderWasNull <- obj.ReferenceEquals(s, null))) + |> ignore + + source.Trigger 6 + senderWasNull |> equal true + + [] + let ``Classes can trigger CLI events on interfaces`` () = + let mutable result = 0 + let mutable senderWasReceived = false + let classWithEvent = new ClassWithInterfaceWithCLIEvent<_>() + (classWithEvent :> InterfaceWithCLIEvent<_>).Event.AddHandler(fun s x -> + senderWasReceived <- not (obj.ReferenceEquals(s, null)) + result <- x + ) + classWithEvent.TestEvent(5) + equal 5 result + equal true senderWasReceived + + [] + let ``Generic interface expression can have CLI events`` () = + let mutable actualSender = "" + let mutable result = false + let event = Event<_,_>() + let ifaceWithEvent = + { new InterfaceWithCLIEvent<_> with + [] + member _.Event = event.Publish } + ifaceWithEvent.Event.AddHandler(fun sender arg -> + actualSender <- unbox sender + result <- arg) + let expectedSender = "SENDER" + let expectedResult = true + event.Trigger(expectedSender, expectedResult) + equal expectedSender actualSender + equal expectedResult result + + [] + let ``Two-generic event preserves its sender`` () = + let mutable actualSender = "" + let mutable actualValue = false + let source: Event, bool> = Event<_, _>() + source.Publish.AddHandler(fun sender value -> + actualSender <- unbox sender + actualValue <- value) + + source.Trigger("SENDER", true) + equal "SENDER" actualSender + equal true actualValue + [] let ``IEvent.RemoveHandler works`` () = let mutable result = 0 @@ -210,26 +250,23 @@ module tests = source.Trigger 6 equal 0 result - // NOTE (Rust): disabled — see the ClassWithCLIEvent / ClassWithNonCLIEvent - // note above (passing `this` as an event value + IEvent->IObservable coercion - // for `.Add`/`.Subscribe`). These pass on JS/TS/Python. - // [] - // let ``Classes can trigger CLI events`` () = - // let mutable result = 0 - // let classWithEvent = new ClassWithCLIEvent() - // classWithEvent.Event.Add(fun (sender, x) -> result <- x) - // classWithEvent.TestEvent(5) - // equal 5 result - // - // [] - // let ``Classes can trigger non-CLI events`` () = - // let mutable result = "" - // let classWithEvent = new ClassWithNonCLIEvent() - // let disp = classWithEvent.Event.Subscribe(fun (sender, arg) -> result <- arg) - // classWithEvent.TestEvent("Hello") - // disp.Dispose() - // classWithEvent.TestEvent("Bye") - // equal "Hello" result + [] + let ``Classes can trigger CLI events`` () = + let mutable result = 0 + let classWithEvent = new ClassWithCLIEvent() + classWithEvent.Event.Add(fun (sender, x) -> result <- x) + classWithEvent.TestEvent(5) + equal 5 result + + [] + let ``Classes can trigger non-CLI events`` () = + let mutable result = "" + let classWithEvent = new ClassWithNonCLIEvent() + let disp = classWithEvent.Event.Subscribe(fun (sender, arg) -> result <- arg) + classWithEvent.TestEvent("Hello") + disp.Dispose() + classWithEvent.TestEvent("Bye") + equal "Hello" result // NOTE (Rust): disabled — needs the two-generic Event<_,_> (FSharpEvent`2, // 2-arg Trigger with sender) and CLI events declared on interfaces, which the diff --git a/tests/Rust/tests/src/InterfaceTests.fs b/tests/Rust/tests/src/InterfaceTests.fs index 9bbc2e4beb..10dda94d94 100644 --- a/tests/Rust/tests/src/InterfaceTests.fs +++ b/tests/Rust/tests/src/InterfaceTests.fs @@ -15,6 +15,16 @@ type Adder3 (m: int) = interface IHasAdd with member _.Add x y = x + y - m +type IHasValue = + abstract Value: int + +type ValueHolder(value: int) = + interface IHasValue with + member _.Value = value + +let castToIHasValue (value: obj) : IHasValue = + value :?> IHasValue + let addWithAdder (i: IHasAdd) = i.Add 3 4 @@ -40,6 +50,13 @@ let ``Class interface impl works`` () = let res = aCasted.Add 2 1 res |> equal 3 +[] +let ``Interface cast through obj works`` () = + let original = ValueHolder(42) :> IHasValue + let boxed: obj = original :> obj + let casted = castToIHasValue boxed + casted.Value |> equal 42 + [] let ``Class interface impl works II`` () = let a = Adder2(1) diff --git a/tests/Rust/tests/src/ListTests.fs b/tests/Rust/tests/src/ListTests.fs index 9ad39191ac..63b3fa51ce 100644 --- a/tests/Rust/tests/src/ListTests.fs +++ b/tests/Rust/tests/src/ListTests.fs @@ -659,6 +659,14 @@ let ``List.toSeq works`` () = |> Seq.tail |> Seq.head |> equal 2 +[] +let ``Lists can be enumerated`` () = + let xs = [1.; 2.; 3.; 4.] + let mutable total = 0 + for x in xs do + total <- total + int x + total |> equal 10 + [] let ``List.tryPick works`` () = [1; 2] @@ -1316,3 +1324,15 @@ let ``List.randomSampleBy works`` () = // Out-of-range randomizer should throw throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + +[] +let ``List random functions match FSharp.Core`` () = + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + throwsErrorContaining "randomizer returned NaN, should be in range [0.0, 1.0)." (fun () -> List.randomChoiceBy (fun () -> nan) [1; 2]) + throwsErrorContaining "The input sequence was empty." (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + +[] +let ``List.randomShuffleWith and randomSampleWith draw like Array's`` () = + List.randomShuffleWith (System.Random(42)) [0 .. 9] |> equal (List.ofArray (Array.randomShuffleWith (System.Random(42)) [|0 .. 9|])) + List.randomSampleWith (System.Random(42)) 3 [0 .. 9] |> equal (List.ofArray (Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|])) diff --git a/tests/Rust/tests/src/MapTests.fs b/tests/Rust/tests/src/MapTests.fs index 2e456284ed..00854db440 100644 --- a/tests/Rust/tests/src/MapTests.fs +++ b/tests/Rust/tests/src/MapTests.fs @@ -335,6 +335,14 @@ let ``Map.toSeq works`` () = (Seq.item 2 xs |> snd) = (Seq.item 2 zs |> snd) |> equal true +[] +let ``Maps can be enumerated`` () = + let xs = Map [1,1.; 2,4.; 3,9.; 4,16.] + let mutable total = 0 + for KeyValue(k, v) in xs do + total <- total + k + int v + total |> equal 40 + [] let ``Map.toSeq generates sequences that can be iterated multiple times`` () = // See #2242 let pr (sequence: seq) = diff --git a/tests/Rust/tests/src/ObservableTests.fs b/tests/Rust/tests/src/ObservableTests.fs index 0ed72d768e..d45ff6320d 100644 --- a/tests/Rust/tests/src/ObservableTests.fs +++ b/tests/Rust/tests/src/ObservableTests.fs @@ -9,22 +9,24 @@ type MyObserver<'T>(f) = member x.OnError e = () member x.OnCompleted() = () -// NOTE: This mirrors the JS/TS/Python reference `MyObservable`, adapted for two -// Rust-backend limitations (the runtime combinators themselves are unaffected): -// 1. The disposable is a named generic class, not an inline -// `{ new IDisposable with ... }` object expression: the Rust backend does not -// propagate the enclosing type's generic parameter into a generated -// object-expression struct (it emits an inner struct that references `T` -// without declaring it -> rustc E0401). -// 2. Subscriptions are keyed by an int id and removed by id, rather than +type ErrorObserver(onError: exn -> unit) = + interface IObserver with + member _.OnNext _ = () + member _.OnError error = onError error + member _.OnCompleted() = () + +type CountingObserver(onNext: unit -> unit, onError: exn -> unit, onCompleted: unit -> unit) = + interface IObserver with + member _.OnNext _ = onNext () + member _.OnError error = onError error + member _.OnCompleted() = onCompleted () + +// NOTE: This mirrors the JS/TS/Python reference `MyObservable`, adapted for one +// Rust-backend limitation (the runtime combinators themselves are unaffected): +// Subscriptions are keyed by an int id and removed by id, rather than // `listeners.Remove(w)`: interface trait objects (`IObserver<'T>`) do not // implement Rust `PartialEq`, so removal-by-value of an interface element is // not supported. Behaviour is identical to the reference. -type private Unsubscriber<'T>(listeners: ResizeArray>, id: int) = - interface IDisposable with - member _.Dispose() = - let idx = listeners.FindIndex(fun (i, _) -> i = id) - if idx >= 0 then listeners.RemoveAt(idx) type MyObservable<'T>() = let listeners = ResizeArray>() @@ -32,15 +34,41 @@ type MyObservable<'T>() = member x.Trigger v = for (_, lis) in listeners do lis.OnNext v + member x.TriggerError error = + for (_, lis) in listeners do + lis.OnError error + member x.TriggerCompleted() = + for (_, lis) in listeners do + lis.OnCompleted() interface IObservable<'T> with member x.Subscribe w = let id = nextId nextId <- nextId + 1 listeners.Add((id, w)) - new Unsubscriber<'T>(listeners, id) :> IDisposable + { new IDisposable with + member _.Dispose() = + let idx = listeners.FindIndex(fun (i, _) -> i = id) + if idx >= 0 then listeners.RemoveAt(idx) } module tests = + let assertMatchesDotNetAfterCallbackError (source: MyObservable) (observable: IObservable) = + let mutable next = 0 + let mutable errors = 0 + let mutable completed = 0 + observable.Subscribe(CountingObserver( + (fun () -> next <- next + 1), + (fun _ -> errors <- errors + 1), + (fun () -> completed <- completed + 1))) + |> ignore + source.Trigger 1 + source.Trigger 2 + source.TriggerError (Exception "source") + source.TriggerCompleted() + equal 0 next + equal 3 errors + equal 0 completed + [] let ``IObservable.Subscribe works`` () = let source = MyObservable() @@ -81,6 +109,15 @@ module tests = source.Trigger (Choice1Of2 2) source.Trigger (Choice2Of2 3) +#if !NO_STD_NO_EXCEPTIONS + [] + let ``Observable.choose matches .NET after callback error`` () = + let source = MyObservable() + let chosen: IObservable = + Observable.choose (fun _ -> (failwith "boom" : int option)) source + assertMatchesDotNetAfterCallbackError source chosen +#endif + [] let ``Observable.filter works`` () = let source = MyObservable() @@ -96,6 +133,40 @@ module tests = |> Observable.add (equal false) source.Trigger true +#if !NO_STD_NO_EXCEPTIONS + [] + let ``Observable.map matches .NET after callback error`` () = + let source = MyObservable() + let mapped: IObservable = + Observable.map (fun _ -> (failwith "boom" : int)) source + assertMatchesDotNetAfterCallbackError source mapped + + [] + let ``Observable.map forwards callback exceptions to OnError`` () = + let source = MyObservable() + let mutable errors = 0 + let mapped = Observable.map (fun _ -> failwith "boom") source + mapped.Subscribe(ErrorObserver(fun _ -> errors <- errors + 1)) |> ignore + source.Trigger 1 + equal 1 errors + + [] + let ``Observable.scan forwards collector exceptions to OnError`` () = + let source = MyObservable() + let mutable errors = 0 + let scanned = Observable.scan (fun _ _ -> failwith "boom") 0 source + scanned.Subscribe(ErrorObserver(fun _ -> errors <- errors + 1)) |> ignore + source.Trigger 1 + equal 1 errors + + [] + let ``Observable.scan matches .NET after collector error`` () = + let source = MyObservable() + let scanned: IObservable = + Observable.scan (fun _ _ -> (failwith "boom" : int)) 0 source + assertMatchesDotNetAfterCallbackError source scanned +#endif + [] let ``Observable.merge works`` () = let source1 = MyObservable() diff --git a/tests/Rust/tests/src/QuotationTests.fs b/tests/Rust/tests/src/QuotationTests.fs index 353a081eba..9f9172ae8c 100644 --- a/tests/Rust/tests/src/QuotationTests.fs +++ b/tests/Rust/tests/src/QuotationTests.fs @@ -3,6 +3,7 @@ module Fable.Tests.QuotationTests open Util.Testing open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Microsoft.FSharp.Linq.RuntimeHelpers // NOTE: quotations are deconstructed through an untyped `Expr` parameter (helpers below @@ -196,10 +197,22 @@ let ``Evaluate nested lambda`` () = let r = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> (fun y -> x + y)) 3 4 @> unbox r |> equal 7 +[] +let ``Evaluate union quotation`` () = + let result = LeafExpressionConverter.EvaluateQuotation (<@ Some 42 @> :> Expr) + obj.ReferenceEquals(result, null) |> equal false + +[] +let ``Evaluate list quotation`` () = + let result = LeafExpressionConverter.EvaluateQuotation (<@ [ 1; 2; 3 ] @> :> Expr) + obj.ReferenceEquals(result, null) |> equal false + // --- NewUnionCase / NewRecord deconstruction --- type private MyRec = { X: int; Y: int } +type private MyDefaultValueRec = { A: int; [] mutable B: int; C: int } + let private newUnionCaseName (e: Expr) = match e with | NewUnionCase(uci, _args) -> uci.Name @@ -236,6 +249,10 @@ let ``NewUnionCase exposes the tag`` () = let ``NewRecord exposes the argument count`` () = newRecordArgCount <@ ({ X = 1; Y = 2 }: MyRec) @> |> equal 2 +[] +let ``NewRecord leaves out DefaultValue fields`` () = + newRecordArgCount <@ ({ A = 1; C = 2 }: MyDefaultValueRec) @> |> equal 2 + // --- TupleGet evaluation (B3) --- // A tuple-deconstructing let lowers to a NewTuple bound to a var, then TupleGet nodes // pulling out each element. Evaluating it exercises both the ExprNewTuple arm (builds a @@ -354,3 +371,100 @@ let ``Instance call inside a quotation has an instance`` () = match q with | Lambda(_, Call(Some _, _mi, _)) -> () | _ -> failwith "Expected Lambda with an instance Call body" + +[] +let ``GetFreeVars includes call instances`` () = + let q = <@ fun (s: string) -> s.Trim() @> + + match q with + | Lambda(_, body) -> + body.GetFreeVars() + |> Seq.map (fun v -> v.Name) + |> Seq.toList + |> equal [ "s" ] + | _ -> failwith "Expected Lambda" + +[] +let ``GetFreeVars visits nested union and list nodes`` () = + let q = <@ fun x -> [ Some x ] @> + + match q with + | Lambda(_, body) -> + body.GetFreeVars() + |> Seq.map (fun v -> v.Name) + |> Seq.toList + |> equal [ "x" ] + | _ -> failwith "Expected Lambda" + +[] +let ``Quotation ToString works`` () = + let text = (<@ 2 + 3 @>).ToString() + (text.Length > 0) |> equal true + +[] +let ``Quotation Substitute replaces variables`` () = + let quotation = (<@ fun x -> x + 1 @> :> Expr) + let replacement = (<@ 42 @> :> Expr) + let substituted = + quotation.Substitute(fun _ -> Some replacement) + + match substituted with + | Lambda(_, Call(_, _, [ Value(value, _); _ ])) -> + unbox value |> equal 42 + | _ -> failwith "Expected the lambda variable to be substituted" + +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + +[] +let ``DerivedPatterns AndAlso, OrElse and SpecificCall work`` () = + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" + +// A local captured by a quotation is spliced in by .NET as a Value node holding +// its value. It used to come through as a Var carrying only a name, because the +// emitter treated every identifier as a quotation variable -- and the binding was +// then inlined away underneath it, since neither reference counting nor the +// inliner looked inside a Quote. +[] +let ``a captured local is a Value, not a Var`` () = + let captured = "SE" + + let describe (e: Expr) = + match e with + | Lambda(_, body) -> + match body with + | Value(v, _) -> "Value:" + unbox v + | Var v -> "Var:" + v.Name + | _ -> "other" + | _ -> "not a lambda" + + describe <@ fun (_: int) -> captured @> |> equal "Value:SE" + describe <@ fun (_: int) -> "SE" @> |> equal "Value:SE" + // a variable genuinely bound by the quotation stays a Var + (match <@ fun (x: string) -> x @> with + | Lambda(_, Var v) -> v.Name + | _ -> "?") + |> equal "x" diff --git a/tests/Rust/tests/src/RecordReflectionTests.fs b/tests/Rust/tests/src/RecordReflectionTests.fs index bfa7462820..483dc4513f 100644 --- a/tests/Rust/tests/src/RecordReflectionTests.fs +++ b/tests/Rust/tests/src/RecordReflectionTests.fs @@ -1,6 +1,5 @@ -// Value-based reflection needs the type registry, which is std-only -// (it uses thread_local), so these tests do not apply to no_std builds. -[] +// Value-based reflection uses thread_local on std and OnceInit on no_std. +[] module Fable.Tests.RecordReflectionTests open Util.Testing @@ -13,6 +12,7 @@ open Microsoft.FSharp.Reflection type TestRecord = { String: string; Int: int } type OtherRecord = { Z: int } +type UnprimedRecord = { Value: int } type SampleUnion = | CaseA @@ -49,15 +49,19 @@ let ``FSharpType.GetRecordFields exposes PropertyInfo names`` () = [] let ``FSharpValue.GetRecordFields returns boxed field values`` () = - // Value-based reflection resolves the type via the runtime registry, which is - // populated when typeof is evaluated (as the JS record test also does first). - FSharpType.IsRecord(typeof) |> equal true let record = { String = "a"; Int = 1 } let values = FSharpValue.GetRecordFields(record) values.Length |> equal 2 unbox values.[0] |> equal "a" unbox values.[1] |> equal 1 +[] +let ``FSharpValue.GetRecordFields works without prior typeof`` () = + let record: UnprimedRecord = { Value = 7 } + let values = FSharpValue.GetRecordFields(record) + values.Length |> equal 1 + unbox values.[0] |> equal 7 + [] let ``FSharpValue.GetRecordField reads an individual field`` () = let record = { String = "a"; Int = 1 } @@ -104,7 +108,6 @@ let ``FSharp.Reflection: Record (end to end)`` () = [] let ``obj.GetType round-trips to the concrete record type`` () = let record = { String = "a"; Int = 1 } - FSharpType.IsRecord(typeof) |> equal true // ensure registration ((box record).GetType() = typeof) |> equal true let rebuilt = unbox (FSharpValue.MakeRecord((box record).GetType(), FSharpValue.GetRecordFields(record))) @@ -123,3 +126,10 @@ let ``Boxing a record to obj and unboxing preserves reference identity`` () = let o: obj = box r let r2 = unbox o System.Object.ReferenceEquals(r, r2) |> equal true + +[] +let ``obj.GetType resolves an unregistered primitive`` () = + let typeForEquality = (box 1).GetType() + let typeForName = (box 1).GetType() + (typeForEquality = typeof) |> equal true + typeForName.FullName |> equal "System.Int32" diff --git a/tests/Rust/tests/src/RecordTests.fs b/tests/Rust/tests/src/RecordTests.fs index 068c6db9b9..422f3238b5 100644 --- a/tests/Rust/tests/src/RecordTests.fs +++ b/tests/Rust/tests/src/RecordTests.fs @@ -2,6 +2,34 @@ module Fable.Tests.RecordTests open Util.Testing +type Camera() = class end + +type Scene = { + Camera: Camera +} + +let getCamera (scene: Scene) = + scene.Camera + +[] +type StructRecordCopied = { T: float; S: string } + +[] +let ``Copy-and-update of a let mutable struct record keeps the unchanged fields`` () = + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + +[] +let ``Class-valued record fields compile`` () = + let scene = { Camera = Camera() } + let _camera = getCamera scene + () + type MyRecord = { a: int b: string @@ -111,14 +139,22 @@ let processStructByValue (s: StructRecord) = [] let ``Struct record works`` () = - let r1 = { i=1; s="hello" } - let r2 = { i=1; s="world" } + let r1 = { i = 1; s = "hello" } + let r2 = { i = 1; s = "world" } let (sres1, ires1) = r1 |> processStructByValue let (sres2, ires2) = r1 |> processStructByValue let (sres3, ires3) = r2 |> processStructByValue //cannot actually test this, but since this is the only reference, the output should not .clone() ires2 |> equal 2 sres3.s |> equal "world" +[] +let ``Struct record temp locals work`` () = + let r1 = { i = 1; s = "hello" } + let r2 = { i = 2; s = "world" } + let r3 = { r1 with i = r2.i ; s = r2.s } + r3.i |> equal 2 + r3.s |> equal "world" + type MutableRecord = { mutable MutValue: int } diff --git a/tests/Rust/tests/src/RegexTests.fs b/tests/Rust/tests/src/RegexTests.fs index 8ed732e583..b198b0353e 100644 --- a/tests/Rust/tests/src/RegexTests.fs +++ b/tests/Rust/tests/src/RegexTests.fs @@ -31,6 +31,14 @@ let ``Literal regex works with whitespace characters`` () = // See #2635 Regex("\\t").IsMatch("foo\tbar") |> equal true Regex(@"\\t").IsMatch("foo\tbar") |> equal false +[] +let ``RegexOptions.NonBacktracking is accepted`` () = + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + [] let ``Regex.Options works`` () = let option1 = RegexOptions.IgnoreCase diff --git a/tests/Rust/tests/src/RuneTests.fs b/tests/Rust/tests/src/RuneTests.fs new file mode 100644 index 0000000000..5bdcfb0a33 --- /dev/null +++ b/tests/Rust/tests/src/RuneTests.fs @@ -0,0 +1,385 @@ +module Fable.Tests.RuneTests + +open System +open System.Globalization +open System.Text +open Util.Testing + +[] +let ``Rune(Char) constructor works`` () = + Rune('A').Value |> equal 65 + +[] +let ``Rune(Int32) constructor works`` () = + Rune(0x1F600).Value |> equal 0x1F600 + +[] +let ``Rune(Int32) constructor rejects invalid scalar values`` () = + throwsAnyError (fun () -> Rune(-1) |> ignore) + throwsAnyError (fun () -> Rune(0xD800) |> ignore) + throwsAnyError (fun () -> Rune(0x110000) |> ignore) + +[] +let ``Rune(UInt32) constructor works`` () = + Rune(uint32 0x10FFFF).Value |> equal 0x10FFFF + +[] +let ``Rune(UInt32) constructor rejects invalid scalar values`` () = + throwsAnyError (fun () -> Rune(uint32 0xD800) |> ignore) + +[] +let ``Rune(Char, Char) constructor rejects non-surrogate input`` () = + throwsAnyError (fun () -> Rune('a', 'b') |> ignore) + +[] +let ``Rune.TryCreate(Char) works`` () = + Rune.TryCreate('A') |> equal (true, Rune('A')) + +[] +let ``Rune.TryCreate(Char, Char) rejects non-surrogate input`` () = + Rune.TryCreate('a', 'b') |> equal (false, Rune(0)) + +[] +let ``Rune.TryCreate(Int32) works`` () = + Rune.TryCreate(0x1F600) |> equal (true, Rune(0x1F600)) + Rune.TryCreate(-1) |> equal (false, Rune(0)) + +[] +let ``Rune.TryCreate(UInt32) works`` () = + Rune.TryCreate(uint32 0x10FFFF) |> equal (true, Rune(0x10FFFF)) + Rune.TryCreate(uint32 0x110000) |> equal (false, Rune(0)) + +[] +let ``Rune.IsValid(Int32) works`` () = + Rune.IsValid(-1) |> equal false + Rune.IsValid(0) |> equal true + Rune.IsValid(0xD7FF) |> equal true + Rune.IsValid(0xD800) |> equal false + Rune.IsValid(0xDFFF) |> equal false + Rune.IsValid(0xE000) |> equal true + Rune.IsValid(0x10FFFF) |> equal true + Rune.IsValid(0x110000) |> equal false + +[] +let ``Rune.IsValid(UInt32) works`` () = + Rune.IsValid(uint32 0xD800) |> equal false + Rune.IsValid(uint32 0x10FFFF) |> equal true + Rune.IsValid(UInt32.MaxValue) |> equal false + +[] +let ``Rune.Value works`` () = + Rune('A').Value |> equal 65 + Rune(0x1F600).Value |> equal 0x1F600 + Unchecked.defaultof.Value |> equal 0 + +[] +let ``Rune.Utf8SequenceLength works`` () = + Rune('A').Utf8SequenceLength |> equal 1 + Rune('\u00E9').Utf8SequenceLength |> equal 2 + Rune('\u20AC').Utf8SequenceLength |> equal 3 + Rune(0x1F600).Utf8SequenceLength |> equal 4 + +[] +let ``Rune.Utf16SequenceLength works`` () = + Rune('A').Utf16SequenceLength |> equal 1 + Rune(0x1F600).Utf16SequenceLength |> equal 2 + +[] +let ``Rune.IsAscii works`` () = + Rune(0x7F).IsAscii |> equal true + Rune(0x80).IsAscii |> equal false + +[] +let ``Rune.IsBmp works`` () = + Rune(0xFFFF).IsBmp |> equal true + Rune(0x10000).IsBmp |> equal false + +[] +let ``Rune.Plane works`` () = + Rune(0xFFFF).Plane |> equal 0 + Rune(0x10000).Plane |> equal 1 + Rune(0x10FFFF).Plane |> equal 16 + +[] +let ``Rune.ReplacementChar works`` () = + Rune.ReplacementChar |> equal (Rune(0xFFFD)) + +[] +let ``Rune.GetUnicodeCategory works`` () = + Rune.GetUnicodeCategory(Rune('A')) |> equal UnicodeCategory.UppercaseLetter + +[] +let ``Rune.GetNumericValue works`` () = + Rune.GetNumericValue(Rune('4')) |> equal 4.0 + +[] +let ``Rune.IsControl works`` () = + Rune.IsControl(Rune('\u0000')) |> equal true + +[] +let ``Rune.IsDigit works`` () = + Rune.IsDigit(Rune('4')) |> equal true + +[] +let ``Rune.IsLetter works`` () = + Rune.IsLetter(Rune('A')) |> equal true + Rune.IsLetter(Rune('家')) |> equal true + +[] +let ``Rune.IsLetterOrDigit works`` () = + Rune.IsLetterOrDigit(Rune('A')) |> equal true + Rune.IsLetterOrDigit(Rune('-')) |> equal false + +[] +let ``Rune.IsLower works`` () = + Rune.IsLower(Rune('a')) |> equal true + +[] +let ``Rune.IsNumber works`` () = + Rune.IsNumber(Rune('4')) |> equal true + +[] +let ``Rune.IsPunctuation works`` () = + Rune.IsPunctuation(Rune('.')) |> equal true + +[] +let ``Rune.IsSeparator works`` () = + Rune.IsSeparator(Rune(' ')) |> equal true + +[] +let ``Rune.IsSymbol works`` () = + Rune.IsSymbol(Rune('+')) |> equal true + +[] +let ``Rune.IsUpper works`` () = + Rune.IsUpper(Rune('A')) |> equal true + +[] +let ``Rune.IsWhiteSpace works`` () = + Rune.IsWhiteSpace(Rune('\n')) |> equal true + +[] +let ``Rune Unicode category predicates work`` () = + Rune.IsControl(Rune('\u0085')) |> equal true + Rune.IsDigit(Rune('\u0661')) |> equal true + Rune.IsLetter(Rune('Ж')) |> equal true + Rune.IsLetterOrDigit(Rune('\u0661')) |> equal true + Rune.IsLower(Rune('я')) |> equal true + Rune.IsNumber(Rune('\u2167')) |> equal true + Rune.IsPunctuation(Rune('\u201C')) |> equal true + Rune.IsSeparator(Rune('\u2028')) |> equal true + Rune.IsSymbol(Rune('\u20AC')) |> equal true + Rune.IsUpper(Rune('Ж')) |> equal true + Rune.IsWhiteSpace(Rune('\u2007')) |> equal true + +[] +let ``Rune.ToLower with invariant culture works`` () = + Rune.ToLower(Rune('A'), CultureInfo.InvariantCulture) |> equal (Rune('a')) + +[] +let ``Rune.ToLowerInvariant works`` () = + Rune.ToLowerInvariant(Rune('A')) |> equal (Rune('a')) + +[] +let ``Rune.ToUpper with invariant culture works`` () = + Rune.ToUpper(Rune('b'), CultureInfo.InvariantCulture) |> equal (Rune('B')) + +[] +let ``Rune.ToUpperInvariant works`` () = + Rune.ToUpperInvariant(Rune('b')) |> equal (Rune('B')) + +[] +let ``Rune.ToString works`` () = + Rune('A').ToString() |> equal "A" + Rune(0x1F600).ToString() |> equal "\uD83D\uDE00" + +[] +let ``Rune string conversion works`` () = + string (Rune(0x1F600)) |> equal "\uD83D\uDE00" + +[] +let ``Rune.GetRuneAt works`` () = + let input = "A\uD83D\uDE00Z" + + Rune.GetRuneAt(input, 0) |> equal (Rune('A')) + Rune.GetRuneAt(input, 1) |> equal (Rune(0x1F600)) + Rune.GetRuneAt(input, 3) |> equal (Rune('Z')) + +[] +let ``Rune.GetRuneAt rejects invalid positions`` () = + let input = "A\uD83D\uDE00Z" + + throwsAnyError (fun () -> Rune.GetRuneAt(input, -1) |> ignore) + throwsAnyError (fun () -> Rune.GetRuneAt(input, 2) |> ignore) + throwsAnyError (fun () -> Rune.GetRuneAt(input, 4) |> ignore) + +[] +let ``Rune.TryGetRuneAt works`` () = + let input = "A\uD83D\uDE00Z" + + Rune.TryGetRuneAt(input, 0) |> equal (true, Rune('A')) + Rune.TryGetRuneAt(input, 1) |> equal (true, Rune(0x1F600)) + Rune.TryGetRuneAt(input, 2) |> equal (false, Rune(0)) + Rune.TryGetRuneAt(input, 3) |> equal (true, Rune('Z')) + +[] +let ``Rune.TryGetRuneAt rejects out of range positions`` () = + let input = "A\uD83D\uDE00Z" + + throwsAnyError (fun () -> Rune.TryGetRuneAt(input, -1) |> ignore) + throwsAnyError (fun () -> Rune.TryGetRuneAt(input, 4) |> ignore) + +[] +let ``Rune equality operator works`` () = + Rune('a') = Rune('a') |> equal true + Rune('a') = Rune('b') |> equal false + +[] +let ``Rune inequality operator works`` () = + Rune('a') <> Rune('b') |> equal true + +[] +let ``Rune less than operator works`` () = + Rune('a') < Rune('b') |> equal true + +[] +let ``Rune less than or equal operator works`` () = + Rune('a') <= Rune('a') |> equal true + +[] +let ``Rune greater than operator works`` () = + Rune('b') > Rune('a') |> equal true + +[] +let ``Rune greater than or equal operator works`` () = + Rune('b') >= Rune('b') |> equal true + +[] +let ``Rune.CompareTo works`` () = + Rune('a').CompareTo(Rune('b')) |> equal -1 + Rune('a').CompareTo(Rune('a')) |> equal 0 + Rune('b').CompareTo(Rune('a')) |> equal 1 + +[] +let ``Rune comparison works`` () = + compare (Rune('a')) (Rune('b')) |> equal -1 + compare (Rune('a')) (Rune('a')) |> equal 0 + compare (Rune('b')) (Rune('a')) |> equal 1 + +[] +let ``Rune.Equals works`` () = + Rune('a').Equals(Rune('a')) |> equal true + Rune('a').Equals(Rune('b')) |> equal false + +[] +let ``Rune.GetHashCode works`` () = + Rune('a').GetHashCode() |> equal (Rune(97).GetHashCode()) + Rune(0x1F600).GetHashCode() |> equal (Rune.GetRuneAt("😀", 0).GetHashCode()) + +[] +let ``Rune.GetRuneAt works with BMP characters`` () = + let rune = Rune.GetRuneAt("ABC", 1) + rune.Value |> equal 0x42 + rune.ToString() |> equal "B" + +[] +let ``Rune.GetRuneAt works with supplementary characters`` () = + let rune = Rune.GetRuneAt("𠀀", 0) + rune.Value |> equal 0x20000 + rune.ToString() |> equal "𠀀" + +[] +let ``Rune.GetRuneAt uses UTF-16 code unit indexes`` () = + Rune.GetRuneAt("A𠀀B", 0).ToString() + |> equal "A" + + Rune.GetRuneAt("A𠀀B", 1).ToString() + |> equal "𠀀" + + Rune.GetRuneAt("A𠀀B", 3).ToString() + |> equal "B" + +[] +let ``Rune.GetRuneAt throws when index points inside a surrogate pair`` () = + throwsAnyError <| fun () -> + Rune.GetRuneAt("A𠀀B", 2) + |> ignore + +[] +let ``Rune.GetRuneAt throws when index is out of range`` () = + throwsAnyError (fun () -> Rune.GetRuneAt("ABC", -1) |> ignore) + throwsAnyError (fun () -> Rune.GetRuneAt("ABC", 3) |> ignore) + +[] +let ``Rune.GetRuneAt returns ReplacementChar for unpaired surrogate characters`` () = + Rune.GetRuneAt("\uD800", 0) |> equal Rune.ReplacementChar + Rune.GetRuneAt("\uDC00", 0) |> equal Rune.ReplacementChar + +[] +let ``Rune.Value returns the Unicode scalar value`` () = + Rune.GetRuneAt("A", 0).Value + |> equal 0x41 + + Rune.GetRuneAt("𠀀", 0).Value + |> equal 0x20000 + +[] +let ``Rune.Utf16SequenceLength returns the number of UTF-16 code units`` () = + Rune.GetRuneAt("A", 0).Utf16SequenceLength + |> equal 1 + + Rune.GetRuneAt("𠀀", 0).Utf16SequenceLength + |> equal 2 + +[] +let ``Rune.Utf16SequenceLength works at the BMP boundary`` () = + Rune.GetRuneAt("\uFFFF", 0).Utf16SequenceLength + |> equal 1 + + Rune.GetRuneAt("\U00010000", 0).Utf16SequenceLength + |> equal 2 + +[] +let ``Rune.ToString returns the represented Unicode scalar as a string`` () = + Rune.GetRuneAt("A", 0).ToString() + |> equal "A" + + Rune.GetRuneAt("𠀀", 0).ToString() + |> equal "𠀀" + +[] +let ``F# string conversion works with Rune`` () = + Rune.GetRuneAt("A", 0) + |> string + |> equal "A" + + Rune.GetRuneAt("𠀀", 0) + |> string + |> equal "𠀀" + +[] +let ``String.EnumerateRunes enumerates Unicode scalar values`` () = + "A𠀀B".EnumerateRunes() + |> Seq.map (fun rune -> rune.Value) + |> Seq.toList + |> equal [ 0x41; 0x20000; 0x42 ] + +[] +let ``String.EnumerateRunes works with Rune string conversion`` () = + "A𠀀B".EnumerateRunes() + |> Seq.map string + |> Seq.toList + |> equal [ "A"; "𠀀"; "B" ] + +[] +let ``String.EnumerateRunes replaces unpaired surrogates`` () = + // .NET String.EnumerateRunes replaces invalid UTF-16 sequences with U+FFFD + // https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.replacementchar + + // Note: For Rust, this only works for string literals (replaced at compile time) + let input = "\uD800A\uDC00" + + input.EnumerateRunes() + |> Seq.map _.Value + |> Seq.toList + |> equal [ 0xFFFD; 0x41; 0xFFFD ] diff --git a/tests/Rust/tests/src/SeqTests.fs b/tests/Rust/tests/src/SeqTests.fs index 5f07d3d78e..43500ec317 100644 --- a/tests/Rust/tests/src/SeqTests.fs +++ b/tests/Rust/tests/src/SeqTests.fs @@ -1412,3 +1412,22 @@ let ``Seq.randomSampleBy works`` () = Array.distinct sample |> Array.length |> equal 3 throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + +[] +let ``Seq.randomShuffleWith and randomSampleWith draw like Array's`` () = + Seq.randomShuffleWith (System.Random(42)) [0 .. 9] |> Seq.toArray |> equal (Array.randomShuffleWith (System.Random(42)) [|0 .. 9|]) + Seq.randomSampleWith (System.Random(42)) 3 [0 .. 9] |> Seq.toArray |> equal (Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|]) + +[] +let ``Seq.randomChoicesBy draws on enumeration`` () = + let mutable calls = 0 + let xs = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + xs |> Seq.toList |> equal [5; 5; 5] + xs |> Seq.toList |> equal [5; 5; 5] + calls |> equal 6 + let rng = System.Random(42) + let ys = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal (System.Random(42).Next()) + ys |> Seq.length |> equal 3 + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Seq.randomChoicesWith rng -1 [1]) diff --git a/tests/Rust/tests/src/SetTests.fs b/tests/Rust/tests/src/SetTests.fs index 3beab8dc32..d5a6e62512 100644 --- a/tests/Rust/tests/src/SetTests.fs +++ b/tests/Rust/tests/src/SetTests.fs @@ -363,6 +363,14 @@ let ``Set.toSeq works`` () = (Seq.item 2 xs) = (Seq.item 2 zs) |> equal true +[] +let ``Sets can be enumerated`` () = + let xs = set [1.; 2.; 3.; 4.] + let mutable total = 0 + for x in xs do + total <- total + int x + total |> equal 10 + [] let ``Comparing large sets works`` () = // See #2203 let largeSetA = Set.ofArray [| for i in 1 .. 100_000 -> i |] diff --git a/tests/Rust/tests/src/StringTests.fs b/tests/Rust/tests/src/StringTests.fs index 7f67365f7a..c6387e885f 100644 --- a/tests/Rust/tests/src/StringTests.fs +++ b/tests/Rust/tests/src/StringTests.fs @@ -1590,3 +1590,31 @@ let ``String.filter with Char.IsDigit as a predicate doesn't hang`` () = // let s3: FormattableString = $"""I have no holes""" // s3.GetStrings() |> equal [|"I have no holes"|] // #endif + +// A string is enumerable in F#, but `string` is `LrcStr` in Rust with no +// GetEnumerator to fall through to, so `for ch in s do` did not compile. +[] +let ``for-in over a string works`` () = + let mutable count = 0 + let mutable upper = "" + + for ch in "abc" do + count <- count + 1 + upper <- upper + string (System.Char.ToUpper ch) + + count |> equal 3 + upper |> equal "ABC" + +[] +let ``for-in over an empty string does nothing`` () = + let mutable count = 0 + + for _ in "" do + count <- count + 1 + + count |> equal 0 + +[] +let ``a string works with the Seq functions`` () = + "hello" |> Seq.filter (fun c -> c = 'l') |> Seq.length |> equal 2 + "hello" |> Seq.toList |> List.length |> equal 5 diff --git a/tests/Rust/tests/src/SystemIOTests.fs b/tests/Rust/tests/src/SystemIOTests.fs index 4fcfadb38c..4417a5ee45 100644 --- a/tests/Rust/tests/src/SystemIOTests.fs +++ b/tests/Rust/tests/src/SystemIOTests.fs @@ -29,6 +29,21 @@ let ``Path.GetDirectoryName works`` () = Path.GetDirectoryName("temp/test.txt") |> equal "temp" +[] +let ``Path.GetDirectoryName preserves the root`` () = + Path.GetDirectoryName("/foo") + |> equal "/" + +[] +let ``Path.GetDirectoryName returns null for the root itself`` () = + isNull (Path.GetDirectoryName "/") + |> equal true + +[] +let ``Path.GetDirectoryName returns null for a null path`` () = + isNull (Path.GetDirectoryName null) + |> equal true + [] let ``Path.GetExtension works`` () = Path.GetExtension("temp/test.txt") diff --git a/tests/Rust/tests/src/TaskTests.fs b/tests/Rust/tests/src/TaskTests.fs index 0b77a38245..a6ae7eb406 100644 --- a/tests/Rust/tests/src/TaskTests.fs +++ b/tests/Rust/tests/src/TaskTests.fs @@ -1,3 +1,4 @@ +[] module Fable.Tests.TaskTests open Util.Testing @@ -7,7 +8,7 @@ type DisposableAction(f) = interface System.IDisposable with member _.Dispose() = f () -let runSynchronously<'T> (tsk: Task<'T>) : 'T = +let inline runSynchronously<'T> (tsk: Task<'T>) : 'T = //tsk.GetAwaiter().GetResult() tsk.Result @@ -68,6 +69,25 @@ let ``Task exceptions are handled correctly`` () = result f true + f false |> equal 22 +[] +let ``Task failures propagate from already started tasks`` () = + let failing = + task { + failwith "boom" + return 0 + } + + let result = + task { + try + return! failing + with + | _ -> return 42 + } + |> runSynchronously + + result |> equal 42 + [] let ``Simple task is executed correctly`` () = let mutable result = false @@ -80,6 +100,26 @@ let ``Simple task is executed correctly`` () = |> runSynchronously result |> equal true +[] +let ``Task Combine runs computations in order`` () = + let mutable result = "" + let tsk = + task { + if true then + do! task { result <- result + "a" } + result <- result + "b" + } + tsk |> runSynchronously + result |> equal "ab" + +[] +let ``Task ReturnFrom returns inner task`` () = + let result = + task { return! task { return 42 } } + |> runSynchronously + + result |> equal 42 + [] let ``task use statements should dispose of resources when they go out of scope`` () = let mutable isDisposed = false @@ -98,6 +138,23 @@ let ``task use statements should dispose of resources when they go out of scope` step2ok <- isDisposed (step1ok && step2ok) |> equal true +[] +let ``task use statements dispose of resources when the body fails`` () = + let mutable isDisposed = false + let resource = + task { return new DisposableAction(fun () -> isDisposed <- true) } + + let failing = + task { + use! r = resource + ignore r + failwith "boom" + return 0 + } + + throwsAnyError (fun () -> failing.Result |> ignore) + isDisposed |> equal true + [] let ``Try ... with ... expressions inside async expressions work the same`` () = let mutable result = "" @@ -128,14 +185,14 @@ let ``Try ... with ... expressions inside async expressions work the same`` () = result |> equal "abcdef" -[] -let ``TaskCompletionSource is executed correctly`` () = - let x = - task { - let tcs = TaskCompletionSource() - tcs.SetResult 42 - return! tcs.Task - } - let result = - x |> runSynchronously - result |> equal 42 +// [] +// let ``TaskCompletionSource is executed correctly`` () = +// let x = +// task { +// let tcs = TaskCompletionSource() +// tcs.SetResult 42 +// return! tcs.Task +// } +// let result = +// x |> runSynchronously +// result |> equal 42 diff --git a/tests/Rust/tests/src/TupleTests.fs b/tests/Rust/tests/src/TupleTests.fs index ef0d9938a2..85f3db2446 100644 --- a/tests/Rust/tests/src/TupleTests.fs +++ b/tests/Rust/tests/src/TupleTests.fs @@ -103,3 +103,13 @@ let ``Can create tuples of single element`` () = let t1b = Tuple.Create "b" t1.Item1 |> equal 4 t1b.Item1 |> equal "b" + +// Rust has no Display for tuples and the orphan rule rules out adding one, so +// `string` on a tuple did not compile. Building the text from each element's own +// ToString also matches .NET, where a string element is unquoted and a bool +// prints as True -- neither of which a derived Debug would give. +[] +let ``string on a tuple matches .NET`` () = + string (1, 2) |> equal "(1, 2)" + string (1, "a", true) |> equal "(1, a, True)" + string ((1, 2), 3) |> equal "((1, 2), 3)" diff --git a/tests/Rust/tests/src/TypeTests.fs b/tests/Rust/tests/src/TypeTests.fs index 752315a872..42e093d1da 100644 --- a/tests/Rust/tests/src/TypeTests.fs +++ b/tests/Rust/tests/src/TypeTests.fs @@ -275,19 +275,11 @@ type ValueTypeR = type StructUnion = Value of string [] -type SimpleRecord = { A: string; B: string } - -[] -type NestedStruct = - val A: float - val B: bool - new(a, b) = { A = a; B = b } - -[] -type TopLevelStruct = - val A: float - val B: NestedStruct - new(a, b) = { A = a; B = b } +type SimpleRecord = + { A: string; B: string } + member this.WithA(a: string) = + let this = this + { this with A = a } type Point2D = struct @@ -296,7 +288,33 @@ type Point2D = new(xy: float) = { X = xy; Y = xy } end -// exception MyEx of int*string +type ClassWithDefaultValueInstanceFields = + val X: int + [] + val mutable D: int + [] + val mutable S: string + val Y: string + new(x, y) = { X = x; Y = y } + +[] +type StructWithDefaultValueInstanceField = + val X: int + [] + val mutable D: int + val Y: string + new(x, y) = { X = x; Y = y } + +type ClassWithDefaultValueStaticAndInstanceFields = + [] + static val mutable private count: int + val X: int + [] + val mutable D: int + val Y: int + new(x, y) = { X = x; Y = y } + +exception MyEx of int * string // type MyEx2(f: float) = // inherit exn(sprintf "Code: %i" (int f)) @@ -344,6 +362,118 @@ type TypeWithClassAttribute = // [] val mutable StringValue: string // [] val mutable ObjValue: System.Collections.Generic.Dictionary +// [] +// type MyUser = +// class end + +// // See #2739: static [] fields must be zero-initialized in JS/TS +// type ClassWithDefaultValueStaticFields() = + +// [] +// static val mutable private int: int +// [] +// static val mutable private bool: bool +// [] +// static val mutable private string: string +// [] +// static val mutable private guid: Guid +// [] +// static val mutable private char: char + +// [] +// static val mutable private dateTime: DateTime + +// [] +// static val mutable private timeOnly : TimeOnly +// [] +// static val mutable private dateOnly : DateOnly +// [] +// static val mutable private dateTimeOffset : DateTimeOffset +// [] +// static val mutable private int8 : int8 +// [] +// static val mutable private uInt8 : uint8 +// [] +// static val mutable private int16 : Int16 +// [] +// static val mutable private uInt16 : UInt16 +// [] +// static val mutable private int32 : Int32 +// [] +// static val mutable private uInt32 : UInt32 +// [] +// static val mutable private int64 : Int64 +// [] +// static val mutable private uInt64 : UInt64 +// [] +// static val mutable private bigInt : bigint +// [] +// static val mutable private nativeInt : nativeint +// [] +// static val mutable private uNativeInt : unativeint +// [] +// static val mutable private float32 : float32 +// [] +// static val mutable private float64 : float +// [] +// static val mutable private decimal : Decimal + +// [] +// static val mutable private timeSpan: TimeSpan +// [] +// static val mutable private allowNullLiteralClass: MyUser +// [] +// static val mutable private nullableInt: Nullable +// [] +// static val mutable private tuple2: TimeSpan * DateTime +// [] +// static val mutable private structTuple2: struct (TimeSpan * DateTime) + +// static member IncrCount() = +// ClassWithDefaultValueStaticFields.int <- ClassWithDefaultValueStaticFields.int + 1 +// ClassWithDefaultValueStaticFields.int + +// static member Int = ClassWithDefaultValueStaticFields.int +// static member Bool = ClassWithDefaultValueStaticFields.bool +// static member String = ClassWithDefaultValueStaticFields.string +// static member Guid = ClassWithDefaultValueStaticFields.guid +// static member Char = ClassWithDefaultValueStaticFields.char +// static member TimeSpan = ClassWithDefaultValueStaticFields.timeSpan +// static member AllowNullLiteralClass = ClassWithDefaultValueStaticFields.allowNullLiteralClass +// static member NullableInt = ClassWithDefaultValueStaticFields.nullableInt +// static member Tuple2 = ClassWithDefaultValueStaticFields.tuple2 +// static member DateTime = ClassWithDefaultValueStaticFields.dateTime +// static member TimeOnly = ClassWithDefaultValueStaticFields.timeOnly +// static member DateOnly = ClassWithDefaultValueStaticFields.dateOnly +// static member DateTimeOffset = ClassWithDefaultValueStaticFields.dateTimeOffset +// static member Int8 = ClassWithDefaultValueStaticFields.int8 +// static member UInt8 = ClassWithDefaultValueStaticFields.uInt8 +// static member Int16 = ClassWithDefaultValueStaticFields.int16 +// static member UInt16 = ClassWithDefaultValueStaticFields.uInt16 +// static member Int32 = ClassWithDefaultValueStaticFields.int32 +// static member UInt32 = ClassWithDefaultValueStaticFields.uInt32 +// static member Int64 = ClassWithDefaultValueStaticFields.int64 +// static member UInt64 = ClassWithDefaultValueStaticFields.uInt64 +// static member BigInt = ClassWithDefaultValueStaticFields.bigInt +// static member NativeInt = ClassWithDefaultValueStaticFields.nativeInt +// static member UNativeInt = ClassWithDefaultValueStaticFields.uNativeInt +// static member Float32 = ClassWithDefaultValueStaticFields.float32 +// static member Float64 = ClassWithDefaultValueStaticFields.float64 +// static member Decimal = ClassWithDefaultValueStaticFields.decimal +// static member StructTuple2 = ClassWithDefaultValueStaticFields.structTuple2 + +[] +type NestedStruct = + val A: float + val B: bool + new(a, b) = { A = a; B = b } + +[] +type TopLevelStruct = + val A: float + val B: NestedStruct + new(a, b) = { A = a; B = b } + // type Default1 = int // type Distinct1 = @@ -509,10 +639,36 @@ type MangledAbstractClass5(v) = inherit MangledAbstractClass4(v + 5) override _.MyMethod(x: int) = base.MyMethod(x) + v + 7 +[] +type AbstractClassWithResizeArrayProp() = + abstract Warnings: ResizeArray with get + +type IGenericMethodHelpers<'JsonValue> = + abstract encodeString: string -> 'JsonValue + +type IGenericMethodEncodable = + abstract member Encode<'JsonValue> : helpers: IGenericMethodHelpers<'JsonValue> -> 'JsonValue + +// Class implementation that names the method's type parameter ('json) differently +// from the interface ('JsonValue) +type GenericMethodEncodableClass() = + interface IGenericMethodEncodable with + member _.Encode<'json>(helpers: IGenericMethodHelpers<'json>) = helpers.encodeString "x" + +type IGenericMethodBox<'T> = + abstract Get: unit -> 'T + +// The lambda inside `Get` references the outer generic parameter 'item (via the +// captured `value`); it must not redeclare and shadow it. +let mapGenericMethodBox<'item> (value: 'item) : IGenericMethodBox<'item seq> = + { new IGenericMethodBox<'item seq> with + member _.Get() : 'item seq = Seq.map (fun (x: 'item) -> value) [ value ] + } + type ConcreteClass1() = inherit MangledAbstractClass5(2) -// See #3895 - super call in multi-level generic class hierarchy used wrong mangled name +// See #3895 - super call with generic class hierarchy uses wrong overload hash type IGenericAttach3895<'C> = abstract Attach: 'C -> unit @@ -610,6 +766,62 @@ type Model = unit let update (model: Model) = model, () +// Test types for generic parameter static member resolution +type TestTypeA = + static member GetValue() = "A" + static member Combine(x: int, y: int) = x + y + 100 + +type TestTypeB = + static member GetValue() = "B" + static member Combine(x: int, y: int) = x + y + 200 + +type TestTypeC = + static member GetValue() = "C" + static member Combine(x: int, y: int) = x + y + 300 + +// Inline functions for testing multiple generic parameters with static member constraints +let inline getTwoValues<'a, 'b when 'a: (static member GetValue: unit -> string) + and 'b: (static member GetValue: unit -> string)> () = + 'a.GetValue(), 'b.GetValue() + +let inline getThreeValues<'a, 'b, 'c when 'a: (static member GetValue: unit -> string) + and 'b: (static member GetValue: unit -> string) + and 'c: (static member GetValue: unit -> string)> () = + 'a.GetValue(), 'b.GetValue(), 'c.GetValue() + +let inline getValuesAndCombine<'a, 'b when 'a: (static member GetValue: unit -> string) + and 'a: (static member Combine: int * int -> int) + and 'b: (static member GetValue: unit -> string) + and 'b: (static member Combine: int * int -> int)> x y = + let aVal = 'a.GetValue() + let bVal = 'b.GetValue() + let aCombined = 'a.Combine(x, y) + let bCombined = 'b.Combine(x, y) + (aVal, aCombined), (bVal, bCombined) + +let inline getReversed<'x, 'y when 'x: (static member GetValue: unit -> string) + and 'y: (static member GetValue: unit -> string)> () = + 'y.GetValue(), 'x.GetValue() + +let inline innerGet<'t when 't: (static member GetValue: unit -> string)> () = + 't.GetValue() + +let inline outerGet<'a, 'b when 'a: (static member GetValue: unit -> string) + and 'b: (static member GetValue: unit -> string)> () = + innerGet<'a>(), innerGet<'b>() + +// type LazyTree<'a> = LazyNode of 'a * LazyTree<'a> list + +// let describeLazy (x: Lazy<'a>) : string = string (x.Force()) + +// let walkLazyTree (tree: LazyTree>) : string = +// let rec loop (LazyNode(root, xs)) = +// match xs with +// | [] -> describeLazy root +// | x :: _ -> loop x + +// loop tree + [] let ``Unit arguments work`` () = update () |> equal ((), ()) @@ -719,8 +931,9 @@ let ``Can implement interface optional properties with object expression`` () = // StaticClass.DefaultParam2(Nullable()) |> equal 3 // StaticClass.DefaultParam2() |> equal 3 +// // See #3326 // [] -// let ``DefaultParameterValue works with null`` () = // See #3326 +// let ``DefaultParameterValue works with null`` () = // StaticClass.DefaultNullParam() |> isNull |> equal true // StaticClass.DefaultNullParam(5) |> isNull |> equal false @@ -863,8 +1076,9 @@ let ``Type test with BigInt`` () = box 5I |> isBigInt |> equal true box 50 |> isBigInt |> equal false +// See #168 [] -let ``Property names don't clash with built-in JS objects`` () = // See #168 +let ``Property names don't clash with built-in JS objects`` () = let gameState = { Now = 1 Map = "dungeon" @@ -992,6 +1206,11 @@ let ``Lazy constructor works`` () = search "b" |> equal (Some "b") search "d" |> equal None +// [] +// let ``Local function generic over a Lazy subtype constraint works`` () = +// LazyNode(lazy 5, [LazyNode(lazy 7, [])]) |> walkLazyTree |> equal "7" +// LazyNode(lazy 5, []) |> walkLazyTree |> equal "5" + [] let ``Secondary constructors work`` () = let s1 = SecondaryCons(3) @@ -1013,8 +1232,9 @@ let ``Multiple constructors work`` () = equal 9 m2.Value equal 14 m3.Value +// // See #505 // [] -// let ``Abstract methods with default work`` () = // See #505 +// let ``Abstract methods with default work`` () = // let x = ConcreteClass() // x.MethodWithDefault() |> equal "Hello " // x.MustImplement() |> equal "World!!" @@ -1028,8 +1248,9 @@ let ``Abstract properties with getters and setters work`` () = x.MyProp <- 2 equal 7 x.MyProp +// See #505 [] -let ``Interface setters don't conflict`` () = // See #505 +let ``Interface setters don't conflict`` () = let x = XISomeInterface () :> ISomeInterface x.Sender |> equal 0 x.Sender <- 5 @@ -1071,8 +1292,9 @@ let ``Interface setters don't conflict`` () = // See #505 // (foo :> IFoo).Foo() |> equal "BARFOO" // mangleFoo foo |> equal "BARFOO" +// // See #1452 // [] -// let ``Interface casting round-trip`` () = // See #1452 +// let ``Interface casting round-trip`` () = // let d = new DowncastTest(3) :> System.IDisposable // let t = d :?> DowncastTest // t.Value |> equal 3 @@ -1081,32 +1303,37 @@ let ``Interface setters don't conflict`` () = // See #505 // | :? DowncastTest as t2 -> t2.Value // | _ -> 5 +// See #701 [] -let ``Calling default implementation of base members don't cause infinite recursion`` () = // See #701 +let ``Calling default implementation of base members don't cause infinite recursion`` () = let x = ExtendedClass() x.Init() |> equal 7 (x :> BaseClass).Init() |> equal 7 +// See #701 [] -let ``Calling default implementation of base properties don't cause infinite recursion`` () = // See #701 +let ``Calling default implementation of base properties don't cause infinite recursion`` () = let x = ExtendedClass() x.Prop |> equal "base-extension" (x :> BaseClass).Prop |> equal "base-extension" +// See #1464 [] -let ``Calling base members works`` () = // See #1464 +let ``Calling base members works`` () = let bar = ExtendedClass2() bar.B() |> equal 1 +// See #569 [] -let ``Circular dependencies work`` () = // See #569 +let ``Circular dependencies work`` () = let location = { name="NY"; employees=[] } let alice = { name="Alice"; age=20.0; location=location } location.name |> equal "NY" alice.age |> equal 20. +// See #568 [] -let ``Value Type records work`` () = // See #568 +let ``Value Type records work`` () = let foo1 = ValueType<_>("foo") let foo2 = ValueType<_>("foo") foo1.Value |> equal "foo" @@ -1151,6 +1378,32 @@ let ``struct without explicit ctor works`` () = t1 |> equal t2 (compare t1 t2) |> equal 0 +[] +let ``Instance DefaultValue fields are skipped by explicit constructors`` () = + let c = ClassWithDefaultValueInstanceFields(1, "y") + c.X |> equal 1 + c.D |> equal 0 + c.S |> equal null + c.Y |> equal "y" + c.D <- 5 + c.S <- "s" + c.D |> equal 5 + c.S |> equal "s" + + let mutable s = StructWithDefaultValueInstanceField(2, "z") + s.X |> equal 2 + s.D |> equal 0 + s.Y |> equal "z" + s.D <- 6 + s.D |> equal 6 + +[] +let ``Explicit constructors skip static fields next to DefaultValue fields`` () = + let c = ClassWithDefaultValueStaticAndInstanceFields(1, 2) + c.X |> equal 1 + c.D |> equal 0 + c.Y |> equal 2 + [] let ``Struct with mutable fields works`` () = let x = ValueTypeR(-10.0) @@ -1164,8 +1417,9 @@ let ``Unchecked.defaultof works for fields on nested structs`` () = top.B.A |> equal 0 top.B.B |> equal false +// See #3371 [] -let ``copying struct records works`` () = // See #3371 +let ``copying struct records works`` () = let simple : SimpleRecord = { A = ""; B = "B" } let simpleRecord = { simple with A = "A" } simpleRecord.A |> equal "A" @@ -1173,6 +1427,14 @@ let ``copying struct records works`` () = // See #3371 simple.A |> equal "" simple.B |> equal "B" +// See #3828 +[] +let ``copy-update expression on this in struct member works`` () = + let r : SimpleRecord = { A = "hello"; B = "world" } + let r2 = r.WithA("foo") + r2.A |> equal "foo" + r2.B |> equal "world" + // [] // let ``Custom F# exceptions work`` () = // try @@ -1205,8 +1467,9 @@ let ``copying struct records works`` () = // See #3371 // with ex -> ex.Message // |> equal "Will I be reraised?" +// // See #1444 // [] -// let ``This context is not lost in closures within implicit constructor`` () = // See #1444 +// let ``This context is not lost in closures within implicit constructor`` () = // ThisContextInConstructor(7).Value() |> equal 7 // [] @@ -1218,8 +1481,9 @@ let ``copying struct records works`` () = // See #3371 // f1.Add2(4, 5) |> equal -1 // f2.Add2(4, 5) |> equal -1 +// See #573 [] -let ``ClassAttribute works`` () = // See #573 +let ``ClassAttribute works`` () = let t1 = TypeWithClassAttribute(8) t1.Pos |> equal 8 @@ -1242,8 +1506,9 @@ let ``ClassAttribute works`` () = // See #573 // // withDefaultValue.ObjValue |> equal Unchecked.defaultof> // // withDefaultValue.ObjValue |> equal null +// // See #2070 // [] -// let ``Private fields don't conflict with parent classes`` () = // See #2070 +// let ``Private fields don't conflict with parent classes`` () = // let a1 = InfoBClass({ InfoA = { Foo = "foo" }; Bar = "bar" }) :> InfoAClass // let a2 = a1.WithFoo("foo2") // a1.Foo |> equal "foo" @@ -1386,8 +1651,9 @@ let ``Non-mangled interfaces work with classes`` () = // typeof.IsInstanceOfType("hello") |> equal false // #if FABLE_COMPILER +// // See #2485 // [] -// let ``Choice with arity 3+ is represented correctly`` () = // See #2485 +// let ``Choice with arity 3+ is represented correctly`` () = // Choice2Of3 55 |> Fable.Core.Reflection.getCaseName |> equal "Choice2Of3" // Choice3Of3 55 |> Fable.Core.Reflection.getCaseName |> equal "Choice3Of3" // #endif @@ -1406,6 +1672,124 @@ let ``Super call works correctly in multi-level generic class hierarchy`` () = // Each override delegates to base before logging, so the chain unwinds Base -> Mid -> Leaf List.ofSeq log |> equal [ "Base"; "Mid"; "Leaf" ] +// Test for generic type parameter static member resolution in inline functions +// https://github.com/fable-compiler/Fable/issues/4093 +[] +let ``Inline function with two generic parameters resolves static members correctly`` () = + let result = getTwoValues() + result |> equal ("A", "B") + +[] +let ``Inline function with three generic parameters resolves static members correctly`` () = + let result = getThreeValues() + result |> equal ("A", "B", "C") + +[] +let ``Inline function with multiple constraints per type parameter works`` () = + let result = getValuesAndCombine 10 20 + result |> equal (("A", 130), ("B", 230)) + +[] +let ``Inline function with reversed type parameter order works`` () = + let result = getReversed() + result |> equal ("B", "A") + +[] +let ``Nested inline functions resolve generic parameters correctly`` () = + let result = outerGet() + result |> equal ("A", "B") + +[] +let ``Different type parameter combinations work correctly`` () = + let result1 = getTwoValues() + result1 |> equal ("B", "A") + + let result2 = getTwoValues() + result2 |> equal ("C", "A") + + let result3 = getTwoValues() + result3 |> equal ("B", "C") + +// // See https://github.com/fable-compiler/Fable/issues/2739 +// [] +// let ``Static [] fields are zero-initialized`` () = +// ClassWithDefaultValueStaticFields.Int |> equal 0 +// ClassWithDefaultValueStaticFields.Bool |> equal false +// ClassWithDefaultValueStaticFields.String |> equal null +// ClassWithDefaultValueStaticFields.Guid |> equal Guid.Empty +// ClassWithDefaultValueStaticFields.Char |> equal '\000' +// ClassWithDefaultValueStaticFields.TimeSpan |> equal TimeSpan.Zero +// ClassWithDefaultValueStaticFields.TimeOnly |> equal TimeOnly.MinValue +// ClassWithDefaultValueStaticFields.DateOnly |> equal DateOnly.MinValue +// ClassWithDefaultValueStaticFields.DateTime |> equal DateTime.MinValue +// ClassWithDefaultValueStaticFields.DateTimeOffset |> equal DateTimeOffset.MinValue +// ClassWithDefaultValueStaticFields.Int8 |> equal 0y +// ClassWithDefaultValueStaticFields.UInt8 |> equal 0uy +// ClassWithDefaultValueStaticFields.Int16 |> equal 0s +// ClassWithDefaultValueStaticFields.UInt16 |> equal 0us +// ClassWithDefaultValueStaticFields.Int32 |> equal 0 +// ClassWithDefaultValueStaticFields.UInt32 |> equal 0u +// ClassWithDefaultValueStaticFields.Int64 |> equal 0L +// ClassWithDefaultValueStaticFields.UInt64 |> equal 0UL +// ClassWithDefaultValueStaticFields.BigInt |> equal 0I +// ClassWithDefaultValueStaticFields.NativeInt |> equal 0n +// ClassWithDefaultValueStaticFields.UNativeInt |> equal 0un +// ClassWithDefaultValueStaticFields.Float32 |> equal 0.f +// ClassWithDefaultValueStaticFields.Float64 |> equal 0. +// ClassWithDefaultValueStaticFields.Decimal |> equal 0M + +// ClassWithDefaultValueStaticFields.IncrCount() |> equal 1 + +[] +let ``Unchecked.defaultof works for fields on structs`` () = + let top = TopLevelStruct() + top.A |> equal 0 + top.B.A |> equal 0 + top.B.B |> equal false + +// [] +// let ``Abstract class property backed by captured variable in object expression works`` () = +// let warnings = ResizeArray() + +// let reader = +// { new AbstractClassWithResizeArrayProp() with +// member __.Warnings = warnings +// } + +// reader.Warnings.Add("Warning 1") +// reader.Warnings.Add("Warning 2") + +// reader.Warnings.Count |> equal 2 + +// [] +// let ``Object expression implementing a generic interface method works`` () = +// let helpers = +// { new IGenericMethodHelpers with +// member _.encodeString v = "S:" + v +// } + +// let encodable = +// { new IGenericMethodEncodable with +// member _.Encode(helpers) = helpers.encodeString "x" +// } + +// encodable.Encode(helpers) |> equal "S:x" + +// [] +// let ``Class implementing a generic interface method with a renamed type parameter works`` () = +// let helpers = +// { new IGenericMethodHelpers with +// member _.encodeString v = "S:" + v +// } + +// let encodable = GenericMethodEncodableClass() :> IGenericMethodEncodable +// encodable.Encode(helpers) |> equal "S:x" + +[] +let ``Nested lambda does not shadow an outer generic type parameter`` () = + let box = mapGenericMethodBox 42 + box.Get() |> List.ofSeq |> equal [ 42 ] + [] let ``ArgumentException with message and inner exception works`` () = let inner = exn "the inner cause" @@ -1417,3 +1801,37 @@ let ``ArgumentException with message and inner exception works`` () = let ``Exception InnerException is null when not provided`` () = let ex = System.ArgumentException("no inner") isNull (box ex.InnerException) |> equal true + +// Constructing an F# exception value used to crash the compiler outright: +// getEntityFieldsAsIdents prepends a synthetic __base__ field, and makeRecord +// zipped it against a value list that has no base value. Raising and catching +// one by its pattern is a separate matter and still unsupported here, so this +// stays on the construct/destructure path that a fixed makeRecord covers. +[] +let ``F# exception values can be constructed and destructured`` () = + let e = MyEx(4, "ERROR") + + match e with + | MyEx(code, msg) -> + code |> equal 4 + msg |> equal "ERROR" + | _ -> failwith "expected MyEx" + +// `match (o: obj) with :? string as s` binds s as the tested type, and codegen +// emits `if let Some(s) = try_downcast(..)`. try_downcast returned a reference, +// so s came out as &T wherever a T was expected and the crate did not compile. +let private describeObj (o: obj) = + match o with + | :? string as s -> "'" + s + "'" + | :? int as i -> string i + | :? float as f -> string f + | :? bool as b -> (if b then "1" else "0") + | _ -> "?" + +[] +let ``type test binding on obj yields a usable value`` () = + describeObj (box "a") |> equal "'a'" + describeObj (box 7) |> equal "7" + describeObj (box 2.5) |> equal "2.5" + describeObj (box true) |> equal "1" + describeObj (box 'c') |> equal "?" diff --git a/tests/Rust/tests/src/UnionTests.fs b/tests/Rust/tests/src/UnionTests.fs index 1f095aaf67..3b8378a02d 100644 --- a/tests/Rust/tests/src/UnionTests.fs +++ b/tests/Rust/tests/src/UnionTests.fs @@ -2,190 +2,7 @@ module Fable.Tests.UnionTests open Util.Testing -type IntUnion = - | IntCase1 of int - | IntCase2 of int - | IntCase3 of int - -[] -type Point2D = - | Planar2D of float * float - | Infinity - -[] -let ``Struct unions work`` () = - let distance p = - match p with - | Planar2D (x, y) -> sqrt (x * x + y * y) - | Infinity -> infinity - let p = Planar2D (3, 4) - let res = distance p - res |> equal 5. - -[] -let ``Union case matching works`` () = - let x = IntCase1 5 - let res = - match x with - | IntCase1 a -> a - | IntCase2 b -> b - | IntCase3 c -> c - res |> equal 5 - -[] -let ``Union case equality works`` () = - IntCase1 5 = IntCase1 5 |> equal true - IntCase1 5 = IntCase2 5 |> equal false - IntCase3 2 = IntCase3 3 |> equal false - IntCase2 1 = IntCase2 1 |> equal true - IntCase3 1 = IntCase3 1 |> equal true - -let unionFnAlways1 = function - | IntCase1 x -> x - | _ -> -1 - -let unionFnRetNum = function - | IntCase1 a -> a - | IntCase2 b -> b - | IntCase3 c -> c - -[] -let ``Union fn call works`` () = - let x = IntCase1 3 - let res = unionFnAlways1 x - let res2 = unionFnAlways1 x //deliberately force clone/borrow break - let res3 = unionFnRetNum x - let res4 = unionFnRetNum (IntCase2 24) - res |> equal 3 - res2 |> equal 3 - res3 |> equal 3 - res4 |> equal 24 - -type WrappedUnion = - | AString of string - -[] -let ``Union with wrapped type works`` () = - let a = AString "hello" - let b = match a with AString s -> s + " world" - b |> equal "hello world" - -type DeepRecord = {Value: string} - -type DeepWrappedUnion = - | DeepWrappedA of string * DeepRecord - | DeepWrappedB of string - | DeepWrappedC of int - | DeepWrappedD of DeepRecord - | DeepWrappedE of int * int - | DeepWrappedF of WrappedUnion - | DeepWrappedG of {| X: DeepRecord; Y: int|} - -let matchStrings = function - | DeepWrappedA (s, d) -> d.Value + s - | DeepWrappedB s -> s - | DeepWrappedC c -> "nothing" - | DeepWrappedD d -> d.Value - | DeepWrappedE(a, b) -> "nothing2" - | DeepWrappedF (AString s) -> s - | DeepWrappedG x -> x.X.Value - -let matchNumbers = function - | DeepWrappedA (s, d) -> 0 - | DeepWrappedB s -> 0 - | DeepWrappedC c -> c - | DeepWrappedD d -> 0 - | DeepWrappedE(a, b) -> a + b - | DeepWrappedF _ -> 0 - | DeepWrappedG x -> x.Y - -[] -let ``Deep union with wrapped type works`` () = - let a = DeepWrappedA (" world", {Value = "hello"}) - let b = DeepWrappedB "world" - let c = DeepWrappedC 42 - let d = DeepWrappedD { Value = "hello" } - let f = DeepWrappedF (AString "doublewrapped") - let g = DeepWrappedG {|X={Value="G"}; Y=365|} - a |> matchStrings |> equal "hello world" - b |> matchStrings |> equal "world" - c |> matchStrings |> equal "nothing" - d |> matchStrings |> equal "hello" - f |> matchStrings |> equal "doublewrapped" - g |> matchStrings |> equal "G" - -[] -let ``Deep union with tuped prim type works`` () = - let e = DeepWrappedE (3, 2) - let c = DeepWrappedC 42//todo to string! - let g = DeepWrappedG {|X={Value="G"}; Y=365|} - e |> matchNumbers |> equal 5 - c |> matchNumbers |> equal 42 - g |> matchNumbers |> equal 365 - -[] -let ``Multi-case Union with wrapped type works`` () = - let b = DeepWrappedB "hello" - let res = match b with DeepWrappedB s -> s + " world" | _ -> "" - res |> equal "hello world" - -[] -let ``Should correctly import union in other file and allow creation`` () = - let r = Common.Imports.MyUnion.A 1 - let expected = Common.Imports.MyUnion.createA 1 - r |> equal expected - - -let matchStringWhenStringNotHello = function - | DeepWrappedB s when s <> "hello" -> "not hello" - | _ -> "hello" - -[] -let ``Match with condition works`` () = - let b1 = DeepWrappedB "hello" - let b2 = DeepWrappedB "not" - b1 |> matchStringWhenStringNotHello |> equal "hello" - b2 |> matchStringWhenStringNotHello |> equal "not hello" - -[] -type ArcUnion = - | ArcS of string - | ArcI of int - -let add1 = function - | ArcS s -> s + "1" |> ArcS - | ArcI i -> i + 1 |> ArcI - -[] -let ``Union with pointer type works`` () = - let a = ArcS "hello" - let b = ArcI 42 - let aa = add1 a - let bb = add1 b - aa |> equal (ArcS "hello1") - bb |> equal (ArcI 43) - -#if FABLE_COMPILER -open Fable.Core - -[")>] -let ensureMyUnionWrapped s = nativeOnly - -[] -let ensureIsStructUnionUnwrapped s = nativeOnly - -[] -let ``Normal union should be wrapped in a Lrc`` () = - IntCase1 1 |> ensureMyUnionWrapped |> ignore - -[] -let ``Struct union should not be wrapped in a Lrc`` () = - Infinity |> ensureIsStructUnionUnwrapped |> ignore - -#endif - - -// type Gender = Male | Female +type Gender = Male | Female // type Either<'TL,'TR> = // | Left of 'TL @@ -246,9 +63,9 @@ type Tree = [] type MyUnion = -| Case1 -| Case2 -| Case3 + | Case1 + | Case2 + | Case3 type R = { Name: string @@ -274,6 +91,119 @@ type T8 = T8 type T9 = T9 type 'a more = More of 'a +// [] +// type LangCode = +// | EN +// | ``pt-Br`` + +type S = S of string + +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First + +[] +type StructPoint2D = + | Planar2D of float * float + | Infinity + +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + member this.Area = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + member this.Area = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + +type WrappedUnion = + | AString of string + +type DeepRecord = { Value: string } + +type DeepWrappedUnion = + | DeepWrappedA of string * DeepRecord + | DeepWrappedB of string + | DeepWrappedC of int + | DeepWrappedD of DeepRecord + | DeepWrappedE of int * int + | DeepWrappedF of WrappedUnion + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" + // [] // let ``Union cases matches with no arguments can be generated`` () = // let x = Male @@ -382,7 +312,7 @@ let ``Union cases called Tag still work (bug due to Tag field)`` () = [] let ``Equality works in filter`` () = - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> r.Case = MyUnion.Case1) |> Array.length @@ -390,12 +320,20 @@ let ``Equality works in filter`` () = [] let ``Pattern matching works in filter`` () = - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) |> Array.length |> equal 2 +[] +let ``Union field can have the same name as its type`` () = + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + // #if FABLE_COMPILER // [] // let ``Erased union type testing works`` () = @@ -458,4 +396,199 @@ let ``Compiler does not complain if the same type appears twice in nested U9`` ( U9> = !^42 x |> equal (U9.Case4 42) +[] +let ``Implicit conversion to erased unions works`` () = + let s: U2 = "foo" + s |> equal (U2.Case2 "foo") + let b: U3 = true + b |> equal (U3.Case3 true) + +#endif + +// [] +// let ``Verify that union cases can have `-` in their names`` () = +// // See https://github.com/fable-compiler/Fable/issues/4071 +// let x = LangCode.``pt-Br`` +// equal x LangCode.``pt-Br`` + +// [] +// let ``sprintf formats strings cases correctly`` () = +// let s = sprintf "%A" (S "1") +// s |> equal "S \"1\"" + +// [] +// let ``Union cases with no fields are physically equal`` () = +// obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true +// obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false +// obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true +// obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false +// obj.ReferenceEquals(T1, T1) |> equal true + +[] +let ``Named union case fields work when a field is called name`` () = + let value = NamedCase("v1", "v2") + match value with + | NamedCase(name, optional) -> + name |> equal "v1" + optional |> equal "v2" + +[] +let ``Static union member returning the union type works`` () = + Example.All |> Array.length |> equal 2 + Example.All.[0] |> equal Example.First + Example.All.[1] |> equal Example.Second + +[] +let ``Static union member with nested self-reference works`` () = + Example.Head |> equal (Some Example.First) + Example.Nested |> List.head |> Array.head |> equal Example.First + Example.Pair |> fst |> equal Example.First + Example.Pair |> snd |> equal Example.Second + Example.Factory() |> equal Example.First + +[] +let ``Struct unions work`` () = + let distance p = + match p with + | Planar2D (x, y) -> sqrt (x * x + y * y) + | Infinity -> infinity + let p = Planar2D (3, 4) + let res = distance p + res |> equal 5. + +[] +let ``Struct unions with fields work`` () = + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + +[] +let ``Struct unions without fields work`` () = + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + +[] +let ``Nested union match keeps case-specific field access`` () = + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + +[] +let ``Union case matching works`` () = + let x = IntCase1 5 + let res = + match x with + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + res |> equal 5 + +[] +let ``Union case equality works`` () = + IntCase1 5 = IntCase1 5 |> equal true + IntCase1 5 = IntCase2 5 |> equal false + IntCase3 2 = IntCase3 3 |> equal false + IntCase2 1 = IntCase2 1 |> equal true + IntCase3 1 = IntCase3 1 |> equal true + +[] +let ``Union fn call works`` () = + let x = IntCase1 3 + let res = unionFnAlways1 x + let res2 = unionFnAlways1 x //deliberately force clone/borrow break + let res3 = unionFnRetNum x + let res4 = unionFnRetNum (IntCase2 24) + res |> equal 3 + res2 |> equal 3 + res3 |> equal 3 + res4 |> equal 24 + +[] +let ``Union with wrapped type works`` () = + let a = AString "hello" + let b = match a with AString s -> s + " world" + b |> equal "hello world" + +[] +let ``Deep union with wrapped type works`` () = + let a = DeepWrappedA (" world", {Value = "hello"}) + let b = DeepWrappedB "world" + let c = DeepWrappedC 42 + let d = DeepWrappedD { Value = "hello" } + let f = DeepWrappedF (AString "doublewrapped") + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + a |> matchStrings |> equal "hello world" + b |> matchStrings |> equal "world" + c |> matchStrings |> equal "nothing" + d |> matchStrings |> equal "hello" + f |> matchStrings |> equal "doublewrapped" + g |> matchStrings |> equal "G" + +[] +let ``Deep union with tuped prim type works`` () = + let e = DeepWrappedE (3, 2) + let c = DeepWrappedC 42 //todo to string! + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + e |> matchNumbers |> equal 5 + c |> matchNumbers |> equal 42 + g |> matchNumbers |> equal 365 + +[] +let ``Multi-case Union with wrapped type works`` () = + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" + +[] +let ``Match with condition works`` () = + let b1 = DeepWrappedB "hello" + let b2 = DeepWrappedB "not" + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" + +[] +let ``Should correctly import union in other file and allow creation`` () = + let r = Common.Imports.MyUnion.A 1 + let expected = Common.Imports.MyUnion.createA 1 + r |> equal expected + +#if FABLE_COMPILER +open Fable.Core + +[] +type ArcUnion = + | ArcS of string + | ArcI of int + +let add1 = function + | ArcS s -> s + "1" |> ArcS + | ArcI i -> i + 1 |> ArcI + +[] +let ``Union with pointer type works`` () = + let a = ArcS "hello" + let b = ArcI 42 + let aa = add1 a + let bb = add1 b + aa |> equal (ArcS "hello1") + bb |> equal (ArcI 43) + +[")>] +let ensureMyUnionWrapped s = nativeOnly + +[] +let ensureIsStructUnionUnwrapped s = nativeOnly + +[] +let ``Normal union should be wrapped in a Lrc`` () = + IntCase1 1 |> ensureMyUnionWrapped |> ignore + +[] +let ``Struct union should not be wrapped in a Lrc`` () = + Infinity |> ensureIsStructUnionUnwrapped |> ignore + #endif diff --git a/tests/Rust/tests/src/UriTests.fs b/tests/Rust/tests/src/UriTests.fs index d3c6eb5526..b2c1f66550 100644 --- a/tests/Rust/tests/src/UriTests.fs +++ b/tests/Rust/tests/src/UriTests.fs @@ -64,6 +64,33 @@ let ``Uri from baseUri with relative string works`` () = equal "#c" uri.Fragment equal "http://www.test2.com/hello?a=b#c" uri.AbsoluteUri +[] +let ``Uri relative resolution preserves base query and fragment when omitted`` () = + let baseUri = Uri("http://host/a/b?old=1#base") + let same = Uri(baseUri, "") + equal "http://host/a/b?old=1#base" same.AbsoluteUri + + let fragmentOnly = Uri(baseUri, "#new") + equal "http://host/a/b?old=1#new" fragmentOnly.AbsoluteUri + + let queryOnly = Uri(baseUri, "?new=2") + equal "http://host/a/b?new=2" queryOnly.AbsoluteUri + +[] +let ``Uri host excludes user info and exposes ports`` () = + let uri = Uri("http://user:pass@host:8080/path") + equal "host" uri.Host + equal 8080 uri.Port + equal false uri.IsDefaultPort + + let defaultUri = Uri("http://host/path") + equal 80 defaultUri.Port + equal true defaultUri.IsDefaultPort + + let ftpUri = Uri("ftp://host/path") + equal 21 ftpUri.Port + equal true ftpUri.IsDefaultPort + [] let ``Uri from baseUri with relativeUri works`` () = let uri = Uri(Uri("http://www.test3.com/"), Uri("/hello?a=b#c", UriKind.Relative)) @@ -85,6 +112,15 @@ let ``Uri from baseUri with absolute Uri works`` () = equal "www.example.com" uri.Host equal "http://www.example.com/" uri.AbsoluteUri +[] +let ``Uri from baseUri with a different absolute Uri yields the absolute one`` () = + // .NET: when the second URI is absolute it wins and the base is ignored. + let baseUri = Uri("http://www.base.com/", UriKind.Absolute) + let absUri = Uri("http://www.other.com/path", UriKind.Absolute) + let uri = Uri(baseUri, absUri) + equal "www.other.com" uri.Host + equal "http://www.other.com/path" uri.AbsoluteUri + [] let ``TryCreate from absolute string with kind Absolute works`` () = let (valid, uri) = Uri.TryCreate("http://www.test0.com/hello?a=b#c", UriKind.Absolute) @@ -140,6 +176,21 @@ let ``TryCreate from absolute uri and absolute uri should work`` () = equal true valid equal absUri uri +[] +let ``TryCreate from absolute uri and relative string works`` () = + let (valid, uri) = Uri.TryCreate(Uri("https://example.com/base"), "child") + equal true valid + equal "https://example.com/child" uri.AbsoluteUri + +[] +let ``TryCreate from absolute uri and different absolute uri yields the second`` () = + // .NET: when the second URI is absolute it wins and the base is ignored. + let baseUri = Uri("http://www.base.com/", UriKind.Absolute) + let other = Uri("http://www.other.com/path", UriKind.Absolute) + let (valid, uri) = Uri.TryCreate(baseUri, other) + equal true valid + equal "www.other.com" uri.Host + [] let ``TryCreate from absolute uri and relative uri should work`` () = let (valid, uri) = Uri.TryCreate(Uri("https://example.com", UriKind.Absolute), Uri("test", UriKind.Relative)) @@ -163,6 +214,27 @@ let ``Uri.ToString works`` () = uri.ToString() |> equal "http://www.test4.com/a b c.html" sprintf "%A" uri |> equal "http://www.test4.com/a b c.html" +[] +let ``Uri from authority-less absolute scheme works`` () = + // mailto:/tel:/urn: are absolute URIs without an authority; must not panic. + let mailto = Uri("mailto:a@b.com") + equal true mailto.IsAbsoluteUri + equal "mailto" mailto.Scheme + equal "mailto:a@b.com" mailto.AbsoluteUri + let tel = Uri("tel:+1") + equal true tel.IsAbsoluteUri + equal "tel" tel.Scheme + let urn = Uri("urn:x:y") + equal true urn.IsAbsoluteUri + equal "urn" urn.Scheme + +[] +let ``Uri relative resolution collapses dot segments`` () = + let uri = Uri(Uri("http://h/a/b"), "../c") + equal "http://h/c" uri.AbsoluteUri + let uri2 = Uri(Uri("http://h/a/b/"), "../../x") + equal "http://h/x" uri2.AbsoluteUri + [] let ``Uri.OriginalString works`` () = let cases = [ @@ -174,3 +246,21 @@ let ``Uri.OriginalString works`` () = for case in cases do let uri = Uri(case) uri.OriginalString |> equal case + +[] +let ``Uri.EscapeDataString works`` () = + Uri.EscapeDataString("Hello World!") |> equal "Hello%20World%21" + Uri.EscapeDataString("aA1-_.~") |> equal "aA1-_.~" + // multi-byte chars percent-encode each UTF-8 byte + Uri.EscapeDataString("ä") |> equal "%C3%A4" + +[] +let ``Uri.UnescapeDataString works`` () = + Uri.UnescapeDataString("Hello%20World%21") |> equal "Hello World!" + Uri.UnescapeDataString("%C3%A4") |> equal "ä" + // an invalid escape sequence is left untouched + Uri.UnescapeDataString("100%") |> equal "100%" + +[] +let ``Uri.EscapeUriString keeps reserved characters`` () = + Uri.EscapeUriString("http://test.com/a b?c=d&e") |> equal "http://test.com/a%20b?c=d&e" diff --git a/tests/Rust/tests/src/main.fs b/tests/Rust/tests/src/lib.fs similarity index 51% rename from tests/Rust/tests/src/main.fs rename to tests/Rust/tests/src/lib.fs index 6a938fa462..dd4a08ea7a 100644 --- a/tests/Rust/tests/src/main.fs +++ b/tests/Rust/tests/src/lib.fs @@ -5,7 +5,3 @@ open Fable.Core.Rust let _imports() = importAll "./ExtInteropTests.rs" () - -// [] -[] -let main _args = 0 diff --git a/tests/TypeScript/Fable.Tests.TypeScript.fsproj b/tests/TypeScript/Fable.Tests.TypeScript.fsproj index 580c200cc9..40ecc987de 100644 --- a/tests/TypeScript/Fable.Tests.TypeScript.fsproj +++ b/tests/TypeScript/Fable.Tests.TypeScript.fsproj @@ -33,6 +33,7 @@ + diff --git a/tests/TypeScript/LiteTsInteropTests.fs b/tests/TypeScript/LiteTsInteropTests.fs index cdb0e1797b..a33996f40f 100644 --- a/tests/TypeScript/LiteTsInteropTests.fs +++ b/tests/TypeScript/LiteTsInteropTests.fs @@ -26,10 +26,75 @@ module PojoInterface = member val name = name member val breed = breed member val age = age with get, set + +module TaggedUnion = + type Take = + abstract ``type``: string + abstract fromIndex: int + abstract toIndex: int + + type Edit = + abstract text: string + + [] + type Command = + | Take of Take + | Edit of edit: Edit + | Save + +// Top level values get TS annotations, so tsc checks the typed array type names exist +module TypedArrays = + let int8s = JS.Constructors.Int8Array.Create [| 1y; -2y |] + let uint8s = JS.Constructors.Uint8Array.Create [| 1uy; 2uy |] + let uint8Clamped = JS.Constructors.Uint8ClampedArray.Create [| 1; 300 |] + let int16s = JS.Constructors.Int16Array.Create [| 1s; -2s |] + let uint16s = JS.Constructors.Uint16Array.Create [| 1us; 2us |] + let int32s = JS.Constructors.Int32Array.Create [| 1; -2 |] + let uint32s = JS.Constructors.Uint32Array.Create [| 1u; 2u |] + let float32s = JS.Constructors.Float32Array.Create [| 1.5; -2.5 |] + let float64s = JS.Constructors.Float64Array.Create [| 1.5; -2.5 |] + let bigInt64s = JS.Constructors.BigInt64Array.Create 2 + let makeFloat32s () = JS.Constructors.Float32Array.Create [| 0.0; 1.0 |] + +// tsc checks the declarations against the unmangled names the JavaScript uses, see #5158 +module MangleInterfaces = + [] + type INotMangled = + abstract Foo: int -> int + abstract Bar: string + abstract Baz: int with get, set + + [] + type IMangled = + abstract Foo: int -> int + + let useNotMangled (x: INotMangled) = x.Foo 1, x.Bar + let incrementBaz (x: INotMangled) = x.Baz <- x.Baz + 1 + let useMangled (x: IMangled) = x.Foo 1 + +module MangleAbstractClasses = + [] + type AttachedBase() = + abstract Run: int -> int + + type AttachedImpl() = + inherit AttachedBase() + override _.Run x = x + 3 + + let useAttached (x: AttachedBase) = x.Run 1 #endif +type GenericNameRecord<'a> = + { Value: 'a } + + static member Stringify v = string v + let tests = testList "LiteTsInterop" [ + testCase "Compiler-generated generic names do not clash with the type's own" <| fun () -> + GenericNameRecord.Stringify 456 |> equal "456" + { Value = "x" }.Value |> equal "x" + #if FABLE_COMPILER testCase "jsOptions works" <| fun _ -> let opts = jsOptions(fun o -> @@ -60,5 +125,55 @@ let tests = dog.age |> equal None dog.age <- Some 5 dog.age |> equal (Some 5) + + testCase "TS tagged union case with an interface field compiles to the value itself" <| fun () -> + let take = + { new TaggedUnion.Take with + member _.``type`` = "take" + member _.fromIndex = 1 + member _.toIndex = 3 } + let describe = function + | TaggedUnion.Command.Take t -> sprintf "take: %d-%d" t.fromIndex t.toIndex + | TaggedUnion.Command.Edit e -> sprintf "edit: %s" e.text + | TaggedUnion.Command.Save -> "save" + let command = TaggedUnion.Command.Take take + obj.ReferenceEquals(command, take) |> equal true + command |> describe |> equal "take: 1-3" + TaggedUnion.Command.Edit { new TaggedUnion.Edit with member _.text = "hi" } |> describe |> equal "edit: hi" + TaggedUnion.Command.Save |> describe |> equal "save" + !!{| ``type`` = "take"; fromIndex = 2; toIndex = 5 |} |> describe |> equal "take: 2-5" + + testCase "Fable.Core typed arrays are annotated with native TS types" <| fun () -> + TypedArrays.int8s.[1] |> equal -2y + TypedArrays.uint8s.[1] |> equal 2uy + TypedArrays.uint8Clamped.[1] |> equal 255uy + TypedArrays.int16s.[1] |> equal -2s + TypedArrays.uint16s.[1] |> equal 2us + TypedArrays.int32s.[1] |> equal -2 + TypedArrays.uint32s.[1] |> equal 2u + TypedArrays.float32s.[1] |> equal -2.5f + TypedArrays.float64s.[1] |> equal -2.5 + TypedArrays.bigInt64s.length |> equal 2 + TypedArrays.makeFloat32s().[1] |> equal 1.0f + + testCase "Interfaces with [] are declared with unmangled members" <| fun () -> + let baz = ref 1 + let notMangled = + { new MangleInterfaces.INotMangled with + member _.Foo x = x + 1 + member _.Bar = "bar" + member _.Baz with get () = baz.Value and set v = baz.Value <- v } + MangleInterfaces.useNotMangled notMangled |> equal (2, "bar") + notMangled?Bar |> equal "bar" + MangleInterfaces.incrementBaz notMangled + notMangled.Baz |> equal 2 + notMangled?Baz |> equal 2 + let mangled = { new MangleInterfaces.IMangled with member _.Foo x = x + 2 } + MangleInterfaces.useMangled mangled |> equal 3 + + testCase "Abstract classes with [] ignore [] in declarations" <| fun () -> + let attached = MangleAbstractClasses.AttachedImpl() + MangleAbstractClasses.useAttached attached |> equal 4 + attached?Run(2) |> equal 5 #endif ] diff --git a/tests/TypeScript/tsconfig.json b/tests/TypeScript/tsconfig.json index db3671e62d..8bc46c9334 100644 --- a/tests/TypeScript/tsconfig.json +++ b/tests/TypeScript/tsconfig.json @@ -2,6 +2,9 @@ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "es2020", + // Temporal types (used by the --jsTemporal library files) live in lib.esnext.temporal; + // the rest mirrors the default lib set for target es2020. + "lib": ["es2020", "dom", "esnext.temporal"], "module": "es2020", "rewriteRelativeImportExtensions": true, "erasableSyntaxOnly": true, diff --git a/uv.lock b/uv.lock index 9938ac2746..75f4140779 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.12, <4.0" [[package]] @@ -31,14 +31,14 @@ wheels = [ [[package]] name = "dunamai" -version = "1.26.2" +version = "1.26.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/18/020d3b27a10450ddb11429f637404e8ea67ecf4d9fd999d4f1d553f25506/dunamai-1.26.2.tar.gz", hash = "sha256:84ea45eddf9bb4b40df7610b1b22a03137365e6257dbf9d7b72128fdccca564c", size = 46536, upload-time = "2026-08-02T03:32:50.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/62/edf5352d8ce1371129ccfe971e6de3a1ce4dc7f9bdc80390fb79856b78d4/dunamai-1.26.3.tar.gz", hash = "sha256:fe2f732186710ee5048044fdbef6c76c6afbb9ee7de4f6d00cf34256e9ec2e7f", size = 46820, upload-time = "2026-10-14T20:13:30.921Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/31/2aaabe7d03f395c7b6d955f09a4d1440a2c49920abea42fde88cacc2ef97/dunamai-1.26.2-py3-none-any.whl", hash = "sha256:4234be3a90c3ec13ecf75d94a2248068e5a3018e8112892fcd9f3a1f287c9c32", size = 27478, upload-time = "2026-08-02T03:32:49.348Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f4/e3be26f6301535ab4a3a940b09e55615c6286195b07fec9aa2cf8b842563/dunamai-1.26.3-py3-none-any.whl", hash = "sha256:cc486963f21ad5b5b20d95424909ff5029990241eea3e2272865e1542c5fd19f", size = 27628, upload-time = "2026-10-14T20:13:29.974Z" }, ] [[package]] @@ -63,13 +63,13 @@ dev = [ [package.metadata.requires-dev] dev = [ { name = "attrs", specifier = ">=26.1.0" }, - { name = "dunamai", specifier = ">=1.26.2,<2" }, - { name = "maturin", specifier = ">=1.14.1,<2" }, - { name = "pydantic", specifier = ">=2.13.4" }, - { name = "pyright", specifier = ">=1.1.411" }, - { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.16.3,<0.17" }, - { name = "ty", specifier = ">=0.0.72" }, + { name = "dunamai", specifier = ">=1.26.3,<2" }, + { name = "maturin", specifier = ">=1.16.0,<2" }, + { name = "pydantic", specifier = ">=2.14.2" }, + { name = "pyright", specifier = ">=1.1.414" }, + { name = "pytest", specifier = ">=9.2.0,<10" }, + { name = "ruff", specifier = ">=0.17.3,<0.18" }, + { name = "ty", specifier = ">=0.0.95" }, ] [[package]] @@ -83,23 +83,23 @@ wheels = [ [[package]] name = "maturin" -version = "1.14.1" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/71/eb078174335ff30bde1be2ad54fb6256db4b160eb3efa81163fa30f8c814/maturin-1.16.0.tar.gz", hash = "sha256:13e69ca12542999d8c5467131de72a49bb76e4c284b9a45c5783df7c0fcb3a8a", size = 396641, upload-time = "2026-10-29T14:09:43.096Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, - { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, - { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, - { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, - { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, - { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, - { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f8/235d4a8b3d61af3c7744582ae9e6ccb8f096e9f68f06fe62d7d03e5d7b2d/maturin-1.16.0-py3-none-linux_armv6l.whl", hash = "sha256:deacbb204a20326862e194c7df4958e0514a0411c9a3ccf228a00067c11b5c7e", size = 10425070, upload-time = "2026-10-29T14:09:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d5/f49a90d8c489dc5f134654ec91d12a78a51599fdc8c2b5a1bc2391bca0ac/maturin-1.16.0-py3-none-macosx_10_12_universal2.macosx_10_12_x86_64.macosx_11_0_arm64.whl", hash = "sha256:131fcc17ddb3ccc48aa183d2172ebf994c322b858e1b9694fb8744d783df66f6", size = 19839189, upload-time = "2026-10-29T14:09:09.441Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8e/d65633d7c25484cb82735a51e15bc969f9874a13557e2b352d27df574c20/maturin-1.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2216d731c9324869d3bff72c0c73009bb6e640b3fa7d340174ae70dc874d1d9f", size = 10271659, upload-time = "2026-10-29T14:09:12.596Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dc/e77f490557cb5e045a75c3d18a7965425ac576235c970dfff4d903eb2c74/maturin-1.16.0-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:837db2f4d93d4899c3ac393fb3cc9eebf35b6002336e5d574f4a4fb6c49fe283", size = 10763361, upload-time = "2026-10-29T14:09:16.11Z" }, + { url = "https://files.pythonhosted.org/packages/fa/eb/535564a4211105fd64611fa4f9437c8925eded26a97d58de84565f5aebfa/maturin-1.16.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:399fadcd4af39532a173e010240c34930eacc1bb1746bb179863481c68bcf1ab", size = 10442856, upload-time = "2026-10-29T14:09:18.64Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/570e13ce81f103be6609c067a06c5bac08da5af0cf7ea700c9ae43419be9/maturin-1.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e285ec9eb10243d45d10417178cb2eb76e0cbb184d0aa4378b503489fa12ee78", size = 10298796, upload-time = "2026-10-29T14:09:21.385Z" }, + { url = "https://files.pythonhosted.org/packages/88/a6/26b40c31c354359fe4e49398113d018694c7035968fe1b569bb13c3905af/maturin-1.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:609228d639c5b888dc41364cbd318547a59f027481f53cba85838f46da4b3f02", size = 10268023, upload-time = "2026-10-29T14:09:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/2b/19/faca1784d917c2eecd498648dff6a0179b299c8142cc448ea5b1183545ec/maturin-1.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:7001fd3ac591820d01f2fa041552c367ec587c98649d9f10a817cc7f28e59062", size = 13739038, upload-time = "2026-10-29T14:09:26.662Z" }, + { url = "https://files.pythonhosted.org/packages/36/c1/d67e77aa07100e3b2d4c0d9ef6922646d9c4bab25cc04f91023aefc72418/maturin-1.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b0573a146803f24566f5b301a4572a8e49bcab4917175407c730da13b46e134", size = 11100193, upload-time = "2026-10-29T14:09:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/76/44/ccef52f02ab0e1f7258bc8fe0140b4991c6287458ef883af1027f946a253/maturin-1.16.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:c16d3589cbddda9420237fdb1394b1a9b36483c0bce83b23de6118c908a8e940", size = 10663849, upload-time = "2026-10-29T14:09:32.183Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9b/3104f62dbb428f9e26e35cba2c80cfc42ba9bb1dc4968882c19d1bfe594b/maturin-1.16.0-py3-none-win32.whl", hash = "sha256:9e221c047931caa79283cc0c2278904ae1b25880dde3574b0447657d72907689", size = 9124894, upload-time = "2026-10-29T14:09:34.857Z" }, + { url = "https://files.pythonhosted.org/packages/63/b8/c25ce706c4797ee1663bcf1f386ffe91d8a3b3589700a135a204b49bc4b0/maturin-1.16.0-py3-none-win_amd64.whl", hash = "sha256:9d43a2adf51a2794e907a8f7dbad06eb114d5edb0909c73c1fa9040f15e584e9", size = 10575645, upload-time = "2026-10-29T14:09:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/f69aba8b22539e5ed58323d4a6ee974afd9edda238489c73926cc0fc9d51/maturin-1.16.0-py3-none-win_arm64.whl", hash = "sha256:95f34ce53249fb7095ff4f2aff64d2562c511d6edf3e216c15dbffcd82b72b4f", size = 9886475, upload-time = "2026-10-29T14:09:40.603Z" }, ] [[package]] @@ -131,7 +131,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -139,84 +139,115 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/d5/b4867fa7c7f04c34997d7d8aad744f64d210bb99d67cf18bf3fb1eea1aed/pydantic-2.14.2.tar.gz", hash = "sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d", size = 911596, upload-time = "2026-10-29T16:37:12.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/97/01/c71a71258b58659c72504e5fd65b451f6e36e34dc931d0b237ded213c814/pydantic-2.14.2-py3-none-any.whl", hash = "sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118", size = 490403, upload-time = "2026-10-29T16:37:10.181Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.50.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/8e/23ec1021834e35041fbb2f10946141e6acdfd39379c45cc4d65a4e8a1d98/pydantic_core-2.50.2.tar.gz", hash = "sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775", size = 518711, upload-time = "2026-10-29T16:34:57.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/6609a66cda909d414c58438f9bd70b1e947fe12c25f6bd40c93121afd7ef/pydantic_core-2.50.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738", size = 2220341, upload-time = "2026-10-29T16:32:10.193Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/92463ae7be78d73885487f72c4599902c2cc190378bccaad5718b7495250/pydantic_core-2.50.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9", size = 2093701, upload-time = "2026-10-29T16:32:11.502Z" }, + { url = "https://files.pythonhosted.org/packages/62/d3/24147a1aa22f7eade546172750e1110fd7b2793998057809800d3ebeae4e/pydantic_core-2.50.2-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756", size = 2253622, upload-time = "2026-10-29T16:32:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/2628ca3c83a0d9a6fda3305fe59b1b28e40c0221e8f234cd1ef86fb64745/pydantic_core-2.50.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6", size = 2114101, upload-time = "2026-10-29T16:32:14.702Z" }, + { url = "https://files.pythonhosted.org/packages/21/c8/cc6d7304a93f91f5e06260ccad71cc7d882489663a37dec009532379fc46/pydantic_core-2.50.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234", size = 2199541, upload-time = "2026-10-29T16:32:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/18f44ac5696b3e2b2384cb89c9fbe6ed69860f528e232b9fb46cc5b45a17/pydantic_core-2.50.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4", size = 2368597, upload-time = "2026-10-29T16:32:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/1d/22/5f2a48ef819a7e1b74fb003a85d27576ad2f9ddf3901b88998ac63ce1e00/pydantic_core-2.50.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a", size = 2451701, upload-time = "2026-10-29T16:32:18.956Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/2a6556bbdb8e918175af9d849a5d0b367b5644e41939d040f3a7215b1339/pydantic_core-2.50.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a", size = 2242782, upload-time = "2026-10-29T16:32:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/90/ed/dd9f29a92257f32eb5b99d5120826186ee0632ecc3aaf6c23e2fab89ed6d/pydantic_core-2.50.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe", size = 2253706, upload-time = "2026-10-29T16:32:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/11/19/1ccf3bf38eb4ed97d8aeefc558458834ae0ad53fdbb1edf61af7228ea500/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798", size = 2288603, upload-time = "2026-10-29T16:32:23.465Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cd/bae2320fc89a65bcd87bf66ec83aac51b3b43b970a2edece15feb8af64dd/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3", size = 2476597, upload-time = "2026-10-29T16:32:24.824Z" }, + { url = "https://files.pythonhosted.org/packages/db/35/1bdb8d52c8e562e95c5e6ebb5784792af568ac6a81470dd546d95f392b60/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2", size = 2478664, upload-time = "2026-10-29T16:32:26.197Z" }, + { url = "https://files.pythonhosted.org/packages/12/72/0179b40ac6a6b0899c61826f2e3e80f88063f6d821e0dd9641927de46700/pydantic_core-2.50.2-cp312-cp312-win32.whl", hash = "sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f", size = 2108820, upload-time = "2026-10-29T16:32:27.736Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fb/679144362d7e5162519d9f6631ec982521417de3196351c6c56485f5b29f/pydantic_core-2.50.2-cp312-cp312-win_amd64.whl", hash = "sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314", size = 2252238, upload-time = "2026-10-29T16:32:29.572Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/e3ccc885c3e2086ff84d7ca496f45b974a9f0a49ce2da8973aaa3a369a4b/pydantic_core-2.50.2-cp312-cp312-win_arm64.whl", hash = "sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db", size = 2162565, upload-time = "2026-10-29T16:32:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fc/ac3ae9d934932ce3c380e7fe53e01372b49ef5dcb49db56f1a1f005b4d8c/pydantic_core-2.50.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437", size = 2222463, upload-time = "2026-10-29T16:32:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/7605223654ed4e777d05668e914d32b476b1fa2bb756bff9820fe270456b/pydantic_core-2.50.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e", size = 2093995, upload-time = "2026-10-29T16:32:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/29/4a1a653cfedd3f5e1ccd3b1e919fe324fb45607d12c30fdd7ca5af16ba8d/pydantic_core-2.50.2-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4", size = 2253178, upload-time = "2026-10-29T16:32:34.77Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/03ce5ed01c98b60272cdee8ac1abb819b64ef347717c11d96215e32d9952/pydantic_core-2.50.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23", size = 2113894, upload-time = "2026-10-29T16:32:39.241Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bc/f7644474ac0faad9a7fdf5179239893b21339169b8b4f89e3075c4be16b2/pydantic_core-2.50.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d", size = 2198248, upload-time = "2026-10-29T16:32:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8f/3d3a1c1d313e02774f6c034eb1910eb6e795922e66e81f0f053f98afe866/pydantic_core-2.50.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057", size = 2368505, upload-time = "2026-10-29T16:32:43.755Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/38fb4545c00ce2bfebc2a0ffd99f7f580f1e1efb8db3b2faf88def640435/pydantic_core-2.50.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63", size = 2451704, upload-time = "2026-10-29T16:32:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/79/be/b559a1ceaff8b1d4581733d47d2ffa785635ac05716eae4c964466aac6e8/pydantic_core-2.50.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b", size = 2241760, upload-time = "2026-10-29T16:32:46.382Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7c/3c8ab7cb45e4dc7687a4f16a953fee14d2eca18dd90d6be8bd470ef06962/pydantic_core-2.50.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432", size = 2254115, upload-time = "2026-10-29T16:32:47.818Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/3f3471206224cdac2af3492a3f7c724c47a82f5f8850ff364c2e36023497/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c", size = 2288598, upload-time = "2026-10-29T16:32:49.404Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/2070b388b1d8633cfb72e3964ac3c95aaeb195c240209894ff386847b494/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601", size = 2475116, upload-time = "2026-10-29T16:32:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/96/e96ff19527c73b9aaddce2a4424006dc15439fd89ef2d39641048c5ee85a/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544", size = 2478926, upload-time = "2026-10-29T16:32:52.856Z" }, + { url = "https://files.pythonhosted.org/packages/35/10/2fefc082eb46f959e80735e859161305c0c2fbb3d5d20ac5547c49a84a3b/pydantic_core-2.50.2-cp313-cp313-win32.whl", hash = "sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d", size = 2108354, upload-time = "2026-10-29T16:32:54.276Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/14eeec394ec1422f9c4e9743cd746f4502232a7c65eb235d1a03729d94ab/pydantic_core-2.50.2-cp313-cp313-win_amd64.whl", hash = "sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45", size = 2250556, upload-time = "2026-10-29T16:32:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/75/b7/a014acf1ae4b4aa7d32ff957e2250698637daa3a29448444180360b92284/pydantic_core-2.50.2-cp313-cp313-win_arm64.whl", hash = "sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67", size = 2162633, upload-time = "2026-10-29T16:32:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/fc525bb981a9f287cfa34346e7ed1d5ef7c043c2b9b517f54c6136032d10/pydantic_core-2.50.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329", size = 2226414, upload-time = "2026-10-29T16:32:58.559Z" }, + { url = "https://files.pythonhosted.org/packages/db/68/315701095f25687938ab79d263a6d939337456c2840fe69038fd38000338/pydantic_core-2.50.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02", size = 2095041, upload-time = "2026-10-29T16:32:59.873Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/efd3abf31aa24bc97b311735f418886f947b4f193df02a1977643963b03e/pydantic_core-2.50.2-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2", size = 2257635, upload-time = "2026-10-29T16:33:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6c/146b36eed4de8115a0fda76840703e75f5f8b36aeb3af86976bf094d1a17/pydantic_core-2.50.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7", size = 2116807, upload-time = "2026-10-29T16:33:02.692Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/db006ea79bbe405cc38b25899319c7d461ff1813cbdfe6aa90a65ed1dfff/pydantic_core-2.50.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9", size = 2201037, upload-time = "2026-10-29T16:33:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/41/f9/ea2bc1ec13a67af4a63c9a887fa0aa6ef4ade9018e2bb412d4fc80110e19/pydantic_core-2.50.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108", size = 2374904, upload-time = "2026-10-29T16:33:05.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/8e/26a6a075301f0e563646037ea69bef8c63911d38fbd257f94cec69cdf985/pydantic_core-2.50.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92", size = 2457626, upload-time = "2026-10-29T16:33:07.271Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/8eeabaf1d394f0d3b9a27129ac21556eabee6f2ddcb418069ef4037e00b0/pydantic_core-2.50.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51", size = 2245505, upload-time = "2026-10-29T16:33:08.824Z" }, + { url = "https://files.pythonhosted.org/packages/bd/54/449778cd397e2682b30d1bcfab0872f6ac36702b59cc326296b7f4f14928/pydantic_core-2.50.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a", size = 2259563, upload-time = "2026-10-29T16:33:10.107Z" }, + { url = "https://files.pythonhosted.org/packages/26/c2/2f65691d25488ee0f3c17861d079e8ccb7817955522d07cd3c3970d30af5/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d", size = 2290470, upload-time = "2026-10-29T16:33:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/aa7c40234cd24536afa0895ff1d26f9f15638004e25242c5193ce053b96b/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee", size = 2477272, upload-time = "2026-10-29T16:33:12.771Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/c347c68e040cf93d8487a8f4fc28f2768312834c7d21c301538f416f9955/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58", size = 2484352, upload-time = "2026-10-29T16:33:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/72556126c8b6154fe7acaf757b96b2daa37aeffc3a517deb76e272f5ad09/pydantic_core-2.50.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3", size = 1491226, upload-time = "2026-10-29T16:33:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/66/004c85511812964a36b3535af022c5c0dfcb760a31e4905b96310e8dd6c8/pydantic_core-2.50.2-cp314-cp314-win32.whl", hash = "sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99", size = 2113134, upload-time = "2026-10-29T16:33:16.87Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/dcd3df20ab2a7279d42a8415fc184e8e77bcf9aa3c9ac3e938c1300a242c/pydantic_core-2.50.2-cp314-cp314-win_amd64.whl", hash = "sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31", size = 2253407, upload-time = "2026-10-29T16:33:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fb/0685895f5d199b215011dc4eb8ccea032a64ec23dfa88eeb5acca939c2d2/pydantic_core-2.50.2-cp314-cp314-win_arm64.whl", hash = "sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3", size = 2160456, upload-time = "2026-10-29T16:33:19.782Z" }, + { url = "https://files.pythonhosted.org/packages/84/2f/4c8f67efe62837a508b74cf910904f73029cb211371351c5ceb36d56e85e/pydantic_core-2.50.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1", size = 2217558, upload-time = "2026-10-29T16:33:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/e0ef88cfbcca5409f57751289bc20be63a58af596a36e94b8cd795e59747/pydantic_core-2.50.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c", size = 2074335, upload-time = "2026-10-29T16:33:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cc/47844a3179ebce27064e13619594b3fc1a9b4509d8dec1e3960c5f87f5bb/pydantic_core-2.50.2-cp314-cp314t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40", size = 2264201, upload-time = "2026-10-29T16:33:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/30e9d3c86d81884ee3199560ddfa2c1a66a8dc0736441748eae43064abbc/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b", size = 2094353, upload-time = "2026-10-29T16:33:26.739Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/392a5999480d0bcab4539db0dafa93265152396fd63e87be4d24e13d6d82/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d", size = 2203119, upload-time = "2026-10-29T16:33:28.154Z" }, + { url = "https://files.pythonhosted.org/packages/cb/80/6e4e276a3fde5500224a39150e06a4f134f136165908be69402720718a36/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b", size = 2392881, upload-time = "2026-10-29T16:33:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/d5/5eb56c6317ab4abf33ef9519a50bfac0c96f7f408bad1bc36da6f27d43e4/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910", size = 2457290, upload-time = "2026-10-29T16:33:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f1fe9384bb37e4e728c51c79151808deacf7b2502e245f50b57c85eeb199/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11", size = 2241568, upload-time = "2026-10-29T16:33:32.04Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f1/cffc4f8456c35690bcb65a8f2304afbf2f519766345a28aba553d5a55cc8/pydantic_core-2.50.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead", size = 2257185, upload-time = "2026-10-29T16:33:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/ccd4df1cc39cb1e42fb7ec86a0dc0a23e8479de04d3012ca6b0bf2279a03/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c", size = 2299384, upload-time = "2026-10-29T16:33:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/83/60/404b4b0c416a61867ee642cdc1450bca4d8a9a10e5f5f21e1ecaad05fdc8/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef", size = 2479384, upload-time = "2026-10-29T16:33:36.106Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/57c81418a2fdfde0578c71396df773965adebd4421d9985a0f8dcbe9ec4d/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636", size = 2504616, upload-time = "2026-10-29T16:33:37.423Z" }, + { url = "https://files.pythonhosted.org/packages/08/66/dec78e28a604f3f4985f5a795d4da3e5fef78b0921aeb7f614ed7c780a32/pydantic_core-2.50.2-cp314-cp314t-win32.whl", hash = "sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d", size = 2093179, upload-time = "2026-10-29T16:33:38.705Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ea/79d9ee60648463804145d44b08f1e7e2474427d9704e3331d814483908b3/pydantic_core-2.50.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977", size = 2220575, upload-time = "2026-10-29T16:33:39.929Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/aa6a7bd017c3228a41942da4fd788b86fb4aad9677212a4737a0c157fe74/pydantic_core-2.50.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb", size = 2160866, upload-time = "2026-10-29T16:33:41.237Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7f/ec2974015b4be5e50266ce732b8942d3d0b3bff751ace8900999c6577bce/pydantic_core-2.50.2-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5", size = 2230936, upload-time = "2026-10-29T16:33:43.656Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/af99a09eddc348c1d0744c0df28dfcd4839ca497067f0fa2168208b41b1b/pydantic_core-2.50.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd", size = 2100409, upload-time = "2026-10-29T16:33:44.946Z" }, + { url = "https://files.pythonhosted.org/packages/84/75/8db3c46f02d6005277e5c2ff5933c85e7e1eb8798aa334c676c828650c3f/pydantic_core-2.50.2-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b", size = 2259728, upload-time = "2026-10-29T16:33:46.461Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9d/98c8d524921ca24e5c8a81a4b8f12209ac6ab05d0dd58b374e357da0ffc5/pydantic_core-2.50.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97", size = 2121771, upload-time = "2026-10-29T16:33:47.952Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/68c3b1fa919946012c940dea88e94e33e7dde46abe3e3d57053793c9bab8/pydantic_core-2.50.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7", size = 2196328, upload-time = "2026-10-29T16:33:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9b/cfb6312a5b65b02eab6bc00fb0d258d0e2e49468dab29b430e7cb49b2ceb/pydantic_core-2.50.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca", size = 2380152, upload-time = "2026-10-29T16:33:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/7fcced1d84ea294200abfa93e488f36d00732101d17bf5e1f1389e66de26/pydantic_core-2.50.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc", size = 2462299, upload-time = "2026-10-29T16:33:51.884Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d0/dd0e355694094f0045922f4da60ac9bc7a314babc494e46089fb00640fd3/pydantic_core-2.50.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986", size = 2249999, upload-time = "2026-10-29T16:33:53.333Z" }, + { url = "https://files.pythonhosted.org/packages/3a/79/f5f4b36434604d22aacb7152667f87a470e68847c89b136c68a278cf409d/pydantic_core-2.50.2-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f", size = 2262745, upload-time = "2026-10-29T16:33:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/a4c47a286745857758a4bb8d4d5e260c736a7bd6fab5da618f5711d1a3f1/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd", size = 2293663, upload-time = "2026-10-29T16:33:56.132Z" }, + { url = "https://files.pythonhosted.org/packages/0d/db/25330e609fd219ce16d0a089af1b4b62095f01595dbe6972f7a7dfc313b9/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46", size = 2473247, upload-time = "2026-10-29T16:33:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/53/16/704ca88b82156dc485da2dfb11d5d52a10b9ef15a294639c0aa5ebd1643e/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f", size = 2490536, upload-time = "2026-10-29T16:33:59.297Z" }, + { url = "https://files.pythonhosted.org/packages/50/34/1fdd0eebee9f8b6c4ec3c2490f49dc59a652c8c5a7fa0cbcaada0d91cda9/pydantic_core-2.50.2-cp315-cp315-win32.whl", hash = "sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8", size = 2117204, upload-time = "2026-10-29T16:34:00.514Z" }, + { url = "https://files.pythonhosted.org/packages/24/6b/702adc91486fb96ba55675d58ee15ddc38fb48553fb35e26fd005e303206/pydantic_core-2.50.2-cp315-cp315-win_amd64.whl", hash = "sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41", size = 2261064, upload-time = "2026-10-29T16:34:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/7f117432b8614526aa0d96fadd2b7fb7393156ddf2caf26a6fe606e6cfb6/pydantic_core-2.50.2-cp315-cp315-win_arm64.whl", hash = "sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a", size = 2161783, upload-time = "2026-10-29T16:34:02.966Z" }, + { url = "https://files.pythonhosted.org/packages/45/1c/2ba7a34fe8da4c83ec200fb9f85ab4d54b961be08367922743623f987849/pydantic_core-2.50.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f", size = 2223184, upload-time = "2026-10-29T16:34:04.214Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/f71a8da1f53a5be1787d2a97ba6fd9adba115305c7f15ea0bf733ed8336c/pydantic_core-2.50.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3", size = 2078174, upload-time = "2026-10-29T16:34:05.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0a/847e1f5427372692f6eb5c38261fed6b8cedf379e37d9f6134d48b9c8ee9/pydantic_core-2.50.2-cp315-cp315t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0", size = 2268813, upload-time = "2026-10-29T16:34:06.733Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bb/12966dc82b5e9c347981658b52e24349b6242902b644725c00ee7bdf4267/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd", size = 2098331, upload-time = "2026-10-29T16:34:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/340416b7ebe664524e7ada16ad2e3e069888b168010d75bda6a502bf2bf8/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a", size = 2197432, upload-time = "2026-10-29T16:34:10.091Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4d/425162c3d88af0d190c4dc4f940402cc6dd44d934359a37f50da6b7e4672/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e", size = 2399300, upload-time = "2026-10-29T16:34:11.462Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/2882bddf2e2038c8f41fa69f16ddeb6eeb1c002cc0cc0eb2157f73bb34e9/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b", size = 2462672, upload-time = "2026-10-29T16:34:12.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/d7e6bfc6a95573647d1a699e19228dd99914ae6aab79a2bec293a038e980/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853", size = 2246293, upload-time = "2026-10-29T16:34:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/64/97473ccaeb4ae5eb338162bd8479af121484e67f7078499730e3ee0a5592/pydantic_core-2.50.2-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376", size = 2259507, upload-time = "2026-10-29T16:34:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/78a9b7a6a0d0679ce1e46cdd38ff95afd18dd2d6a01ed87c80a01bc003f7/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04", size = 2303550, upload-time = "2026-10-29T16:34:17.162Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/b697173f82e99cdcf5ec6654b2edb66a00f2f92ef9d9a168861fca3fcded/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b", size = 2474919, upload-time = "2026-10-29T16:34:18.841Z" }, + { url = "https://files.pythonhosted.org/packages/71/10/986be76723784ae87f9664658fae39df340eaa21681cea95b6339d2e1b3e/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f", size = 2509095, upload-time = "2026-10-29T16:34:20.16Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0b/38dbd38c67de7642f87348da70122b89ca4c5fe8227cfdd5cc00c1cdb454/pydantic_core-2.50.2-cp315-cp315t-win32.whl", hash = "sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773", size = 2096972, upload-time = "2026-10-29T16:34:21.715Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/154baf8413b08d6daf2f5dec211c303ec6bed50c032717f354f8dd85c994/pydantic_core-2.50.2-cp315-cp315t-win_amd64.whl", hash = "sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6", size = 2226659, upload-time = "2026-10-29T16:34:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/eea7a1e7d98a79ef11b6d31748b70c58f8723f87d7ee42edbb6a101f6245/pydantic_core-2.50.2-cp315-cp315t-win_arm64.whl", hash = "sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02", size = 2166387, upload-time = "2026-10-29T16:34:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/64/4d/23560b11366b547e3e68a3b8f9c02557cee707effa54fd39dbb3c4fff1fa/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b", size = 2204868, upload-time = "2026-10-29T16:34:34.745Z" }, + { url = "https://files.pythonhosted.org/packages/06/58/b9ddfaad189143d6568627166a730b6624ca90bd9a9a4af4854df88d444b/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d", size = 2049162, upload-time = "2026-10-29T16:34:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/8b/20/99d259bf7835b2f13c6d0b3eb7bcf669fb6349986736fa7a8308a9e620b0/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371", size = 2112135, upload-time = "2026-10-29T16:34:37.777Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/652c8fe599abacafd852479059d209e608596ad3de82f1bff22e9f0f02fe/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324", size = 2270339, upload-time = "2026-10-29T16:34:39.039Z" }, ] [[package]] @@ -230,20 +261,20 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.411" +version = "1.1.414" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, ] [[package]] name = "pytest" -version = "9.1.1" +version = "9.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -252,59 +283,59 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, ] [[package]] name = "ruff" -version = "0.16.3" +version = "0.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, - { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, - { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, - { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, - { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, - { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, - { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, - { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, - { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, - { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, - { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" }, + { url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" }, ] [[package]] name = "ty" -version = "0.0.72" +version = "0.0.95" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/df/656e684bafb13c1d146e7d5b5f3e7978ca177232acc84998ff36427e9462/ty-0.0.72.tar.gz", hash = "sha256:ec2b8066b618df18cab4cb8e992f8da45d360332acb23fa34df7fa29cd1b9d3a", size = 6654939, upload-time = "2026-08-14T21:35:42.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/08/d63954788578b9189dbf85081d8cbdba9a32a9a6a29729129a0da09cd3a0/ty-0.0.95.tar.gz", hash = "sha256:a480853a74d43074b2d4b28025d406b9d7223f7fe9f3a6c2adec3eacdbf14ca0", size = 8115525, upload-time = "2026-10-29T19:21:51.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/3b/f51461239a4e66565d4b362f97a3b55fe7fdba2e944068341f87c62f6743/ty-0.0.72-py3-none-linux_armv6l.whl", hash = "sha256:fda86db153ffd85ee52000cf175d6a3f1c0223772cf7c5b6f726200bf92c7b44", size = 12621989, upload-time = "2026-08-14T21:35:01.676Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/79ddf683affc679ca856f3510b5640ec3a88a842ba5f654f5d4bc78f1786/ty-0.0.72-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ceb944c612529b9023acfdc9cf4c0dcbb722549f9d17d46baecd1141baf01d7f", size = 12233910, upload-time = "2026-08-14T21:35:04.334Z" }, - { url = "https://files.pythonhosted.org/packages/5d/45/10562a0d84802158db8fa4ec46de54aa9fdcecdeeaabbfe3639ae7042b66/ty-0.0.72-py3-none-macosx_11_0_arm64.whl", hash = "sha256:108d76218333d6c092e5f1cebf8e9b06f25738613a0236a28e2dd47c936ee52c", size = 12084108, upload-time = "2026-08-14T21:35:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/a1/dc/1fe1aef8d697e3509face271a5331700c7aa1d1e44a4b622707bdfa41d4b/ty-0.0.72-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f3943f186f741a2499a31053872169250c9264a9a49684920e48d8fcf4ef4f5", size = 12132640, upload-time = "2026-08-14T21:35:09.305Z" }, - { url = "https://files.pythonhosted.org/packages/14/46/41ceb265e96969487311a2014bd0e53abb4fbc1395efb2ebe411fcb4db62/ty-0.0.72-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf283c07dc3cc52ca48a3ad8ab100fb5aec3aebbd03ef6a12d5f910b8e596fc5", size = 12402489, upload-time = "2026-08-14T21:35:11.555Z" }, - { url = "https://files.pythonhosted.org/packages/2b/45/30bf43cb4fd505c5c2dd30fda27dde5f05208686cd21217adec77c954204/ty-0.0.72-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95f3b6462c38f9f115d10cee21f47fedf715fcf2040daf36eef210359300bc7c", size = 13130835, upload-time = "2026-08-14T21:35:13.746Z" }, - { url = "https://files.pythonhosted.org/packages/31/2f/03bba754d2613f640df168335c41f83f41db150bb515839c60d80e3a7880/ty-0.0.72-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30caf658feb8ffb250d9e9e47107657a78f5f3425c227df1664d8df2ebe38880", size = 13590392, upload-time = "2026-08-14T21:35:16.839Z" }, - { url = "https://files.pythonhosted.org/packages/04/c7/03c67f00e63005ec41585653dc3096064570b1e6273742baae2798cd242f/ty-0.0.72-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27bdc012ddfbeec8948e4a6036c0dc39ac7cf2c8ec7c7d48dc7d2fd56d57b399", size = 13309629, upload-time = "2026-08-14T21:35:19.169Z" }, - { url = "https://files.pythonhosted.org/packages/c1/df/102d3b264eb7f2a58dd11952f229bb5150bb5668d176a6154976a6675981/ty-0.0.72-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:802c5970a77d7739e6f499921fbb6984fb7ad8a31d95e1ff42fd46f3642e4f3b", size = 12734028, upload-time = "2026-08-14T21:35:22.099Z" }, - { url = "https://files.pythonhosted.org/packages/61/85/d0737c8c54d0ba67366ddfb9f31d88edf0b02299e65923e6945ae60ebcb5/ty-0.0.72-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:47dce65114fdc615c68ca0edb393b433df0956447e4267df0e264137a789598d", size = 13174832, upload-time = "2026-08-14T21:35:24.71Z" }, - { url = "https://files.pythonhosted.org/packages/1e/31/497f5a96c36d9b586ab6afe0574986835c6fd5b835a89773d2bec4711b49/ty-0.0.72-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:325144fa07e2675d0faa337fcc864213c272a499eb0cfe5bde2fdc62282d27bc", size = 12215005, upload-time = "2026-08-14T21:35:26.892Z" }, - { url = "https://files.pythonhosted.org/packages/df/7d/46e65b17b4966c7cd0140f134380d33d8e84fe6efccd761533ce793dc502/ty-0.0.72-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5c9f15d0f58e43707d8848274be1821a0ef408eccb8aa7dda28a4a9eddf7640", size = 12421298, upload-time = "2026-08-14T21:35:29.301Z" }, - { url = "https://files.pythonhosted.org/packages/08/2a/12ada4ec17700b3cb1d4fd3bc3e5b1852df9e6885288429318cade87b3c1/ty-0.0.72-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee508d64b381871529cc22c412b41071bf5e908b7aa5d66a38f3f6b2573a806", size = 12669242, upload-time = "2026-08-14T21:35:31.444Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1a/4692536880790fb550ed6d44a6096778dc71bb112f2c6d615cebb01a57e5/ty-0.0.72-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3699e2ec7921d44da79d6b089f7bf239b2cc53c4e45a5a38430adc34ee9e9a55", size = 12988199, upload-time = "2026-08-14T21:35:33.749Z" }, - { url = "https://files.pythonhosted.org/packages/9a/0d/f5e5a50322e9c45865e7b7a428ba6cd6527387cf0f2472492ac3cf746243/ty-0.0.72-py3-none-win32.whl", hash = "sha256:f25f72a67bd36cd247707c4784e52fad0b6b4f42a1b7dd14804110fa95c486ed", size = 11939708, upload-time = "2026-08-14T21:35:36.006Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4e/8af3534b2e4214e6184a5a59c34101e94a68d578f081f97b995866bab1bf/ty-0.0.72-py3-none-win_amd64.whl", hash = "sha256:cdeee869341717e1736cea2e2d7856738c6957c320f584ed2f68c8f90100d2f5", size = 12643876, upload-time = "2026-08-14T21:35:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/a2606e654c7276bd08586391a2525b0af3f3bf60228a8c57b2d248f273f9/ty-0.0.72-py3-none-win_arm64.whl", hash = "sha256:1bd3ac3ed4424a6d6990a85dc388556aea012bd752de21349a84b685951de0d8", size = 12394857, upload-time = "2026-08-14T21:35:40.277Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/8f912c9d91503aa26b704838e2ecba448fae5713a799e2022e03e20d5186/ty-0.0.95-py3-none-linux_armv6l.whl", hash = "sha256:4e0745137dbd472027e71827ef9dec803439395319a182835fea2bb7ccc264cb", size = 14526217, upload-time = "2026-10-29T19:21:11.573Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/25feb9b4c04da26d2aa91527b0cc3de281aa45c9857abb900f1c4275e1aa/ty-0.0.95-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b9587b262312f94bce6b8c139db0542c44a1dce8c2224b4d33ab649b287b119", size = 14099685, upload-time = "2026-10-29T19:21:14.433Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6b/2a084a72c8069f8496fcc0f576ec053f648b1baeed07532b3eb84c4982e4/ty-0.0.95-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3e7902477410e897a60d2cf29ca89c28ef2a2abdb81698d8ae4be2e33b934d4b", size = 13144133, upload-time = "2026-10-29T19:21:16.734Z" }, + { url = "https://files.pythonhosted.org/packages/84/ad/e4088e627b3e3cd8cbb6ad08668cde33fc7093887606de6f23e4e3e639b9/ty-0.0.95-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cef0d06e2f286db6596e004f5820d4dd3b3a1a698465840e89a5e8a5934aca7c", size = 13996368, upload-time = "2026-10-29T19:21:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/67b0ee993698496e0c93b5afbec7838cc26003ac74486fc11d89c15b52fb/ty-0.0.95-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e032018e2aaeba51a48b639acacfd949be335cd2395faa205a305a9ff524b9f", size = 14262316, upload-time = "2026-10-29T19:21:21.303Z" }, + { url = "https://files.pythonhosted.org/packages/14/23/7214c42a9d8533c5f8a6384ba6d82adda9adab2347d5fe1c395c32c4e4e9/ty-0.0.95-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c98a2ea2d1a731d0db27a440bceea055ba3555e3e80d694e567110ba2a477a", size = 15217155, upload-time = "2026-10-29T19:21:23.597Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/4f51df84ba70438fbf2d481baa59cc8801d86e26a472a1c0b885c01c9c99/ty-0.0.95-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c26f09e9d6039f5ffc1b35e24830da67ff6202fef8583991e2ed169c0b188a54", size = 15607522, upload-time = "2026-10-29T19:21:26.245Z" }, + { url = "https://files.pythonhosted.org/packages/bb/7d/958cfc1e9cd56390f916f36d97a00b7aa15db88b2cf1a39c22f524c6078d/ty-0.0.95-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e01e493e87aede663405e6f2cf38c65a95456c8181ab04e9e2e9662538ea717", size = 15562445, upload-time = "2026-10-29T19:21:28.642Z" }, + { url = "https://files.pythonhosted.org/packages/90/64/636308717989afa2fe765604ae2a76189f470d566e5e58da4482b625d64e/ty-0.0.95-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d47be2db5db80cd70c582b240a71dd8e9be215ce9288298c863c7c3611ef02b", size = 16312046, upload-time = "2026-10-29T19:21:30.801Z" }, + { url = "https://files.pythonhosted.org/packages/a9/75/e57a2c0bdce3dcf3d43ac15f3c9adc34212330e056db8a70208ab7f3730f/ty-0.0.95-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3843233ed7ddca6d1d2e38034bf28d2618ff2fc8b16190d2efbbe46b066ea524", size = 15070529, upload-time = "2026-10-29T19:21:33.7Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/2efd45b625fef61491986feae6f348449051ee474de4d79a77819c678aa9/ty-0.0.95-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0b24691b55fc64d480d612bbf555ff369c80d56e26c33af94acb2cbadf1a37f6", size = 13962242, upload-time = "2026-10-29T19:21:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a5/d5082fbb65305af5689f1e50f3b6b8897287f1ded5d06ef713faaed17e97/ty-0.0.95-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bf72de3cc1d528b1b19020209f4692997f402bba40d8b3e5654906edf61d1c84", size = 14265678, upload-time = "2026-10-29T19:21:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e6/2347c9519c35179d7e2ca74b0ea0b9d78a503d6673905ae911dfcc13f2ed/ty-0.0.95-py3-none-musllinux_1_2_i686.whl", hash = "sha256:721260b0dc05f9bb3fe79db642a2eed94aec99de2d995c7c92e56ffd8637b1a1", size = 14636684, upload-time = "2026-10-29T19:21:40.466Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b4/bac1a90c887c0251955ed17e5da8139b4ac87352d67fcb3d1ffdb1b8723e/ty-0.0.95-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:156abe019cea9aa54e581d89ca34a48f81b8e4e9c49c203943feb60a06b822af", size = 14985737, upload-time = "2026-10-29T19:21:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/68/ef/8840c75892f50107a541e7ca3fc0912d9dce133e4e13728838981901e1c1/ty-0.0.95-py3-none-win32.whl", hash = "sha256:b1903d8c17645c29ee566e708bee569d971c461f3429872305a574ccdae4c732", size = 13503067, upload-time = "2026-10-29T19:21:45.085Z" }, + { url = "https://files.pythonhosted.org/packages/b2/df/46fdcc84de81159e1d40ca103d24ac08b2450a2a51a1572333e8e796b8fc/ty-0.0.95-py3-none-win_amd64.whl", hash = "sha256:d07bd09f8a525759a595e04051e923aa54f8dc483f90e6a0b003d77a3fae0f04", size = 14097073, upload-time = "2026-10-29T19:21:47.462Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e4/84cf4f3c313448d97550fdce50e7c70b934b38765772dd4f7fdaac064cca/ty-0.0.95-py3-none-win_arm64.whl", hash = "sha256:74ac66df3cb1851e3f157c4e664aec1f28857b467c7e13f62ccf53efc6884c82", size = 13575281, upload-time = "2026-10-29T19:21:49.773Z" }, ] [[package]] @@ -318,12 +349,12 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ]