Raven Core Library
Raven.Core is the standard library that ships with the compiler. It contains
foundational types implemented in Raven and compiled into Raven.Core.dll so
programs can rely on a consistent Option/Result vocabulary across projects,
plus a handful of generated shims the compiler produces automatically when it
builds the library.
How Raven.Core is built
The src/Raven.Core project builds directly from the Raven sources in
Option.rav and Result.rav. During a solution build, MSBuild runs the
CompileRavenCore target, which:
- Invokes
Raven.Compilerwith--emit-core-types-onlyto compile the Raven sources into a class library for the current target framework. - Emits the assembly to
src/Raven.Core/bin/<Configuration>/<TargetFramework>/Raven.Core.dll. - Emits
Raven.Core.xmlbeside the assembly. The compiler loads this sidecar for metadata symbols, including projection adapters, so hover and other editor surfaces can display the Raven-facing API documentation. - Exposes the DLL via the standard
BuiltProjectOutputGroupso it is copied alongside the CLI and into compilation outputs together with its XML documentation.
Because this target runs before the rest of the build, dotnet build Raven.sln
produces Raven.Core.dll automatically and keeps it in sync with the compiler.
You can still override the reference with rvn --raven-core <path> when
compiling standalone projects.
Framework API projections
Raven exposes a curated Raven-facing view of selected framework APIs by
default. The catalog projects TryParse(string, out T) overloads for int,
long, double, decimal, Guid, DateTime, DateTimeOffset, DateOnly,
TimeOnly, and TimeSpan, including the common numeric style/provider and
DateTime provider/style forms:
val number = int.TryParse(text) // Option<int>
val currency = decimal.TryParse(text, NumberStyles.Currency, culture)
// Option<decimal>
It also replaces int.Parse(string) and Guid.Parse(string) with
result-returning Raven signatures:
val number = int.Parse(text)
// Result<int, FormatException | OverflowException>
val id = Guid.Parse(text)
// Result<Guid, FormatException>
Dictionary lookup is projected with the same explicit approach:
val value = dictionary.TryGetValue(key) // Option<TValue>
None means that the key is absent. A nullable declared value remains nullable
inside Some, so Dictionary<string, string?> produces Option<string?> and
does not conflate a present null value with an absent key.
The catalog is an exact, versioned mapping rather than a naming convention.
Each entry records its exact source signature, projected signature, stable
projection ID, bridge implementation, and expected exception mapping. The
TryParse entries catch no exceptions: a false result becomes None, while
source exceptions remain exceptions. The int.Parse and Guid.Parse bridges
put expected format/range failures in Result.Error. Exceptions that require
violating a non-null projected signature, such as passing a forced null and
triggering ArgumentNullException, propagate as faults instead.
This exception-preserving surface is intended for predictable .NET interop. At an application or domain boundary, map those framework exceptions into your own error records or error unions so the domain model does not expose infrastructure-specific failures throughout the program.
Set RavenFrameworkProjections to None in a project to restore the ordinary
.NET member families, including their out parameters. See the framework API
projections proposal for the planned mapping-file extension model.
Types included in Raven.Core
Option<T>
A union carrier with two cases:
Some(value: T)holds a value.Nonerepresents the absence of a value.
Option<T> is a plain union that uses Raven's standard struct carrier model:
- runtime values are
Option<T>carriers - case values convert into the carrier
- the carrier exposes a conventional
Value: object?property containing the active case value - the carrier exposes
HasValue: boolfor initialization-safe inspection across interop boundaries - extraction uses
TryGetValue(out Some<T>)/TryGetValue(out None)and pattern matching
For the built-in Option<T> struct carrier, HasValue is true for
constructed values and false for the default carrier value. Value points at
the active case object (Some<T> or None) when initialized. The carrier
itself is the stable runtime type; Value is the authoritative active-case
projection.
Because Option<T> is a struct union, default(Option<T>) is an inactive
carrier state rather than None. Raven code that receives an Option<T> as a
parameter or self can assume an active carrier because the call boundary
rejects possibly inactive arguments before entry. Fields and properties remain
storage/interop boundaries until local flow proves them active. Match
exhaustiveness is still source-checked over the semantic cases (Some and
None); the inactive carrier is not a source case. Use a catch-all arm only when
the program intentionally handles a physically possible inactive carrier. Locals
initialized from Some(...) or None are known active and report redundant
catch-all arms.
Option<T> extension helpers:
- State checks:
HasSome,HasNone - Mapping and composition:
Map,Then,Where,Filter,OrElse - Interop with
Result:ThenResult,MapResult,IsOkOr(error),IsOkOr(errorFactory) - Pattern/value helpers:
Match,Tap,TapNone - Unwrap helpers:
UnwrapOrElse,UnwrapOrDefault,UnwrapOrThrow,UnwrapOr(defaultValue) - Sequence helpers:
ToEnumerable,GetEnumerator - Nested helper (
Option<Option<T>>):Flatten - Nullable conversions:
Option<T : class> <-> T?Option<T : struct> <-> T?
Result<T, E>
Result<T, E> is also a plain union struct carrier:
Ok(value: T)for success.Error(data: E)for failure.
Like Option<T>, Result<T, E> uses carrier semantics rather than inheritance semantics. Case values convert into the carrier, and extraction happens through TryGetValue(out CaseType) or pattern matching.
The carrier also exposes a conventional Value: object? property
containing the active case value.
The carrier also exposes HasValue: bool, where constructed values report
true and the default carrier value reports false.
For constructed Result<T, E> values, Value points at either the active
Ok<T> or Error<E> case object.
Because Result<T, E> is a struct union, default(Result<T, E>) is an inactive
carrier state rather than an Error. Raven code that calls a Result<T, E>
helper with a visible default-capable receiver is diagnosed at the call boundary.
If external code or unsafe construction still forces an inactive carrier into a
helper, the source match is exhaustive over Ok and Error and the emitted
defensive fallback handles the invalid representation state. Code that forwards
a Result<T, E> across a call or return boundary must pass or return a value
that flow analysis knows is active.
Result<T, E> extension helpers:
- State checks:
HasOk,HasError - Channel projection:
IsOk,IsError - Mapping and composition:
Map,Then,MapError,OrElse - Pattern/value helpers:
Match,Tap,TapError - Unwrap helpers:
UnwrapOrElse,UnwrapOrDefault,UnwrapOrThrow,UnwrapOr(defaultValue),UnwrapError - Sequence helpers:
ToEnumerable,GetEnumerator
LINQ-style carrier extensions (System.Linq)
IEnumerable<T> gets Raven.Core helpers for carrier-friendly queries:
- Option-returning:
FirstOrNone(),FirstOrNone(predicate)LastOrNone(),LastOrNone(predicate)SingleOrNone(),SingleOrNone(predicate)ElementAtOrNone(index)
- Result-returning (custom error):
FirstOrError(errorFactory),FirstOrError(predicate, errorFactory)LastOrError(errorFactory),LastOrError(predicate, errorFactory)SingleOrError(errorFactory),SingleOrError(predicate, errorFactory)SingleOrError(errorIfNone, errorIfMany)SingleOrError(predicate, errorIfNone, errorIfMany)ElementAtOrError(index, errorFactory)
- Result-returning (captured exception):
ToArrayOrException() -> Result<T[], Exception>ToListOrException() -> Result<List<T>, Exception>ToHashSetOrException() -> Result<HashSet<T>, Exception>ToDictionaryOrException(keySelector) -> Result<Dictionary<TKey, T>, Exception>ToDictionaryOrException(keySelector, elementSelector) -> Result<Dictionary<TKey, TValue>, Exception>
- Result-returning (mapped error):
ToArrayOrError(errorFactory: Exception -> E) -> Result<T[], E>ToListOrError(errorFactory: Exception -> E) -> Result<List<T>, E>ToHashSetOrError(errorFactory: Exception -> E) -> Result<HashSet<T>, E>ToDictionaryOrError(keySelector, errorFactory) -> Result<Dictionary<TKey, T>, E>ToDictionaryOrError(keySelector, elementSelector, errorFactory) -> Result<Dictionary<TKey, TValue>, E>
These unions provide lightweight error-handling primitives while keeping Raven programs compatible with the .NET type system.
Raven.Core union stability contract
Raven.Core treats its built-in union carriers as .NET-compatible value types by default. This keeps the ABI aligned with the C# generated-union direction:
- Plain
uniondeclarations synthesize struct carriers. - C# callers can use the public single-payload constructors,
HasValue,Value, and typedTryGetValue(out T)overloads without Raven-specific runtime helpers. union classremains available for reference-type carrier semantics and is the right choice when a union must satisfyclassconstraints or intentionally behave like an object hierarchy at runtime.default(Option<T>),default(Result<T, E>), and default values of standardUnion<...>carriers are inactive carrier states. They are not semanticNone,Error, ornullcases.- Raven.Core helpers should rely on the active
selfcontract rather than writing source-level default arms. Visible default-capable receivers are rejected at the call boundary; forced invalid carriers are handled by the emitted match fallback. - Match expressions and statements are source-exhaustive when they cover the declared semantic cases. Raven source does not require a discard/default arm just because a struct carrier has a physical inactive default state.
- Lowering and emit preserve a defensive runtime fallback for source-exhaustive matches, throwing when no arm matches so metadata consumers and forced default carriers cannot fall through silently.
- Code may still write an explicit discard/default arm when it intentionally wants to handle a possible inactive carrier at runtime. Such an arm is not redundant when flow says the matched struct-union value may be default.
- Passing or returning a struct-union value across a method boundary requires a
value that flow analysis knows is active. Parameters and
selfare already active inside the callee; fields, properties, and default-capable locals must be proven active before they are forwarded. - Local struct-union variables may temporarily hold the inactive default state,
either because they were explicitly assigned
defaultor because analysis has not yet proved an active assignment. That is a valid intermediate program state. The diagnostic boundary is where such a value is passed, returned, or otherwise exposed to code that expects an initialized carrier. - JSON converters defensively serialize inactive default carriers as JSON
null. This is a runtime safety behavior for forced serialization of an invalid carrier state, not a language-levelnullunion case. - Built-in carriers have a documented payload-first
System.Text.Jsoncontract. Tagged/discriminator serialization is opt-in throughRavenTaggedUnionJsonConverterAttributeor explicit converter registration; no tagged union policy is installed globally.
This contract deliberately separates Raven semantics from the physical .NET carrier representation. A union's declared cases describe the semantic domain; the inactive default carrier exists because struct values in .NET always have a zero-initialized state.
Future work
The current implementation is intentionally conservative. Future Raven.Core and compiler work should focus on:
- Exhaustiveness: keep fallback lowering covered as new match forms are added, including the planned prefix-standard and trailing/postfix match expression split.
- Boundary analysis: keep expanding assignment/active-state analysis only where new language constructs create genuinely new boundaries. The current contract covers ordinary calls, generic method calls, extension receivers, delegate invocation, params elements, constructors, async/lambda returns, and field/property forwarding; future work should focus on richer control-flow joins, nested projections, and interop/imported-union cases while keeping diagnostics at the call or return boundary.
- JSON deserialization policy: decide whether JSON
nullshould deserialize to activeNone/fallbackError, remain a serializer-only representation for inactive carriers, or become configurable per converter. - Null modeling: defer any
nullpseudo-type,Nullcase-stand-in, or nullable union extension until the C# union design settles further. Raven should not reintroducenullas a pseudo member type in the core union ABI. - Analyzer support: add lightweight Raven.Core audits that flag helper methods
returning raw boundary carriers or unconstrained
defaultvalues fromOption,Result, orUnion<...>APIs. - Extension coverage: continue adding focused runtime tests for Raven.Core
extension helpers that accept union receivers, especially helpers that forward
self, return nested unions, or bridgeOptionandResult.
Project builds include a generated prelude source file that globally imports
the standard System namespaces, System.Result.*, and System.Option.*.
That prelude is why standard cases such as Ok, Error, Some, and None
are usually available as simple names in project code. User-defined union cases
require qualification, target-typed member syntax, or an explicit
import UnionType.*. Global imports are hoisted across the compilation but
still bind as ordinary imports; type-scope and direct nested-case imports require
the imported type or nested type to be available to the compilation.
When you want OOP-style subtype semantics, prefer Raven sealed hierarchies instead of unions.
JSON serialization behavior for Option<T>, Result<T, E>, standard type
unions, and opt-in tagged Raven unions is documented in
Raven Core JSON serialization.
Unit
unit is Raven's "no meaningful value" type. The compiler represents it as a
System.Unit struct with a single static Value field so callers can observe a
unit value even though the underlying IL maps the type to void. Code that
returns unit emits Unit.Value as needed to bridge the gap between the
expression-oriented language semantics and .NET metadata.
When unit appears in generic type arguments or parameter positions, the
compiler preserves it as System.Unit so constructed signatures can resolve
against emitted union cases and helper methods.
Generated types
When the compiler builds Raven.Core (and when it embeds core shims for
standalone compilations) it also synthesizes a few supporting types that aren't
hand-written in src/Raven.Core:
System.Runtime.CompilerServices.UnionAttributeandUnionCaseAttributeannotate union types and their cases so the emitted IL retains union metadata.System.Unitis emitted whenever a project depends on theunittype, ensuring theUnit.Valueshim is available even when compiling without an existing Raven.Core reference.