Functions are a set of statements that are executable. They can take a number of inputs (known as parameters), and outputs (known as return/error parameters). By default, functions are able to run at compile-time (known as constexpr
).
1: | func {IDENTIFIER|OPERATOR} = {TEMPLATE_PARAMETER_PACK?} ( {PARAMETERS*} ) {ATTRIBUTES*} -> {TYPE} { {STATEMENTS*} } |
2: | func {IDENTIFIER|OPERATOR} = {TEMPLATE_PARAMETER_PACK?} ( {PARAMETERS*} ) {ATTRIBUTES*} -> ( {RETURN_PARAMETERS+} ) { {STATEMENTS*} } |
3: | func {IDENTIFIER|OPERATOR} = {TEMPLATE_PARAMETER_PACK?} ( {PARAMETERS*} ) {ATTRIBUTES*} -> {TYPE} < {TYPE} > { {STATEMENTS*} } |
4: | func {IDENTIFIER|OPERATOR} = {TEMPLATE_PARAMETER_PACK?} ( {PARAMETERS*} ) {ATTRIBUTES*} -> ( {RETURN_PARAMETERS+} ) < {ERROR_PARAMETERS*} > { {STATEMENTS*} } |
Syntaxes 1, and 3 create a function that has a single return value. A return type of Void
means that the function doesn't return anything. Syntaxes 2 and 4 have explicit return parameters, which allows returning of multiple values. Syntaxes 3 and 4 are for functions that may error.
The list of parameters, return parameters, and error parameters are delimited by a comma (,
). A comma may also go at the end of a parameter even if there is no following parameter(s).
To return a value from a function, the return
statement is used. Likewise, to error a function, the error
statement is used (although this may only happen if the declaration defines that the function errors).
Function overloading is allowed. This means that two functions may have the same name if they have a different set of template parameters and parameters.
1: | {IDENTIFIER}: {TYPE} {ATTRIBUTES*} |
2: | {IDENTIFIER}: {TYPE} {read|mut|in} {ATTRIBUTES*} |
3: | this |
4: | this {read|mut} |
Parameters are references values. The value the parameter references may not be changed.
All parameters have a qualifier. If a parameter does not have an explicit qualifier (syntaxes 1 and 3), then they are defaulted to the read
qualifier.
The read
parameter qualifier means that the parameter is read-only. They have a value category of concrete-const. If the parameter type trivially-sized
, trivially-copyable
, and trivially-destroyable
, the ABI guarantees that the parameter passing will be by copy (instead of by pointer).
The mut
parameter qualifier means that the parameter is mutable. If the parameter is not a this
parameter, it can only accept concrete
values. They have a value category of concrete-mutable
.
The in
parameter qualifier means that the parameter is mutable. They can only accept ephemeral
values. They have a value category of concrete-forwardable.
In general, in
parameters is passed by pointer. This means that if the argument given is a copy or a move, the copy or move is not actually done at the call-site, rather it is done at a operator forward assignment
. This allows for what is known as perfect forwarding - a pointer can be passed through from function to function through operator forward
and the copy or move operation is only made once actually necessary. If the value is trivially-copyable
or trivially-moveable
for a copy or move respectively, and it is trivially-sized
, the copy or move is made at the site of any forward.
1: | {IDENTIFIER}: {TYPE} |
Explicit return/error parameters are pointers to the target, which means that using them guarantees Return-Value-Optimization (RVO).
If a function explicit return parameters, a return statement in that function must be "return...;
". Likewise, if a function has explicit error parameters, an error statement in that function must be "error...;
". The "...
" is a separate token which means there may be white-space in-between it and the respective keyword, although this goes against the Panther style guide
.
Explicit return/error parameters begin as uninitialized
. If the function returns, all return parameters must be initialized
, and all initialized
error parameters will automatically have their operator delete
called on them. This works similarly for if the function errors.
If a function errors, it must be called through a try
expression or statement.
Erroring functions signal if the error or not through returning boolean value. If the function has a single return value, that value becomes an "out" parameter. The error return values themselves are in a packed struct stored right on the stack. This allows for a single pointer to be passed to the function as a parameter in the ABI for all the error values needed so as to lower the performance affect on the normal return path as much as possible.
(TODO)
(TODO)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36def std = @import("std"); // Create a function called "sum" which adds together "lhs" and "rhs" and returns that sum func sum = (lhs: Int, rhs: Int) -> Int { return lhs + rhs; } // Running sum at compile-time def SUM_OF_1_AND_2: Int = sum(1, 2); // Create a function called "divide" that divides "lhs" and "rhs" and returns that result // If an error occurs, print out a message. This function must be runtime (#rt) as std.println is runtime // If "rhs" is 0, error with no error return value func divide = (lhs: Int, rhs: Int) #rt -> Int <Void> { if(rhs == 0){ std.println("Cannot divide by 0"); error; } return lhs / rhs; } // Call the divide function // Use an explicit return parameter for the return func divide_handled = (lhs: Int, rhs: Int) #rt -> (output: Int) { output = try divide(lhs, rhs) else 0; return...; } // template version of the "sum" function func sum = <{T: Type}> (lhs: T, rhs: T) -> T { return lhs + rhs; }