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.0
val y = 2.0
val 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.0
val py = p.y // 4.0Field Mutation
Fields on a mut binding can be reassigned, at any depth, with or without a compound
operator. A field is a place, the same as
a binding is:
mut cursor = Point { x: 0.0, y: 0.0 }
cursor.x = 5.0
cursor.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: cannot assign to field 'x' of immutable binding 'fixed'Passing 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. Everything in
this section applies to an enum too; see
Enum methods and trait impls.
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 a shared borrow: the method can read fields but cannot move an owning one out (see Consuming Methods).
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).
Consuming Methods (self)
A bare self receiver takes the value: the method owns it, and the caller loses it.
This is what an into_* conversion needs, because the method may move a non-Copy
field straight out of the receiver instead of cloning it.
struct Label {
text: string
}
impl Label {
func peek(&self) -> u64 {
self.text.len()
}
func into_text(self) -> string {
self.text // moved out: the receiver is this method's to spend
}
}
func main() -> i32 {
val label = Label { text: "sensor-a" }
val size = label.peek() // borrows: `label` survives
val text = label.into_text() // consumes: `label` is gone after this
// val again = label.peek() // Error: use of moved value 'label'
return text.len() as i32
}- Calling a consuming method moves the receiver. A later use of it is a
use of moved valueerror, exactly as for a by-value argument to a free function. - A receiver that is a borrow has nothing to give away: calling a consuming method on a
&T/&mut T, or on theselfof a&self/&mut selfmethod, iscannot move out of. Bind a.clone()first, or take the value by a binding that owns it. - The receiver is destroyed when the method returns, so a consuming method may not return a reference into it.
- On a
Copystruct aselfreceiver duplicates rather than moves, so the caller keeps its value. This is what lets an operator-trait methodfunc add(self, ...)work. - A
Droptype's destructor runs inside the consuming method, at its exit: once, never twice.
See examples/ownership/consuming_self.nr.
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
}Self
Inside an impl block, Self names the type being extended. It goes wherever that type's
name goes: parameter and return types, local annotations, struct literals, Self::func()
calls, and enum variants in expressions and patterns.
enum Shape {
Square(i32),
Empty
}
impl Shape {
func unit() -> Self { Self::Square(1) }
func side(&self) -> i32 {
match self {
Self::Square(s) => s,
Self::Empty => 0
}
}
}In impl<T> Cell<T>, Self is Cell<T>. A generic type applied to the impl's own
parameter is not supported yet, so there Self is refused just as Cell<T> is.
Self outside an impl or a trait is an error.
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. -
Reassigning a
mutbinding runs the displaced value's destructor at the assignment. The new value is built first, so a reassignment may read the value it replaces; a value already moved out is not destroyed a second time. -
A holder owns what it holds. When a value is destroyed, so is every owner inside it: a struct field, an array or tuple element, an enum payload, and a newtype's inner value. The holder's own
dropruns first, then its fields in declaration order, and each position is destroyed exactly once. -
A position given up first is destroyed on its own path instead. Moving
p.fieldinto another binding leaves its siblings owned byp, and assigning top.fielddestroys the value it displaces there.
struct Slot { guard: Guard }
func main() -> i32 {
mut dropped: i32 = 0
{
val held = Slot { guard: Guard { sink: &mut dropped } }
} // the struct goes, and its field goes with it
return dropped // 1
}Not yet supported: a match arm that binds some of a variant's owning fields and not
others (A(x, _)) disowns the whole scrutinee, so the field it did not bind leaks rather
than being destroyed twice. An arm that binds every owning field, or none, releases
everything exactly once.
Derived Traits (@derive)
A struct opts into compiler-generated impls with @derive(...). The derivable set is Copy,
Clone, Debug, PartialEq and Hashable; 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
}The derives recurse, and they are generated inline over the fields rather than through a method. That has two consequences worth knowing:
- Every field must itself be handled by the same derive: a scalar,
string,char,bool, another struct carrying the same derive (Hashablealso refuses a float field), or a struct or enum that implements the trait by hand, whose ownfmt_debug,eq,hashorclonethe derive calls for that field. A newtype field is handled as its inner value, except that a hand-writtenCloneon it is called too. Anything else is aDeriveFieldUnsupportederror naming the field. ACopystruct's.clone()is a copy of the whole value, so it calls no field'sclone. - Deriving a trait and also writing its
implfor the same struct is aDeriveConflictsWithImplerror. Pick one: the derive for the field-wise behavior, theimplwhen it should be something else.@derive(Copy)derivesClonetoo.
A struct that derives PartialEq and Hashable is a HashMap key with no hand-written impls.
{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.
Derivable traits by hand and as bounds
Each derivable name is also an ordinary trait. The prelude declares four of them:
trait Copy {} // requires `Clone`
trait Clone { func clone(&self) -> Self }
trait Debug { func fmt_debug(&self, out: &mut StringBuilder) }
trait Hashable { func hash(&self) -> u64 }PartialEq is the operator trait behind ==.
A struct may implement any of them by hand. {x:?} calls a hand-written fmt_debug, and a
hand-written impl Copy is accepted only where @derive(Copy) would be: on a struct whose
fields are all Copy, with no Drop, and with Clone implemented as well. On a generic struct,
impl<T: Copy> Copy for Cell<T> {} may bound its parameters by Copy and nothing else.
Derived or written, a generic struct's Copy holds per instance: Cell<i32> copies, and
Cell<string>, whose field is not Copy, moves.
struct Money { cents: i64 }
impl Debug for Money {
func fmt_debug(&self, out: &mut StringBuilder) {
out.push_str("${self.cents / 100}.{self.cents % 100:02}")
}
}
impl Clone for Money {
func clone(&self) -> Self { Money { cents: self.cents } }
}
impl Copy for Money {}Any of the five names may bound a type parameter. A derive, a hand-written impl and a builtin
type meet the bound alike: the scalars implement all five (a float is not Hashable), string
all but Copy, and every Copy type is Clone. So one generic body serves all of them:
func label<T: Debug>(name: string, v: &T) -> string {
"{name} = {v:?}" // a derived struct, `Money`, or an `i32`
}.clone() on a Copy value is a copy, and .hash() on an integer, bool, char, string
or a struct that derives Hashable returns the hash a HashMap uses for that key. v.eq(&w)
is v == w on anything == already compares, and a T: PartialEq (or T: Comparable) body
may call it too. What a
T: Copy or T: Clone bound gives a generic body is in
Functions. See
examples/structs/derivable_traits.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 { // by value: `&self` cannot give away an owning `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. Type arguments carry no Copy requirement: an instance holds
the value, so Wrapper<string> owns the string it holds and moves with it (see
Types → Ownership of an element). A generic struct is usable
only with type arguments; its bare name is rejected.
A generic at the enclosing parameter. A field, an impl signature or a body may instantiate a
generic with a parameter in scope: struct Outer<U> { i: Inner<U> } holds an Inner<i32> in an
Outer<i32>. Inside impl<T> Cell<T>, both Cell<T> and Self are the type self has. An
associated function of a generic type infers the instance from its arguments, so Cell::new(42)
calls new on Cell<i32>; when the arguments do not mention the parameter, supply it with a
turbofish (Bag::nothing::<i32>()).
struct Cell<T> {
v: T,
}
impl<T> Cell<T> {
func new(v: T) -> Cell<T> {
Cell { v: v }
}
func maybe(self) -> Option<T> {
Option::Some(self.v)
}
}Generic methods
A method may declare type parameters of its own, after its name and in addition to its impl's.
A call infers them from its arguments; when the arguments do not mention one, a turbofish after
the method name supplies it. Each set of type arguments a program uses compiles to its own copy
of the method, exactly as a generic function does. Bounds and where clauses work as they do
on a function.
impl<T> Cell<T> {
func with<U>(self, other: U) -> Pair<T, U> {
Pair { first: self.v, second: other }
}
}
val p = Cell::new(5).with("five") // U = string, inferred
val q = Cell::new(true).with::<i64>(40i64) // U = i64, from the turbofishAn associated function takes its own parameters the same way. On a generic type, one turbofish
lists the type's parameters first and the function's own after them:
Cell::empty::<i32, f64>(). A method's parameter may not reuse a name its impl declares. Trait
methods take no type parameters of their own, so a generic method lives in an inherent impl.
generic_methods.nr runs the forms above.
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
declares the const parameter the same way, and its methods read it as a value:
impl<T, const CAP: u32> Buffer<T, CAP> {
func capacity(&self) -> u32 { CAP }
}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.
Inside a trait, Self names whichever type implements it:
trait Resizable {
func grown(&self, by: i32) -> Self
}
impl Resizable for Square {
func grown(&self, by: i32) -> Self { Self { side: self.side + by } }
}The impl may write Self or its own name (-> Square). A default method naming Self
means each implementor in turn, and through a bound T: Resizable, grown returns T. A
trait that names Self anywhere but the receiver cannot be used as dyn Trait
(object safety).
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, see Functions) and the
operator traits build on the same trait machinery.
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 has no dyn form yet. The form that will work
binds the associated type in the trait-object type itself (&dyn Channel<Sample = i32>),
which is what keeps the vtable's signatures resolvable after the implementor is erased. The
compiler does not accept it today and says so: dyn on such a trait is rejected as not
object-safe, naming the associated type and the bound form that is not implemented.
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 |
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 |
MethodGenericShadowsImpl | A generic method's parameter reuses a name its impl declares |
GenericMethodInTraitImpl | A method of a trait impl declares type parameters of its own |
UnknownDerive | A @derive argument that names no derivable trait |
DuplicateDerive | The same trait listed twice in a @derive list |
DeriveFieldUnsupported | A field a derive cannot render, compare, clone or hash |
DeriveConflictsWithImpl | A struct both derives a trait and declares an impl of it |
InvalidCopyImpl | impl Copy on an enum or a newtype, or a generic impl Copy bounding a parameter by another trait |
MissingSupertraitImpl | impl Copy without Clone, or impl Comparable without PartialEq |
UnrenderableStruct | A struct hole with no Debug, derived or by hand, or missing the :? specifier |