Structs and Methods
Structs are user-defined types that group related fields together. Methods add behaviour to structs via impl blocks.
Struct Definition
struct Point { x: f64, y: f64} struct Rectangle { width: i32, height: i32}Fields are declared as name: Type and separated by commas. Trailing commas are allowed.
Field visibility
A field is private to the module (the file) that declares its struct unless it carries
export. In a single-file program every field is reachable throughout it, so this only
begins to matter once a second file reads your code:
export struct Config { export host: string, export port: i32, timeout: i32 // private, internal implementation detail}export is per field, so an exported struct can still hold something back. Another module
cannot read c.timeout, assign to it, list it in a literal, or reach it through ..base
which is what makes a constructor like Config::new the only way in. See
Modules → Visibility.
Struct Instantiation
Use StructName { field: value, ... } to create a value:
val origin = Point { x: 0.0, y: 0.0 }val rect = Rectangle { width: 10, height: 5 }All fields must be provided. Field order in the literal does not need to match the definition order.
Field-Init Shorthand
When a local variable has the same name as the field it initialises, write the name once:
val x = 1.0val y = 2.0val p = Point { x, y } // equivalent to Point { x: x, y: y }Shorthand and explicit field: value entries may be mixed: Point { x, y: 2.0 }. A shorthand field references the same-named binding in scope; if no such binding exists, it is an ordinary undefined-name error.
Functional Update (..base)
A trailing ..base supplies every field not listed explicitly from an existing value of the same struct type. Listed fields override the base:
val p = Point { x: 1.0, y: 2.0 }val shifted = Point { x: 10.0, ..p } // x = 10.0, y inherited from p (2.0)val copy = Point { ..p } // all fields copied from pThe base must be the same struct type as the literal (otherwise a type Mismatch error). When a base is present, omitting fields is not a MissingStructField error, the base fills them in. The base is evaluated and its fields are copied into the new value; no allocation is introduced.
Field Access
Use dot notation to read a field:
val p = Point { x: 3.0, y: 4.0 }val px = p.x // 3.0val py = p.y // 4.0Field Mutation
Fields on a mut binding can be reassigned:
mut cursor = Point { x: 0.0, y: 0.0 }cursor.x = 5.0cursor.y = 3.0Mutating a field of a val binding is a compile error:
val fixed = Point { x: 1.0, y: 2.0 }// fixed.x = 3.0 // Error: AssignToImmutableFieldPassing Structs to Functions
Structs can be passed as function parameters:
func area(r: Rectangle) -> i32 { return r.width * r.height} func main() -> i32 { val rect = Rectangle { width: 6, height: 7 } return area(rect) // 42}A function of any kind may return a struct — free functions, associated functions, and
methods alike. The by-value struct ABI covers all three, which is what makes both a plain
factory function and the Type::new(...) constructor pattern work.
impl Blocks
Use impl TypeName { ... } to add methods and associated functions to a struct.
Instance Methods (&self)
Instance methods take &self as their first parameter and can read any field:
struct Counter { value: i32} impl Counter { func get(&self) -> i32 { self.value } func add(&self, n: i32) -> i32 { self.value + n }} func main() -> i32 { val c = Counter { value: 10 } return c.add(32) // 42}selfinside the method refers to the receiver struct value.- All struct fields are accessible via
self.field. - The receiver is passed by value (read-only snapshot).
Mutating Methods (&mut self)
A method may take &mut self to mutate the receiver in place. Writes to
self.field propagate back to the caller's value because the receiver is passed
by pointer:
struct Accumulator { total: i32} impl Accumulator { func add(&mut self, n: i32) { self.total = self.total + n } func get(&self) -> i32 { self.total }} func main() -> i32 { mut acc = Accumulator { total: 0 } acc.add(10) acc.add(32) return acc.get() // 42}- Calling a
&mut selfmethod requires amutreceiver (or one reached through a&mut T). Calling it on avalbinding is acannot mutably borrowerror. - The call takes an exclusive borrow of the receiver for its duration, so it is rejected while another borrow of that receiver is live (aliasing rule).
Associated Functions (no self)
Associated functions belong to the type but do not take a receiver. They are called via TypeName::func(args):
struct Point { x: i32, y: i32} impl Point { func new(x: i32, y: i32) -> Point { Point { x: x, y: y } }} func main() -> i32 { val p = Point::new(10, 32) return p.x + p.y // 42}Combining Methods and Associated Functions
struct Rect { width: i32, height: i32} impl Rect { func new(w: i32, h: i32) -> Rect { Rect { width: w, height: h } } func area(&self) -> i32 { self.width * self.height }} func main() -> i32 { val r = Rect::new(6, 7) return r.area() // 42}Destructors (impl Drop)
A struct can define a destructor by implementing the built-in Drop trait. Its
drop(&mut self) method runs automatically when an owner of the value goes out
of scope:
struct Guard { sink: &mut i32} impl Drop for Guard { func drop(&mut self) { *self.sink = *self.sink + 1 // record that the destructor ran }} func main() -> i32 { mut dropped: i32 = 0 { val g = Guard { sink: &mut dropped } } // `g` goes out of scope here → `drop` runs, dropped == 1 return dropped // 1}Rules:
- Destructors run only on normal scope exit: fall-through,
return,break, andcontinue. A panic aborts the process without running any destructor (there is no stack unwinding). - When several owned values leave the same scope, they are dropped in reverse declaration order (LIFO).
- A value that has been moved out (rebound, returned, passed by value, or stored into a struct) is dropped exactly once, by its final owner, never twice. Reading a moved value is already a compile error.
- A
Copytype may not implementDrop(a type with a destructor is moved, not duplicated).@derive(Copy)together withimpl Dropis a compile error. - The
dropmethod must be exactlydrop(&mut self): no extra parameters and no return type, and animpl Dropblock may contain no other methods.
Not yet supported: reassigning a Drop binding does not run the prior value's
destructor, and a struct's Drop-typed fields are not dropped automatically
(no recursive destructor glue).
Derived Traits (@derive)
A struct opts into a small set of compiler-generated traits with @derive(...). The
derivable-and-implemented set is Copy, Clone, Debug, PartialEq; any other name in the
list is a compile error rather than a silent no-op.
@derive(Debug, PartialEq)struct Sensor { id: i32, label: string } @derive(Debug, PartialEq)struct Reading { sensor: Sensor, value: f64 } func main() -> i32 { val a = Reading { sensor: Sensor { id: 7, label: "intake" }, value: 21.5 } val b = Reading { sensor: Sensor { id: 7, label: "intake" }, value: 21.5 } println("{a:?}") // Reading { sensor: Sensor { id: 7, label: "intake" }, value: 21.5 } if a == b { return 1 } // structural equality, recursing into `Sensor` return 0}Both derives recurse, and both are generated inline over the fields rather than through a method. That has two consequences worth knowing:
- Every field must itself be renderable / comparable — a scalar,
string,char,bool, or another struct carrying the same derive. Anything else is aDeriveFieldUnsupportederror naming the field. - Deriving
PartialEqand also writingimpl PartialEq forthe same struct is aDeriveConflictsWithImplerror. Pick one: the derive for structural equality, theimplwhen the comparison is not field-wise (or when the struct is aHashMap/BTreeMapkey, which calls the trait method and so needs theimpl).
{value:?} is the only hole a struct renders in — a struct has no display form, so "{p}"
is an error even with @derive(Debug). See examples/structs/derives.nr.
Definition Order Independence
Structs and impl blocks can appear in any order. Forward references are supported:
impl Score { func doubled(&self) -> i32 { self.value * 2 }} struct Score { value: i32} func main() -> i32 { val s = Score { value: 21 } return s.doubled() // 42}Generic Structs and Impls
A struct may declare type parameters in <...> after its name; each distinct set of
concrete type arguments is monomorphized into its own specialized struct at zero runtime
cost. Type arguments are inferred from the field values at a struct literal, or
written explicitly in a type annotation (Pair<i32, f64>).
struct Pair<T, U> { first: T, second: U} // A generic inherent impl: `impl<T> Wrapper<T>` specializes its methods per instance.struct Wrapper<T> { value: T} impl<T> Wrapper<T> { func get(&self) -> T { self.value }} func first_of(p: &Pair<i32, i32>) -> i32 { p.first} func main() -> i32 { val p = Pair { first: 40, second: 2 } // Pair<i32, i32> inferred val w = Wrapper { value: 30 } // Wrapper<i32> val flag = Wrapper { value: true } // Wrapper<bool>, distinct instance return first_of(&p) + w.get() // 40 + 30 = 70}Restrictions (this phase). Type arguments are restricted to Copy types (a bare type
parameter has no move semantics yet). A generic struct is usable only with type arguments;
its bare name is rejected. A generic instantiated with an enclosing type parameter (a
Wrapper<T> field inside another generic struct) is a documented limitation, deferred with
broader generic support.
Const (value) parameters
A generic struct may also declare a const parameter, a compile-time value used as an array
length, so [T; CAP] is sized concretely per instance:
struct Buffer<T, const CAP: u32> { data: [T; CAP], count: u32} // CAP is inferred from the field array's length (Buffer<i32, 4>):val buf = Buffer { data: [1, 2, 3, 4], count: 4 } // or written explicitly in a type annotation:val other: Buffer<i32, 4> = Buffer { data: [5, 6, 7, 8], count: 4 }Each distinct CAP produces its own monomorphized struct at zero runtime cost. A generic impl
over a struct's const parameter is a documented limitation deferred to broader generic support.
Traits
A trait defines shared behaviour that many types can implement. Traits are Neuro's
mechanism for bounded polymorphism. A trait method is either required (a signature
with no body, implementors must provide one) or a default method (a signature with a
body that implementors inherit unless they override it).
trait Shape { // Required: every implementor must define this. func area(&self) -> i32 // Default (provided): inherited unless the implementor writes its own. func is_big(&self) -> i32 { if self.area() > 20 { 1 } else { 0 } }}Implement a trait for a type with impl Trait for Type:
struct Square { side: i32 } impl Shape for Square { func area(&self) -> i32 { self.side * self.side } // `is_big` is not written, so Square inherits the trait's default.}The compiler checks each trait impl for conformance: every required method must be present, each method's signature must match the trait's, and an impl may only contain methods the trait declares.
Trait bounds on generics
A generic parameter may be bounded by a trait (<T: Shape>). Inside the body, the trait's
methods may be called on the bounded parameter; at the call site the concrete type
argument must implement the trait:
func scaled_area<T: Shape>(s: &T, factor: i32) -> i32 { s.area() * factor // dispatched through the `Shape` bound}A trait-bounded generic is fully monomorphized and erased: each impl lowers to ordinary
methods and each bound is specialized per concrete type, so there is no vtable and no runtime
cost. Supertraits (Comparable requires PartialEq), dynamic dispatch (dyn Trait, which
does use a vtable), and the operator traits have all
landed.
Associated types
A trait may declare an associated type: a member type each implementor chooses. The trait
writes the name alone; the impl binds it, and either side names it as Self::Name:
trait Channel { type Sample func sample(&self) -> Self::Sample} struct Tally { counted: i32 } impl Channel for Tally { type Sample = i32 func sample(&self) -> Self::Sample { self.counted }}Self::Sample resolves to whatever the impl bound, in a signature, in a method body, and
nested inside another type (Option<Self::Sample>). An impl may spell the position either
way — -> i32 above means the same thing. Conformance requires every declared associated
type to be bound, and rejects a binding the trait never declared.
Constraining an associated type in a bound
A bare T: Channel bound erases the implementor, and with it the only thing that says what
Sample is — so a generic body cannot call a method whose signature names it. The
Trait<Assoc = T> form puts the answer in the bound:
func scaled<T: Channel<Sample = i32>>(source: &T, factor: i32) -> i32 { source.sample() * factor // typed as i32 by the bound}The constraint is written wherever a bound is:
func clause<T>(source: &T) -> i32 where T: Channel<Sample = i32> { source.sample() } func anonymous(source: &impl Channel<Sample = i32>) -> i32 { source.sample() } func make(seed: i32) -> impl Channel<Sample = i32> { Tally { counted: seed } }At the call site the concrete type argument's own binding must match the one the bound
demands: passing a Channel implementor whose type Sample = f64 to a Sample = i32 bound
is an error naming both types. A bound may only constrain an associated type the trait
declares, and a bare bound still cannot type a call to a method that names one.
A trait declaring an associated type remains not object-safe, so it has no dyn form: a
trait object erases the implementor, which is exactly what the binding would have to come
from.
Unsupported (Phase 1+)
The following are not yet implemented and will be rejected at compile time:
self(consuming) on a non-Copystruct (needs the by-value struct ABI). On aCopystruct an ownedselfis accepted, because copying by value is ABI-identical to&self(this is what lets an operator-trait methodfunc add(self, ...)work)- Nested structs as field types
// Consuming `self` on a non-`Copy` struct is rejected with a clear error:struct Wrapper { value: i32 } // no @derive(Copy)impl Wrapper { func unwrap(self) -> i32 { ... } // Error: UnsupportedSelfParam}Nominal Typing
Neuro uses nominal typing for structs: two struct types are compatible only if they have the same name, regardless of field layout.
Error Types
| Error | Trigger |
|---|---|
UnknownStruct | Using an undefined struct name |
StructAlreadyDefined | Redefining a struct |
UnknownField | Accessing a field that doesn't exist |
MissingStructField | Omitting a field in a struct literal |
DuplicateStructField | Providing the same field twice in a literal |
AssignToImmutableField | Mutating a field on a val binding |
MethodNotFound | Calling a method that doesn't exist on the type |
UnsupportedSelfParam | Using consuming self (by value) in a method |
UnknownTrait | Implementing a trait that was never declared |
MissingTraitMethod | A trait impl omits a required method |
NotATraitMethod | A trait impl defines a method the trait does not declare |
TraitMethodSignatureMismatch | An impl method's signature differs from the trait's |
TraitBoundNotSatisfied | A generic argument does not implement a required trait |
UnknownDerive | A @derive argument that names no derivable trait |
UnimplementedDerive | A @derive argument the spec allows but no pass generates yet |
DuplicateDerive | The same trait listed twice in a @derive list |
DeriveFieldUnsupported | A field a derived Debug / PartialEq cannot render or compare |
DeriveConflictsWithImpl | A struct both derives a trait and declares an impl of it |
UnrenderableStruct | A struct hole missing @derive(Debug), or missing the :? specifier |