Structs, Properties and Impl

Sec separates stored representation, behavior, controlled access and lifecycle into distinct language constructs. A type's data layout is visible in its type declaration; behavior lives in impl; properties define member-like behavior; lifecycle members handle construction and exceptional cleanup.

type Counter struct {
    value: int,
}

impl Counter {
    fn Increment() void {
        self.value += 1
    }

    property Value: int {
        get {
            return self.value
        }
    }
}
Core rule: The type declaration owns stored instance representation. impl owns behavior and associated declarations. A property is behavior, not hidden storage.

Separation of Responsibilities

ConstructResponsibility
type Name structStored fields and nominal aggregate identity
impl NamePrimary behavior and associated declarations
impl extends NameAdditional same-module implementation fragments
propertyControlled or computed member-like access
initLifecycle construction when construction requires logic or may fail
freeCustom cleanup not already represented by owned-field destruction
interfaceBehavioral contract

Sec does not need a class declaration that mixes all of these concerns into one construct. Composition, interfaces and ordinary functions provide reuse without implementation inheritance.

Structs

A struct defines a named aggregate type with stored fields. The declaration contains data, not methods or executable behavior.

type Coordinate struct {
    x: int,
    y: int,
    z: int,
}

A struct declaration creates a nominal type. Two separately declared structs remain different types even if every field has the same name and type.

type Left struct {
    value: int,
}

type Right struct {
    value: int,
}

let left := Left { value: 10 }
let right: Right := left    // invalid

An empty struct is valid:

type Marker struct {
}

Struct Fields

Stored fields use typed-identifier syntax:

name: Type
type User struct {
    ID: uint64,
    Name: string,
    Active: bool,
}

Struct declarations follow these field rules:

  • each field has one name and one resolved type;
  • field names must be unique within the struct;
  • unknown field types are compile-time errors;
  • field declaration order is preserved;
  • fields are comma-separated;
  • a trailing comma is allowed;
  • line breaks do not replace commas in a struct type declaration.
type Point struct {
    x: int
    y: int
}

The declaration above is invalid because the fields are not separated by commas.

Struct Field Tags

A stored field may carry an optional raw-string tag after its type:

type User struct {
    ID: uint64 `json:"id" db:"user_id"`,
    Name: string `json:"name"`,
    Password: string `json:"-"`,
}

Tags use a Go-inspired key/value form:

key:"value" key:"value"

Tags are metadata. The compiler does not hard-code a closed set of tag keys. Domain-specific consumers may define meanings for keys such as json, xml, db, yaml or csv.

Field tags do not by themselves change field type, nominal identity, defaultability, ownership, copyability, equality or destruction semantics.

Struct Literals

A struct value is constructed with a named struct literal:

let user := User {
    ID: 42
    Name: "Anna"
    Active: true
}

Commas are also accepted in multiline literals:

let user := User {
    ID: 42,
    Name: "Anna",
    Active: true,
}

A single-line literal uses explicit separators:

let user := User { ID: 42, Name: "Anna", Active: true }

The compiler validates the target type, field names, duplicate entries and every field input.

Rule: A struct literal is construction. Each field input follows the ordinary copy, move and ownership rules of its type.

Omitted Fields and Defaults

A struct literal may omit fields whose types have semantic defaults.

type Position struct {
    line: int,
    column: int,
    valid: bool,
    name: string,
}

let position := Position {
    line: 10
}

The omitted fields receive their type defaults:

Position {
    line: 10
    column: 0
    valid: false
    name: ""
}

No readable struct value contains undefined omitted fields. If an omitted field has no default and no spread supplies it, construction is invalid.

A struct is defaultable exactly when every stored field is defaultable.

let origin := Position {}
let mut current: Position

Both forms construct complete default values.

Struct Spread

Struct literals support postfix spread:

let updated := User {
    original...
    Name: "Anna"
}

Field resolution conceptually proceeds as follows:

  1. evaluate explicit and spread entries in source evaluation order;
  2. resolve spread-provided fields and explicit overrides;
  3. reject duplicate explicit fields and conflicts;
  4. initialize remaining omitted fields from semantic defaults.

Spread does not bypass nominal identity, visibility, ownership, copy/move rules or non-defaultable required fields.

Field Access

Stored fields use ordinary member syntax:

let name := user.Name
user.Name = "Maria"

Assignment requires a mutable destination and must satisfy the field type's ordinary transfer, ownership and borrowing rules.

Field access and property access share member syntax, but they are different semantic operations resolved by the compiler.

Struct Value Semantics

Copyability, destruction and equality are derived from stored fields together with applicable nominal policies.

Copy and move

A struct may be copyable when its fields and nominal policies permit safe copying. A move-only field, resource invariant, address-stability requirement or @noCopy restriction can make the complete struct move-only or non-copyable.

Equality

Ordinary struct equality is available when every stored field is semantically equality-comparable and no nominal rule disables derived equality.

if left == right {
    ...
}

Methods, properties and field tags do not participate in derived struct equality.

Impl Blocks

