.NET Implementation Notes
This document outlines how Raven constructs map to the .NET runtime and metadata. For language semantics, see language-specification.md.
Unit type
When interacting with .NET, methods that return void are projected as returning unit, and Raven's unit emits as void unless the value is observed. After any call that returns metadata void, the compiler loads Unit.Value so the invocation still produces a unit result. In an expression statement that value is discarded, enabling nested unit-returning calls such as Console.WriteLine(Console.WriteLine("foo")). The unit type is a value type (struct) and participates in generics, tuples, and unions like any other type.
Return statements
A return without an expression in a method that returns unit emits IL with no value. If the underlying method returns void, Unit.Value is loaded to produce a unit result before the ret instruction.
Attributes
Source attributes are bound using the same import and namespace lookup rules as other type references, so import System.* enables [Obsolete] without a fully qualified name. When Raven creates metadata AttributeData, it accepts the subset of argument expressions supported by the runtime: literals (including null), enum constants, typeof expressions, array/collection literals (including empty collections when a target type is known), constant enum flag expressions such as .Class | .Delegate, and conversions among those forms. Enum constants may be written with a qualified name such as AttributeTargets.Delegate, or with target-typed member syntax such as .Delegate when the attribute constructor parameter supplies the enum type. The compiler lowers these argument forms to typed constants before emitting the attribute payload.
Raven also validates explicit target prefixes (assembly:, return:, etc.)
against declaration position:
assembly:binds only at the compilation-unit level.return:binds to callable return metadata, not to declaration-level method attributes.- Target prefixes used in an invalid declaration context are rejected with an attribute-target diagnostic.
Extension members
Raven both declares and consumes extension members using CLR extension metadata, but it classifies extension semantics per emitted member rather than treating an entire container as one kind of extension surface. Source extensions arise from two forms:
- An
extensiondeclaration emits astaticclass named after the container. Instance extension members becomestaticmethods whose first parameter represents theselfreceiver. The compiler synthesizes that parameter, applies theExtensionAttribute, and copies any explicit parameters written in source onto the emitted method signature. - Existing static methods annotated with
[Extension]continue to be recognised as extensions.
Computed instance extension properties declared inside an extension body lower
to accessor methods that follow the same pattern. The compiler synthesizes
get_ and set_ methods, inserts the receiver as the leading parameter, and
marks each accessor with ExtensionAttribute. Property metadata is emitted
alongside the accessors so reflection reports a property with the expected
accessor pair even though the backing logic is implemented by static methods.
To interoperate with C# extension blocks (C# 14), Raven also emits an extension
marker nested type for each extension declaration. The marker type is named
<>__RavenExtensionMarker and contains a single <Extension>$ method whose
parameter encodes the receiver type. Each emitted extension member (methods and
properties, including static extension members) is annotated with
System.Runtime.CompilerServices.ExtensionMarkerNameAttribute pointing to the
marker type name, enabling C# to recover the extension receiver signature when
consuming Raven-compiled assemblies.
When importing metadata, Raven distinguishes classic extension methods from
static extension members per member. Method-level ExtensionAttribute
continues to identify classic extension methods, while receiver-marker metadata
identifies static extension members that participate in Type.Member lookup
without being treated as classic extension methods. This keeps mixed extension
containers compatible with .NET/C# lookup expectations.
In both cases the emitted metadata matches C#'s expectations.【F:src/Raven.CodeAnalysis/Symbols/Source/SourceMethodSymbol.cs†L197-L233】 When binding a member-style invocation, Raven merges instance methods with any imported extensions that can accept the receiver, then rewrites the call to pass the receiver as the leading static argument during lowering and IL emission.【F:src/Raven.CodeAnalysis/Binder/BlockBinder.cs†L1946-L2001】【F:src/Raven.CodeAnalysis/BoundTree/Lowering/Lowerer.Invocation.cs†L8-L29】 The same rewrite applies to extension-property access: getters become static calls that receive the target as their first argument, and setters pass both the target and assigned value to the synthesized method.
The CLI ships with regression coverage that compiles and runs extension-heavy programs, including LINQ-style pipelines that rely on lambda arguments, to ensure the metadata load context path continues to resolve delegate constructors and emit correct IL.【F:test/Raven.CodeAnalysis.Samples.Tests/SampleProgramsTests.cs†L66-L140】【F:test/Raven.CodeAnalysis.Tests/Semantics/ExtensionMethodSemanticTests.cs†L563-L703】
Properties and fields
Raven is property-first for type members:
val/vardeclarations in classes/structs emit CLR properties with accessor methods (get_/set_/initas applicable).- Stored and auto-style properties synthesize backing storage when needed.
fieldused inside a property accessor refers to that synthesized backing field for the current property.
Explicit field declarations are emitted as CLR fields and are intended for
storage/layout-sensitive scenarios (for example interop with
StructLayout(LayoutKind.Sequential|Explicit) and FieldOffset).
readonly field emits an initonly field. Dedicated const declarations emit
metadata literal fields (static/literal).
Lifecycle declarations
Raven lifecycle declarations map to CLR methods:
init { ... }lowers as an instance constructor body (.ctor) with no explicit parameter list in source.static init { ... }lowers as a static constructor (.cctor).init(...)remains constructor-shape syntax and maps to.ctoroverloads.finally { ... }lowers as aFinalizeoverride.
Union interop (C#)
Raven unions compile into a carrier type plus independent case types that C#
can consume directly. Each case becomes a public type with a constructor that
accepts the payload values, a set of get-only properties for those payloads,
and a Deconstruct(out ...) method that mirrors the payload order. The union
carrier exposes overloaded TryGetValue(out CaseType value) helpers to safely
extract a case instance. For parenthesized unions such as
union Either<T1, T2>(T1 | T2), the carrier is declared over existing member
types instead of synthesized case types, so the carrier constructor and
TryGetValue overloads operate directly on those member types.
Raven follows the C# union ABI for metadata recognition: a union carrier is a
class or struct with System.Runtime.CompilerServices.UnionAttribute, a public
Value property of object or object?, and at least one public
one-parameter constructor. Those constructors define the case set. Public
TryGetValue(out T) methods are an access pattern and do not introduce
additional cases when constructors are present. Nullable active contents are
derived from nullable constructor parameter types, not from Value being
object?.
For nullable members in a parenthesized union declaration, Raven emits nullable-capable constructor parameter types for the listed members and does not emit a synthetic null constructor:
union JsonValue(string? | double | bool | JsonObject | JsonValue[])
Raven pattern matching still treats nullable member contents as the non-null
listed member cases plus a distinct null branch.
Producing a union from C# is done by constructing the case and then converting it to the carrier:
// Raven
// union Token { Identifier(text: string) Number(value: int) }
var identifier = new TokenIdentifier("hello");
Token token = identifier;
Token other = new TokenNumber(42);
Consuming a union from C# involves calling an overloaded TryGetValue helper
and then using the case properties or deconstruction to extract payload values:
if (token.TryGetValue(out TokenIdentifier identifier))
{
var (text) = identifier; // Deconstruct(out string text)
Console.WriteLine(text);
}
For a parenthesized union, extraction is performed directly on the member type:
// Raven
// union Either<T1, T2>(T1 | T2)
Either<int, string> value = 42;
if (value.TryGetValue(out int left))
{
Console.WriteLine(left);
}
These members allow C# callers to work with Raven unions without needing reflection, while Raven still relies on the synthesized metadata attributes to preserve the union semantics for other tools.
Struct union default state
Raven source union declarations emit struct carriers by default, matching the
C# union direction. Like any value type, a struct union can be zero-initialized
with default(U) before any union constructor has populated it. In that
inactive carrier state, Value is null, HasValue is false, and no case is
active.
The inactive carrier state is a runtime representation state, not a declared union case. Raven therefore keeps it separate from the source case set:
- A local value initialized from a union case is known active, so matching it is exhaustive when every declared case/member is covered. A catch-all arm after all cases is redundant.
- A local initialized with
default, or a local that may flow fromdefault, may still use a catch-all arm to intentionally handle the inactive carrier, but the inactive carrier is not a source exhaustiveness case. - Function parameters and
selfare active inside the callee because the call boundary rejects possibly inactive arguments before entry. Forwarding them across another call or return boundary is allowed. - Fields and properties are storage/interop boundaries that may be inactive/default. Forwarding those values across another call or return boundary requires local flow to prove an active value, usually by copying or reconstructing a declared case.
- Passing a possibly inactive struct-union value to a struct-union parameter is
rejected at the call site with
RAV0405. - Returning a possibly inactive struct-union value is rejected at the return
boundary with
RAV0406.
union class carriers do not have an extra zero-initialized carrier state. A
class union value exists only after construction or conversion through one of
its union cases or constructors, subject to normal nullable-reference rules for
the carrier reference itself.
Generic variance
The Raven compiler surfaces the CLR's variance metadata directly. When importing
types from reference assemblies, the GenericParameterAttributes flag on a type
parameter controls the reported VarianceKind: Covariant maps to out and
Contravariant maps to in. These annotations influence interface
implementation checks and reference conversions so that, for example,
IEnumerable<string> is recognised as an implementation of
IEnumerable<object>, and IComparer<object> satisfies a requirement for
IComparer<string>.
Source interface declarations may annotate their type parameters with out or
in. Raven maps those modifiers onto the same metadata flags when emitting
symbols, so variant source interfaces interoperate with metadata-defined
counterparts without requiring any special handling.