Structs and Methods¶
Accepted
Accepted for V1 source semantics, including struct contract requirements and method syntax.
Structs¶
A struct is:
- concrete memory layout
- namespace
- encapsulation boundary
Fields and methods are public by default. Private members are explicit with priv:
const File = struct {
priv handle: OsHandle
fn read(self: *Self, buf: []u8) usize! {
}
}
This matches the trust-the-programmer philosophy while still allowing encapsulation where it matters.
V1 has only this public/priv member visibility split. Future finer-grained visibility can be added later if needed, but V1 examples and diagnostics should treat public-by-default with explicit priv as final.
Non-generic structs are ordinary const bindings to Type values:
const File = struct {
priv handle: OsHandle
}
Generic structs follow the Catalyst generics model: an ordinary comptime function returns a Type value.
fn Buffer(comptime T: Type) Type {
return struct {
data: []T
}
}
Expression-bodied functions are valid shorthand for generic type factories. The canonical => rules are documented in Function Shape and Capabilities.
fn Buffer(comptime T: Type) => struct {
data: []T
}
Struct Contract Requirements¶
A struct(...) argument list declares semantic contracts the struct must implement:
const File = struct(Disposable) {
fn dispose(self: *Self) void {
}
}
This is equivalent to declaring the struct and then a same-scope bare implementation:
const File = struct {
fn dispose(self: *Self) void {
}
}
impl File as Disposable {
}
Entries must be fully applied semantic contract Type values:
const Buffer = struct(Sequence(u8), Disposable) {
}
Generic contract factories must be called, such as Sequence(u8), not Sequence. Structural constraints such as satisfies(...) and mutable_fields(...) are not accepted in struct(...) in V1.
Contract entries are private by default, matching ordinary impl declarations. A pub entry exports the implied implementation:
pub const Buffer = struct(pub Sequence(u8)) {
}
This is equivalent to:
pub const Buffer = struct {
}
pub impl Buffer as Sequence(u8) {
}
A public contract entry requires both the struct type and the contract expression to be publicly visible. Visibility errors are reported at the pub contract entry.
The struct body is the only place for member declarations. struct(...) entries do not contain inline implementation bodies. If a contract operation needs behavior different from the inherent member that would be used by inherent-member fill, write an explicit impl.
Inherent-member fill for struct(...) follows the same rules as bare impls: required operations may be filled from compatible inherent members, but default contract methods are not automatically overridden by inherent same-name methods.
Fields are mutable by default. Field-level immutability is explicit with const:
const Header = struct {
const sample_rate: f32 = 48_000
channels: u32 = 2
}
A const field must be initialized during construction and is copied during value copy. It cannot be assigned through field access after initialization, even when the containing value is stored in a var binding. Whole-value assignment to a mutable binding is still allowed when the type is otherwise assignable.
Mutable and const fields may have default values. A field default is used when aggregate construction omits that field. It is part of the source-level construction contract, not layout, ABI shape, assignment compatibility, or structural/contract satisfaction.
Field default semantics mirror default parameter values:
- names in default expressions are resolved and type-checked in the struct declaration scope
- default expressions must be comptime-evaluable in V1
- generic struct factories may capture already-known
comptimevalues in returned struct defaults - default expressions cannot refer to sibling fields
- runtime-dependent defaults should be provided by an
initfunction
Invariant-safe defaults
Field defaults should be used only when omitting the field cannot violate the struct's invariants. Prefer a named default value or an init function when initialization requires coordinated field values or runtime-known inputs.
Adding a public field default is source-additive. Removing a public field default is source-breaking for callers that omitted the field. Changing a default is source-compatible but behavior-changing; this is tooling and versioning policy, not layout or codegen behavior.
Aggregate Construction¶
Struct values are constructed with the owner type followed by named field initializers:
const header = Header{ .sample_rate = 48_000, .channels = 2 }
Rules:
- every field without a default value must be initialized.
- fields with defaults may be omitted.
- each initializer uses
.field_name = expression. - unknown field names are errors.
- duplicate field initializers are errors.
- explicit field initializers are checked in source order.
- field values are checked against the declared field types.
Construction analysis may materialize omitted field defaults internally, but construction-site expansion metadata belongs to AST/SIR/IR design rather than Type.Field.
When the expected type is already known, expected-type shorthand may omit the owner type:
const header: Header = .{ .sample_rate = 48_000, .channels = 2 }
Fields and inherent methods share one ordinary member namespace. A type cannot define both a field and an inherent method with the same name. Semantic contract implementations may need qualified syntax to avoid conflicts with ordinary fields.
Self¶
Self is a comptime type constant available inside type bodies. It is not a runtime value.
Receivers¶
By value:
fn len(self: Self) f32
Mutable/reference:
fn read(self: *Self, buf: []u8) usize!
Readonly reference:
fn name(self: *const Self) String
*Self is a borrowed mutable receiver. *const Self is a borrowed readonly receiver.
Method Calls¶
Methods are ordinary functions with call sugar:
file.read(buf)
approximately desugars to:
File.read(file, buf)
The call sugar may auto-address only the first parameter/receiver position. It does not apply to arbitrary non-receiver arguments. General extension methods are deferred to CEP-0011: General Extension Methods.
IR must not contain source-level method calls. Sema or lowering resolves them before backend-facing IR.
See Reference and Mutability for receiver mutability and auto-addressing rules.