impl defines behavior and associated declarations for a named type. It never changes the target's stored instance representation.

type Counter struct {
    value: int,
}

impl Counter {
    fn Increment() void {
        self.value += 1
    }
}

Ordinary user implementations belong to the module that defines the target type. A module cannot add an ordinary implementation to a nominal type owned by another module.

Primary Impl

A named type has one primary ordinary implementation in its defining module:

impl Device {
    fn Start() void {
        ...
    }
}

The primary implementation is the main human-facing entry point for the type's behavior, lifecycle and interface conformance. A second primary implementation for the same type is invalid.

Impl Extensions

Additional behavior may be organized in same-module implementation fragments:

impl extends Device {
    fn Stop() void {
        ...
    }
}

An extension fragment:

  • belongs to the same module as the primary implementation;
  • may live in another source file of that module;
  • may appear before or after the primary implementation in source order;
  • requires the primary implementation to exist;
  • has the same implementation-member capabilities as the primary block.

The primary implementation and all valid extensions form one combined member surface. Duplicate and conflicting members are checked across that complete surface.

Rule: extends is organizational. It does not create inheritance or a different runtime object model.

Implicit Self

Ordinary instance methods receive compiler-provided self. The receiver is not written in the parameter list.

impl Counter {
    fn Increment() void {
        self.value += 1
    }

    fn Value() int {
        return self.value
    }
}

The compiler derives and validates receiver borrowing and mutation requirements from the method body, calls, ownership rules and borrow analysis.

Static members have no instance receiver:

impl Counter {
    static fn Maximum() int {
        return 100
    }
}

A static member cannot use self.

Impl Members

An implementation may contain members permitted by their specialized rules:

  • instance methods;
  • properties;
  • static methods and properties;
  • static storage;
  • immutable associated values;
  • init lifecycle members;
  • free lifecycle cleanup;
  • associated and nested type declarations;
  • nested implementations of owned associated types;
  • type-family-specific associated metadata where defined.

An implementation cannot directly add stored representation:

impl Vehicle {
    engine: Engine    // invalid
}

Associated and Nested Types

An implementation may define nested types because those types own their own representation; they do not add storage to the outer target.

type Vehicle struct {
    engine: Vehicle.Engine,
    fuel: Vehicle.FuelType,
}

impl Vehicle {
    type Engine struct {
        MaxPower: int,
    }

    enum FuelType {
        Petrol,
        Diesel,
        Electric,
    }
}

Outside the implementation context, nested types use qualified names:

Vehicle.Engine
Vehicle.FuelType

Inside the owning primary implementation or extension, a short name may be used when it is unambiguous.

Nested types may have their own nested implementation:

impl Vehicle {
    type Engine struct {
        MaxPower: int,
    }

    impl Engine {
        fn Start() void {
            ...
        }
    }
}

Static and Associated Members

Type-level methods and mutable static storage use explicit static. Immutable associated values use direct let.

impl Counter {
    let Maximum: int := 100
    static let mut GlobalCount: int := 0

    static fn IsValid(value: int) bool {
        return value <= Counter.Maximum
    }
}

Static members are accessed through the type and do not receive self.

Properties

A property defines controlled member-like behavior for reading or assigning a value. It does not add stored instance data.

type Vehicle struct {
    _speed: Speed,
}

impl Vehicle {
    property Speed: Speed {
        get {
            return self._speed
        }

        set speed {
            self._speed = speed
        }
    }
}

The property is used with normal member syntax:

let current := vehicle.Speed
vehicle.Speed = requestedSpeed
Best practice: Use a field when no behavioral boundary is needed. Use a property when access must validate, compute, synchronize, translate, observe or otherwise enforce behavior.

Property Accessors

A property may contain one getter and one setter form:

property Name: string {
    get {
        return self._name
    }

    set nextName {
        self._name = nextName
    }
}

A property must have at least one accessor.

A getter must return a value assignable to the property type on every required reachable path. A property without a getter is write-only.

A setter explicitly names its incoming value parameter. The parameter has the property's declared type; value is conventional but never implicit.

A property without set or try set is read-only.

Instance property bodies receive implicit self.

Fallible Properties

A setter that may reject an assignment is declared with try set and an explicit error type:

enum SpeedError error {
    TooLow
    TooHigh
}

impl Vehicle {
    property TopSpeed: Speed {
        get {
            return self._speed
        }

        try set speed SpeedError {
            if speed < MinimumSpeed {
                return Err(SpeedError.TooLow)
            }

            if speed > MaximumSpeed {
                return Err(SpeedError.TooHigh)
            }

            self._speed = speed
        }
    }
}

The setter error type is part of the public contract and is not inferred from the setter body. Normal completion or bare return means success. A try set body does not return Ok().

Assignment through a fallible setter requires try:

try vehicle.TopSpeed = requestedSpeed {
    Err(error) => Handle(error)
}

Fallible compound assignment uses the same rule:

try vehicle.TopSpeed += delta {
    Err(error) => Handle(error)
}

Property getters are infallible in Sec 0.1.

