Functions
Functions are statically typed callable declarations with explicit signatures. A function signature states what values the function receives, how those values are transferred or borrowed, and what type the function returns.
fn Add(left: int, right: int) int {
return left + right
}
Functions are registered before their bodies are analyzed, so declaration order does not determine whether one function may call another in the same valid scope.
Function Declaration
An ordinary named function uses:
fn Name(ParameterList) ReturnType {
Body
}
Example:
fn Distance(x: float64, y: float64) float64 {
return x - y
}
An ordinary function always has:
- a name;
- an explicit parameter list;
- an explicit return type;
- a body.
Ordinary Sec functions do not use C-style bodyless prototypes.
fn Calculate(value: int) int // invalid as an ordinary function
Bodyless callable signatures are used only where another language construct
defines their meaning, such as interface requirements and extern
declarations.
interface Calculator {
fn Calculate(value: int) int
}
Return Types
Every ordinary fn declaration has an explicit return type.
fn Double(value: int) int {
return value * 2
}
A function that returns no value uses void:
fn Notify(message: string) void {
fmt.Println(message)
}
Return-type inference is not used for ordinary named function declarations.
fn Double(value: int) {
return value * 2
}
This is invalid because the return type is missing.
Lifecycle members are different constructs
init and free are lifecycle members, not ordinary
functions written with fn.
impl Resource {
init(size: int) {
...
}
free {
...
}
}
Parameters
Ordinary parameters use:
name: Type
Parameters are comma-separated:
fn Mix(left: int, right: int) int {
return left + right
}
A trailing comma is allowed:
fn Mix(
left: int,
right: int,
) int {
return left + right
}
Parameter names must be unique within the function signature.
fn Invalid(value: int, value: int) int {
return value
}
Parameter types must resolve to valid types.
Sec has three central parameter relationships:
| Form | Meaning |
|---|---|
value: T |
Owned by-value parameter; copies or moves according to the concrete type |
value: ref T |
Shared borrow; caller retains ownership |
value: ref mut T |
Exclusive mutable borrow; caller retains ownership |
-> value: T |
Explicit consuming by-value parameter |
Owned By-Value Parameters
An ordinary by-value parameter is owned by the callee for the duration of the call.
fn ClampToZero(value: int) int {
if value < 0 {
value = 0
}
return value
}
Owned by-value parameter bindings are mutable by default. No mut
marker is required on the parameter.
For an implicitly copyable type, the callee owns a copy and the caller's original value remains valid:
fn Increment(value: int) int {
value += 1
return value
}
let original := 10
let incremented := Increment(original)
// original is still 10
For a move-only type, ordinary by-value transfer moves ownership into the callee. A reusable source place must make that transfer visible at the call site.
fn Process(buffer: Buffer) void {
...
}
let buffer := CreateBuffer()
Process(<-buffer)
After successful call entry, buffer is unavailable when
Buffer is move-only.
A fresh temporary does not need a redundant move marker:
Process(CreateBuffer())
Borrowed Parameters
Shared borrow
fn Inspect(value: ref Buffer) void {
...
}
The callee does not own the referenced Buffer.
The caller retains ownership and the callee receives shared read access
according to the borrowing rules.
Mutable borrow
fn Modify(value: ref mut Buffer) void {
...
}
The caller still owns the Buffer, while the callee receives
temporary exclusive mutable authority.
The reference parameter binding itself is not treated like an owned mutable working-value parameter merely because ordinary by-value parameters are mutable.
Explicit Consuming Parameters
A by-value parameter may explicitly require ownership consumption with
->:
fn Transform(-> data: BigArray) BigArray {
...
}
A consuming parameter transfers ownership to the callee regardless of whether the concrete type would otherwise be implicitly copied.
let data := CreateLargeArray()
let result := Transform(<-data)
Use(data) // invalid
The call site uses <- when a reusable source is consumed:
Transform(<-data)
This makes caller-visible ownership loss explicit.
-> is an ownership contract, not an ABI hint.
It does not mean that the machine must physically pass the value differently.
Consuming a reference is not owning its referent
These forms are invalid:
fn Take(-> value: ref Buffer) void {
}
fn Take(-> value: ref mut Buffer) void {
}
If ownership transfer is intended, the parameter itself must be an owned type.
Consuming mode does not create an overload
These declarations conflict:
fn Process(value: Buffer) void {
}
fn Process(-> value: Buffer) void {
}
Overload resolution must never decide whether the caller retains or loses ownership.
Function Calls
A function is called with ordinary call syntax:
let sum := Add(10, 20)
The call expression has the selected callable's return type. Argument count, argument types, borrow modes, consuming modes and variadic shape must satisfy the selected signature.
The parser preserves call syntax. Semantic analysis resolves whether the callable is a free function, method, function value, constructor, conversion or another callable entity.
Ownership at the call site
Inspect(buffer)
Modify(buffer)
Process(<-buffer)
The signature determines whether the value is borrowed, copied or moved. A move marker is required where consuming a reusable source would otherwise be hidden.
Argument Evaluation Order
Function and method arguments are evaluated strictly left to right.
Process(
CreateFirst(),
CreateSecond(),
CreateThird(),
)
The semantic order is:
CreateFirst()CreateSecond()CreateThird()- enter
Process(...)
This order is part of the language semantics. Optimizations may rearrange machine operations only when the observable behavior remains equivalent.
The same rule applies when ordinary arguments and spread arguments are mixed.
Call Transfer Commit
Ownership transfer into callee-owned parameters is committed only after every argument has been evaluated successfully and the call is ready to enter the callee.
Conceptually, a call proceeds as follows:
- resolve the callable;
- evaluate arguments left to right;
- perform required conversions and prepare borrows and owned temporaries;
- if argument evaluation fails, clean caller-owned temporaries and do not enter the callee;
- commit consuming transfers once every argument is ready;
- enter the callee.
let resource := OpenResource()
try Use(
<-resource,
LoadConfiguration(),
) {
Err(error) => {
Handle(error)
}
}
If evaluating LoadConfiguration() fails before Use
begins, ownership of the prepared resource argument has not yet
committed to Use.
This does not undo effects or ownership transfers that occurred inside an argument expression itself. Each nested call has its own completed semantics.
Return Values
A non-void function returns one value:
fn Answer() int {
return 42
}
The returned expression must be assignable to the declared return type, and every reachable path that must produce a value must satisfy return analysis.
A void function may use bare return:
fn Notify() void {
if Done() {
return
}
Work()
}
A void function may also reach the end of its body when normal
control-flow rules permit.
One return value
Sec functions return one value, not language-level tuples of return values.
fn Split(value: int) (int, int) {
...
}
This is invalid. Use an explicit type:
type SplitResult struct {
left: int,
right: int,
}
fn Split(value: int) SplitResult {
return SplitResult {
left: value / 2
right: value - value / 2
}
}
Return ownership
Returning an owned value transfers the returned value to the caller according to normal copy and move rules.
fn CreateBuffer() Buffer {
let buffer := Buffer.Create(4096)
return buffer
}
A move-only local can be returned without creating a duplicate owner.
return <-buffer is also valid, but the move marker is optional at
this terminal ownership boundary.
Receiving the fresh returned value does not require a move marker:
let buffer := CreateBuffer()
Function Overloads
Functions may share a name when their parameter type signatures differ.
fn Print(value: int) void {
...
}
fn Print(value: string) void {
...
}
Return type alone never distinguishes overloads.
fn Convert(value: int) string {
...
}
fn Convert(value: int) bool {
...
}
This pair is invalid.
Overload ranking
Resolution follows ordinary conversion ranking:
- exact parameter matches are preferred;
- the compiler does not invent conversions merely to prefer an inferior candidate;
- if several candidates remain equally valid, the call is ambiguous;
- a fixed-arity overload is preferred over an otherwise equally ranked variadic overload.
Named types preserve their identity during overload resolution:
type Percentage int range 0..100
fn Set(value: int) void {
}
fn Set(value: Percentage) void {
}
let percentage: Percentage := 50
Set(percentage) // Percentage overload
Set(50) // int overload
Generic Functions
Generic type parameters follow the function name:
fn Identity[T](value: T) T {
return value
}
Constraints use ::
fn Save[T: Serializable](value: T) Result[void, IOError] {
...
}
Multiple constraints use &:
fn Save[T: Serializable & Comparable](value: T) void {
...
}
Generic arguments may be inferred or supplied explicitly according to the generic rules.
let first := Identity(10)
let second := Identity[int](10)
Method-level generics
Methods may introduce additional generic parameters:
impl Stack[T] {
fn Map[U](mapper: fn(T) U) Stack[U] {
...
}
}
Generic instantiation and monomorphization are described in the Generics chapter.
Methods and Implicit Self
A concrete method is an ordinary fn declared inside an
impl block.
impl Counter {
fn Increment() void {
self.value += 1
}
fn Value() int {
return self.value
}
}
Instance methods receive compiler-provided implicit self.
Concrete method parameter lists do not declare ref self or
ref mut self.
Semantic analysis derives whether the receiver needs shared or mutable authority from the method body.
Ordinary instance methods do not consume the complete self value.
Whole-instance lifecycle termination belongs to free.
Static methods
impl Counter {
static fn Zero() Counter {
return Counter {
value: 0
}
}
}
A static fn has no receiver and cannot use self.
Interface receiver contracts
Interfaces have no method bodies from which receiver capability can be inferred, so interface syntax states that contract explicitly:
interface Resource {
fn Status() Status
mut fn Reset() void
static fn Parse(value: string) Result[Resource, ParseError]
}
| Interface form | Receiver contract |
|---|---|
fn | Shared / non-mutating receiver |
mut fn | Mutable / exclusive receiver |
static fn | No receiver |
Function Values
A resolved named function may be used as a function value.
fn IsPositive(value: int) bool {
return value > 0
}
let predicate: fn(int) bool := IsPositive
An overloaded function name must be resolved to one concrete callable before it becomes a runtime function value.
Callable types preserve more than parameter and return types. They also preserve:
- callable environment capability;
- borrowed parameter modes;
- consuming parameter modes;
- variadic shape;
- return type;
- other callable properties required by the type system.
Callable environment capability
fn(int) int
mut fn() int
-> fn() Resource
These describe authority over the callable value or closure environment:
| Callable type | Capability |
|---|---|
fn | Reusable shared callable |
mut fn | Callable requiring mutable environment authority |
-> fn | Consuming callable |
Callable capability is independent of parameter ownership:
fn(-> Resource) void
describes a reusable callable whose parameter is consuming, while:
-> fn() Resource
describes a callable value that is itself consumed by invocation.
Lambda and closure capture behavior is described in the lambda rules.
Result and Try
Functions may explicitly return Result[T, E]:
fn Load() Result[Data, IOError] {
...
}
Result, Ok, Err, try,
local handlers and propagation are part of Sec's Error Handling model.
The function signature still exposes the complete result type explicitly.
fn LoadConfiguration() Result[Config, ConfigError] {
let text := try ReadConfigFile()
return ParseConfig(text)
}
Error handling is not implicit exception behavior layered on top of an ordinary return type.
Discarding Call Results
A standalone call may discard an ordinary discardable non-void
temporary result.
Calculate()
Compiler-known must-use result categories cannot be silently ignored.
Examples include Result[T, E] and concurrency handles where
the owning rules require handling.
LoadConfiguration() // invalid when Result is ignored
Explicit discard is permitted only when the complete returned
value is itself legally discardable:
discard Calculate()
Discard never bypasses required cleanup, error handling, task responsibility or other must-use semantics.
Native Variadic Functions
Sec supports typed native variadic parameters:
fn Sum(values: ...int) int {
let mut total := 0
for value in values {
total += value
}
return total
}
The syntax is:
name: ...T
A function may declare at most one variadic parameter, and it must be last:
fn Log(level: LogLevel, parts: ...string) void {
...
}
Zero variadic arguments are valid:
Log(LogLevel.Info)
Every captured argument must satisfy the element type. Native Sec variadics are typed; they are not C-style heterogeneous varargs.
There is no consuming-variadic declaration form:
fn Consume(-> values: ...Resource) void {
}
This is invalid.
The Variadic Parameter Pack
Inside the function, a variadic parameter is a compiler-known ephemeral parameter pack.
It is not:
- an array;
- a
list; - a slice with a guaranteed representation;
- a heap allocation;
- a first-class storable collection;
- a C
va_list.
The pack supports read-only sequence operations:
values.Len
values[index]
for value in values {
...
}
The pack structure is read-only:
fn Process(values: ...int) void {
values[0] = 42 // invalid
}
The pack has invocation lifetime and cannot escape:
fn Invalid(values: ...int) ref int {
return ref values[0]
}
References backed by the pack cannot outlive the call.
Ownership of elements
Each supplied argument follows ordinary by-value semantics for the element type.
- copyable elements are copied into the call;
- move-only individually supplied elements transfer ownership into the call.
fn ConsumeAll(values: ...Resource) void {
...
}
ConsumeAll(<-first, <-second)
After successful call entry, the supplied move-only resources are owned by the call.
Individual move-only elements cannot later be moved back out of the pack:
fn Invalid(values: ...Resource) Resource {
return values[0]
}
This prevents partial-move state inside the compiler-created pack.
Spread Arguments
Ordinary arguments and spread arguments may be mixed when the destination accepts compatible variadic elements.
fn Write(values: ...byte) void {
...
}
let data: byte[] := ReadPacket()
Write(
0x01,
data...,
0xff,
)
Spread uses postfix .... A spread source is evaluated exactly once,
and all arguments and spread sources follow left-to-right evaluation order.
Runtime-length spread is valid for a variadic destination because the destination explicitly accepts runtime arity.
Spread itself is non-consuming. A spread is invalid when satisfying the call would require moving individual move-only elements out of the source collection.
A variadic pack may itself be re-spread when element-transfer rules permit:
fn Forward(values: ...int) void {
Other(values...)
}
The pack does not promise contiguous storage and does not expose a source-level pointer to its representation.
Recursion
Sec functions may be directly or mutually recursive:
fn IsEven(value: uint) bool {
if value == 0 {
return true
}
return IsOdd(value - 1)
}
fn IsOdd(value: uint) bool {
if value == 0 {
return false
}
return IsEven(value - 1)
}
Declaration order does not invalidate a recursive reference when both declarations are otherwise visible.
Recursion still participates in ordinary compiler analysis, including:
- ownership and borrowing;
- effects;
- stack and resource analysis;
- termination-related analysis where enabled.
Source Semantics and the ABI Boundary
Function semantics are defined by Sec before physical calling convention is chosen.
A by-value argument may physically be passed in registers, on the stack, indirectly, or in a split representation. Those choices do not change whether the source-language operation was a copy, move or borrow.
Likewise, a -> parameter does not require a unique machine calling
convention merely because its source-level ownership contract is consuming.
Native variadic packs intentionally have no source-visible fixed layout.
Exported ABI rules, foreign calling conventions, foreign varargs and callback adaptation are described separately in the ABI and FFI chapters.
Best Practices
- Keep function signatures explicit and self-describing.
- Use ordinary by-value parameters for ordinary ownership semantics.
- Use
reffor shared access without ownership transfer. - Use
ref mutfor exclusive mutable access without ownership transfer. - Use
->when unconditional consumption is part of the API contract. - Do not create overloads that differ only in consuming mode.
- Prefer one explicit named result type when several related values must be returned.
- Use native
...Tonly when variable arity is genuinely part of the API. - Keep variadic element types specific.
- Do not depend on variadic pack layout, contiguity or pointer identity.
- Keep foreign varargs behind explicit FFI boundaries.
- Let compiler analysis derive concrete method receiver capability rather than writing receiver boilerplate.
Summary
- Ordinary functions use
fn Name(parameters) ReturnType { ... }. - Every ordinary function has an explicit return type and a body.
- Functions returning no value use
void. - Parameters use
name: Typesyntax. - Owned by-value parameter bindings are mutable inside the callee.
- Ordinary by-value parameters copy or move according to the concrete type.
- A reusable move-only source uses
<-at a consuming call site. ref Tis a shared borrowed parameter.ref mut Tis an exclusive mutable borrowed parameter.-> name: Texplicitly requires consuming ownership transfer.- Overloads cannot differ only by consuming parameter mode.
- Arguments are evaluated left to right.
- Outer-call ownership transfer commits only after all arguments are successfully prepared.
- Functions return one value.
- Returning an owned value transfers it to the caller.
- The move marker is optional on a terminal
returnownership transfer. - Return type does not distinguish overloads.
- Exact overload matches are preferred over conversions.
- Generic functions and method-level generics are supported.
- Concrete instance methods receive implicit
self. - Concrete receiver capability is inferred from the method body.
- Function values preserve callable capability and parameter ownership modes.
Resultreturn types expose fallibility explicitly.- Must-use function results cannot be silently ignored.
- Native variadics use typed
name: ...Tsyntax. - A variadic parameter pack is ephemeral, read-only and not a first-class collection.
- Spread arguments may feed variadic parameters without changing spread into a consuming operation.
- Recursion is legal and remains subject to normal compiler analysis.
- ABI implementation choices do not change source-language function semantics.