Morphir.SDK.FSharp
0.4.0-alpha.4
Prefix Reserved
See the version list below for details.
dotnet add package Morphir.SDK.FSharp --version 0.4.0-alpha.4
NuGet\Install-Package Morphir.SDK.FSharp -Version 0.4.0-alpha.4
<PackageReference Include="Morphir.SDK.FSharp" Version="0.4.0-alpha.4" />
<PackageVersion Include="Morphir.SDK.FSharp" Version="0.4.0-alpha.4" />
<PackageReference Include="Morphir.SDK.FSharp" />
paket add Morphir.SDK.FSharp --version 0.4.0-alpha.4
#r "nuget: Morphir.SDK.FSharp, 0.4.0-alpha.4"
#:package Morphir.SDK.FSharp@0.4.0-alpha.4
#addin nuget:?package=Morphir.SDK.FSharp&version=0.4.0-alpha.4&prerelease
#tool nuget:?package=Morphir.SDK.FSharp&version=0.4.0-alpha.4&prerelease
Morphir.SDK.FSharp
F# runtime library providing Morphir SDK types and functions for generated F# code.
Overview
Morphir.SDK.FSharp provides the F# runtime used by generated Morphir code.
Its namespace and runtime assembly remain Morphir.SDK.
Installation
dotnet add package Morphir.SDK.FSharp
Migrating existing consumers
Replace the Morphir.SDK package reference with Morphir.SDK.FSharp, using the
same release version. Keep open Morphir.SDK and existing generated source
unchanged. Project-reference consumers use
src/Morphir.SDK.FSharp/Morphir.SDK.FSharp.fsproj.
The legacy Morphir.SDK package remains installable. It depends on the exact
matching canonical package version and contains no runtime assembly. Installing
either package or both resolves the same Morphir.SDK.dll. Do not reference
the legacy pack-only project as a runtime project dependency.
The migration preserves the Morphir.SDK.Order type identity, existing public
entry points and F# aliases. Aliases such as Maybe<'a>, Dict<'k,'v>,
LocalDate and LocalTime retain their FSharp.Core/BCL types. They do not
become new CLR wrapper or forwarding types.
Select the FSharp.Core version required by the package. The captured baseline
uses 11.0.100; a compiler's lower implicit dependency can otherwise cause
NU1605 and runtime loading failures. For an SDK whose implicit version is lower:
<PropertyGroup>
<FSharpCoreImplicitPackageVersion>11.0.100</FSharpCoreImplicitPackageVersion>
<WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors>
</PropertyGroup>
The bounded conformance contract and migration corrections are tracked in issue #465. Packaging identity preservation does not claim that behavior corrections replace old code already inlined into an existing binary.
Covered behavior changes
The package migration also corrects the cases admitted by the shared
morphir-dotnet-sdk-v1 / dotnet-sdk-v1 contract. It does not certify every SDK
operation or every host representation.
Dict.union left rightnow keepsleft's value for a shared key. Previously the second dictionary won. For example, unioning[("rate", 1)]with[("rate", 9)]now yields[("rate", 1)]. Both operands remain unchanged.- Int32
Basics.add,subtract,multiplyandnegatereport overflow instead of wrapping. The generic public entry points remain available. Basics.remainderBy -1 System.Int32.MinValueandBasics.modBywith those operands return zero. Previously the host remainder instruction overflowed.Basics.comparecompares direct String operands by ordinal UTF-16 code units, without culture or normalization. For example, decomposed"e\u0301"sorts before precomposed"\u00e9"; they no longer compare equal through collation.Decimal.toStringuses invariant positional text and removes fractional trailing zeros.1.50Mbecomes"1.5", and scaled zero becomes"0".Decimal.fromStringparses this canonical representation first under every culture, then tries the caller's culture for other input. For example, underde-DE, both"1.5"and localized"1,5"mean1.5M. Ambiguous dot-separated input follows canonical decimal-point semantics rather than localized grouping. Parsing remains a native API compatibility check outside the frozen inventory.- Additive
Basics.round,floor,ceilingandtruncateconvert Float to Int32 with non-finite and overflow checks. Round resolves halves toward positive infinity:round -2.5 = -2. - Additive
Decimal.divreturnsNonefor a zero divisor andSome quotientotherwise. ExistingDecimal.dividestill returns decimal and retains its existing zero-divisor exception behavior. Exact terminating Decimal96 division is covered; nonterminating or unrepresentable results are separate exclusions.
Already compiled inline calls can retain their old arithmetic, union or formatting behavior. Recompile consumers to use corrected inline bodies. Installing the canonical runtime alone preserves their type loading; it cannot replace code embedded in another assembly.
Features
- Type Aliases: Morphir types mapped to idiomatic F# types (e.g.,
Maybe<'a>=Option<'a>) - Standard Library: F# SDK functions; the shared contract defines the bounded conformance inventory
- Immutable Collections: Dict (Map), Set, List operations
- Date/Time Support: LocalDate (DateOnly), LocalTime (TimeOnly)
- AOT Compatible: No reflection, ready for Native AOT compilation
Modules
Core Types
- Basics: Fundamental operations (Order type, comparison, arithmetic)
- Maybe: Optional values (
Option<'a>alias with helper functions) - Result: Error handling (
Result<'v, 'e>with combinators)
Collections
- List: List operations and extensions
- Dict: Immutable dictionaries (Map<'k, 'v> alias)
- Set: Set operations
- Tuple: Tuple helper functions
Primitives
- String: String manipulation functions
- Int: Integer operations
- Bool: Boolean logic
- Char: Character operations
- Decimal: Decimal arithmetic
Date/Time
- LocalDate: Date without timezone (DateOnly alias)
- LocalTime: Time without timezone (TimeOnly alias)
Usage Examples
Maybe (Option)
open Morphir.SDK
let user = { name = "Alice"; email = Some "alice@example.com" }
let emailDomain =
user.email
|> Maybe.map (String.split "@")
|> Maybe.andThen List.last
|> Maybe.withDefault "unknown"
Result
open Morphir.SDK
let validateAge age =
if age < 0 then
Result.err "Age cannot be negative"
elif age > 150 then
Result.err "Age is too high"
else
Result.ok age
let processAge =
validateAge 25
|> Result.map (fun age -> age + 1)
|> Result.withDefault 0
List
open Morphir.SDK
let numbers = [1; 2; 3; 4; 5]
let result =
numbers
|> List.map (fun x -> x * 2)
|> List.filter (fun x -> x > 5)
|> List.sum
// result = 18
Dict
open Morphir.SDK
let scores =
Dict.fromList [
("Alice", 95)
("Bob", 87)
("Charlie", 92)
]
let aliceScore = Dict.get "Alice" scores
// Some 95
let updatedScores = Dict.insert "David" 88 scores
LocalDate
open Morphir.SDK
let today = LocalDate.today()
let nextWeek = LocalDate.addDays 7 today
let formatted = LocalDate.toIsoString nextWeek
// "2025-01-07"
let parsed = LocalDate.fromIsoString "2025-12-31"
// Some (LocalDate(2025, 12, 31))
Relationship to morphir-elm
This library provides F# implementations of types and functions from morphir-elm's SDK. The API is designed to maintain semantic compatibility while leveraging F#'s native types and idioms.
Key Mappings
| morphir-elm | Morphir.SDK (F#) | F# Native Type |
|---|---|---|
Maybe a |
Maybe<'a> |
Option<'a> |
Result e v |
F# Result<'v, 'e> |
Result<'v, 'e> |
List a |
'a list |
List<'a> |
Dict k v |
Dict<'k, 'v> |
Map<'k, 'v> |
Set a |
Set<'a> |
Set<'a> |
LocalDate |
LocalDate |
DateOnly |
LocalTime |
LocalTime |
TimeOnly |
Design Principles
- Type Aliases Over Wrappers: Use F# built-in types directly when possible
- Zero-Cost Abstractions: No runtime overhead for type conversions
- Idiomatic F#: Follow F# conventions and naming patterns
- AOT Compatibility: No reflection or dynamic code generation
- Semantic Compatibility: Maintain morphir-elm behavior
Version Compatibility
- Canonical package:
Morphir.SDK.FSharp - Compatibility package:
Morphir.SDK, at the matching version - .NET: 10.0+
- F#: 9.0+
- morphir-elm: Compatible with Morphir IR 2.x and 3.x
Contributing
This library is part of the morphir-dotnet project. For contributions, please see the main repository's CONTRIBUTING.md.
License
Apache License 2.0 - See LICENSE in the morphir-dotnet repository.
Links
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- FSharp.Core (>= 10.1.403)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Morphir.SDK.FSharp:
| Package | Downloads |
|---|---|
|
Morphir.SDK
Compatibility package for the Morphir F# runtime. Depends on Morphir.SDK.FSharp, which retains the Morphir.SDK namespace and runtime assembly identity. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.5.0-alpha.3 | 49 | 11/14/2026 |
| 0.5.0-alpha.2 | 46 | 11/13/2026 |
| 0.5.0-alpha.1 | 46 | 11/13/2026 |
| 0.4.0-alpha.5 | 48 | 11/13/2026 |
| 0.4.0-alpha.4 | 50 | 11/10/2026 |
| 0.4.0-alpha.3 | 58 | 11/9/2026 |
| 0.4.0-alpha.2 | 59 | 11/8/2026 |
| 0.4.0-alpha.1 | 57 | 11/7/2026 |
### Added
- `morphirup`, the standalone Native AOT toolchain manager, first slice: `morphirup --version`, plan-only `morphirup setup --plan` and `morphirup doctor` over local files. It needs neither .NET, ICU nor OpenSSL: it runs in invariant globalization mode, normalizes to NFC with its own Unicode 17.0.0 tables and computes SHA-256 with BouncyCastle's managed implementation. Setup resolves a `.morphir/toolchain.toml` request against a recipe file for every platform the lock would record and prints the plan with its `planSha256`. The plan carries the complete lock setup would write, so the digest pins every platform's closure and the request's exact bytes; `--expect-plan` refuses another plan with `plan_changed`. `--repository-id` is copied into the plan and lock as given and not checked, and the plan's sources are labels, not verified provenance. Without `--request`, the project request is discovered from the start directory up to the discovery boundary, by default the root of the file system; discovery stops at a `.morphir/toolchain.toml` in any form, and anything there that is not a readable regular file is `document_unavailable`. Doctor reads the lock beside the request it found or was given, and checks that the lock binds the request bytes, records the current platform and, with `--recipe` and `--repository-id`, names that repository (`repository_mismatch`) and is a closure the recipe allows. A request whose bytes differ from the locked ones only by CRLF line endings or a byte order mark is `request_changed` naming that cause. `setup --plan` and `doctor` write one manager result document to stdout. Files are read only when they are regular files, up to 1 MiB for a request, lock or sizes file and 16 MiB for a recipe, with five seconds to open and thirty seconds for each read; read errors are reported in `morphirup`'s own words, and a request, lock or recipe that cannot be read is `document_unavailable`. A document nested deeper than 64 levels is `invalid_document`. Acquisition, installation and activation are not implemented. Exit code 2 means the command was not run, for example an unknown option, a missing `--recipe` or `--repository-id`, an invalid option value, a machine that is not one of the eight platforms, or an artifact with no size in `--artifact-sizes`; it writes nothing to stdout. Provisional: `--project`, `--boundary`, `--recipe`, `--repository-id`, `--recipe-target`, `--artifact-sizes`, `--request` and `--lock`; the file bounds; the version bound (256 characters, 15 digits per numeric identifier, refused as `invalid_value`); and exit code 3 (internal error), which writes nothing to stdout ([#558](https://github.com/finos/morphir-dotnet/issues/558))
- Releases attach `morphirup` for `linux-x64`, `linux-arm64`, `win-x64` and `osx-arm64`: the executable `morphirup-<rid>` (`morphirup-win-x64.exe` on Windows), the archive `morphirup-<rid>-v<version>.tar.gz` with the executable, `LICENSE`, `NOTICE` and `THIRD-PARTY-NOTICES.txt` under `<rid>/`, the same notices as `morphirup-v<version>-THIRD-PARTY-NOTICES.txt`, and the checksum file `morphirup-v<version>.sha256` in `sha256sum -c` format. The notices are generated from the assemblies the Native AOT compiler linked, with each package's license and notice files; the build fails when a linked package has no license text. The release job builds each with Native AOT on a runner of its platform and runs the published file, not a source build, from a folder whose path has a space, with `PATH` reduced to the system folders and no `DOTNET_*` variable: it must print the release version and make a plan-only setup with the recorded plan digest. The Linux executables are linked in `ubuntu:22.04`, pinned by digest, so they need glibc 2.35 or later (Ubuntu 22.04, Debian 12 or later); the release job fails if one needs a newer glibc version, and runs each in `debian:12-slim` and `ubuntu:22.04` (with its `libssl3` package removed), with no .NET, `libicu`, `libssl` or `libcrypto`. A Linux executable needs nothing else from the machine. They do not run on musl distributions such as Alpine. Nothing else is checked. The files are not code-signed and the macOS file is not notarized, so Gatekeeper can refuse a copy that a browser downloaded until its quarantine attribute is removed; this was not checked on a Mac. The release does not download, install, update or activate a toolchain and does not verify signed metadata. The pull request workflow `morphirup` runs the same build and check on all four platforms ([#558](https://github.com/finos/morphir-dotnet/issues/558))
- `Morphir.Toolchain`: readers and writers for the `0.1.0-draft.1` toolset recipe, project toolchain request, portable lock, installation receipt and manager result of finos/morphir, with pure resolution, lock, plan and project discovery functions, pinned to finos/morphir `1451c081f`. A provider's `protocolVersion` is a MEP version on the supported 0.2 line (every prerelease of `0.2.0` and every final `0.2.x`); MEP `0.1` is refused, and an external provider carries one `modelTuple` and no `modelInput` or `modelOutput`. Plan digests and target paths are normalized to NFC with the library's own Unicode 17.0.0 tables, generated by `scripts/generate-nfc-tables.fsx` and checked against all of `NormalizationTest.txt`, so neither depends on the machine's ICU. It runs every case of the pinned finos/morphir contract manifests that is a pure function of documents and authenticated metadata. The cases that need a TUF client to verify metadata, or the installation work of later slices, are skipped by name with their reason ([#558](https://github.com/finos/morphir-dotnet/issues/558))
- The F# frontend extension `morphir-fsharp-frontend` has a self-contained process delivery: a folder with the guest, the F# compiler service and its own .NET runtime, published with the `SelfContainedProcess` profile or the build target `PublishFSharpFrontendProcess --rid <rid>`, which also writes a zip or `.tar.gz` archive (files at the root, ordinal order, fixed time stamp and modes) and keeps the publish's lock file beside it. A host such as the native `morphir` starts it with `--extension`. Checked on Windows ARM64 only, in an environment that hides the machine's .NET (no `dotnet` on `PATH`, no `DOTNET_*` variables, an empty NuGet folder and no global install): the automated test runs the guest there, and the native `morphir` compiled F# through it there by hand. It was not run on a machine with no .NET at all. Releases do not publish it yet. The framework-dependent dotnet tool is unchanged
- `morphir-fsharp-backend`: the F# generator as a MEP `0.2.0-draft.1` guest, packaged as the framework-dependent dotnet tool `Morphir.CodeGeneration.FSharp.Extension` with the extension id `org.finos.morphir.fsharp-backend`. It serves `morphir.backend.generateModel` for the target `fsharp` with JSON model tuples for IR `4.0.0` and `3.0.0`, for hosts that have no built-in F# backend. The built-in `morphir.fsharp` stays the backend of `morphir generate`, which does not start the extension. Releases publish the package next to `Morphir.Tool`
- `Morphir.Mep` backend messages: the pinned `generateModel` request, generate result and artifact types, and `GenerateModel.admit`, the guest check of a request. Compatibility note for F# code that opens `Morphir.Mep`: like the other wire types, the new records `GenerateModelRequest`, `GenerateResult`, `Artifact` and `AdmittedGenerateRequest` are at the top level of the namespace, and their labels (`Model`, `Input`, `Target`, `Options`, `Path`, `Content`, `Binary`, `Success`, `Artifacts`, `Diagnostics`, `Request`, `Guarantees`) can change which record type F# infers for an unannotated record expression or member access. Annotate the record type where that happens. The type name `Artifact` also exists as `Morphir.Conformance.Artifact`; qualify it when both namespaces are open
- `MorphirApplication` in `Morphir.Tooling`: the shared command layer (the scenarios, `info` and `--version`) that every entry point runs. `Morphir.Program.Run` and `Morphir.Program.CreateApp` keep their signatures and delegate to it
- A managed `morphir` entry point (`src/Morphir.Managed`) and the managed composition it shares with `dotnet-morphir` (`src/Morphir.Tooling.Managed`), which holds the in-process F# frontend. It is not packaged or released yet
- Build: named release profiles for the four .NET deployment offerings. `PublishRelease` and `PackRelease` with `--deployment native-aotself-containedframework-dependentdotnet-tool` and `--rid` produce `artifacts/release/morphir-dotnet-<release>-<deployment>-<rid>.<ziptar.gz>` (or `Morphir.Tool.<release>.nupkg`) with a build-side `.build-launch.json` beside it, for win-x64, win-arm64, osx-x64, osx-arm64, linux-x64 and linux-arm64. The managed profiles are untrimmed folders with the in-process F# frontend. The same publish folder always gives the same archive bytes (fixed time stamp, ordinal entry order, mode 0755 for the launch file and `createdump`, 0644 otherwise). `PackRelease` fails when the publish folder contradicts the launch metadata, and checks the tool package as `PackTool` does. A release with build metadata (`1.2.3+abc`) is refused. Releases do not publish these archives yet, and no platform is qualified yet ([#558](https://github.com/finos/morphir-dotnet/issues/558))
### Changed
- `dotnet-morphir` runs the shared command layer with the managed composition and no longer references the AOT-compatible `morphir` project, so the tool package no longer contains `morphir.dll`. Its commands and its F# frontend are unchanged
- Build: `PublishExecutable`, `PublishSingleFile` and `PublishSingleFileUntrimmed` take their settings from the release profile model. `PublishSingleFile` keeps the settings and names of the legacy release assets `morphir-<rid>[.exe]` and `morphir-<rid>-v<version>.tar.gz`. `PublishExecutable` now leaves out debug symbols and documentation files, and finds `morphir` (not `Morphir`) on Linux and macOS. Size figures of `PublishExecutable` output from before this change included the symbol file, so they are not directly comparable with new figures ([#558](https://github.com/finos/morphir-dotnet/issues/558))
- Build: publishes for one runtime identifier write their NuGet lock files under `obj/publish-lock/<rid>/` (the `MorphirRidSpecificPublish` property), so they no longer add RID sections to the committed `packages.lock.json` files ([#558](https://github.com/finos/morphir-dotnet/issues/558))
- Build: `src/Morphir.Managed` fails to build with `PublishAot` or `PublishTrimmed`, and a project whose references resolve two different `morphir.dll` assemblies (`src/Morphir` and `src/Morphir.Managed`) fails to build
- `just publish-executable`, `just publish-single-file` and `just publish-single-file-untrimmed` run the Nuke targets; their `VERSION` argument is gone, as the version comes from `CHANGELOG.md`. `scripts/measure-compile-aot.sh` publishes with the native-aot profile's settings
### Removed
- `scripts/publish-single-file.cs` and `scripts/publish-single-file-untrimmed.cs`, which called a removed `codegen write` command. Use the `PublishSingleFile` and `PublishSingleFileUntrimmed` targets ([#558](https://github.com/finos/morphir-dotnet/issues/558))
### Fixed
- The F# frontend takes its framework references from the framework's runtime assembly list, so in a self-contained folder it no longer gives the compiler the application's own assemblies (`Morphir.*`, the compiler service). The references of the dotnet tool and the in-process frontend are unchanged. It reads the deps files that the host lists (`APP_CONTEXT_DEPS_FILES`, joined with `;` on every platform). When it cannot give the compiler its references (no file location, a self-contained folder without a framework list, no trusted platform assemblies, or a listed deps file that cannot be read), compilation fails with one `fsharp.references_unavailable` error instead of an exception
- `Morphir.Myriad.SystemTextJson` build targets: an absolute `IntermediateOutputPath` (for example with `--artifacts-path`) no longer gives an invalid output folder for the generated JSON contexts