This chapter describes what a routine is when a program calls it: the kinds of routine and their headings, what a call evaluates and hands over, the ways a parameter receives its argument, what a function answers and how its body builds the answer, routines that call themselves and each other, and routines nested in routines. How a routine is declared, its defaults and the scopes its names are looked up in are chapter 3, which this chapter builds on rather than repeats. Each construct names its rule in the grammar, says what it means, and quotes what the compiler answers when a source breaks the rule. Procedures and parameters teaches the same ground from the start.
Every example is a complete program the website’s gate compiles with the compiler as it is written here, and the output shown beneath it is the output the program prints.
The kinds of routine
A routine’s heading names its kind, its name, its parameters in parentheses and, for a function, the type it answers (routine-heading):
| Kind | Heading | What a call does |
|---|---|---|
| a procedure | procedure Report(count : int64); | runs the body and answers nothing (procedure-heading) |
| a function | function Area(side : float64) : float64; | runs the body and answers a value of the result type (function-heading) |
| a task | task Worker(id : int64); | runs beside the routine that schedules it, as chapter 11 describes |
| a generator | generator Readings() : stream of int64; | answers a stream of the values it emits, as chapter 11 describes |
| a callback | callback Visit(context : pointer Tally) : int32; | is called back by a C library, as callbacks teaches |
Any of them may end its heading with fails and an error domain, which states
the failures the routine hands to its caller, as
error domains teaches. Before the heading, exp
exports the routine (chapter 2),
heapless promises that it never uses the heap (heapless routines),
and elemental applies a body written for one value to every element of a
vector, a matrix or a tensor (chapter 3).
One name names one routine in a block: there is no second routine of the same name with other parameters, and a routine is told apart from another by its name alone.
| Written | Code | The compiler’s words |
|---|---|---|
a function without its result type, function F(); | 2020 | expected colon and return identifier for function, found semicolon |
a procedure with a result type, procedure P() : int64; | 2021 | unexpected return identifier for procedure, found identifier |
a second Show beside procedure Show(n : int64);, procedure Show(s : string); | 5019 | procedure identifier already declared: Show |
a routine named value, procedure value(); | 2095 | 'value' cannot name a routine: 'value (' always reads what the pointer in the parentheses points at, so no call of the routine could be written — name it 'Value' instead |
| a variable section behind a routine, in a block or in a routine | 2044 | a variable declaration cannot follow a function or procedure; move all of the block's constant, type, and variable declarations above its functions and procedures |
Calling a routine
A call names the routine and its arguments in parentheses, one for each parameter in the order of the heading, apart from the trailing parameters whose default chapter 3 lets a call leave out (actual-parameters). A procedure is called as a statement, and a function as an operand of an expression, wherever a value of its result type may stand, or as a statement, where its answer is dropped.
A call may also give a value by its parameter’s name, the name and a colon written before the value, as a record value names a field (actual-parameter). The values by position come first, and once a call names a value it names every value behind it. Each parameter takes one value, by position or by name, and a parameter the call leaves out takes its default wherever it stands, so a call can give a later parameter and leave an earlier one out:
y := Clip(x, high: 2.0);
y := Clip(high: 2.0, low: -1.0, x: v);The first call passes 0.0 for low, and the second gives all three
parameters their values in an order of its own. A parameter without a default
may also stand behind one with a default, as
chapter 3 shows: the
values by position fill the parameters from the left, so Pick(5, 2) writes
both of its parameters and Pick(b: 2) leaves a to its default. The names a call writes are
those of the routine’s heading, or for a routine imported from a library, a C
library among them, those its contract gives the parameters. A sum type’s case
takes its values by its fields’ names under the same rules,
CommandTurn(quarters: 1, right: True), as sum types
shows. An operation the language builds in, such as Length, has operands
without names, so its values are written by position.
A call evaluates its arguments from the left, as it writes them, named ones
too, each one before the routine is
entered, then hands each to its parameter as the parameter’s mode says, and
runs the routine’s body in a frame of its own: the routine’s parameters and
variables are new at every call, its variables at their start values, and they
end when it does. When the body ends, at its last statement or at a leave,
the call ends, and a function’s call answers its result.
Each argument has the type of its parameter, or a type that type admits as chapter 4 states, and a constant argument is a value its parameter’s type holds.
| Written | Code | The compiler’s words |
|---|---|---|
a string for an int64 parameter, Show("x"); | 5180 | incompatible parameter and argument data types at position 1 in call to Show: expected int64, got string |
an int64 variable for an int32 parameter, Show(big); | 5180 | incompatible parameter and argument data types at position 1 in call to Show: expected int32, got int64 |
a constant a uint8 parameter cannot hold, Show(300); | 5233 | constant value 300 is outside the domain 0..255 of 'uint8' |
a procedure’s call as a value, n := Hello(); | 5104 | 'Hello' names a procedure here and cannot stand as a value: a routine is invoked, never read as a variable — if a variable of this name lives in an outer scope, this declaration shadows it, so rename whichever one you did not mean |
a value by position behind a named one, Clip(high: 2.0, 3.0) | 5777 | argument 2 of the call to 'Clip' is written by position behind the named argument 'high': once a call names an argument, every argument behind it names its parameter, so write the parameter's name and ':' before it, or move it ahead of 'high' |
a name no parameter has, Clip(3.0, hgh: 2.0) | 5778 | 'hgh' names no parameter of 'Clip', whose parameters are 'x', 'low' and 'high': write the name of the parameter the value is for before ':' |
a parameter given twice, Clip(3.0, 1.0, low: 2.0) | 5779 | parameter 'low' of 'Clip' is given a value twice, by argument 2 and by argument 3: a call gives each parameter one value, so remove one of the two |
a parameter without a default left out, Clip(high: 2.0) | 5780 | the call to 'Clip' leaves out parameter 'x', which has no default: write its value by position in its place, or by its name, 'x:' and the value |
a name for an operation the language builds in, Length(text: "abc") | 5781 | 'text:' cannot name a value in the call to 'Length': 'Length' is an operation the language builds in, whose operands have no names, so its values are written by position: remove 'text:' and write the value in its place |
the arrow of another language, Clip(x, high => 2.0) | 1049 | '=>' is not an operator in Mica: a call names an argument with a colon, as in 'Clip(x, high: 2.0)' |
A call with too many or too few arguments, and a call without its parentheses, are refused as chapter 3 quotes.
How a parameter is handed over
A parameter receives its argument in one of these ways (parameter-group):
| Declared | The routine receives | The caller writes |
|---|---|---|
place : Point | its own copy of the value, a record or an array copied whole, which it may change while the caller’s value stays as it was | any value of the type |
const limit : int64 | the value to read, never to assign | any value of the type |
place : pointer Point | the address of the caller’s variable, which it reads and writes through | address spot |
numbers : array[low..high : int64] of int64 | a view of the caller’s array, read in place, with its bounds in low and high | the array |
numbers : pointer array[low..high : int64] of int64 | the same view, written through | address readings |
numbers : span of int64 | a view of the caller’s elements, read in place | the array |
A copy is the routine’s own: a routine that changes a value parameter changes
nothing the caller sees. A pointer parameter is the road by which a routine
writes a variable of its caller, and the caller grants it at the call with
address, so every call that may write a caller’s variable says so where it
stands, as value and pointer teaches. A field of a
record behind a pointer parameter is read and written by its name,
place.x := Target, and a scalar behind one with value, value counter.
const is a promise that the routine assigns nothing through the parameter. It
stands on a value of a scalar, record or array type and on a pointer, through
which the routine then writes nothing. A string is passed at the cost of two
words whatever its length, so const adds nothing to it and the compiler
refuses the word there.
An array parameter written with two bound names is a conformant array: it
takes an array of any bounds whose index and element types match, and the
routine reads the caller’s bounds in the two names. A span takes the elements
of an array, a dynamic array or another span. Both read the caller’s storage in
place and never copy it, so the routine writes through neither, and a
conformant array that the routine writes is declared behind pointer, which
const does not join. Conformant arrays and
spans teach both views.
A call hands a place that a parameter can write to that parameter alone: two
arguments of one call may not reach one place when the parameter receiving
either of them can write it. A pointer parameter can write what it reaches, a
borrowed pointer among them, unless const stands before it; a value
parameter receives a copy taken before the call and never counts; and two
elements of one array are two places only when their indices are two different
constants. Ownership and borrowing
teaches the rule with the way out of each refusal.
{
Parameters - the ways a routine receives its arguments: a copy, a read-only value, an address and an array view.
'Moved' changes its own copy of the point, so the caller's point keeps its place, and 'MoveTo' writes the caller's
point through the address it was handed. 'Largest' reads an array of any bounds in place, its bounds arriving in
'low' and 'high', and 'Scale' writes through the same view behind 'pointer', which the caller grants with 'address'.
The arguments of every call are evaluated from the left before the routine is entered.
}
program Parameters;
imp
WriteLn : std;
const
CopyLine = "Moved answers x = %lf, and the caller's point is still at x = %lf";
AddressLine = "MoveTo wrote x = %lf through the address";
LargestLine = "the largest reading is %lld";
ScaledLine = "scaled by %lld, the readings are %lld %lld %lld";
Target = 9.0;
Factor : int64 = 10;
type
Point = record
x, y : float64;
end;
Readings = array[1..3] of int64;
var
spot, moved : Point;
readings : Readings := (4, 16, 9);
{ The point moved to 'Target', computed in the routine's own copy of its parameter. }
function Moved(place : Point) : Point;
begin
place.x := Target;
Moved := place;
end;
{ The caller's point moved to 'Target', written through the address the parameter holds. }
procedure MoveTo(place : pointer Point);
begin
place.x := Target;
end;
{ The largest of an array's elements, read in place whatever its bounds. }
function Largest(numbers : array[low..high : int64] of int64) : int64;
var
index : int64;
begin
Largest := numbers[low];
for index := low + 1 to high do
if numbers[index] > Largest then
Largest := numbers[index];
end;
{ Every element of an array multiplied by 'factor', written through the view behind 'pointer'. }
procedure Scale(numbers : pointer array[low..high : int64] of int64, const factor : int64);
var
index : int64;
begin
for index := low to high do
numbers[index] := numbers[index] * factor;
end;
begin
moved := Moved(spot);
WriteLn(CopyLine, moved.x, spot.x);
MoveTo(address spot);
WriteLn(AddressLine, spot.x);
WriteLn(LargestLine, Largest(readings));
Scale(address readings, Factor);
WriteLn(ScaledLine, Factor, readings[1], readings[2], readings[3]);
end.It prints:
Moved answers x = 9.000000, and the caller's point is still at x = 0.000000
MoveTo wrote x = 9.000000 through the address
the largest reading is 16
scaled by 10, the readings are 40 160 90| Written | Code | The compiler’s words |
|---|---|---|
a variable passed for a pointer parameter without address, Bump(n); | 5180 | incompatible parameter and argument data types at position 1 in call to Bump: expected pointer int64, got int64 |
the address of a literal, Bump(address 5); | 5120 | memory operation 'address of' requires an identifier or field selection operand, found literal |
a write through a const pointer, value p := 3; | 5146 | the parameter 'p' is declared 'const', so the routine cannot assign it: drop 'const' to give the routine a copy of its own to change, or assign the value to a variable of the routine |
const on a string, const name : string | 5543 | parameter 'name' is a string, a two-word descriptor already passed at the cheapest cost: write 'name : string' — the constant mode admits a pointer or a plain value only |
an element of a conformant array assigned, a[lo] := 1; | 5146 | the parameter 'a' is a view the routine reads in place, so the routine cannot assign it: declare it 'pointer array[lo..hi : int64] of int64' to write through it, or copy it into a variable of the routine |
const before the writable view, const a : pointer array[lo..hi : int64] of int64 | 2060 | the writable view 'a' cannot be constant: 'pointer array[..]' exists to write through, so drop 'const' or drop 'pointer' |
one counter for two pointer parameters, Twice(address a, address a); | 5784 | the call to 'Twice' reaches 'a' through parameter 'first', which can write it, and through parameter 'second', which can write it, and a place one parameter can write reaches the routine through that parameter alone: hand 'a' to one of the two, and give the other a place of its own |
two elements chosen by computed indices, Swap(address row[first], address row[last]); | 5785 | the call to 'Swap' reaches two elements of 'row' through parameter 'first', which can write one of them, and through parameter 'second', which can write one of them, and their indices are not two different constants, so the compiler cannot tell the two elements apart: hand 'Swap' one 'address row' and the two indices, and let it select both elements |
Results
Inside a function, its name is a variable of the result type
(function-heading).
It starts at the type’s start value, the body reads and writes it as any
variable, a field or an element of it included, and the call answers the value
it holds when the body ends. A function whose body never assigns its name
answers the start value, zero for a number. leave ends the body where it
stands, and leave when where its condition holds, so a function that has
found its answer assigns it and leaves.
A function answers a value of any type a variable may have: a scalar, a
string, a record, an array, a vector or a matrix. The call is an operand like
any other, so a selector may follow it, Squares()[2], and a record or an
array answered is copied into the variable it is assigned to.
{
Results - what a function answers: a record, an array and a string, built through the function's name.
Inside a function its name is a variable of the result type, so 'Midpoint' assigns the fields of its result one by
one and 'Squares' its elements. A call is an operand like any other, and a selector may follow it. 'Find' answers
the value its name holds when 'leave' ends it, and minus one when no square matches.
}
program Results;
imp
WriteLn : std;
const
MidpointLine = "the midpoint is at %lf, %lf";
SquaresLine = "the squares are %lld %lld %lld, and the last one alone is %lld";
GreetingLine = "%ls";
FindLine = "the root of 49 is %lld, and 50 has none: %lld";
Salutation = "hello, ";
Name = "Ada";
NotFound = -1;
Searched = 10;
type
Point = record
x, y : float64;
end;
Triple = array[0..2] of int64;
var
middle : Point;
squares : Triple;
{ The point halfway between two points, built field by field in the function's result. }
function Midpoint(const a : Point, const b : Point) : Point;
begin
Midpoint.x := (a.x + b.x) / 2.0;
Midpoint.y := (a.y + b.y) / 2.0;
end;
{ The squares of 0, 1 and 2, built element by element in the function's result. }
function Squares() : Triple;
var
index : int64;
begin
for index := 0 to 2 do
Squares[index] := index * index;
end;
{ A greeting for a name, joined into a new string. }
function Greeting(name : string) : string;
begin
Greeting := Salutation + name;
end;
{ The number whose square is 'target', or minus one when no number up to 'Searched' is. }
function Find(const target : int64) : int64;
var
root : int64;
begin
Find := NotFound;
for root := 1 to Searched do
if root * root = target then
begin
Find := root;
leave;
end;
end;
begin
middle := Midpoint((x: 1.0, y: 2.0), (x: 3.0, y: 6.0));
WriteLn(MidpointLine, middle.x, middle.y);
squares := Squares();
WriteLn(SquaresLine, squares[0], squares[1], squares[2], Squares()[2]);
WriteLn(GreetingLine, Greeting(Name));
WriteLn(FindLine, Find(49), Find(50));
end.It prints:
the midpoint is at 2.000000, 4.000000
the squares are 0 1 4, and the last one alone is 4
hello, Ada
the root of 49 is 7, and 50 has none: -1Recursion
A routine may call itself, and two routines may call each other, since every routine of a block sees every routine beside it, above or below, as chapter 3 states. Each call has a frame of its own, so every level of a recursion keeps its own parameters and variables.
{
Recursion - a function that calls itself, and two functions that call each other.
'Factorial' calls itself until the number reaches one. 'IsEven' and 'IsOdd' call each other, which a block allows
because every routine of a block sees every routine beside it, above or below. Each call has a frame of its own,
so every level keeps its own 'number'.
}
program Recursion;
imp
WriteLn : std;
const
FactorialLine = "the factorial of %lld is %lld";
EvenLine = "%lld is even";
OddLine = "%lld is odd";
Number : int64 = 10;
Tested : int64 = 7;
{ The product of the numbers from 1 to 'number'. }
function Factorial(const number : int64) : int64;
begin
if number <= 1 then
Factorial := 1
else
Factorial := number * Factorial(number - 1);
end;
{ Whether a number that is not negative is even, asked of the number below it. }
function IsEven(const number : int64) : bool;
begin
if number = 0 then
IsEven := True
else
IsEven := IsOdd(number - 1);
end;
{ Whether a number that is not negative is odd, asked of the number below it. }
function IsOdd(const number : int64) : bool;
begin
if number = 0 then
IsOdd := False
else
IsOdd := IsEven(number - 1);
end;
begin
WriteLn(FactorialLine, Number, Factorial(Number));
if IsEven(Tested) then
WriteLn(EvenLine, Tested)
else
WriteLn(OddLine, Tested);
end.It prints:
the factorial of 10 is 3628800
7 is oddA recursion runs as deep as the stack holds its frames. One that goes deeper
stops the program with a trap report whose first line names the reason,
Mica runtime failure: reason=stack_overflow (21), and whose next lines name
the file, the line and the column of the call and show its source line, as
the traps describe. A program that needs a deeper stack
asks for it with --stack-size, which the command line
lists.
Nested routines
A routine’s block may declare routines of its own behind its sections, and such a routine reads and writes the parameters and variables of every routine around it, is called by its routine’s statements alone, and nests at most sixteen deep, as chapter 3 states with its refusals. A nested routine’s call reaches the frame of the routine around it at that call, so a nested routine called by a recursion reads the variables of the level that called it.