Table of Contents

Type declarations and initialization

Classes and structs group related data and behavior into a type. Their initialization rules ensure that a new value has the members it needs before it is used.

Raven supports classes and structs with properties, methods, constructors, indexers, fields, and const members. The model is property-first: val/var declarations in type bodies define properties by default. Explicit field and const declarations provide explicit storage forms when needed.

Modifiers are C#-like but validated by the binder (e.g., abstract members require an abstract type; override requires a virtual base member).

class Counter(name: string) {
    val Name: string => name
    private field _value: int = 0

    var Value: int {
        get => _value
        private set => _value = value
    }

    // Indexer
    this[i: int]: int {
        get => _value + i
    }

    func Increment() -> () => _value = _value + 1
}

Rules

  • val/var type members declare properties.
  • field declarations are explicit storage members.
  • const declarations are compile-time constants and map to CLR literal fields.
  • Accessor-level access (e.g., private set) is supported.
  • Methods/ctors/properties/indexers may use arrow bodies.
  • Members can be marked static to associate them with the type rather than an instance.
  • Members that intentionally hide inherited members should use the new modifier; otherwise the compiler emits a warning.
  • A member name cannot match its immediate containing type name.

Delegate types are documented in Delegate declarations.

Static classes

Classes marked static are utility containers. They are implicitly abstract and sealed, and all members must be static. Instance fields, methods, constructors (including primary constructors), properties, events, or indexers are not permitted inside a static class.

