Impl Blocks
An impl defines the implementation surface of a named Sec type.
It can provide methods, properties, instance bindings, static members,
lifecycle behavior, nested types and explicit interface conformance.
type Counter struct {
value: int,
}
impl Counter {
static let Maximum: int := 100
let Label: string := "counter"
fn Increment() void {
self.value += 1
}
}
A type declaration and its implementation cooperate to define the complete type.
The type declaration introduces the nominal type and its direct representation.
The implementation supplies behavior and may also contribute explicitly declared
per-instance let storage.
Core rule: A bare let or let mut
inside impl is instance-bound. Type-owned state and values use
explicit static.
Purpose
impl keeps a type's implementation members together while preserving
clear distinctions between:
- direct type representation;
- additional instance bindings;
- instance behavior;
- type-associated behavior and storage;
- construction and destruction;
- nested declarations;
- interface conformance.
For example:
type Device struct {
id: uint64,
}
impl Device {
let Connected: bool := false
static let MaximumConnections: uint := 32
fn IsConnected() bool {
return self.Connected
}
}
id is a direct struct field.
Connected is an instance binding contributed by the implementation.
MaximumConnections belongs to the type itself.
Eligible Targets
Ordinary impl blocks target user-defined named nominal types.
Eligible targets include, where their specialized rules permit it:
- named scalar types;
- constrained named types;
- structs;
- enums;
- unions;
- registers;
- units;
- generic named types;
- other user-defined nominal types.
type Percent int range 0..100
impl Percent {
fn IsHigh() bool {
return self > Percent(80)
}
}
Interfaces are not ordinary implementation targets.
Interface declaration and conformance have their own rules.
User code cannot use impl to extend compiler-known primitive
types merely because those types exist in the language.
Defining-Module Ownership
An ordinary implementation belongs to the module that defines the target type.
Another module cannot import a type and add ordinary members to it:
module application
import graphics
impl graphics.Image {
fn DebugDump() void {
...
}
}
This is invalid.
The restriction gives each named type one coherent implementation owner and prevents
unrelated modules from changing the member surface of imported types.
Rule: Ordinary implementation is owned by the defining module.
Sec does not use cross-module extension methods as part of the ordinary
impl model.
Primary Impl
A type's ordinary implementation surface has one primary implementation:
impl Device {
fn Start() void {
...
}
}
The primary implementation is the human-facing entry point for the type's behavior.
A second primary block is invalid:
impl Device {
fn Start() void {
...
}
}
impl Device {
fn Stop() void {
...
}
}
Additional implementation fragments use impl extends.
impl extends
A type's implementation may be split across multiple source files in the same module:
impl Device {
fn Start() void {
...
}
}
impl extends Device {
fn Stop() void {
...
}
}
The canonical spelling is:
impl extends Device
not:
impl Device extends
An extension fragment:
- belongs to the same module as the target type;
- requires a primary implementation in that module;
- may live in another source file in the module;
- may be discovered before or after the primary block;
- has the same ordinary member capabilities as the primary block.
extends is an organizational feature.
It does not create subclassing, inheritance or a weaker implementation category.
One Combined Member Surface
The primary implementation and every valid extension fragment form one logical
member namespace.
File boundaries do not create separate method or member scopes.
impl Device {
fn Start() void {
}
}
impl extends Device {
fn Start() void {
}
}
This is a duplicate method even when the declarations are in different source files.
The combined surface includes:
- instance bindings;
- methods and overload groups;
- properties;
- static members;
init overloads;
free;
- nested type names;
- other target-specific implementation members.
Members Allowed in Impl
Subject to the target type's own rules, an impl may contain:
- instance
let bindings;
- instance
let mut bindings;
- instance methods;
- properties;
static fn methods;
- static properties;
static let values;
static let mut storage;
init lifecycle members;
- one
free lifecycle member;
- nested or associated type declarations;
- nested implementations of owned nested types;
- unit metadata and other target-specific associated declarations.
Some declarations remain invalid directly inside the implementation.
For example, bare field syntax is not an implementation member:
impl Vehicle {
engine: Engine // invalid
}
When instance storage belongs in the implementation, declare it explicitly with
let or let mut.
Instance let Bindings
A non-static binding directly inside impl belongs to each instance.
type Counter struct {
value: int,
}
impl Counter {
let Label: string := "counter"
let mut Enabled: bool := true
}
Every Counter instance has its own Label and
Enabled storage.
Access requires an instance:
let label := counter.Label
counter.Enabled = false
Inside instance behavior:
impl Counter {
fn Disable() void {
self.Enabled = false
}
}
This is invalid:
let label := Counter.Label
because Label is instance-bound rather than static.
Representation
Instance implementation bindings contribute to the complete representation of
the owning type.
The compiler must account for their:
- storage;
- initialization;
- alignment and layout;
- ownership;
- copy/move classification;
- destruction.
All instance bindings from the primary implementation and every
impl extends fragment are known before final layout and lowering.
Important: let in impl is not shorthand
for static let. Instance and static bindings are different semantic categories.
Static Members
Type-owned members use explicit static.
impl Counter {
static let Maximum: int := 100
static let mut Total: int := 0
static fn IsValid(value: int) bool {
return value <= Counter.Maximum
}
}
Static members require no instance.
let maximum := Counter.Maximum
let valid := Counter.IsValid(50)
Static state is not part of an ordinary instance's representation.
Static members do not receive self.
impl Counter {
static fn Invalid() void {
self.value = 0 // invalid
}
}
Sec does not infer static membership merely because a method body happens not to
use self. A static method is written explicitly as
static fn.
Static vs instance binding
impl Counter {
let Label: string := "counter"
static let Maximum: int := 100
}
Then:
counter.Label // valid
Counter.Label // invalid
Counter.Maximum // valid
counter.Maximum // invalid
The distinction is visible in source, lifetime, storage and lookup.
Implicit self
Ordinary instance methods receive compiler-provided self.
impl Counter {
fn Increment() void {
self.value += 1
}
fn Value() int {
return self.value
}
}
The programmer does not write self in the parameter list.
These receiver forms are invalid:
fn Read(ref self) int
fn Update(ref mut self, value: int) void
For a concrete method, the compiler derives the required receiver access from
the method body and the ordinary ownership and borrowing rules.
For example:
fn Value() int {
return self.value
}
requires only the authority needed to read the instance.
fn Increment() void {
self.value += 1
}
requires mutable/exclusive receiver authority.
Interface declarations are different because they have no body from which receiver
capability can be inferred; interface contracts therefore spell out their receiver
capability separately.
Methods
An ordinary function declaration inside impl is an instance method.
impl Vehicle {
fn Start() void {
...
}
}
Its qualified member identity is conceptually:
Vehicle.Start
Call it through an instance:
vehicle.Start()
Method parameter, result, generic, ownership, fallibility and variadic semantics
follow the ordinary function rules.
Overloading
impl Parser {
fn Parse(value: string) Result[Value, ParseError] {
...
}
fn Parse(value: ref byte[]) Result[Value, ParseError] {
...
}
}
Methods may overload where their parameter signatures distinguish them.
Return type alone is not an overload discriminator.
Overloads are collected across the complete primary and extension implementation surface.
Properties
Properties are implementation members:
impl Vehicle {
property TopSpeed: Speed {
get {
return self._topSpeed
}
}
}
Properties provide behavioral access and do not create hidden storage merely because
a property exists.
Storage used by the property must come from direct type representation, instance
implementation bindings or another explicitly defined storage mechanism.
Properties may appear in the primary implementation or an extension fragment.
Duplicate property and member names are checked across the combined surface.
Instance property accessors receive implicit self.
Getter, setter and fallible setter semantics are covered by the Properties chapter.
Nested and Associated Types
An implementation may declare types owned by the implementation target's namespace.
impl Vehicle {
type Engine struct {
MaxPower: int,
}
enum FuelType {
Petrol
Diesel
Electric
}
type Fuel union {
Petrol
Diesel
Electric
}
type Flags register[8] {
Enabled: bit,
_: bit[7],
}
}
Outside the implementation, use qualified names:
Vehicle.Engine
Vehicle.FuelType
Vehicle.Fuel
Vehicle.Flags
Inside impl Vehicle or a valid same-module extension, the short nested
name may be used when unambiguous.
Nested declarations do not leak their short names into the surrounding module namespace.
A nested type has its own representation. Declaring Vehicle.Engine
does not itself add an engine field to every Vehicle.
Nested Impl
The primary implementation of an owned nested type may be written inside the
enclosing implementation:
impl Vehicle {
type Engine struct {
MaxPower: int,
}
impl Engine {
fn Start() void {
...
}
}
}
The nested block above is the primary implementation of
Vehicle.Engine.
A nested implementation must target a type owned by the enclosing type.
It cannot be used as a grouping device for an unrelated module-level type.
Additional fragments may use the qualified name:
impl extends Vehicle.Engine {
fn Stop() void {
...
}
}
The ordinary same-module, primary/extension and duplicate rules apply to nested types too.
Lifecycle Members
Sec defines two special implementation members for explicit lifecycle behavior:
init
free
Neither is declared with fn.
init establishes a fully valid instance.
free performs cleanup that cannot be expressed completely by automatic
destruction of owned fields.
A type does not need either lifecycle member when ordinary construction and
compiler-derived destruction already express the required semantics.
init
An infallible initializer is written:
impl Point {
init(x: int, y: int) {
self.x = x
self.y = y
}
}
init has no ordinary success return type.
Successful completion means that construction produced a fully valid instance.
This is not lifecycle syntax:
fn init(x: int, y: int) void {
...
}
The lifecycle member is the bare contextual form init(...) inside
impl.
When to use init
Use an explicit initializer when construction:
- establishes non-trivial invariants;
- performs meaningful initialization logic;
- selects between meaningful construction algorithms;
- acquires resources;
- can fail for a meaningful typed reason.
Do not write an empty or trivial init() merely to duplicate an
already valid implicit/default construction path.
Fallible init
A fallible initializer writes its construction error type after the parameter list:
impl Connection {
init(address: Address) ConnectError {
...
}
}
ConnectError is not the return type of init.
It describes only the construction error channel.
Success produces the completed Connection instance.
The initializer does not return an independent success payload.
Fallible initialization may use ordinary Sec error propagation consistent with the
declared construction error type.
Initializer Overloads
init may be overloaded by parameter signature:
impl Buffer {
init() {
}
init(size: uint) AllocationError {
...
}
init(data: ref byte[]) AllocationError {
...
}
}
The error type does not distinguish initializer overloads.
This is invalid:
impl Resource {
init(path: string) IOError {
...
}
init(path: string) ParseError {
...
}
}
Both declarations have the same parameter signature.
Construction Must Complete a Valid Instance
Successful init completion must establish a fully valid target value.
The completed value must satisfy:
- required field and instance-binding initialization;
- named-type contracts and invariants;
- ownership requirements;
- resource ownership requirements;
- target-specific validity rules.
A partially initialized outer instance cannot escape from init.
If ordinary/default construction already provides an unambiguous valid zero-argument
construction path, no explicit init() is required just to reproduce it.
new
new explicitly selects lifecycle construction.
Infallible:
let point := new Point(10, 20)
Fallible:
let connection := try new Connection(address)
A successful new Type(...) expression has type Type.
let mut connection := try new Connection(address)
binds:
connection: Connection
It does not bind the initializer's error type.
When no explicit matching init() exists, new Type() may
use a permitted unambiguous implicit construction path defined by the target type's
construction rules.
new vs Conversion
Lifecycle construction and conversion are separate operations.
let percent := Percent(50)
uses Percent's conversion semantics.
let connection := try new Connection(address)
selects lifecycle construction through init.
The compiler must not silently substitute one form for the other.
Rule: Type(value) means conversion or the
target type's ordinary constructor-like conversion semantics.
new Type(args...) means lifecycle construction.
new Does Not Mean Heap Allocation
new selects a construction path.
It does not select a storage location.
let point := new Point(10, 20)
The resulting value may be represented in:
- stack storage;
- SSA values;
- registers;
- inline aggregate storage;
- another location permitted by ordinary storage and optimization rules.
Allocation happens only when the selected construction semantics actually require it.
Resource Acquisition During init
Resources acquired during construction must have a statically valid cleanup path.
For every acquired owned resource, cleanup must exist for:
- construction failure after acquisition;
- eventual destruction of the successfully constructed value.
This does not mean every resource-owning type needs a custom free.
When ownership is represented by ordinary owned fields whose types already have
correct destruction behavior, compiler-derived field destruction is sufficient.
A custom free is primarily for cleanup that cannot be represented by
ordinary owned Sec fields.
Partial Initialization and Failure
During init, fields, instance bindings and resources may become
initialized in stages.
If construction fails:
- only successfully initialized owned state is cleaned up;
- ordinary destruction rules determine that cleanup;
- the incomplete outer value cannot escape;
- the completed-value
free member is not invoked for an object that never completed construction;
- construction temporaries are cleaned up according to their ordinary ownership rules.
This makes fallible construction compatible with deterministic resource ownership
without requiring a half-valid sentinel object.
free
free defines custom lifecycle cleanup:
impl ForeignBuffer {
free {
unsafe {
ForeignRelease(self.pointer)
}
}
}
free:
- is not declared with
fn;
- receives compiler-provided destruction access to
self;
- has no ordinary return value;
- is not an ordinary method;
- cannot be called as
value.free();
- must not let the complete value escape or be resurrected;
- runs before automatic destruction of remaining initialized owned fields according to destruction rules;
- may appear at most once across the complete merged implementation surface.
Prefer compiler-derived destruction when owned fields already express the resource model correctly.
Construction and Cleanup Must Balance
init and free do not need to appear as a textual pair.
The real semantic requirement is:
Every owned resource established during construction must have a valid
cleanup path for both failed construction and successful lifetime completion.
Automatic field destruction may satisfy the successful-lifetime side entirely.
Write a custom free only when the necessary cleanup cannot otherwise
be represented by owned fields and ordinary destruction.
Generic Impl
A generic implementation uses the parameters of its generic target:
type Holder[T] struct {
value: T,
}
impl Holder[T] {
fn Get() T {
return self.value
}
}
An extension uses the same target parameters:
impl extends Holder[T] {
fn HasValue() bool {
...
}
}
The primary block and every valid extension combine into one concrete member surface
for each monomorphized Holder[T].
Methods may introduce additional method-level generic parameters:
impl Holder[T] {
fn Transform[U](transform: fn(T) U) Holder[U] {
...
}
}
The method sees both the owner's T and its own U.
Interface Conformance
Explicit interface conformance is declared on the primary implementation:
impl FileReader implements Reader, Closeable {
fn Read(buffer: ref mut byte[]) Result[int, IOError] {
...
}
fn Close() void {
...
}
}
The implements list belongs to the primary impl.
Extension fragments may contribute methods and other members that help satisfy the
complete implementation surface, but they do not redeclare the interface list:
impl extends FileReader {
fn Helper() void {
...
}
}
This is invalid:
impl extends FileReader implements Reader {
...
}
Interface requirement matching, receiver capabilities and interface values are
described in the Interfaces chapter.
Name Lookup and Forward References
The complete implementation surface is collected before member bodies are analyzed.
This includes:
- the primary implementation;
- all valid extension fragments;
- instance bindings;
- nested type names;
- nested implementation targets;
- properties;
- method overload groups;
- static members;
init overloads;
free presence;
- other target-specific implementation declarations.
Source-file discovery order therefore has no semantic effect.
A method in one valid fragment may refer to a member declared in another valid
fragment when normal member lookup permits it.
Duplicate and Conflict Rules
The complete implementation must reject:
- more than one primary implementation;
impl extends without a primary implementation;
- ordinary implementation outside the target's defining module;
- duplicate non-overloadable member names;
- duplicate method signatures;
- duplicate
init parameter signatures;
- more than one
free;
- conflicts between direct type fields and implementation members;
- conflicts between instance and static member names;
- conflicts between nested type names and other members;
- nested implementations of unrelated types;
- target representation declarations using syntax not permitted inside
impl.
Duplicate detection operates across all files participating in the same implementation surface.
Diagnostics should identify both declarations when a conflict has two source locations.
Runtime Model
The impl block itself has no runtime representation.
Merely declaring methods or nested types does not add:
- a hidden instance field;
- a method table;
- a vtable;
- a hidden metadata pointer;
- runtime reflection metadata.
A separate interface/runtime rule may require dynamic-dispatch metadata for an
interface value, but ordinary methods do not create it automatically.
Instance let and let mut bindings are different:
they explicitly contribute per-instance state and therefore affect complete
representation and lifecycle.
Static storage has separate lifetime and representation from ordinary instances.
new does not force heap allocation.
Source Organization
Keep the primary implementation near the type declaration when practical.
The primary block is a good place for:
- central methods;
- the public conceptual entry points for the type;
init declarations;
free when needed;
- the
implements list;
- important nested declarations.
Use impl extends when one type has enough behavior to justify coherent
source-file separation.
Good examples of extension boundaries include:
- parsing;
- serialization;
- protocol support;
- formatting;
- specialized algorithms.
Avoid scattering unrelated methods across many fragments merely because the language permits it.
Instance storage
Use direct type fields for representation that naturally belongs with the type declaration.
Use implementation let/let mut where instance-bound
implementation storage is the intended abstraction.
Use static only when one member belongs to the type rather than to each instance.
Diagnostics and Tooling
Important implementation diagnostics include:
- duplicate primary implementation;
- extension without a primary implementation;
- implementation outside the defining module;
- explicit
self parameter in a concrete method;
self used from a static member;
- bare field syntax inside
impl;
- invalid access to an instance binding through the type;
- invalid access to a static member through an instance;
- nested implementation of an unrelated type;
- duplicate method signature;
- duplicate initializer signature;
- more than one
free;
- fallible construction used without required error handling;
- partially initialized value escaping from
init;
- resource acquired during construction with no provable cleanup path;
- direct call of lifecycle
free;
implements written on an extension fragment.
Tooling should distinguish instance and static members clearly.
For a fallible initializer, signature help should make the lifecycle meaning explicit:
init(address: Address)
constructs: Connection
construction error: ConnectError
Hover for new Connection(address) should report the constructed
type and the construction error where applicable.
Formatting must preserve the semantic distinction between:
Type(value)
new Type(arguments)
and between:
let Member
static let Member
Summary
impl defines implementation members for a named nominal type.
- Ordinary implementation belongs to the type's defining module.
- A type's implementation surface has one primary
impl Type.
- Additional same-module fragments use
impl extends Type.
- All valid fragments form one combined member namespace.
- Source-file order does not affect implementation semantics.
- Instance
let and let mut bindings are per-instance storage.
- Instance implementation bindings contribute to layout, ownership, initialization and destruction.
- Bare field syntax remains invalid inside
impl.
- Type-owned values and storage use explicit
static.
static let is immutable type-owned state or value.
static let mut is mutable type-owned storage.
- Instance members are accessed through an instance; static members are accessed through the type.
- Ordinary methods receive implicit
self.
- Concrete methods never declare
self, ref self or ref mut self parameters.
- The compiler derives concrete receiver requirements from the method body.
- Only
static fn declares a receiver-free type-level function.
- Methods may overload according to normal function overload rules.
- Properties belong in
impl and do not imply hidden property storage.
- Nested structs, enums, unions, registers and other permitted named types may be declared inside an implementation.
- Nested declarations belong to the owning type's namespace.
- A nested implementation may implement a nested type owned by the enclosing type.
init and free are lifecycle members, not ordinary functions.
- An infallible
init has no return or error type.
- A fallible
init(args...) ErrorType declares a construction error channel, not a return type.
init overload identity is determined by its parameter signature.
- Successful construction must establish a fully valid instance.
- A partially initialized outer value cannot escape.
new Type(...) selects lifecycle construction.
Type(value) conversion and new Type(...) construction are distinct operations.
new does not imply heap allocation.
- Fallible construction uses ordinary typed error handling such as
try.
- Resources acquired during construction require cleanup paths for both failure and successful lifetime completion.
- Failed construction cleans up only state that was successfully initialized.
free is custom lifecycle cleanup and cannot be called directly as a method.
- Compiler-derived destruction is preferred when owned fields already describe the cleanup correctly.
- Generic implementation parameters correspond to the generic target's parameters.
- Methods inside generic implementations may introduce additional method-level generic parameters.
- Explicit interface conformance belongs on the primary implementation.
impl extends may contribute members but cannot redeclare the implements list.
- The compiler collects the complete implementation surface before analyzing member bodies.
- Duplicate and conflicting members are checked across the entire merged implementation.
- The
impl block itself has no runtime object or hidden representation.
- Ordinary methods do not automatically add vtables or metadata pointers.