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