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
| Construct | Responsibility |
type Name struct | Stored fields and nominal aggregate identity |
impl Name | Primary behavior and associated declarations |
impl extends Name | Additional same-module implementation fragments |
property | Controlled or computed member-like access |
init | Lifecycle construction when construction requires logic or may fail |
free | Custom cleanup not already represented by owned-field destruction |
interface | Behavioral 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 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:
- evaluate explicit and spread entries in source evaluation order;
- resolve spread-provided fields and explicit overrides;
- reject duplicate explicit fields and conflicts;
- 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.