Expressions and type inference
Raven can often work out a type from the value being used or from the place where the value is expected. This keeps declarations and expressions short when the type is already clear from the surrounding code.
Target typing
Many expressions rely on the type expected by their context, called the target type.
For example, the enum shorthand .B in var grade: Grades = .B uses the declared type Grades to resolve the member.
The constructor shorthand .(...) also requires a target type. The omitted
member name means "construct the target type", so let p: Point = .(2, -1)
binds as Point(2, -1). The same form works in other target-typed contexts
such as constructor arguments and collection elements.
Discriminated union case sugar for unit
When a discriminated union case carries exactly one payload of type unit, Raven permits the case to be written without an argument list in expression position. In such contexts, a bare case name is sugar for supplying the sole unit value ().
func Save() -> Result<(), Error> {
return Ok // sugar for `Ok(())`
}
The target-typed member form .Ok is also valid when the target type is known.
This rule applies uniformly to all discriminated unions, not only Result or Option. The case must declare exactly one constructor parameter whose type is unit; cases with additional parameters or non-unit payloads still require an explicit argument list.
This mirrors the pattern-matching rule where a bare case such as Ok matches
Ok(()) when the payload is unit, with .Ok remaining available as the
target-typed shorthand.
Type inference
When an expression or declaration omits an explicit type, Raven infers one from
the expression. If multiple different types can flow to a location—through
conditional branches or early return statements—the inferred result is the
nearest compatible type for the flow.
val pet = if flag { Dog() } else { Cat() }
// pet has an inferred compatible type
Literal expressions infer the underlying primitive type when used to initialize
val or var bindings. Literal types are subset types of their underlying
primitive, so a literal like 1 can be used wherever an int is expected.
When inference gathers multiple results—such as the branches of an if
expression—it keeps literal precision only when required by explicit
annotations; otherwise literals widen to their underlying primitive types.
To retain a literal's singleton type for a single value, an explicit annotation
is required.
var i = 0 // i : int
val j = 0 // j : int
var k: 1 = 1 // k : 1
Control-flow expressions participate in the same inference. An if expression
whose branches produce different types infers the nearest compatible type for
those results. Literal branches remain precise only when explicitly annotated.
By default, literal branches widen to their underlying primitive types:
Branch type inference
val x: int = 3
val value = if x > 2 { 42 } else { x }
// value : int
val other: long = 0
val widened = if x > 2 { other } else { 42 }
// widened has an inferred compatible numeric type
Each branch contributes its inferred type. Literal branches widen as needed to produce a compatible inferred result type.
Numeric literals choose an underlying primitive type according to their form and optional suffix. The default rules are designed to be predictable while still allowing safe narrowing through implicit constant conversions.
Integer literals
- Unsuffixed integer literals default to
int.- If the value does not fit in
int, the literal is typed aslong.
- If the value does not fit in
- Base prefixes:
0b/0B— binary integer literal0x/0X— hexadecimal integer literal
- The following suffixes override the default:
b/B—bytel/L—long
Unsuffixed integer literals may still convert implicitly to smaller integral
types (such as byte or char) when used as constant expressions and the
value fits in the target type. This mirrors C#’s constant conversion rules and
avoids accidental narrowing for non-constant values.
val a = 42 // int
val b = 4_000_000_000 // long
val x: byte = 12 // OK: constant int fits in byte
val y: byte = 300 // error: constant out of range
val z = 32b // explicit byte literal
val n = 10L // explicit long literal
val bits = 0b1010_0101 // binary int literal
val mask = 0xFF // hexadecimal int literal
Floating-point literals
- Unsuffixed floating-point literals (those containing a decimal point or
exponent) default to
double. - The following suffixes override the default:
f/F—floatd/D—doublem/M—decimal
val d = 3.14 // double
val f = 3.14f // float
val m = 9.99m // decimal
val e = 1e3 // double
Decimal literals do not support exponent notation. Attempting to combine an
exponent with the m/M suffix produces a diagnostic.
Literal values participate in overload resolution and type inference using their underlying primitive type. When a literal appears in a context that supplies a target type, the compiler may apply implicit constant conversions before considering explicit casts.
Overload resolution applies the same rule: a literal argument converts to its
underlying type when selecting among method overloads. For example,
Console.WriteLine(1) binds to Console.WriteLine(int) if such an overload
exists, and Console.WriteLine("test") chooses Console.WriteLine(string).
Functions and methods without an annotated return type default to unit; the
declaration return type is not inferred from body expressions.
Lambdas without an annotated return type infer their result by collecting:
- the types of all explicit
returnstatements, and - the final expression of the outer body when that body has a value-producing tail expression.
Expression statements in nested statement blocks (for example, inside if/while/for
statement bodies) do not participate in lambda return-type inference. If no value-returning
path exists, the lambda return type defaults to unit.
val example = (x: int) -> {
if x > 0 { return x }
"neg"
}
// inferred return type is context-dependent
When a lambda expression is assigned to a binding without an explicit type, Raven
still materialises a concrete delegate. The compiler synthesises an appropriate
System.Func/System.Action definition using the lambda's parameter types and
the inferred return type (treating unit results as actions). Captured
variables participate in the enclosing flow analysis before the delegate type is
constructed, so the lambda observes the same declared type as any other use of
the variable.
val a = 42
val makeAdder = () => a + 3
makeAdder() // returns 45, makeAdder : System.Func<int>
Async function expressions mirror async functions: placing async before the parameter
clause (or using async func) permits await inside the body. When the function expression return type is not annotated
and no delegate supplies one, the compiler wraps the inferred result in
System.Threading.Tasks.Task<T> (or Task when the body produces unit). A delegate
annotation or target type may still specify a concrete Task shape, in which case the
function-expression body must evaluate to the awaited result type rather than the task itself.
Annotating an async function expression with a non-Task return type is an error.
Additional type inference rules (normative)
The following clarifications extend the type inference model:
- Contextual inference: Raven computes a contextual type based on both expression shape and target type. Inference is bidirectional.
- Literal arithmetic: Non-constant operations widen literals to their base type unless constant-folded.
- Generic inference: Type argument inference requires a single consistent set of type arguments that satisfies all constraints.
- Nullability: Nullable flow follows
T?rules and safe-navigation propagation. - Pattern narrowing: See Pattern matching for how
isandmatchrefine variables. - Tuples: Tuple element names do not affect type identity.
- Ref/out parameters:
refrequires exact type match;outcontributes to inference of the parameter type. - Flow stability: Variable declarations have a fixed declared type. Narrowings are ephemeral and do not change declared type. Captured variables use the join of all flows.
- Diagnostics: When conversion fails, diagnostics should identify the conflicting conversions and why. For overloads, diagnostics should explain alternative selections.