Why fsnative Needs to Exist
#3
houstonhaynes
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Why fsnative Needs to Exist
The Fidelity Framework includes fsnative, a hard fork of F# Compiler Services. This is not a decision we made lightly. Forking a compiler is a significant commitment: ongoing maintenance, divergence from upstream, and the burden of explaining to every new contributor why we didn't just use the official toolchain. This discussion explains the technical rationale behind that decision.
The Core Problem: BCL Types Are Hardcoded
F# Compiler Services was designed for .NET compilation. Its type system assumes BCL (Base Class Library) primitives. When you write a string literal in F# code, FCS types it as
System.String. This is not a configurable option; it is hardcoded in the checker.The
g.string_tyhere isMicrosoft.FSharp.Core.string, which isSystem.String. Every string literal in every F# program compiled with FCS carries this type. No amount of downstream processing can change what the type checker decided at parse time.Type Shadows Do Not Solve This
Our initial approach in the Alloy standard library was to shadow BCL types:
Type shadows only affect type annotations, not literal inference:
This creates an asymmetry that cannot be resolved at the library level. User code looks correct:
Console.Write "Hello"appears to use native strings. But FCS has already decided that"Hello"is a BCL string before any Alloy code runs.The Downstream Consequences
Because FCS outputs BCL types, every downstream component faces an impossible choice:
None of these options are acceptable. The fix must happen at the source: the type system itself.
What fsnative Changes
fsnative modifies FCS to use a native type universe where Fidelity's primitives are first-class citizens:
System.String(UTF-16, GC-managed)NativeStr(UTF-8, deterministic lifetime)FSharpOption<T>(reference, heap-allocated)voption<T>(value, stack-allocated)System.StringNativeStrThe modifications are surgical. fsnative is not a rewrite of FCS; it is a targeted intervention in the type resolution layer. The API surface remains compatible with FCS, so Firefly's integration requires minimal changes.
Beyond Type Resolution: Why a Hard Fork
The decision to hard fork rather than maintain a tracking fork stems from two additional concerns that go beyond type resolution.
Shedding MSBuild Infrastructure
FCS carries substantial infrastructure for "project cracking": the tooling required to parse MSBuild project files, resolve NuGet packages, handle multi-targeting, and support the full complexity of .NET's build ecosystem. This infrastructure serves .NET developers well, but it represents dead weight for Fidelity.
We do not need MSBuild integration. Fidelity uses
.fidprojfiles with a simple TOML format. We do not need NuGet resolution; Fidelity uses source-based dependency management. We do not need multi-targeting across .NET framework versions; we target native platforms directly.Maintaining compatibility with this infrastructure would constrain our design choices and burden us with code paths we will never exercise. A hard fork allows us to remove what we do not need, simplifying the codebase and reducing the surface area for bugs.
Memory Mapping at the Front End
The deeper opportunity, and the more significant undertaking, is embedding memory mapping semantics directly into the F# front end. This is not merely about choosing between stack and heap allocation; it is about understanding memory layout, alignment, cache behavior, and data flow at the earliest possible point in the compilation pipeline.
Consider what becomes possible when the type checker understands memory:
Deterministic layouts: When fsnative types a record, it can compute the exact byte layout at parse time. Field offsets, padding, and alignment become statically known properties of the type, not runtime discoveries.
Cache-conscious data structures: With knowledge of target cache line sizes, the compiler can warn about or automatically adjust structures that would cause false sharing or cache thrashing. This analysis belongs in the front end, where type definitions are visible, not in a downstream optimization pass.
Zero-copy validation: BAREWire's type-safe binary encoding relies on compile-time knowledge of memory layout. When the front end guarantees deterministic layouts, BAREWire can validate wire format compatibility without runtime checks.
Region-aware typing: Memory regions (stack, heap, arena, peripheral) can become part of the type system rather than annotations checked after the fact. A function that returns a stack-allocated value cannot escape that value; the type system enforces this.
This integration is both a huge opportunity and a significant undertaking. Even if we eventually implement a plugin system based on Farscape bindings or Alloy abstractions, having the right intervention point in the nanopass infrastructure justifies the fork. Guaranteeing the integrity of memory operations is deeply integral to what fsnative must become.
The Semantic Alignment with OCaml
This brings us to a deeper observation. F#'s type system was designed to interoperate with .NET, but F#'s semantics trace back to OCaml. F# began as "Caml for .NET" before developing its own identity. Many F# idioms, particularly around pattern matching, algebraic data types, and immutability, are OCaml idioms adapted to the CLR.
The BCL imposes different semantics:
System.StringSystem.Tuple<>(heap) orValueTuple<>fsnative's type universe aligns more naturally with OCaml semantics than with BCL semantics:
NativeStr(UTF-8, deterministic lifetime)voption<T>(value type, stack-allocated)structtuple, stack-allocatedThis alignment matters for F* integration, which is on our roadmap. F* (the proof-oriented language from Microsoft Research and INRIA) extracts verified code to OCaml as its primary, well-maintained target. The F# extraction path is documented as lagging behind because of the semantic mismatch between F*'s expectations and BCL types.
When F* extracts to fsnative-compatible F#, the translation becomes more direct. The proofs F* generates about value semantics, memory behavior, and null-freedom map accurately to what the compiled code actually does.
Cache-Aware Compilation and BAREWire
The memory mapping capabilities in fsnative connect directly to Firefly's cache-aware compilation strategy. Modern processors exhibit a fifty-fold performance difference between L1 cache hits and main memory access. Effective cache utilization cannot be achieved through runtime heuristics alone; it requires semantic understanding of data access patterns that only the compiler can provide.
With deterministic layouts from fsnative, Firefly can perform cache analysis at compile time:
Structure padding for cache alignment: When fsnative knows a structure will be accessed frequently, it can align critical fields to cache line boundaries. The
[<BAREField>]attribute system allows explicit control when needed, but the compiler can infer appropriate layouts for most cases.Access pattern analysis: The Program Semantic Graph captures data flow across function boundaries. Combined with fsnative's memory region typing, this enables the compiler to predict which allocations benefit from cache residency and which should bypass cache entirely.
Actor placement guidance: Prospero, the orchestration layer, uses compile-time knowledge of actor communication patterns to place related actors in the same cache domain. Messages between producer-consumer pairs can pass through L3 cache without touching main memory.
BAREWire's zero-copy message passing depends on this foundation. When sender and receiver agree on memory layout at compile time, serialization becomes memcpy and deserialization becomes pointer casting. There is no parsing, no allocation, no garbage. The type system guarantees that both parties see identical byte layouts.
This is why memory semantics belong in the front end. By the time code reaches MLIR generation, the opportunity for cache-conscious layout decisions has passed. The types have been erased, the structures have been flattened, and the semantic information needed for intelligent placement is gone. fsnative preserves this information through the entire pipeline.
Why Not Work Around It?
We spent considerable effort attempting to work around FCS's BCL assumptions before concluding that a fork was necessary. Some approaches we tried:
Namespace tricks: Placing native types in the same namespace as BCL types, hoping FCS would find them first. This fails because FCS's type universe is resolved at initialization, not at lookup time.
Post-processing transformations: Rewriting BCL types to native types after FCS produces the typed tree. This creates a semantic gap: the type checker validated one set of types, but code generation receives another. Bugs in this gap are subtle and difficult to diagnose.
SRTP redirection: Using statically resolved type parameters to redirect operations from BCL types to native types. This works for some operations but fails when FCS has already inlined the BCL implementation.
Each workaround added complexity and created new edge cases. The fundamental problem remained: FCS assumes .NET compilation at a level too deep to override from outside.
The Layer Separation Principle
With fsnative, the Fidelity stack achieves clean layer separation:
Each layer has a single responsibility. No layer needs to work around another layer's assumptions. The types are correct from the start, so downstream components can trust what they receive.
Relationship to the Broader Ecosystem
fsnative is not intended to replace FCS for .NET development. If you are building applications for .NET, you should use the standard F# compiler. fsnative exists specifically for native compilation scenarios where the BCL is not available or desirable.
The repository structure reflects this:
We maintain a companion specification repository, fsnative-spec (forked from fsharp/fslang-spec), documenting the native F# dialect: what types mean, how literals are typed, and where the semantics diverge from standard F#.
Further Reading
For deeper exploration of the ideas behind fsnative:
Compilation and Architecture
Memory Management
Cache-Aware Compilation
BAREWire and Type Safety
Questions for Discussion
We would value perspectives from developers who have encountered similar problems:
If you have worked with F# in contexts where .NET assumptions caused friction (embedded, WebAssembly, interop with non-.NET systems), what workarounds did you use?
For those familiar with OCaml or F*: does the semantic alignment described here match your intuitions about how these languages relate?
The fork commits us to maintenance. Are there approaches we might have missed that would achieve native type resolution without forking?
How important is it to you that fsnative remain API-compatible with FCS? Would breaking changes be acceptable if they enabled cleaner native semantics?
For developers working on performance-critical systems: how much value do you see in compile-time cache analysis versus runtime profiling and tuning?
We are building this in the open because we believe the conversations along the way matter as much as the destination.
All reactions