Type system
Raven is statically typed. Every expression has a type, and incompatible operations are diagnosed before the program runs.
let count = 3 // int
let name: string = "Ada"
let optional: string? = null
Raven uses the .NET type system directly, so existing framework and library
types can be used without wrappers. At the same time, Raven gives some CLR
shapes a consistent source-language meaning. Nullable reference and value types,
for example, are both written and analyzed as T? even though .NET represents
them differently in metadata.
Type annotations
Use type annotations where inference is insufficient or where a particular target type is required. Locals commonly infer their type from the initializer; function-expression parameters can infer from a delegate target; ordinary function and method parameters still declare their parameter types explicitly:
let a = 2
let b: int = 2
func add(a: int, b: int) -> int { a + b }
Type identity
Type identity follows CLR identity unless Raven defines a source-level form such as nullable annotations, function types, or unions.
A type alias is another name for an existing type, not a new type. It therefore participates in assignment, conversion, and overload resolution exactly as its target does:
alias UserId = int
let id: UserId = 42
let number: int = id
Aliases remain transparent inside tuples and unions, so two otherwise identical types do not become distinct merely because one spelling uses an alias.
Built-in types
Raven provides keywords for the most common .NET types:
| Raven keyword | .NET type | Meaning |
|---|---|---|
sbyte |
System.SByte |
8-bit signed integer |
byte |
System.Byte |
8-bit unsigned integer |
short |
System.Int16 |
16-bit signed integer |
ushort |
System.UInt16 |
16-bit unsigned integer |
int |
System.Int32 |
32-bit signed integer |
uint |
System.UInt32 |
32-bit unsigned integer |
long |
System.Int64 |
64-bit signed integer |
ulong |
System.UInt64 |
64-bit unsigned integer |
nint |
System.IntPtr |
Native-sized signed integer |
nuint |
System.UIntPtr |
Native-sized unsigned integer |
float |
System.Single |
32-bit floating point |
double |
System.Double |
64-bit floating point |
decimal |
System.Decimal |
128-bit decimal floating point |
bool |
System.Boolean |
true or false |
char |
System.Char |
UTF-16 code unit |
string |
System.String |
UTF-16 text |
object |
System.Object |
Base type of .NET reference types |
unit |
System.Unit |
The single no-result value () |
Other .NET types can be referenced by qualified name, imported name, or alias.
unit is a value type and can flow through generics, tuples, and unions. It is
described in Values, expressions, and
statements.
null is a literal, not a standalone type. It can flow only to nullable
locations and never satisfies a non-nullable binding or parameter.
Literal types
Literals use their ordinary primitive types. For example, 1 is an int,
"hello" is a string, and true is a bool:
let one = 1
let text = "hello"
let enabled = true
let widened: double = one
When no target type changes the result, an inferred binding uses the literal's standard primitive type. Literal values are not supported in type position.
See Expressions and type inference for numeric suffixes, target typing, and branch inference.
Arrays
T[] is a one-dimensional array whose length is not part of its source type.
T[N] carries a fixed length:
let open: int[] = [1, 2, 3]
let fixed: int[3] = [1, 2, 3]
Array element types retain their generic arguments and nullability. Raven also
uses CLR multidimensional array shapes such as T[,] where that syntax is
accepted.
A fixed-length T[N] implicitly converts to T[]. The reverse conversion is
not implicit. T[N] converts to another fixed-length T[M] only when N and
M are equal.
Fixed-length inference is conservative. Raven infers or validates a length only when the collection expression makes its element count directly available. See Collection expressions.
Like CLR arrays, a one-dimensional T[] implements IEnumerable<T>,
ICollection<T>, IList<T>, and their read-only counterparts. These
relationships participate in normal interface conversions. Multidimensional
arrays provide the non-generic System.Collections.IEnumerable relationship
instead.
Tuple types
Tuple types describe a small group of values without requiring a named type.
Tuple types use parentheses with comma-separated element types and map to
System.ValueTuple:
let pair: (int, string) = (42, "answer")
Elements may optionally be named with a name: Type pair. Names exist only for
developer clarity and do not participate in type identity or assignment:
let tuple2: (id: int, name: string) = (no: 42, identifier: "Bar")
When a tuple expression is assigned to an explicitly annotated tuple type, each
element is validated against the corresponding element type. Named tuple
expressions expose both their source names and the positional ItemN members:
let tuple = (a: 42, b: 2)
Console.WriteLine(tuple.a)
Console.WriteLine(tuple.Item1)
Tuple types may nest or participate in other type constructs such as unions or nullability.
Function types
Function types describe callable delegates so they can be stored, passed as
arguments, or returned from other functions. Their syntax mirrors a lambda
signature: a comma-separated parameter list followed by -> and the return
type.
let applyTwice: ((int -> int), int) -> int
let thunk: () -> unit
let comparer: (string, string) -> bool
In declaration-oriented lists, a newline may separate entries where an explicit separator would otherwise appear. Omitting the separator between entries on the same line is an error.
Single-parameter functions may omit the surrounding parentheses:
let increment: int -> int
The return portion may itself be any Raven type. Nested arrows associate to the
right, so int -> string -> bool is parsed as int -> (string -> bool).
Function annotations are sugar over delegates. When the parameter and return
types match an existing declaration (including the built-in Func/Action
families), the compiler binds to that delegate. Otherwise it synthesizes an
internal delegate with the appropriate signature so interop with .NET remains
transparent. Parameter modifiers and names are not permitted inside a function
type; specify only the types that flow into and out of the delegate. A unit
return represents an action with no meaningful result.
Function-expression syntax, including explicit func expressions, lambda
shorthand, modifiers, and named recursive forms, is described under Function
expressions.
When a function expression is target-typed by a delegate requirement (for
example, assignment to Action<int> or passing to a delegate-typed parameter),
Raven projects the function value to a compatible delegate. Built-in
Func/Action delegate shapes are displayed as function signatures in Raven
type displays, while custom delegate types remain visibly named delegates.
Union syntax and declarations are covered separately under Unions. Function expressions and delegate selection are covered under Functions, and span lifetime rules under Spans and stack allocation.
Nullable types
Nullability is explicit and uniform. Append ? when a reference or value type
may contain null:
let name: string? = null
let count: int? = 1
Plain T rejects null; T? accepts T or null. Nullable and non-nullable
forms are distinct during type checking and overload resolution.
For a value type, .NET represents T? as System.Nullable<T>. For a reference
type, the runtime representation remains T and nullable-reference metadata
records the annotation. This ABI difference does not change Raven's
source-level rules. The expression's static type remains T?; Raven does not
silently replace it with a separate flow-sensitive type after a null check.
Explicit System.Nullable<T> remains available when interop code needs the CLR
wrapper and its members. It is not the canonical Raven spelling of T?.
Handling a nullable value
A successful typed binding creates a new non-null value:
func inspect(value: string?) {
if let text: string = value {
Console.WriteLine(text.Length)
}
if value is string text {
Console.WriteLine(text.Length)
}
}
A direct is null, is not null, == null, or != null check does not change
the static type of the checked storage. Dereferencing the original value
inside such a branch remains an error when it has type string?. Reference and
value types follow the same rule.
Prefer is null and is not null for identity tests. == null and != null
are valid but may invoke user-defined equality. An analyzer warns about that
difference for non-pointer values:
This comparison may call a custom equality operator. Use
is nulloris not nullto test null identity.
Nullable suppression
Postfix ! treats one nullable expression as non-null:
let name = service.TryGetName()! // string? becomes string
let value = optionalNumber! // int? becomes int
For a nullable reference, suppression changes the static type without inserting
a runtime null check. For a nullable value type, it unwraps the value. The
effect applies only to the annotated expression and reports warning RAV0403
on that full expression.
Raven recommends Option<T> when absence is an intentional part of a domain
API. Nullable types remain useful for .NET interoperability and gradual
adoption. See Nullability and absence.
Generics
Generic types and functions declare placeholders inside <...>:
class Box<T> {
val Value: T { get; }
init(value: T) {
Value = value
}
}
func identity<T>(value: T) -> T => value
let box = Box<string>("hello")
let number = identity(42) // T is inferred as int
Type parameters are in scope throughout their declaration. Constructed generic types use ordinary CLR generic instantiations and interoperate directly with .NET libraries.
A call can provide type arguments explicitly or let Raven infer them from arguments and the expected result. If those inputs do not produce one consistent choice, the type arguments must be written.
Constraints
Constraints restrict acceptable type arguments. They can follow a type
parameter after : or appear in a where clause:
class Repository<TContext: class, IDisposable> {
init(context: TContext) {
// ...
}
}
func parse<T>(text: string) -> T
where T: IParsable<T>
=> T.Parse(text, null)
class requires a reference type and admits nullable references. struct
requires a non-nullable value type and excludes Nullable<T>. Nominal class
and interface constraints require the argument to inherit or implement those
types. Several constraints are conjunctive.
Constraints also make static abstract interface members available through a
type parameter, as in the IParsable<T> example.
Constraint satisfaction is transitive: substituting one constrained type parameter for another carries its constraint set. A violation identifies the type argument and unmet constraint.
Function-specific constraint forms and ordering are described under Generic functions.
Variance
An interface or delegate type parameter may be covariant with out,
contravariant with in, or invariant when no modifier is written:
interface Mapper<in TSource, out TResult> {
func Map(source: TSource) -> TResult
}
Covariance allows Producer<Derived> where Producer<Base> is expected.
Contravariance allows Consumer<Base> where Consumer<Derived> is expected.
Invariant constructed types remain unrelated even when their arguments inherit
from one another.
The rules apply equally to Raven declarations and imported .NET metadata. For
example, IEnumerable<string> converts to IEnumerable<object>, while
IComparer<object> can be used where IComparer<string> is required.
Target typing and inference
Target typing lets surrounding code determine a type. It is used by literals, collection expressions, leading-dot construction, function expressions, method references, and control-flow expressions.
Inferred unions are normalized so their branch set remains stable across compilations. Literal expressions use their primitive type without a target, while control-flow branches contribute their types and may infer a union.
See Expressions and type inference for the full set of inference rules.
Conversions
An implicit conversion is allowed when it cannot lose the intended value under Raven and .NET rules. Common implicit conversions include:
- identity conversion
nullto a nullable typeTtoT?- widening numeric conversions
- a reference to a base class or implemented interface
- boxing a value type
- conversion to a compatible union branch
- variant conversions on covariant or contravariant generic types
When converting to a union, the source must convert to at least one branch. When converting from a union, every possible branch must convert to the destination. These rules also participate in assignment and overload selection.
Explicit casts
Use a cast when a conversion can fail or lose information:
let widened = (double)1
let narrowed = (int)3.14
let text = value as string
(T)expression performs the requested conversion and throws
InvalidCastException when a runtime reference conversion fails. expression as T attempts a reference or nullable conversion and produces null when it
fails.
Overload resolution
When several callable candidates are applicable, Raven compares the implicit conversions required for their parameters. Identity conversions are preferred over numeric widening, followed by reference and boxing conversions. User-defined conversions are considered last. If no applicable candidate is strictly better, the call is ambiguous.
When candidates in the same overload set use
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute, Raven
first keeps only candidates with the highest priority, then performs the normal
conversion and specificity comparison. This applies to Raven declarations and
imported .NET methods.
Calls, named arguments, optional arguments, and collector parameters are covered under Calls. Method-reference overload selection is described under Functions as values.