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.