Lifecycle Members

Sec has two special implementation members for type lifecycle behavior:

init
free

Neither is an ordinary method and neither is declared with fn.

  • init establishes a complete valid instance.
  • free performs custom cleanup that compiler-derived field destruction cannot express by itself.

Types whose normal construction and derived destruction already express the required semantics need neither member.

Init and New

Infallible initialization

type Point struct {
    x: int,
    y: int,
}

impl Point {
    init(x: int, y: int) {
        self.x = x
        self.y = y
    }
}

let point := new Point(10, 20)

An infallible init has no return type or error type. Success is the completed instance itself.

Fallible initialization

impl Connection {
    init(address: Address) ConnectError {
        ...
    }
}

let connection := try new Connection(address)

The type after the parameter list is the construction error type, not a success return type. A successful initializer must establish a fully valid instance and cannot let a partially initialized value escape.

Construction and conversion are different

let percentage := Percent(50)
let connection := try new Connection(address)

Type(value) uses conversion semantics. new Type(arguments) selects lifecycle construction.

Rule: new does not mean heap allocation. Storage is chosen according to ordinary storage, allocation and optimization rules.

Free

free defines custom cleanup for resources that cannot be expressed completely through normal owned-field destruction.

impl ForeignBuffer {
    free {
        unsafe {
            ForeignRelease(self.pointer)
        }
    }
}

free:

  • has compiler-provided destruction access to self;
  • has no ordinary return value;
  • is not called as value.free();
  • cannot let the complete value escape or be resurrected;
  • is followed by automatic destruction of remaining initialized owned fields;
  • may occur only once across the complete merged implementation.

Interface Conformance

Stored representation remains in the type declaration. Explicit interface conformance belongs to the primary implementation:

type FileReader struct {
    handle: FileHandle,
}

impl FileReader implements Reader, Closeable {
    fn Read(buffer: ref mut byte[]) Result[int, IOError] {
        ...
    }

    fn Close() void {
        ...
    }
}

The implements list declares behavioral conformance; it does not add stored fields. Extension fragments may contribute members used to satisfy the contracts but do not redeclare the interface list.

Complete Example

module transport

type Speed int range 0..300

enum VehicleError error {
    InvalidSpeed
}

interface Movable {
    fn CurrentSpeed() Speed

    property RequestedSpeed: Speed {
        get
        try set speed VehicleError
    }
}

type Vehicle struct {
    _currentSpeed: Speed,
    _requestedSpeed: Speed,
    Name: string `json:"name"`,
}

impl Vehicle implements Movable {
    let MaximumSupportedSpeed: Speed := 300

    init(name: string) {
        self.Name = name
    }

    fn CurrentSpeed() Speed {
        return self._currentSpeed
    }

    property RequestedSpeed: Speed {
        get {
            return self._requestedSpeed
        }

        try set speed VehicleError {
            if speed > Vehicle.MaximumSupportedSpeed {
                return Err(VehicleError.InvalidSpeed)
            }

            self._requestedSpeed = speed
        }
    }

    type Telemetry struct {
        RecordedSpeed: Speed,
        Valid: bool,
    }
}

impl extends Vehicle {
    fn Stop() void {
        self._currentSpeed = 0
        self._requestedSpeed = 0
    }

    fn Snapshot() Telemetry {
        return Telemetry {
            RecordedSpeed: self._currentSpeed
            Valid: true
        }
    }
}

fn Prepare(name: string, requested: Speed) Result[Vehicle, VehicleError] {
    let mut vehicle := new Vehicle(name)

    try vehicle.RequestedSpeed = requested {
        Err(error) => return Err(error)
    }

    return Ok(vehicle)
}

Summary

  • A struct is a nominal stored-data type.
  • Struct fields use name: Type syntax and are comma-separated in type declarations.
  • Field tags are metadata and do not change core struct type semantics.
  • Multiline struct literals may use line layout or commas between entries.
  • Omitted struct fields receive semantic defaults when their types are defaultable.
  • A struct is defaultable exactly when every stored field is defaultable.
  • Struct spread uses postfix ... and does not bypass ownership or visibility rules.
  • Struct copy, move, equality and destruction behavior derive from fields and nominal policies.
  • impl defines behavior and associated declarations without changing stored representation.
  • A named type has one primary ordinary implementation in its defining module.
  • Additional same-module behavior uses impl extends Type.
  • Primary and extended implementation fragments form one combined member surface.
  • Instance methods and properties receive compiler-provided implicit self.
  • Receiver borrow and mutation requirements are derived by compiler analysis.
  • Static members are explicit and do not receive self.
  • Associated nested types live in the owning type's namespace.
  • Properties define behavior, not hidden instance storage.
  • Property setters explicitly name their incoming value parameter.
  • Fallible setters use try set name ErrorType and require try assignment.
  • Property getters are infallible in Sec 0.1.
  • init and free are lifecycle members, not ordinary methods.
  • new selects lifecycle construction and does not imply heap allocation.
  • Interface conformance is declared on the primary implementation.