Limbo Basics
- Inferno Business Unit
Lucent Technologies Inc
Revised April 2000, Vita Nuova Limited
Getting Started
This chapter introduces Inferno¿s programming language, Limbo, particularly for those new to Limbo.
This section dissects a simple Limbo program that illustrates the basics of every Limbo program. That is followed by the
Language Basics
section which delves into the basics of the Limbo language itself. It covers the primary aspects of the language and includes many Limbo program samples.
For a formal Limbo language definition, see Appendix A,
The Limbo Language Definition.
1 A Limbo Program
To illustrate the Limbo language, here is a simple
example of a Limbo program. This is a simple greeting program (a twist on the venerable ``Hello, World!''). It illustrates the key parts of a basic Limbo program.
Following the line-by-line program description, use Inferno¿s Limbo compiler to compile the example and then run the program from an Inferno shell (command line).
<InfernoProgramCaption>
greet.b
<IMAGE xml:link="simple" href="limbo_basics-2.jpg" show="embed" actuate="auto"/>
1 implement Command;
2
3 include "sys.m";
4 sys: Sys;
5 include "draw.m";
6
7 Command: module
8 {
9 init: fn(nil: ref Draw->Context, argv: list of string);
10 };
11
12 init(nil: ref Draw->Context, argv: list of string)
13 {
14 sys = load Sys Sys->PATH;
15 # check for command-line arguments
16 if(tl argv != nil)
17 sys->print("Hello, %s!\n", hd tl argv);
18 else
19 sys->print("Hello, World!\n");
20 }
(The line numbers are for reference only.)
Line 1 says that this file is the
implementation of the
Command
module. In this simple example, as is common with simple Limbo programs, the interface and implementation are in the same source file. The file name does not need to match the module name or the implementation name, although conventionally they do. In this example, implementation of
Command
is a special interface for modules (programs) launched at the Inferno shell.
Lines 3 and 5 include the
interface declarations of two modules that the module uses. Line 3 includes the interface to the System Module
(sys.m),
and Line 5 includes the interface to the Draw Module
(draw.m).
Limbo does not have a preprocessor, as does C. Therefore,
include
is a keyword, not a preprocessor directive. It is analogous to C¿s
#include
directive, however. In Limbo, the file name following the
include
keyword is always enclosed in double quotes ("mod.m").
Line 4 declares the variable
sys
to be of type
Sys
(Limbo is case-sensitive). The
Sys
type is a module type ,
as declared in the
sys.m
file:
Sys: module
{
...
};
The variable
sys
is declared to be a handle to the functions and data of the
Sys
module. At this point, however, its value is
nil.
Lines 7 to 10 is the declaration of the
Command
module that we stated was implemented in this file in Line 1. One function,
init,
is declared for this module. This function has two arguments, a
ref Draw->Context
and a
list of string.
The
init
function
and the two arguments are required of all programs that are invoked by the Inferno shell command line.
It is somewhat analogous to the
main
function in C-it specifies where execution starts in the module and captures the graphics context and command-line arguments, if any.
The
ref Draw->Context
is used to grab the
context of the display and is defined in
draw.m,
hence the reason that file was included (Line 5). This argument is required even if the program does not do any graphics. If that is the case, as with our example, the argument can be named
nil.
The
list of string
is a
list of the arguments passed to this module via the command line. Conventionally this is named
argv.
If the program does not expect any arguments passed from the command line, it can be named
nil.
Lines 12 to 20 contain the definition of the
init
function, or the statements that will be executed.
Earlier, in Line 4, we mentioned that the initial value of
sys
was
nil.
In Line 14, via the
load
keyword ,
we create a reference to the
Sys
module implementation, assigned to
sys.
This connects the program with the
Sys
library module and gives the program access to the functions and data implemented in the
Sys
module.
Line 15 is a comment. Comments in Limbo start with
#
and extend to the end of the line. Multi-line comments require a
#
at the beginning of each line.
Lines 16 to 19 print the message using the command line argument passed to
init
via
argv.
The second argument of the
init
function is a
list of string.
Lists
in Limbo are a group of sequential, ordered elements. You move through the elements of a
list
using the
hd
(head) and
tl
(tail) operators. The first element, the head of the
list,
of
argv
is the program name. Since we are only interested in the remainder of the
list,
the tail, we check to see if this is
nil,
which is a keyword that means it contains nothing. If it is not
nil
(has some value) then we print the message and the head of the tail (the second element in the
list).
If there are no arguments, then we print the
Hello, World!
message.
2 Compiling a Limbo Program
At the Inferno shell prompt, invoke the Limbo compiler
with the
limbo
command. For example:
inferno$ limbo greet.b
inferno$
If there are no errors, the Limbo compiler creates the executable file, called a Dis file. The compiler
gives the file the
.dis
extension ,
for example
greet.dis.
See
Writing Limbo Appliations
in this volume
for more details about Limbo file name conventions and compiling Limbo programs. Also, see the Inferno Reference Manual manual page entry
limbo(1)
for more information about the compiler options.
3 Running a Limbo Program
To run the program,
simply type the name of the executable file, excluding the
.dis
extension, at the Inferno shell prompt. For example:
inferno$ greet Reader
Hello, Reader!
inferno$
Any command line arguments, in the above example
Reader,
are passed by Inferno to the
init
function as elements in the
list of string
parameter. The program name itself is passed as the first element in the
list of string
parameter.
4 Language Basics
This section covers:
- *
-
Keywords
- *
-
Operators
- *
-
DataTypes
- *
-
ProgramControl
- *
-
Functions
For a formal Limbo language definition, see
The Limbo Language Definition.
5 Keywords
Limbo includes a small, yet complete, set of reserved words.
The following identifiers are reserved for use as keywords and may not be used otherwise:
adt do len self
alt else list spawn
array exit load string
big fn module tagof
break for nil tl
byte hd of to
case if or type
chan implement pick while
con import real
continue include ref
cyclic int return
6 Operators
Limbo includes several built-in operators. Limbo defines 3 classes of operators: unary, binary, and assignment.
6.1 Unary Operators
Table 1 lists Limbo¿s unary operators.
Table 1. Unary operators
6.2 Binary Operators
Table 2 lists Limbo¿s binary operators, in order of
precedence.
Table 2. Binary operators
6.3 Assignment and Declaration Operators
Table 3 lists Limbo¿s assignment and declaration operators.
Table 3. Assignment Operators
7 Data Types
There are several
built-in data types in Limbo, in two categories:
- *
-
Primitive types: byte, integer, big, real, and string
- *
-
Reference types: array, list, and tuple
In addition to these types, you can define your own type through an abstract data type, an ADT. These can be instantiated as value or reference types.
7.1 Primitive Types
The primitive types in Limbo have fixed definitions, that is, they are of the same size for all platforms. This is a requirement for
portability. Table 4 shows these data types, their range, and meaning.
Table 4. Primitive data types
7.1.1 Strings
String constants in Limbo, enclosed in double quotes, represent text. For example:
s := "Inferno";
This declares
s
to be a
string
and assigns it the value
Inferno.
Inferno does not use the ASCII character set, but rather the
Unicode character set, so a
string
type in Limbo is a vector (array) of Unicode characters. Each Unicode character is 16 bits.
There is no special ``character'' type in Limbo. A single character is enclosed in single quotes and is represented by its integer value. For example:
c := 'I';
This statement delcares
c
to be the integer value of the letter
I,
which is 73.
You can extract a range from a string, called a slice. See
Array Slices
later in this section for more information.
7.1.2 Constants
Identifiers that have constant values of primitive type can be declared using the
con
keyword .
For example:
data: con "/data/user.dat"; # string constant
block: con 512; # int constant
pi: con 3.1415926535897932; # real constant
7.1.2.1 Constant Enumerations
You can use the special
iota
value with the
con
keyword to declare a constant value one greater than the previous value to a group of identifiers. It is similar to enumerations in C.
For example:
Sun, Mon, Tue, Wed, Thu, Fri, Sat: con iota;
This statement declares
Sun
to be a constant value of
0,
Mon
to be
1,
Tue
to be
2,
and so on.
You can use other operators with
iota
to change the starting value and increment values. The most commonly used operators are addition
(+),
multiplication
(*),
and bitwise shift
(<<
and
>>)
operators.
For example:
Ten, Eleven, Twelve: con iota+10;
This sets the starting value to 10 and increases the subsequent values by one.
You can use the multiplication operator (*) to change the increment value. For example:
Zero, Five, Ten, Fifteen: con iota*5;
You can also use the bitwise shift operators. For example:
Two, Four, Eight, Sixteen, ThirtyTwo: con 2<<iota;
There are many variations that can be produced using a combination of operators.
7.2 Reference Types
Limbo reference types are similar to pointers in other languages, such as C or C++.
The primary difference between Limbo reference types and pointers is that Limbo does not allow mathematical manipulation of references. This provides for ``pointer safety''
within Limbo, since a reference can never point to an illegal address.
It can have a
nil
value, however.
7.2.1 A nil Value
Reference types can be assigned to the special value represented by the keyword
nil.
This removes the reference and its associated storage. If it is the last reference to the data, its resources are automatically and immediately reclaimed by the garbage collector .
At declaration, reference types do not have any associated storage; their value is
nil.
A value must be assigned to it before the reference type can be used.
7.2.2 Arrays
Arrays are made up of ordered, indexed, homogenous data. The index begins at 0.
<IMAGE xml:link="simple" href="limbo_basics-5.jpg" show="embed" actuate="auto"/>
<FigCaption>
<IMAGE xml:link="simple" href="limbo_basics-6.jpg" show="embed" actuate="auto"/>
Array Elements
A basic array declaration looks like this:
buf : array of byte;
This declares the variable
buf
to be of type
array of byte.
The constructor to allocate the space for the array looks like this:
array[80] of byte
This expression creates a new byte array with 80 elements. To assign
the storage of this array to a variable, use the assignment operator:
buf = array[80] of byte;
Arrays can be declared and, if at the top level, initialized to 0 (zero), in one statement using the combined
:=
(colon equals) operator.
buf := array[80] of byte;
The following prints a number and its square:
count := array[5] of int;
i := 0;
do {
count[i] = i;
sys->print("%dt%d\n", i, count[i]*i++);
} while(i < 5);
The output looks like this:
0 0
1 1
2 4
3 9
4 16
7.2.2.1 Array Slices
A slice is a consecutive range of elements within an array or string (in Limbo, strings are arrays of Unicode characters). The range is specified as an index within brackets. The range specifies a start value and an end value separated by a colon. It is inclusive of the start value and exclusive of the end value.
For example:
s := "Inferno";
<IMAGE xml:link="simple" href="limbo_basics-7.jpg" show="embed" actuate="auto"/>
<FigCaption>
<IMAGE xml:link="simple" href="limbo_basics-6.jpg" show="embed" actuate="auto"/>
Slice Elements
A slice of this string,
s,
from the 2nd to the 6th element:
s2 := s[2:6];
The value of
s2
is the string
fern.
7.2.3 Lists
Lists are made up of ordered, non-indexed, homogenous elements. The first element in the list is the head, the remaining elements make up the tail.
<IMAGE xml:link="simple" href="limbo_basics-8.jpg" show="embed" actuate="auto"/>
<FigCaption>
<IMAGE xml:link="simple" href="limbo_basics-6.jpg" show="embed" actuate="auto"/>
List Elements
For example:
line: list of string;
This
declares the identifier
line
to be of type
list of string.
The elements in
line
are
string
types.
List elements are accessed, modified, and manipulated using list operators. Table 5 defines the list operators.
Table 5. List operators
7.2.3.1 List constructor (::)
To construct a list, use
::,
the list constructor operator, such as:
line = "First" :: line;
This is similar to pushing an element on to a stack. The list constructor is right associative. The right operand is a single element and must have the same type as the elements of the list. The list identifier is the left operand.
An alternative constructor
list of
is similar to the array constructor
array of.
For example:
triple := list of {1, 2, 3};
which creates a list of the given values.
7.2.3.2 List head (hd)
The list head is a single element and has the same type as the elements of the list. To extract the head of the list, use the
hd
operator. This does not create a new list or change the original list.
For example, using the
line
list created above:
sys->print("The list head is: %s", hd line);
This prints:
The list head is: First
7.2.3.3 List tail (tl)
The list tail is itself a list of the same type as the original list. Using the
tl
operator and assigning its value back to the original list removes the first element from the list, such as:
line = tl line;
A new list can be created by assigning the tail of one list to another, such as:
args := tl line;
7.2.4 Tuples
A tuple is a parenthesized, sequential list of data types handled as a single object.
For example:
retcode := (0, "Success");
This creates a tuple,
retcode,
that stores an integer and a string.
A tuple is unpacked through assignment. For example:
err: int;
msg: string;
(err, msg) = retcode;
This assigns
0
to
err
and the string
Success
to
msg.
Using declare-and-unpack shorthand, the same unpacking could be done in one statement:
(err, msg) := retcode;
This declares
err
and
msg
and assigns them the values from
retcode.
Use the
nil
keyword to ignore part of a tuple. For example:
(err, nil) = retcode;
This assigns the integer value from
retcode,
but ignores the string value.
7.3 Abstract Data Types (ADT)
In Limbo an abstract data type, ADT, is a user-defined type that encapsulates data and functions into a named type. It is similar to a C
struct,
but can include functions, like a C++
class.
Like Limbo modules, however, an ADT is final and does not have inheritence or polymorphism.
7.3.1 Declaring an ADT
An ADT declaration has the general form:
identifier: adt {
adt-member-list
};
ADT member declarations are much the same as data and function declarations elsewhere in a Limbo program.
For example, the following is a declaration of a simple
Inventory
ADT:
Inventory: adt {
id: string;
onhand: int;
cost: real;
value: fn(item: Inventory): real;
};
This declaration contains three data members and one member function. Following the declaration,
Inventory
is a type, like
int
or
real.
Typically, ADT declarations are in a module file
(.m).
ADT member function definitions typically are in the implemenation file
(.b).
ADT member function definitions are the same as regular Limbo function defintions, with the addition of the ADT qualifier in the name using the dot operator
(.).
For example:
Inventory.value(item: Inventory): real
{
return real(item.onhand) * item.cost;
}
7.3.2 Instantiating an ADT
Instantiating an ADT is declaring a variable to be of the ADT type. For example:
part1: Inventory;
This declares
part1
to be of type
Inventory.
7.3.3 Accessing ADT Members
The members of the ADT are accessed using the dot notation (
.)
and can be treated like any other variable or function.
For example:
part1.id = "Widget";
part1.onhand = 250;
part1.cost = 4.23;
This assigns values to the
id,
onhand
and
cost
members of
part1.
Calls to ADT member functions follow a similar pattern, for example:
sys->print("Value On Hand: $%5.2f\n", part1.value(part1));
Using the values assigned above, this statement prints:
Value On Hand: $1057.50
In this example, in order for the member function to operate on the member data, the instance of the ADT must be explicitly passed as a argument to the function. Passing of the ADT instance as an implicit argument can be done using the keyword
self.
7.3.4 Implicit Argument: self
Changing the declaration of the
Inventory
member function to use the keyword
self
passes an instance of the ADT as an implicit argument. For example, note the addition of the keyword
self
to the
value
function declaration and definition:
Inventory: adt {
id: string;
onhand: int;
cost: real;
value: fn(item: self Inventory): real;
};
Inventory.value(item: self Inventory): real
{
return real(item.onhand) * item.cost;
}
The addition of
self
to the member function declarations and definitions enables the calling of the member functions without explicitly passing the ADT instance. The following reflects the change to the statement that calls this function:
sys->print("Value On Hand: $%5.2f\n", part1.value());
Using the values assigned earlier, the statement prints:
Value On Hand: $1057.50
The keyword
self
enables similar implicit argument functionality as the
this
keyword used in C++ and Java.
7.3.5 Ref ADT
As stated earlier, an ADT can be instantiated as a value type or a reference type. The ADTs that we have seen thus far have been instantiated as value ADTs. If an ADT is to be a reference type, called a ref ADT, it must be instantiated with the
ref
keyword.
For example, to instantiate the
Inventory
ADT as a reference type:
part2:= ref Inventory;
This creates a new
Inventory
and places a reference to it in
part2.
The
part2
instance can be used like a regular ADT. For example, assigning values to the member data is exactly the same:
part2.id = "Whatsit";
part2.onhand = 1000;
part2.cost = 0.17;
It is important to note that
part2
has a different type than
part.
This means that
part2
cannot be assigned to an
Inventory
variable or passed directly to member functions of the current
Inventory
ADT. For example, the following statements would generate compile-time errors:
part2 = part1; # type clash error, won¿t compile
part2.value(); # argument type mismatch, won¿t compile
To access a value from a ref ADT, use the asterisk (*) prefix operator:
part1 = *part2; # correct
(*part2).value(); # correct
ADT member functions can accept ref ADT types. This requires the addition of the
ref
keyword to the member function declaration:
clear: fn(item: self ref Inventory);
The member function definition looks like this:
Inventory.clear(item: self ref Inventory)
{
item.onhand = 0;
}
7.3.6 Ref ADT vs. Value ADT
The primary advantage of ref ADT is that it can be passed between functions without copying, making it more efficient, especially for a large ADT. Member functions that accept a ref ADT can overwrite their argument¿s contents. This cannot be done by member functions receiving a value ADT.
However, the interface to a ref ADT can become more obscure, primarily with the issue of being able to overwrite values. The interface to a value ADT is more straightfoward, making it more natural for most circumstances.
7.3.7 Pick ADT
A Limbo pick ADT is similar to a
union
in C. It enables separate instances of an ADT to hold data elements of different types. In this way, a single ADT declaration can instantiate ADTs of different types. Pick ADTs can have common data and function members and data members that are specific to the ADT instance.
There are two parts to a pick ADT: the pick ADT declaration, and the
pick
statement where you access the elements in the pick ADT.
7.3.7.1 The pick Declaration
The general form of the pick ADT declaration is:
identifier: adt {
...
pick {
tag-identifier =>
declaration;
...
tag-identifier =>
declaration;
...
}
};
Each variant has a
tag-identifier,
local to the
adt,
which introduces a set of declarations.
For example:
Apick: adt {
str: string;
pick {
String =>
val: string; # string element
Int =>
val: int; # int element
Real =>
val: real; # real element
Nothing =>
; # no elements
}
};
The common member of this ADT is
str
of type
string.
The pick elements are
String,
Int,
and
Real.
These specify three variations of the
Apick
ADT.
If a pick ADT has common data members, the pick block must be the last data declaration
in the
adt.
Member functions can follow the pick block, however.
A pick ADT must be instantiated as a ref ADT. The specific element of the pick must be specified. For example:
r := ref Apick.Real;
When you assign values to the data members of the ADT, use the variables declared via the pick structure:
r.str = "Pi";
r.val = 3.141593;
You can also declare and initialize an instance of the ADT. For example:
s := ref Apick.String("Greeting", "Hello, World!");
7.3.7.2 The tagof Operator
The
tagof
operator returns the index of the pick ADT element in a particular instance of the ADT. The elements are indexed starting at 0. For example:
tagof r
This returns the index of the
r
instance of the
Apick
ADT created above, which is
2
(the third element).
7.3.7.3 The pick Statement
The
pick
statement enables statement selection based on the variant type of the pick ADT. The general form of the
pick
statement is:
pick localinst := refadtinst {
element1 =>
statements
element2 =>
statements
...
}
The pick statement uses a local copy,
localinst,
of a reference to the ADT,
refadtinst.
The
element
(s) can be one or more of the
elements
declared in the pick ADT.
For example:
t: ref Apick = s;
pick u := t {
String =>
sys->print("%s is %s\n", u.str, u.val);
Int =>
sys->print("%s is %d\n", u.str, u.val);
Real =>
sys->print("%s is %f\n", u.str, u.val);
Nothing =>
sys->print("%s is Nothing\n", u.str);
}
First, prior to the pick statement, a ref ADT of the
s
instance is initialized. Then, the
pick
statement selects the statement to execute based on the variant type of the ADT
t.
Another way typically used to accomplish this is to place the
pick
statement in a function that accepts as an argument the ref ADT. For example:
printval(a: ref Apick) {
pick t := a {
String =>
sys->print("%d: %s\n", tagof t, t.val);
Int =>
sys->print("%d: %d\n", tagof t, t.val);
Real =>
sys->print("%d: %f\n", tagof t, t.val);
Nothing =>
sys->print("%d: Nothing\n", tagof t);
}
}
To call this function, the instance of the ADT that specifies the element of the pick is given as the argument:
printval(s);
The function can also be a member of the ADT. In this way, the ADT can be passed as an implicit argument .
For example:
Apick.printval(v: self ref Apick)
{
pick t := v {
String =>
sys->print("%d: %s\n", tagof t, t.val);
Int =>
sys->print("%d: %d\n", tagof t, t.val);
Real =>
sys->print("%d: %f\n", tagof t, t.val);
}
}
The call to the function then would look like this:
s.printval();
Program 2 is an simple program that uses a pick ADT.
implement Command;
include "sys.m";
sys: Sys;
include "draw.m";
Apick: adt {
pick {
String =>
val: string;
Int =>
val: int;
Real =>
val: real;
}
printval: fn(this: self ref Apick);
};
Command: module
{
init: fn(ctxt: ref Draw->Context, argv: list of string);
};
init(ctxt: ref Draw->Context, argv: list of string)
{
sys = load Sys Sys->PATH;
r := ref Apick.Real;
r.val = 3.141593;
sys->print("%d\n", tagof r);
s := ref Apick.String("Hello, World!");
s.printval();
r.printval();
}
Apick.printval(v: self ref Apick)
{
pick t := v {
String =>
sys->print("%s: %s\n", tag(t), t.val);
Int =>
sys->print("%s: %d\n", tag(t), t.val);
Real =>
sys->print("%s: %f\n", tag(t), t.val);
}
}
tag(t: ref Apick): string
{
case tagof t {
tagof Apick.String => return "String";
tagof Apick.Int => return "Int";
tagof Apick.Real => return "Real";
tagof Apick.Nothing => return "Nothing";
* => return "?"; # can't happen
}
}
Program 2.pick.b
This program produces the following output:
2
String: Hello, World!
Real: 3.141593
7.4 Scope of Data
The scope of data depends on where it is declared:
- *
-
If data is declared in the module declaration, it is available to all functions within the module. This is referred to as module data and is global in the context of the module. Module data is initialized to 0 (zero) or null. If it is declared in a module interface file, it is public, available to other programs that load the module.
- *
-
If data is declared outside of functions and not in the module declaration, it is global in the module, but is private.
- *
-
If data is declared or initialized within a function, it is only available within that function. Function data can be passed between functions. It is not available to other programs, unless explicitly passed.
- *
-
If data is declared or initialized within a statement block, it is only available within that block.
Figure 4 illustrates the dependency of the point of declaration and the scope of data.
<IMAGE xml:link="simple" href="limbo_basics-9.jpg" show="embed" actuate="auto"/>
<FigCaption>
<IMAGE xml:link="simple" href="limbo_basics-6.jpg" show="embed" actuate="auto"/>
Scope of Data</FigCaption>
8 Program Control
Limbo has several program control statements that can be categorized into these groups:
- *
-
Selection
- *
-
Iteration
- *
-
Jump
- *
-
Exit
8.1 Selection Statements
The general types of selection statements in Limbo are:
if,
case,
and
alt.
See the section on
Channels
in the Limbo Language Definition for information about
alt.
8.1.1 if
The
if
statement selects statements to execute based on the evaluation of an expression. The two general formats of the
if
statement are:
if(expression)
statement
if(expression)
statement
else
statement
The
expression
must produce an
int
result. If the result is true (non-zero), then the first statement (or block) is executed. Otherwise, the statement (or block) following the
else
is executed, if the
else
clause is present.
For example:
if(guess == randnum)
sys->print("Correct!\n");
else
sys->print("Oh, sorry.\n");
if
statements can be nested. In Limbo, an
else
statement always refers to the nearest
if
statement that is within the same block as the
else
and that is not already associated with an
else.
For example:
if(i == j) {
if(k == l) { ... }
if(m == n) {
...
} else { # this else is associated with
... # 'if(m == n)'
}
} else { # this else is associated with
... # ¿if(i ==j)¿
}
8.1.2 case
The
case
statement selects from multiple statements to execute based on the evaluation of an expression. The general form of the
case
statement is:
case expression {
qualifier =>
statement
...
}
The
expression
must produce an
int
or a
string
result. The
qualifier(s)
can be simple constants, multiple constants, and ranges. They must evaluate uniquely, none of the ranges or constants may overlap. If there is no matching qualifier and a
default qualifer,
*,
is present, then the default is selected.
Each qualifer and the statements that follow up to the next qualifier form a block.
The general syntax of a Limbo
case
statement is similar to that of the C/C++
switch
statement. For example:
case direction {
"north" or "east" =>
...
"south" =>
...
* =>
...
Unlike C or Java
switch
statements, execution of one alternative in a Limbo
case
statement does not continue (``fall through'') to the next.
No explicit
break
statement is needed, but
break
can be used to end execution of an alternative conditionally.
case
statements can be nested. For example:
case x {
1 =>
case y {
0 =>
...
1 =>
...
}
2 =>
...
}
8.2 Iteration Statements
Limbo has three types of iteration statements that repeat execution of a statement or block of statements until a certain condition is reached: while, do, and for loops.
Each of the loop statements can be preceeded by a label that is used in conjunction with the jump statements break and continue. See
Labels
later in this section for more information.
8.2.1 The for Loop
The
for
loop in Limbo, like C and other languages, is flexible and powerful.
The general format of the
for
loop is:
for(exp1; exp2; exp3)
statement;
Inside the parentheses there are three expressions. These can be any valid Limbo expression. The following are the most commonly used expressions:
- *
-
exp1 is an assignment or initialization for an index variable
- *
-
exp2 is the test for loop continuation
- *
-
exp3 is modification to the index variable, such as increment or decrement, for each loop iteration
The
statement
can be an empty statement, a single statement, or a statement block enclosed in braces
({...}).
For example, the following uses a
for
loop to print the numbers 1 to 100:
i: int;
for(i = 1; i <= 100; i++)
sys->print("%d ", i);
Notice the declaration of the integer variable,
i,
before entering the
for
loop. The declaration and assignment (initialization) can also be done in the first expression of the
for
statement:
for(i := 1; i <= 100; i++)
sys->print("%d ", i);
8.2.1.1 Variations of the for Loop
None of the three expressions in the
for
statement are required. For example, you can create infinite loops, time delay loops, multiple control variable loops, and many others with the flexibility of the
for
loop.
For example, the
for
loop is the iteration statement conventionally used to create an infinite loop. The following example continuously reads one character from standard input until the user
enters a newline (presses the Return or Enter key):
stdin := sys->fildes(0); #get FD for stdin
buf := array[128] of byte;
for(;;) {
n := sys->read(stdin, buf, 1);
#check for new line or end of file
if(int buf[0] == '\n' || n <= 0)
break;
}
For information about
break
used to terminate the loop, see
The break Statement
in §8.3.1.
8.2.2 The while Loop
The
while
loop is a general iteration statement. The general format of the while loop is:
while(condition)
statement;
If the
condition
is true, then the
statement
is executed. The
statement
can be an empty statement, a single statement, or a statement block enclosed in braces
({...}).
For example, the following program fragment prints 1 to 100:
n := 1;
while(n <= 100)
sys->print("%d ", n++);
8.2.3 The do...while Loop
Unlike
for
and
while
loops, the
do...
while
loop tests the condition at the end of the iteration. This means that a
do...
while
loop executes at least once.
The general form of the
do...
while
loop is:
do
statement;
while(condition);
The
statement
can be an empty statement, a single statement, or a statement block enclosed in braces
({...}).
The
statement
is repeated if the
condition
is true.
For example, the following shows the do...while version of the counter:
n := 1;
do {
sys->print("%d ", n++);
} while(n <= 100);
Although the braces are not required when there is only one statement, they are generally used to improve readability.
8.3 Jumps
Limbo has two statements for peforming unconditional jumps:
break
and
continue.
8.3.1 The break Statement
The
break
statement has two uses. It can terminate
case
and
alt
statements, and it can force termination of a loop (
for,
while,
and
do),
bypassing the loop conditional test. (See
Labels
in §8.4 for information about using labels with
break
for goto-like program control.)
If the loop is nested, the break statement only terminates the loop where the break is encountered. For example:
for(t:=1; t<=5; t++) {
i := 1;
for(;;) {
sys->print("%d ", i++);
if(i == 10)
break;
}
}
This prints the numbers 1 to 9 five times. The
break
statement only terminates the inner infinite
for
loop.
8.3.2 The continue Statement
The
continue
statement is similiar to the
break.
Instead of forcing termination, however, it forces the next iteration of the loop. (See
Labels
for information about using labels with
continue
to transfer control to the end of outer loops.)
For example, the following continuously reads bytes from a file until end-of-file:
for(;;) {
n := sys->read(fd, buf, len buf);
if(n <= 0)
break;
if(int buf[0] != 'm' || n != 37)
continue;
}
8.4 Labels
Limbo labels give names to
for,
while
and
do...
while
loops ,
and
case
and
alt
statements .
The labels can be used in
break
and
continue
statements within the labelled statements, to transfer control
to the end of the labelled block.
There is no
go to
statement.
There are two typical uses of labels in Limbo. One is to allow a statement
in an inner loop to
break
or
continue
an outer loop. Another is to break to an outer statement (including a loop) from within a
case
or
alt
statement.
Consider the following example. One or more options can be passed to a command and parsed:
Args:
for(i := 0; i < len in; i++){
case in[i] {
'v' =>
opt |= Verbose;
's' =>
opt |= Suppress;
'o' =>
opt |= OutFile;
* =>
break Args;
}
}
As long as the
i
th element in the string
in
is one of the valid options
(v,
s,
or
o),
procesing continues; the
for
loop starts the next iteration. At the point that any other character is encountered, processing is termintated by the
break
statement.
If the label is not used with the
break,
the
for
loop simply starts the next iteration.
The next example shows how the label can be used to break out of a multi-nested loop:
u := 0;
line := "<A HREF=
URL := "";
Outer:
for(i := 0; i < len line; i++)
if(line[i] == '<') {
while(line[++i] != '>') {
if(line[i:i+6] == "A HREF") {
i += 6;
if(line[i] == '=') {
i += 2;
while(line[i] != '"')
URL[u++] = line[i++];
}
}
break Outer;
}
}
8.5 Exit
The
exit
statement terminates a thread and frees any resources that belong exclusively to it. Unlike the C
exit()
function, however, the Limbo
exit
does not return a value to the system.
9 Functions
Functions in Limbo serve the same purpose as they do in other structured, procedural languages-they contain the statements that are executed when the program runs.
The general form of a Limbo function is:
function_name(arguments): return_type
{
statements
}
The
arguments
is a comma-separated list of variable names and their associated types that are passed to the function when it is called. A function is not required to have parameters, but it still must have the parentheses.
The
return_type
specifies the type of data that the function returns. A function may return any type. A function is not required to return data, and the Limbo compiler does not assume a return result.
9.1 The init Function
When a program is executed from the Inferno shell, the shell looks for a special function called
init.
This function tells Inferno where execution of the program starts. It is somewhat analogous to the
main
function in C.
That init function must have two arguments:
ref Draw->Context
and
list of string.
9.1.1 ref Draw->Context
The
ref Draw->Context
is used to grab the context of the display and is defined in
draw.m.
This argument is required even if the program does not do any graphics. If it is not used, it can be named
nil.
9.1.2 list of string
The
list of string
is a list of the arguments passed to this module via the command line. If the program does not expect any arguments passed from the command line, it can be named
nil.
This is functionally equivalent to the
argv
variable in C, and is conventionally named
argv
in Limbo.
9.2 Declaring Functions
All of a module¿s public functions must be declared. A function declaration in Limbo is similar to a function prototype in C and C++. It provides type information about the arguments and the function¿s return values. This allows the compiler to perform type-checking that ensures type safety at run-time.
The general form of a Limbo function declaration is:
function_name: fn(arguments): return_type;
Function declarations are similar to data declarations. Use the keyword
fn
to specify that the object is a ``function'' type.
Functions that are not declared by the module are private. They cannot be accessed by external modules. The compiler still performs type-checking against the function call(s) and the function definition.
The following example is a module that contains two functions, the public
init
function and a private function.
1 implement Command;
2 include "sys.m";
3 sys: Sys;
4 include "draw.m";
5 Command: module
6 {
7 init: fn(nil: ref Draw->Context, argv: list of string);
8 };
9 init(nil: ref Draw->Context, argv: list of string)
10 {
11 sys = load Sys Sys->PATH;
12 for(i := 1; i <= 10; i++) {
13 sys->print(" %2d %4d\n", i, sqr(i));
14 }
15 sqr(n: int): int
16 {
17 return n*n;
18 }
Program 3. sqr.b
The declaration of the public
init
function on Line 7 is within the module declaration, Lines 5 to 8,
making it visible to any Limbo program that loads this module.
The function definition
of
sqr
in Lines 15 to 18 is private to the module
because it is not declared in the module declaration.
The use of public and private functions is of greater consequence with respect to modules that are intended to be loaded by other modules. This is discussed more in
Modules
in Chapter 3.
9.3 Function Arguments
If a function is to use arguments, it must declare variables that accept the values of the arguments. Arguments behave like other local variables inside the function and are created upon entry into the function and destroyed on exit.
Since Limbo is strongly typed, the compiler checks the argument types in the function declaration, in the function definition, and in the function calls. If any of these do not match, the compiler will complain.
9.3.1 Pass By Value, Pass By Reference
Due to Limbo¿s restricted use of pointers, there is no general call/pass by reference mechanism. Reference types are passed by reference, all other types are passed by value.
When data is passed by value, the receiving function creates an entirely new variable to hold the value of the argument passed to it. Modifications to the variable have no effect on the value in the calling function.
When data is passed by reference, the receiving function receives a reference, a pointer, to the value of the argument passed to it. Modifications made to the data also affect the value in the calling function.
9.4 Function Return Values
As stated earlier, a function may (but need not) return a value of some type.
9.4.1 The return Statement
The
return
statement returns from a function. It passes control from a function back to the calling location. Program control returns to the location where the function was called when control ``falls through'' the end of the function (reaches the end brace). If a function is to return a value, it must use a
return
statement.
A return statement can include an expression which becomes the return value of the function. The expression must be of the same type as the function's declared return value.
Function
sqr
in the Program Listing 3 on page 2-67 (Lines 16 to 19) uses the
return
statement to return the square of the number that was passed to it.
sqr(n: int): int
{
return n*n;
}
Portions copyright © 1995-1999 Lucent Technologies Inc. All rights reserved.
Portions copyright © 2000 Vita Nuova Holdings Limited. All rights reserved.