Generics
Generics let one declaration describe a family of concrete types or callables. Generic parameters are compile-time type parameters; they are not runtime values.
type Pair[A, B] struct {
first: A,
second: B,
}
fn Identity[T](value: T) T {
return value
}
Sec specializes generics for the concrete type arguments that are actually used. A specialization is then type-checked and lowered as ordinary concrete Sec code.
Compile-Time Model
Given:
fn Identity[T](value: T) T {
return value
}
these calls request different concrete specializations:
let number := Identity(10)
let text := Identity("Sec")
Conceptually, the compiler obtains concrete callable instances equivalent to:
Identity[int]
Identity[string]
The generic parameter T does not survive into executable code
as a runtime type variable.
The model preserves:
- static typing;
- nominal type identity;
- explicit interface constraints;
- concrete layout;
- normal ownership and borrowing;
- deterministic concrete symbol identity;
- ordinary backend optimization over concrete code.
Generic Parameters
Generic parameters use square brackets after the declaration name:
type Pair[A, B] struct {
first: A,
second: B,
}
fn Convert[From, To](value: From) To {
...
}
Generic parameter names are type symbols inside the declaration's generic scope.
Rules include:
- parameter names must be unique within the generic parameter list;
- parameter order is semantically significant;
- parameters are types, not runtime values;
- trailing commas are permitted where the ordinary generic-list grammar permits them.
Generic Arguments
Concrete type arguments also use square brackets:
Pair[string, int]
Option[User]
Identity[int](10)
Generic arguments are types.
This is not a generic type argument:
Container[10] // invalid as type-generic syntax
A generic type reference requires a complete generic argument list:
Pair[string, int]
Generic function and method calls may infer arguments when the call provides enough type information.
Concrete Type Identity
A concrete generic type is identified by the source generic declaration and its ordered concrete argument list.
These are the same type:
Pair[string, int]
Pair[string, int]
These are different types:
Pair[string, int]
Pair[int, string]
Different generic declarations remain nominally distinct even when their substituted physical representations happen to be identical.
Generic arguments also preserve their own normal identity. Named types, units, structs, unions, enums, interfaces and other concrete Sec types do not lose identity merely because they appear inside a generic instantiation.
Supported Generic Declarations
Sec 0.1 supports type generics on:
- structs;
- unions;
- enums;
- named types;
- interfaces;
- functions;
- methods;
- eligible implementation targets through their generic type;
- nested declarations where ordinary scope rules permit them.
Register representation has stricter requirements and is discussed separately later in this chapter.
Generic Structs
type Stack[T] struct {
items: list[T],
}
Concrete uses include:
Stack[int]
Stack[string]
Stack[User]
Each concrete specialization has its own concrete field types, layout, copy/move classification, defaultability and destruction behavior.
Generic Unions
Generic union payloads are substituted before construction, matching, layout and destruction.
type Result[T, E] union {
Ok(T)
Err(E)
}
For:
Result[int, IOError]
the compiler operates on a concrete closed union whose payloads are
concretely int and IOError.
Exhaustiveness, payload ownership and destruction are checked using that concrete union.
Generic Enums
Enums may have ordinary type parameters:
enum State[T] {
Ready
Busy
}
State[Connection] and State[Socket] are different
nominal enum types even if both lower to the same physical representation.
Phantom storage parameters are valid
enum Permission[T] {
Read
Write
}
T contributes to the concrete enum's type identity even though
it does not occupy runtime storage.
This is useful when the enum identity itself should be associated with a type:
Permission[File]
Permission[Socket]
These remain distinct Sec types.
Behavior may use the generic parameter
impl Permission[T] {
fn Check(value: T) void {
...
}
}
Generic enum members themselves remain payload-less. Use a union when alternatives need to carry data.
After specialization, ordinary enum rules determine defaults, representation,
conversions, comparison, switch, match and exhaustiveness.
Generic Named Types
A named type may be parameterized:
type ID[T] int
Then:
ID[User]
ID[Product]
are different concrete nominal types.
A generic named type may also use the parameter as its represented type when the ordinary named-type rules permit it:
type Wrapped[T] T
Named-type contracts, conversions, copy/move rules and representation are applied after substitution.
Generic Interfaces
interface Repository[T] {
fn Get(id: ID[T]) Option[T]
}
These are distinct interface types:
Repository[User]
Repository[Product]
Conformance is checked against the concrete instantiated interface contract.
A generic interface may also be used as a constraint:
fn Compare[T: Comparable[T]](left: T, right: T) int {
...
}
Generic Functions
fn Identity[T](value: T) T {
return value
}
Multiple generic parameters are separated with commas:
fn Convert[From, To](value: From) To {
...
}
A generic function is a compile-time template, not one runtime polymorphic callable accepting arbitrary types.
Generic Impl Blocks
The implementation target repeats the generic parameters belonging to the generic nominal type:
type Stack[T] struct {
items: list[T],
}
impl Stack[T] {
fn Push(value: T) void {
self.items.Append(value)
}
}
The T in impl Stack[T] corresponds to the
T declared by Stack.
This is not an unrelated new parameter:
impl Stack[U] {
...
}
when U is not the corresponding declared target parameter.
Generic types may also use same-module extension implementations:
impl extends Stack[T] {
fn IsEmpty() bool {
return self.items.IsEmpty
}
}
Primary and extended implementation fragments form one concrete member surface after substitution.
Method-Level Generics
A method may introduce type parameters that vary independently of the owning type.
impl Converter {
fn Convert[T, U](value: T) U {
...
}
}
A method inside a generic type sees both the owner's parameters and its own:
impl Box[T] {
fn Transform[U](transform: fn(T) U) Box[U] {
...
}
}
Here:
Tbelongs toBox;Ubelongs toTransform.
A method-level parameter may not redeclare a visible generic parameter from the enclosing generic scope.
Static methods may also declare method-level generic parameters.
Generic Methods in Interface Contracts
A generic interface may use its own generic parameters in method requirements:
interface Sink[T] {
mut fn Write(value: T) void
}
An interface method requirement does not introduce an additional method-level generic parameter list:
interface Mapper[T] {
fn Map[U](value: T) U // invalid
}
Interface values may require runtime dispatch, while Sec has no runtime generic dispatch. Once a generic interface itself is concretely instantiated, its dispatch surface must therefore be finite.
Put the variation on the interface type itself, use another interface, or use a generic concrete helper when another operation-level type must vary.
Generic Constraints
Constraints state which interface capabilities a concrete generic argument must provide.
fn Save[T: Serializable](value: T) Result[void, IOError] {
return value.Serialize()
}
T: Serializable does not implement Serializable.
It requires the substituted concrete type to already satisfy that interface contract.
Interface conformance remains explicit on the concrete type:
impl User implements Serializable {
fn Serialize() byte[] {
...
}
}
A type does not satisfy the constraint merely because it happens to contain similarly named methods.
Multiple Constraints
Multiple requirements on one type parameter use &:
fn Save[T: Serializable & Comparable](value: T) void {
...
}
More constraints compose the same way:
fn Process[T: Serializable & Comparable & Printable](value: T) void {
...
}
& means logical conjunction of compile-time constraints.
It does not create a runtime intersection value type.
Commas separate generic parameters:
fn Convert[
T: Serializable & Comparable,
U: Printable,
](value: T) U {
...
}
Constraint Satisfaction
Every constraint must resolve to an interface type or a concrete generic interface instantiation.
Valid:
fn Save[T: Serializable](value: T) void {
...
}
Valid:
fn Compare[T: Comparable[T]](left: T, right: T) int {
...
}
Invalid:
fn Save[T: int](value: T) void {
...
}
int is a type, but it is not an interface constraint.
Constraints do not:
- create runtime interface values;
- imply dynamic generic dispatch;
- automatically implement interfaces;
- infer structural conformance from matching names.
Operations Available on a Generic Parameter
A generic body may use only capabilities guaranteed by the generic declaration.
An unconstrained T cannot be assumed to have an arbitrary method:
fn Encode[T](value: T) byte[] {
return value.Serialize() // invalid without a guarantee
}
Constrain the operation:
fn Encode[T: Serializable](value: T) byte[] {
return value.Serialize()
}
The generic body is checked against the contract, not against accidental features of one concrete call site.
Some properties depend on the final concrete type and are recomputed after substitution, including layout, defaultability and copy/move/destruction classification.
Ownership, Copying and Borrowing
A generic parameter does not imply copyability.
fn Duplicate[T](value: T) Pair[T, T] {
return Pair[T, T] {
first: value,
second: value,
}
}
This is not generally valid unless the generic contract guarantees that using
value twice is legal.
Generic code follows normal Sec:
- ownership;
- moves;
- copies;
- borrowing;
- mutability;
- destruction.
Concrete specializations receive their final ownership classification after substitution.
Consuming generic parameters
fn Store[T](-> value: T) void {
...
}
The consuming contract remains consuming for every concrete T,
including a type that would otherwise be copyable.
Typed variadics
fn Count[T](values: ...T) int {
return values.Len
}
Every concrete specialization has one concrete variadic element type.
Consuming variadics such as -> values: ...T are invalid.
Defaultability
An unconstrained generic parameter is not assumed to be defaultable.
fn Make[T]() T {
let mut value: T
return value
}
This is invalid unless the generic contract provides the required defaultability guarantee.
A later call with one particular defaultable type does not retroactively make an otherwise invalid generic declaration valid.
Generic Type Inference
Function and method generic arguments may be inferred from:
- ordinary call arguments;
- the receiver and owning generic type;
- an expected result type supplied by surrounding context;
- generic arguments already supplied explicitly.
For example:
let value := Identity(10)
may infer T as int.
Expected-result context can also contribute:
let result: Target := Convert(source)
when the generic signature relates one unresolved type parameter to the result.
Inference must resolve one consistent concrete type for every unresolved parameter.
If the compiler cannot infer a parameter uniquely, the programmer must provide more generic arguments or stronger ordinary type context.
Explicit Generic Arguments
All generic function arguments may be supplied explicitly:
let value := Identity[int](10)
Explicit arguments are applied before inference and must satisfy:
- generic arity rules;
- type validity;
- constraints;
- ordinary function argument compatibility.
Partial Explicit Generic Arguments
A generic function or method call may provide a positional prefix of its generic arguments and infer the remainder.
fn Convert[From, To](value: From) To {
...
}
Then:
let result: string := Convert[int](value)
means:
Fromis explicitlyint;Tois inferred from the remaining context.
Explicit generic arguments always bind left-to-right.
Generic argument holes are not supported:
Foo[, B](...) // invalid
Foo[_, B](...) // invalid
Partial inference applies to function and method calls. Generic type references still require the complete argument list.
Static Members of Generic Types
Static storage is specialized per concrete generic type.
impl Cache[T] {
static let mut Count: int := 0
}
These are different storage locations:
Cache[int].Count
Cache[string].Count
A generic source template does not create one hidden global storage location shared by every specialization.
Static methods and static properties are likewise resolved on the concrete specialized type.
Generic Registers
The generic language feature does not by itself authorize arbitrary generic register declarations.
Every concrete register representation must still have a compile-time-known, valid fixed width and field layout.
A generic register form is valid only where the register-specific rules can prove every concrete specialization has a valid representation.
Generic parameters may never leave register width, field boundaries, bit allocation or byte representation unresolved at layout time.
Recursive Generic Types
Generic recursion must resolve to a finite concrete representation.
This direct recursive storage is invalid:
type Node[T] struct {
next: Node[T],
}
Recursive structure requires a valid indirection or container whose own storage rules make the concrete representation finite.
The compiler also rejects generic instantiation chains that keep changing type arguments without converging to a finite concrete type.
Diagnostics should show the specialization path responsible for a recursive instantiation failure.
Monomorphization
Before representation-dependent lowering, every reachable generic use is fully specialized to concrete Sec types.
For a concrete generic type, the compiler resolves facts such as:
- concrete field and variant types;
- size and alignment;
- defaultability;
- copy/move behavior;
- destruction behavior;
- implemented interfaces;
- concrete methods;
- concrete static storage identity.
For a concrete generic function or method, it resolves:
- concrete parameter types;
- concrete result type;
- satisfied constraints;
- concrete called symbols;
- concrete ownership behavior;
- parameter consuming modes and variadic shape.
No unresolved generic type parameter may reach executable representation-dependent IR, ABI lowering or backend code generation.
Phantom generic identity remains real
enum State[T] {
Ready
Busy
}
Even if T occupies no runtime storage,
State[Connection] and State[Socket] remain different
concrete Sec types.
No hidden runtime descriptor is added merely to preserve that distinction.
Demand-Driven Instantiation
Sec uses demand-driven specialization.
Unused generic functions and methods do not need backend code merely because their templates exist.
Repeated requests for the same generic declaration with the same ordered concrete arguments reuse one canonical concrete specialization.
For example, repeated use of:
Pair[string, int]
refers to the same concrete type identity rather than producing a new type on every occurrence.
ABI Boundary
A generic template does not itself define one unresolved runtime ABI.
fn Encode[T](value: T) byte[] {
...
}
There is no runtime ABI for abstract Encode[T].
A concrete specialization can have an ABI:
Encode[Packet]
because the complete parameter and result representation is known.
Foreign and exported interfaces may impose additional restrictions on which concrete generic specializations may cross ABI boundaries.
Generic Functions as Function Values
A generic function declaration is a compile-time template, not a runtime polymorphic function object.
A function value must refer to one concrete specialized callable:
let transform: fn(int) string := Convert[int, string]
Sec does not place an unresolved generic function template into a runtime function value.
No generic lambda templates
Generic anonymous function templates are not part of Sec 0.1:
fn[T](value: T) T {
return value
}
Use a named generic function and specialize it before converting it to a function value.
Generics and Overload Resolution
Generic candidates participate in ordinary function overload resolution.
The compiler considers:
- ordinary parameter compatibility;
- explicit generic prefix arguments;
- inferred generic arguments;
- constraint satisfaction;
- expected result context where permitted.
Inference does not invent conversions merely to make one generic candidate win.
Return type alone still does not distinguish otherwise duplicate overload declarations.
Generic Parameter Scope
Generic parameters are type symbols visible only inside the scopes that own them.
type Box[T] struct {
value: T,
}
T is visible in the generic type and its matching generic
implementation scopes.
A method-level generic parameter is visible only inside that method and its nested scopes.
Nested generic parameters may not redeclare an active enclosing generic parameter when doing so would make substitution ambiguous.
Generic Mechanisms Not in Sec 0.1
Sec 0.1 does not use:
- runtime generic dispatch;
- type erasure;
- implicit boxing;
- higher-kinded types;
- generic parameter packs;
- variadic type-parameter lists;
- generic argument holes;
- automatic structural constraints inferred from member names;
- automatic interface implementation;
- negative constraints;
- union-of-types constraints;
- arbitrary compile-time reflection as a generic mechanism;
- default generic type arguments;
- generic lambda templates.
Type generics are not value generics
The generic syntax described in this chapter parameterizes declarations by types.
Container[T]
It does not imply general const/value generic syntax such as:
Container[10]
Compile-time value parameterization, where defined by another Sec feature, has separate semantics.
Diagnostics
Important generic diagnostics include:
- duplicate generic parameter;
- unknown generic constraint;
- constraint that is not an interface;
- concrete type does not satisfy a constraint;
- generic operation requires an undeclared capability;
- cannot infer a generic parameter;
- too many explicit generic arguments;
- generic argument hole;
- method-level parameter conflicts with an enclosing generic parameter;
- recursive generic instantiation does not produce a finite type;
- invalid concrete layout after substitution;
- unresolved generic parameter reaching a representation-dependent boundary.
A concrete-dependent failure should identify both the generic declaration and the concrete substitution that caused the failure.
error: concrete specialization Box[Resource] requires an operation not valid for Resource
generic declaration:
Box[T]
substitution:
T = Resource
Tooling should preserve the distinction between the source template and its concrete specialization so diagnostics remain understandable.
Best Practices
- Keep generic parameter lists small and semantically meaningful.
- Use constraints only for capabilities the implementation actually requires.
- Prefer
T: Interfaceover manual runtime capability tests. - Use
&when one parameter requires several independent interfaces. - Let inference handle obvious call-site type arguments.
- Use a partial explicit prefix when it improves clarity or resolves ambiguity.
- Use method-level generics when the variation belongs to one operation rather than the whole type.
- Use generic enums when parameterized enum identity or associated behavior is useful.
- Use unions when alternatives carry payload values.
- Do not assume an unconstrained
Tis copyable or defaultable. - Keep per-specialization static state intentional.
- Avoid exposing generic complexity across ABI and FFI boundaries unless it provides real value.
Summary
- Sec generics are compile-time type templates.
- Generic parameters and arguments use square brackets.
- Generic arguments are types, not arbitrary runtime expressions.
- Generic type references require complete argument lists.
- Generic function and method calls may infer missing arguments.
- A concrete generic identity includes the source declaration and ordered concrete type arguments.
- Different concrete argument lists produce different concrete nominal types.
- Identical physical representation does not merge distinct generic types.
- Sec 0.1 supports generic structs, unions, enums, named types, interfaces, functions and methods.
- Generic enums are valid even when a generic parameter contributes only to type identity.
- Generic enum members remain payload-less.
- Generic named types preserve ordinary named-type identity after substitution.
- Generic interfaces become distinct concrete interface types after specialization.
- Generic impl targets reuse the generic parameters declared by their owning type.
- Methods may introduce additional method-level type parameters.
- Interface method requirements do not introduce additional method-level generic parameters.
- Generic constraints use
:and must resolve to interfaces. - Multiple constraints on one parameter use
&. - Constraint satisfaction requires real explicit interface conformance.
- A generic body may use only capabilities guaranteed by its generic contract.
- An unconstrained generic parameter is not assumed copyable.
- An unconstrained generic parameter is not assumed defaultable.
- Generic code follows normal Sec ownership, borrowing, move, copy and destruction rules.
- Forced-consuming generic parameters remain consuming for every concrete specialization.
- Generic functions may use typed variadic parameters.
- Inference may use arguments, receiver context, expected result type and already explicit generic arguments.
- Calls may provide a left-to-right explicit generic prefix and infer the rest.
- Generic argument holes are not supported.
- Static storage in a generic type is distinct per concrete specialization.
- Generic registers are valid only when register-specific rules can prove every concrete layout.
- Recursive generic types must resolve to finite concrete representations.
- Sec monomorphizes reachable generic uses before representation-dependent lowering.
- No unresolved generic parameter reaches executable backend-facing IR.
- Repeated requests for the same specialization reuse one canonical concrete identity.
- Unused generic callables need not produce backend code.
- Generic templates have no unresolved runtime ABI.
- A generic function must be concretely specialized before becoming a function value.
- Generic lambda templates are not part of Sec 0.1.
- Generic resolution participates in ordinary overload resolution.
- Sec generics use no runtime type erasure, dictionaries, generic dispatch or implicit boxing.
- Higher-kinded types, generic parameter packs, default type arguments and generic argument holes are not part of Sec 0.1.
- Type-generic syntax does not imply general const/value generics.