Use final alongside override to seal an override and prevent further overrides in derived types (final override in Raven, equivalent to C#'s sealed override). The compiler reports an error if final is applied without override.

Ref structs

Ref structs are available for specialized stack-only and interop abstractions. Their declarations, managed-reference fields, lifetime rules, and generic anti-constraints are documented under Ref structs and ref safety.

Field declarations (low-level storage)

Fields are explicit CLR storage members. Use them when source code needs direct control over storage/layout (for example interop with StructLayout and FieldOffset, fixed buffers, or ABI-sensitive layouts).

class Counter {
    private field _count: int
    private readonly field _id: int = 42
}

const declarations are separate member declarations:

class MathConstants {
    const Pi: double = 3.141592653589793
}

They remain compile-time constants and are emitted as metadata constants (implicitly static), similar to other .NET languages.

Generic types

Classes and structs optionally declare type parameters immediately after the type name. The parameters become part of the type's identity and are available throughout the member list.

class Box<T> {
    val Value: T { get; }

    init(value: T) { Value = value }
}

val ints = Box<int>(1)
val words = Box<string>("ok")

Instantiating a generic type supplies concrete type arguments between < and > in the same order the parameters were declared. The compiler emits standard CLR constructed types, so Raven generics interoperate seamlessly with existing .NET APIs. When a type argument itself is generic, nest the constructions as needed (Dictionary<string, List<int>>).

Type parameters support constraints using the : syntax. After the colon, specify class, struct, and/or nominal types that the argument must derive from or implement. Constraints are comma-separated and may appear in any order.

class Repository<TContext: class, IDisposable> {
    init(context: TContext) { /* ... */ }
}

The compiler enforces the constraint set whenever the generic type is constructed. Passing an argument that does not satisfy one of the constraints reports an error and identifies the unmet requirement.

Generic type arguments may be inferred from constructor arguments when the type name is invoked without an explicit <...> list. This includes function expressions passed to function-type or delegate-shaped constructor parameters:

open class Endpoint {
    init(handler: Delegate) {}
}

class Route<T> : Endpoint {
    init(pattern: string, handler: T -> string) : base(handler) {}
}

val route = Route("/{id:int}", func (id: int) => id.ToString())
// route : Route<int>

When same-named non-generic and generic types are both in scope, a matching non-generic constructor is selected first. If the non-generic constructor is not applicable, Raven may infer and select a same-named generic type. Multiple successful generic candidates are ambiguous.

Accessibility

Types and members accept the standard access modifiers. Applying more than one keyword produces the expected CLR combinations:

Modifier syntax Meaning
public Visible from any assembly.
internal Visible only within the current assembly.
protected Visible to the declaring type and to derived types.
private Visible only inside the declaring type.
fileprivate Visible only from the current source file.
protected internal Visible to derived types or any code in the same assembly.
private protected Visible to derived types declared in the same assembly.

Default accessibility depends on the declaration context:

  • Namespace-level types—including classes, structs, records, interfaces, enums, unions, delegates, and extension declarations—default to internal. A namespace-level declaration must use public to become part of the assembly's exported API. Because public changes the assembly boundary in this position, it is not redundant.
  • Nested types are type members and default to public.
  • Other type members (fields, methods, properties, indexers, constructors, and lifecycle blocks) default to public for classes, structs, and interfaces. Narrower visibility requires an explicit modifier such as private, internal, or protected.

Constructors and lifecycle declarations follow these rules as well.

Accessibility composes through containment. A public member of an internal type is effectively internal because callers outside the assembly cannot name its containing type. This keeps the assembly export boundary explicit without requiring access modifiers throughout implementation-only types.

This asymmetry is intentional. In an application or in implementation-only library code, namespace-level declarations need no modifier because they stay inside the assembly. In a class library, public visually identifies the declarations deliberately exposed to referencing assemblies. Within a type, members need a modifier only when access is narrowed. The result is less modifier noise and a smaller cognitive burden when scanning either an assembly's exported surface or a type's private implementation details.

An explicit public modifier remains legal on a type member even when public is the default. The compiler may report the style diagnostic RAV0908 for the redundant modifier; projects that prefer explicit member accessibility may suppress that diagnostic and enforce their convention with an analyzer.

fileprivate is a source-level restriction for type-like declarations. The compiler enforces same-file visibility during binding, and mangles the emitted metadata name for the generated type so file-local helpers do not publish a stable CLR-facing type name.

Initialization model

Raven supports type-header parameters plus init blocks as its object initialization model:

  • a type parameter list on the class/struct header (primary-constructor parameters), plus
  • one or more init { ... } blocks for initialization logic.

static init { ... } provides type initialization, and finally { ... } provides finalization logic.

class Widget(name: string) {
    static init {
        // type initialization
    }

    init {
        // primary initialization logic
        // primary parameters are in scope here
    }

    finally {
        // finalization
    }
}

Classes and structs may declare primary-constructor parameters by adding an argument list to the type header. The compiler synthesizes an instance constructor whose signature matches those parameters.

An access modifier between the type name (and any type parameters) and the primary-constructor parameter list controls the synthesized constructor's accessibility. The default is public:

class Session internal (token: string) {}

record struct Year private (val Value: int) {
    static func Create(value: int) -> Year => Year(value)
}

Constructor accessibility is independent of promoted-property accessibility. For example, record struct Year private (val Value: int) has a private constructor and a public Value property, while record struct Year(private val Value: int) has a public constructor and a private property. public, internal, and private are valid on every primary constructor. protected is also valid on class and record-class primary constructors, but is diagnosed on struct and record-struct declarations because value types cannot be inherited.

For class / struct, parameter promotion is explicit:

  • val parameter: promoted to an instance val auto-property.
  • var parameter: promoted to an instance var auto-property.
  • promoted parameters may specify an access modifier (public, internal, protected, private) before val/var to control synthesized property accessibility (default is public).
  • no binding keyword: captured in synthesized private instance storage for member access, but not promoted to a public property.
  • constructor calls must use invocation syntax (Foo()); a standalone type name (Foo) is not a value expression.
  • semantic model note: unqualified identifier access to captured/promoted primary-constructor members resolves to the originating parameter symbol.
class Person(val name: string, var age: int) {
    func GetName() -> string => name
    func GetAge() -> int => age
}

val person = Person("Ada", 42)
val years = person.GetAge()

init(...) declarations

init(...) member declarations are constructor-shape declarations.

  • When used alongside the primary model above, they are secondary constructors.
  • When used without type-header parameters/init {} blocks, they are a valid alternative syntax for initializing the object.
class Widget {
    init(name: string) { /* constructor-shape init */ }
    init(name: string, age: int) { /* secondary overload */ }
}

Record declarations

Records provide value semantics and support three declaration forms:

record Person(name: string, age: int);        // defaults to record class
record class Person(name: string, age: int);  // explicit record class
record struct Point(x: int, y: int);          // explicit record struct

Primary-constructor semantics differ between nominal types and records:

  • class / struct: only val/var parameters are promoted to properties; parameters without a binding keyword are captured in synthesized private instance storage for member access. Access modifiers on primary-constructor parameters are valid only when the parameter is promoted.
  • record class / record struct: positional parameters are promoted to properties by default (as val when no binding keyword is specified, or var when var is specified). The compiler synthesizes value-based members from the complete primary-constructor parameter list (Equals, GetHashCode, deconstruction, ToString, copy/with behavior, and record equality operators), regardless of the promoted property's accessibility.

Record instance data is limited to the primary-constructor parameters. Record bodies may declare computed properties, methods, operators, nested types, static storage, and const members, but may not declare additional instance fields, instance storage/auto-properties, instance events, or instance constructor/initializer declarations. Prefer static factory methods for alternate construction names:

record class Person(Name: string, Age: int) {
    static func Newborn(name: string) -> Person => Person(name, 0)
}

Outside primary-constructor promotion, parameters are ordinary value/by-ref parameters and must not use val/var binding keywords. This applies to functions, methods, operator declarations, and indexer parameter lists.

record class Person(name: string, age: int);

val a = Person("Ada", 42)
val b = Person("Ada", 42)

val same = a == b