Functions
Functions are the primary building blocks of executable behavior in Sec. A function groups statements into a reusable unit, defines which values it accepts, and declares which value it returns.
Functions are not required to belong to a class. Module-level functions are ordinary language
declarations. Methods use the same basic function syntax but are declared inside an
impl block and are described separately in the chapter on structs and implementation.
Function Declarations
A function declaration uses the following general form:
fn name(parameter: Type, parameter: Type) ReturnType {
statements
}
Example:
fn Add(a: int, b: int) int {
return a + b
}
The declaration contains five principal parts:
fnintroduces the function declaration.Addis the function name.(a: int, b: int)is the parameter list.intis the declared return type.- The block enclosed by braces is the function body.
The return type is currently required for every function declaration. A function that does
not return a value declares void explicitly.
Function Names
A function name identifies the function within its module or type-local implementation namespace. Function names follow the normal identifier rules of Sec.
fn CalculateSpeed(distance: Meter, duration: Second) Speed {
return distance / duration
}
Function names may be overloaded. Several functions may therefore share the same name if their parameter type signatures are different. Overloading is described later in this chapter.
Parameters
Parameters define the input values accepted by a function. Every parameter has a name and an explicit type.
name: Type
Example:
fn SetSpeed(speed: Speed) void {
return
}
Parameters use the same typed identifier syntax as struct fields and explicitly typed variable declarations. The parameter name appears first, followed by a colon and its type.
Each parameter type must refer to a known type. Basic types, named types, struct types, generic types and unit-bearing types may all be used as parameter types when valid in that context.
fn RegisterCustomer(id: CustomerID, name: string) void {
return
}
fn Move(distance: Meter, duration: Second) Speed {
return distance / duration
}
Parameter Lists
Parameters are separated by commas:
fn Add(a: int, b: int) int {
return a + b
}
A trailing comma is allowed:
fn CreateUser(
id: CustomerID,
name: string,
age: Age,
) User {
return User{
id: id,
name: name,
age: age,
}
}
Trailing commas are especially useful in multi-line declarations because adding or removing a parameter does not require modifying the preceding line.
Parameter names must be unique within one function declaration.
fn Invalid(a: int, a: int) int {
return a
} // Error: duplicate parameter "a".
Parameter Immutability
Function parameters are immutable local symbols by default. A parameter cannot be assigned a new value inside the function body.
fn Double(value: int) int {
value = value * 2 // Error: parameter value is immutable.
return value
}
When a function needs a mutable working value, it should declare a separate mutable local variable explicitly.
fn Double(value: int) int {
let mut result := value
result *= 2
return result
}
This preserves the original input value and makes mutation visible through the required
mut keyword.
Function Scope
Every function body creates a new lexical scope. Parameters and local variables declared in that scope are visible only within the function body and its nested blocks.
fn CalculateTotal(price: Money, quantity: int) Money {
let total := price * quantity
return total
}
The parameter names price and quantity, and the local variable
total, cease to exist when the function returns.
Nested blocks create child scopes according to the normal block rules. A name declared inside a nested block is not visible after that block ends.
Return Values
Return Types
Every function declaration currently includes an explicit return type after the closing parenthesis of the parameter list.
fn Add(a: int, b: int) int {
return a + b
}
The declared return type is part of the function signature and determines the type of every call expression that invokes the function.
Named types remain distinct in return positions. A function declared to return
Speed must return a value assignable to Speed, not merely a value with the
same underlying representation.
Return Statements
A return statement ends the current function and optionally produces a value:
return expression
Example:
fn Square(value: int) int {
return value * value
}
After a return statement executes, no later statement in the same path is executed. The returned expression must be compatible with the function's declared return type.
Void Functions
A function that does not return a value uses void as its return type.
fn Noop() void {
return
}
A void function may use return without an expression to leave the function early.
fn PrintPositive(value: int) void {
if value <= 0 {
return
}
Print(value)
}
A void function may also reach the end of its body without an explicit return statement. Returning a value from a void function is invalid.
Required Return
A non-void function must return a value. The compiler rejects a function whose body does not provide a valid return path.
fn Invalid() int {
} // Error: function Invalid must return int.
As control-flow analysis becomes more complete, all reachable execution paths must satisfy the return requirement. A return in only one conditional branch is not sufficient if another branch can reach the end of the function.
Return Type Checking
The value returned by a function must be assignable to its declared return type.
fn Valid() int {
return 42
}
Invalid:
fn Invalid() int {
return true
} // Error: function Invalid must return int, got bool.
The same rule applies to named types:
type Speed int
type Money int
fn CurrentSpeed() Speed {
let value: Money := 50
return value
} // Error: function CurrentSpeed must return Speed, got Money.
Function Calls
Calling a Function
A function is called by writing its name followed by a parenthesized argument list:
Name(argument, argument)
Example:
let result := Add(1, 2)
Arguments are evaluated and matched against the function's parameters in declaration order.
Argument Count
The number of arguments in a call must match the number of parameters in the selected function declaration.
fn Add(a: int, b: int) int {
return a + b
}
let value := Add(1, 2) // Valid.
Invalid:
let value := Add(1) // Error: missing argument.
let value := Add(1, 2, 3) // Error: too many arguments.
Argument Types
Every argument must be assignable to the corresponding parameter type.
fn SetSpeed(value: Speed) void {
return
}
let speed: Speed := 80
SetSpeed(speed)
Named type identity is preserved:
type Speed int
type Money int
let price: Money := 80
SetSpeed(price) // Error: expected Speed, got Money.
Untyped literals may be interpreted using the parameter context when the language's literal conversion rules permit it. Variables do not lose their established types during calls.
Call Expression Types
A function call expression has the declared return type of the selected function.
fn Add(a: int, b: int) int {
return a + b
}
let value := Add(1, 2)
The inferred type of value is int because Add returns
int.
fn CalculateSpeed(distance: Meter, duration: Second) Speed {
return distance / duration
}
let speed := CalculateSpeed(Meter(100), Second(10))
The inferred type of speed is Speed.
Function Overloading
Sec allows several functions in the same module scope to share a name. Such declarations are overloads. The compiler selects one overload from the types of the call arguments.
Overload Signatures
Overloads must have different parameter type signatures.
fn Print(value: int) void {
return
}
fn Print(value: string) void {
return
}
These declarations are valid because their parameter types differ.
Duplicate parameter signatures are invalid:
fn Convert(value: int) string {
return ""
}
fn Convert(value: int) bool {
return false
} // Error: duplicate overload signature Convert(int).
Return Types Do Not Distinguish Overloads
The return type is never used to select an overload. Call arguments must be sufficient to identify the function.
Therefore, these declarations conflict:
fn Parse(value: string) int {
return 0
}
fn Parse(value: string) bool {
return false
}
Both functions have the same parameter signature Parse(string). The expected type of
the surrounding expression cannot be used to choose between them.
Overload Resolution
Overload resolution examines the argument expressions and selects an overload whose parameters can accept those arguments.
Exact type matches are preferred over conversions.
fn Print(value: int) void {
return
}
fn Print(value: int64) void {
return
}
let value: int64 := 10
Print(value) // Selects Print(int64).
If no overload can accept the arguments, the compiler reports an argument error. If several overloads remain equally valid, the call is ambiguous.
Named Types and Overloads
Named types remain distinct during overload resolution.
type Percent int range 0..100
fn Set(value: int) void {
return
}
fn Set(value: Percent) void {
return
}
let percent: Percent := 50
Set(percent) // Selects Set(Percent).
Set(50) // Selects Set(int).
Set(Percent(50)) // Selects Set(Percent).
The compiler does not erase the semantic identity of Percent merely because its
underlying type is int.
Ambiguous Calls
A call is ambiguous when more than one overload matches with equal priority.
fn Print(value: int) void {
return
}
fn Print(value: int64) void {
return
}
Print(10)
Integer literals are first interpreted according to their natural or default type. If that does not produce a unique best match, the compiler reports an ambiguous call rather than choosing an overload arbitrarily.
An explicit conversion may be used to make the intended overload clear:
Print(int64(10))
Boolean Expressions
Comparison Operators
Comparison operators produce values of type bool:
==
!=
<
<=
>
>=
Example:
fn IsPositive(value: int) bool {
return value > 0
}
The operands must support the selected comparison. The result of every valid comparison is
always bool.
Logical Operators
The binary logical operators && and || require boolean operands and return bool.
fn IsInside(value: int, minimum: int, maximum: int) bool {
return value >= minimum && value <= maximum
}
The prefix operator ! also requires a boolean operand:
fn IsDisabled(enabled: bool) bool {
return !enabled
}
Invalid:
return !10 // Error: ! requires bool.
return 1 && 2 // Error: && requires bool operands.
Result-returning Functions
A function that may either produce a value or report a typed error returns
Result[T, E]. The first type argument is the success value type. The second is the error
type.
fn CalculateSpeed() Result[Speed, IOError] {
return Ok(Speed(50))
}
Returning Ok
Ok(expression) constructs the successful variant of a Result value.
fn ReadCount() Result[int, IOError] {
return Ok(123)
}
The expression contained by Ok must be assignable to the Result value type.
fn Invalid() Result[int, IOError] {
return Ok(IOError.InvalidValue)
} // Error: Ok value must be int, got IOError.
Returning Err
Err(expression) constructs the error variant of a Result value.
fn ReadCount() Result[int, IOError] {
return Err(IOError.InvalidValue)
}
The expression contained by Err must be assignable to the Result error type.
fn Invalid() Result[int, IOError] {
return Err(123)
} // Error: Err value must be IOError, got int.
Plain Returns Are Invalid
A Result-returning function must return either Ok(...) or Err(...). It may
not return an unwrapped value.
fn Invalid() Result[int, IOError] {
return 123
} // Error: Result-returning function must return Ok(...) or Err(...).
This requirement keeps success and failure explicit at every Result-producing return point.
The try Expression
The try operator extracts the success value from a Result expression or propagates
its error according to the surrounding function's error rules.
let speed := try CalculateSpeed()
If CalculateSpeed() has type Result[Speed, IOError], the type of the complete
try expression is Speed.
The expression following try must have a Result type.
let value := try 123
// Error: try requires Result expression.
try Context
A try expression is valid only where its error can be propagated or handled. In the normal propagation form, the surrounding function must return a compatible Result error type.
fn UseSpeed() Result[Speed, IOError] {
let speed := try CalculateSpeed()
return Ok(speed)
}
Invalid:
fn UseSpeed() Speed {
return try CalculateSpeed()
} // Error: cannot propagate IOError from function returning Speed.
Local try handlers are described in the dedicated error-handling chapter. This chapter only introduces the expression form used for direct propagation.
Result Validation
Result requires exactly two type arguments:
Result[ValueType, ErrorType]
Invalid:
Result[int]
Result[int, IOError, string]
Both type arguments must refer to known types. Ok and Err each require
exactly one expression.
Ok() // Invalid.
Ok(1, 2) // Invalid.
Err() // Invalid.
Err(a, b) // Invalid.
Function Values and Lambdas
Functions are not limited to named declarations. Sec also supports function values: statically typed values that can be assigned to variables, passed to other functions, returned from functions, stored in data structures, and called through ordinary call syntax.
A lambda is an anonymous function expression. Sec deliberately uses the same fn syntax
for named functions and lambdas. This avoids introducing a second, abbreviated function language
with different parameter, return, scope, or error-handling rules.
Function Types
A function type is written with parameter types followed by the return type:
fn(ParameterType, ParameterType) ReturnType
Examples:
fn(int) int
fn(int, int) bool
fn(string) void
fn() bool
Parameter names are not part of a function type. Only the number and order of parameter types, together with the return type, determine function type identity.
fn(left: int, right: int) bool
fn(a: int, b: int) bool
Both declarations have the function type:
fn(int, int) bool
The return type is part of the function type:
fn(int) int
fn(int) bool
These are different and incompatible function types.
Function Values
A variable may hold a value with a function type:
let operation: fn(int, int) int := fn(a: int, b: int) int {
return a + b
}
As with every variable declaration, let is required. The variable is immutable unless
mut is written explicitly.
let mut operation: fn(int) int := fn(value: int) int {
return value
}
operation = fn(value: int) int {
return value * 2
}
The replacement value must have the same complete function type.
Named functions may also be assigned as function values when a unique function is known:
fn IsPositive(value: int) bool {
return value > 0
}
let predicate: fn(int) bool := IsPositive
When a named function is overloaded, an explicit target function type may be required to select the intended overload.
fn Convert(value: int) string {
return ""
}
fn Convert(value: string) int {
return 0
}
let converter: fn(int) string := Convert
Without a target type, the overloaded name is ambiguous:
let converter := Convert
// Error: ambiguous function value Convert; explicit function type required.
Function values do not support semantic equality or ordering. Use of ==,
!=, <, <=, >, or >= with function values is invalid.
A function value is never null. Optional callable values use an explicit option type such as
Option[fn(int) bool].
Lambda Functions
A lambda is an anonymous function value:
fn(parameter: Type) ReturnType {
statements
}
Example:
let double := fn(value: int) int {
return value * 2
}
let result := double(10)
The relationship between named and anonymous functions is direct:
// Named function.
fn Add(a: int, b: int) int {
return a + b
}
// Anonymous function.
fn(a: int, b: int) int {
return a + b
}
The only syntactic difference is the missing function name.
No arrow-only shorthand is currently provided:
fn(value: int) int => value * 2 // Invalid.
value => value * 2 // Invalid.
Use an ordinary function body with an explicit return statement.
Lambda Parameters
Lambda parameters follow exactly the same rules as parameters of named functions. Each
parameter uses name: Type, parameters are comma-separated, and a trailing comma is
allowed.
let compare := fn(
left: int,
right: int,
) bool {
return left < right
}
- Parameter types are explicit.
- Parameters are immutable.
- Duplicate parameter names are invalid.
- Parameter names exist only inside the lambda body.
- Normal no-shadowing rules apply.
Omitted parameter types are not inferred from the target function type:
let predicate: fn(int) bool := fn(value) bool {
return value > 0
} // Invalid: parameter type is required.
Write:
let predicate: fn(int) bool := fn(value: int) bool {
return value > 0
}
Lambda Return Rules
The return type is currently required and is checked using the same rules as a named function.
Use void when the lambda does not return a value.
let identity := fn(value: int) int {
return value
}
let notify := fn(message: string) void {
Print(message)
return
}
A non-void lambda must return its declared type on every continuing path:
let invalid := fn() int {
} // Error: lambda must return int.
let invalid := fn() int {
return true
} // Error: lambda must return int, got bool.
A void lambda may use return without an expression, but may not return a value.
Result and try rules also apply normally. Error propagation occurs through the
lambda's declared return type, not through the function that created the lambda.
let load := fn(path: string) Result[Data, IOError] {
let data := try Read(path)
return Ok(data)
}
Calling Function Values
Any expression with a function type may be called:
operation(1, 2)
callbacks[index](value)
GetHandler()(event)
Calls through function values use the normal call rules:
- The argument count must match.
- Each argument must be assignable to the corresponding parameter type.
- The call expression has the function type's declared return type.
- A void call cannot be used as a value.
let operation := fn(a: int, b: int) int {
return a + b
}
operation(1) // Error: function value expects 2 arguments, got 1.
Passing Lambdas to Functions
Functions may accept function values as parameters:
fn Apply(value: int, operation: fn(int) int) int {
return operation(value)
}
fn main() int {
return Apply(
10,
fn(value: int) int {
return value * 2
},
)
}
The lambda's complete function type participates in normal argument checking and overload resolution.
Returning Lambdas
A function may return a function value:
fn SelectOperation(add: bool) fn(int, int) int {
if add {
return fn(a: int, b: int) int {
return a + b
}
}
return fn(a: int, b: int) int {
return a - b
}
}
Every returned function value must be assignable to the declared function return type. A lambda with different parameter or return types is invalid.
Non-capturing Lambdas
A non-capturing lambda uses only its parameters, local declarations, constants, module symbols, imported symbols, and named functions.
let double := fn(value: int) int {
return value * 2
}
Non-capturing lambdas do not require an environment and should not require heap allocation. They can normally be represented as direct callable code.
Local values from an enclosing function are not captured implicitly. Accessing one without an explicit capture clause is an error.
fn CreateMultiplier(factor: int) fn(int) int {
return fn(value: int) int {
return value * factor
}
} // Error: lambda cannot access outer variable factor without explicit capture.
Explicit Capture
A lambda explicitly captures enclosing local values with a capture(...) clause placed
immediately before fn.
capture(value, otherValue) fn(parameters) ReturnType {
statements
}
Example:
fn CreateMultiplier(factor: int) fn(int) int {
return capture(factor) fn(value: int) int {
return value * factor
}
}
A non-capturing anonymous function is usually called a lambda. A capturing lambda is also known as a closure. Both have the same source-level function type.
Capture entries are comma-separated, and a trailing comma is allowed:
capture(
minimum,
maximum,
) fn(value: int) bool {
return value in minimum..maximum
}
- Each captured name must refer to a visible enclosing local binding.
- A name may appear only once in the capture list.
- A capture name may not conflict with a parameter or local declaration.
- Module symbols and named functions do not require capture.
- Arbitrary expressions cannot appear directly in a capture list.
Capture by Value
A plain capture entry captures the value when the lambda expression is evaluated. The captured binding is immutable inside the lambda.
let mut factor: int := 2
let multiply := capture(factor) fn(value: int) int {
return value * factor
}
factor = 4
let result := multiply(10) // result is 20.
Changing the outer variable later does not change the already captured value. A mutable outer variable may therefore be captured, but the captured copy remains immutable.
let mut factor: int := 2
let operation := capture(factor) fn() void {
factor = 4
} // Error: captured value factor is immutable.
Copyable values may be copied into the closure environment. Capturing a non-copyable value may move ownership into the closure. The compiler must never clone such a value implicitly, and use of a moved outer value is invalid.
Capture Scope and Timing
Captures are evaluated from left to right, exactly once, when execution reaches the lambda expression.
let mut value: int := 1
let first := capture(value) fn() int {
return value
}
value = 2
let second := capture(value) fn() int {
return value
}
first() returns 1, while second() returns 2.
A lambda expression inside a loop is evaluated separately on every iteration that reaches it. Explicit capture therefore records the current iteration's value.
for value in values {
let operation := capture(value) fn() int {
return value
}
Use(operation)
}
Pattern bindings, parameters, and local declarations inside the lambda remain subject to normal lexical scope and no-shadowing rules.
Lambda Function Boundaries
A lambda creates a new function boundary. Its body has its own scope and control-flow context.
returnexits the lambda, not the enclosing function.trypropagates through the lambda's declared return type.breakandcontinuecannot target loops outside the lambda.- Parameters, captures, and local variables belong to the lambda scope.
for item in items {
let operation := fn() void {
break
}
} // Error: break is only valid inside a loop in the current function.
An unnamed lambda cannot directly refer to the variable being initialized by that lambda. Use a named function for direct recursion.
let factorial := fn(value: int) int {
return value * factorial(value - 1)
} // Invalid: factorial is not visible inside its own initializer.
Current Lambda Limitations
The initial lambda model intentionally leaves several advanced forms for later language work:
- No omitted parameter types.
- No expression-body or arrow shorthand.
- No implicit captures.
- No mutable captures.
- No reference captures.
- No generic lambdas.
- No directly recursive unnamed lambdas.
- No function-value equality or ordering.
- No hidden cloning or hidden heap allocation.
Capturing closures that escape their creation scope require a fully defined ownership model for their environment. An implementation that does not yet support such environments must reject them clearly rather than silently introducing unmanaged storage.
Common Errors
Duplicate Parameters
fn Bad(a: int, a: int) int {
return a
}
Each parameter name must be unique within the function.
Missing Return
fn Bad() int {
}
A non-void function must return a value of its declared type.
Wrong Return Type
fn Bad() int {
return true
}
The returned expression must be assignable to the declared return type.
Incorrect Result Return
fn Bad() Result[int, IOError] {
return 123
}
Result-returning functions must return Ok(...) or Err(...).
Wrong Argument Count
fn Add(a: int, b: int) int {
return a + b
}
let result := Add(1)
The number of arguments must match the selected declaration.
Wrong Argument Type
fn SetSpeed(value: Speed) void {
return
}
let price: Money := 50
SetSpeed(price)
Arguments must preserve named type identity and match the corresponding parameter types.
Complete Examples
Basic Function
fn Add(a: int, b: int) int {
return a + b
}
let x := Add(1, 2)
let y: int := Add(3, 4)
Named Types and Units
type Meter decimal<m>
type Second decimal<s>
type Speed decimal<m/s>
fn CalculateSpeed(distance: Meter, duration: Second) Speed {
return distance / duration
}
let distance: Meter := 100
let duration: Second := 10
let speed := CalculateSpeed(distance, duration)
Result and try
fn ReadDistance() Result[Meter, IOError] {
return Ok(Meter(100))
}
fn ReadDuration() Result[Second, IOError] {
return Ok(Second(10))
}
fn ReadSpeed() Result[Speed, IOError] {
let distance := try ReadDistance()
let duration := try ReadDuration()
let speed := CalculateSpeed(distance, duration)
return Ok(speed)
}
Summary of Rules
- Functions are declared with
fn. - Parameters use
name: Type. - Parameters are comma-separated.
- A trailing parameter comma is allowed.
- Every function currently declares an explicit return type.
voidis used when no value is returned.- Function bodies create lexical scopes.
- Parameters are immutable local symbols by default.
- Duplicate parameter names are invalid.
- Non-void functions must return a compatible value.
- Void functions may use
returnwithout an expression. - Function calls must match both argument count and parameter types.
- The type of a function call is the selected function's return type.
- Functions may be overloaded by parameter type signature.
- Return types never distinguish overloads.
- Exact overload matches are preferred over conversions.
- Named types remain distinct during overload resolution.
- Comparison operators return
bool. &&,||and!require boolean operands.Result[T, E]contains a success type and an error type.- Result-returning functions return
Ok(...)orErr(...). tryrequires a Result expression and produces its success type.- Function types use
fn(ParameterTypes) ReturnType. - A lambda is an anonymous
fnexpression. - Lambda parameter and return types are currently explicit.
- Lambdas and named functions follow the same call and return rules.
- Enclosing local variables are never captured implicitly.
capture(...)creates explicit immutable value captures.- Capture occurs when the lambda expression is evaluated.
- A lambda creates its own scope and function boundary.
- Function values do not support equality, ordering, or null.