Limbo is a programming language intended for applications running distributed systems on small computers. It supports modular programming, strong type checking at compile- and run-time, interprocess communication over typed channels, automatic garbage collection, and simple abstract data types. It is designed for safe execution even on small machines without hardware memory protection.
In its initial implementation for the Inferno operating system, object programs generated by the Limbo compiler run using an interpreter for a fixed virtual machine. Inferno and its accompanying virtual machine run either stand-alone on bare hardware or as an application under conventional operating systems like Unix, Windows 95, Windows NT, and Plan 9. For several architectures, including Intel x86 and MIPS, Limbo object programs are transformed on-the-fly into instructions for the underlying hardware.
A Limbo application consists of one or more modules, each of which supplies an interface declaration and an implementation part. A module that uses another module includes its declaration part. During execution, a module dynamically attaches another module by stating the other module's type identifier and a place from which to load the object code for its implementation.
A module declaration specifies the functions and data it will make visible, its data types, and constants. Its implementation part defines the functions and data visible at its interface and any functions associated with its data types; it may also contain definitions for functions used only internally and for data local to the module.
Here is a simple module to illustrate the flavour of the language.
1 implement Command; 2 include "sys.m"; 3 include "draw.m"; 4 sys: Sys; 5 Command: module { 6 init: fn (ctxt: ref Draw->Context, argv: list of string); 7 };
8 # The canonical "Hello world" program, enhanced 9 init(ctxt: ref Draw->Context, argv: list of string) 10 { 11 sys = load Sys Sys->PATH; 12 sys->print("hello world\n"); 13 for (; argv!=nil; argv = tl argv) 14 sys->print("%s ", hd argv); 15 sys->print("\n"); 16 }
Let's look at the program line-by-line. It begins (line 1) by saying that this is the implementation of module Command. Line 2 includes a file (found in a way analogous to C's #include mechanism) named sys.m. This file defines the interface to module Sys; it says, in part,
Sys: module { PATH: con "$Sys"; . . . print: fn (s: string, *): int; . . . };
Line 3 includes draw.m; only one piece of information, mentioned below, is used from it. Line 4 declares the variable sys to be of type Sys; its name will be visible throughout the remainder of the file describing this module. It will be used later to refer to an instance of the Sys module. This declaration initializes it to nil; it still needs to be set to a useful value.
Lines 4-7 constitute the declaration of Command, the module being implemented. It contains only a function named init, with two arguments, a ref Draw->Context and a list of strings, and it doesn't return any value. The ref Draw->Context argument would be used if the program did any graphics; it is a data type defined in draw.m and refers to the display. Since the program just writes text, it won't be used. The init function isn't special to the Limbo language, but it is conventional in the environment, like main in C.
In a module designed to be useful to other modules in an application, it would be wise to take the module declaration for Command out, put it in a separate file called command.m and use include command.m to allow this module and others to refer to it. It is called, for example, by the program loader in the Inferno system to start the execution of applications.
Line 8 is a comment; everything from the # to the end of line is ignored.
Line 9 begins the definition for the init function that was promised in the module's declaration (line 6). The argument that is a list of strings is named argv.
Line 11 connects the program being written to the Sys module. The first token after load is the target module's name as defined by its interface (here found in the include on line 2) The next token is the place where the code for the module can be found; it is a string that usually names a file. Conventionally, in the Inferno system, each module contains a constant declaration for the name PATH as a string that names the file where the object module can be found. Loading the file is performed dynamically during execution except for a few modules built into the execution environment. (These include Sys; this accounts for the peculiar file name $Sys as the value of PATH.)
The value of load is a reference to the named module; line 11 assigns it to the variable sys for later use. The load operator dynamically loads the code for the named module if it is not already present and instantiates a new instance of it.
Line 12 starts the work by printing a familiar message, using the facilities provided by module Sys through its handle sys; the notation sys->print(...) means to call the print function of the module referred to by sys. The interface of Sys resembles a binding to some of the mechanisms of Unix and the ISO/ANSI C library.
The loop at lines 13-14 takes the list of string argument to init and iterates over it using the hd (head) and tl (tail) operators. When executed, this module combines the traditional `Hello world' and echo.
There are several kinds of tokens: keywords, identifiers, constants, strings, expression operators, and other separators. White space (blanks, tabs, new-lines) is ignored except that it serves to separate tokens; sometimes it is required to separate tokens. If the input has been parsed into tokens up to a particular character, the next token is taken to include the longest string of characters that could constitute a token.
The native character set of Limbo is Unicode, which is identical with the first 16-bit plane of the ISO 10646 standard. Any Unicode character may be used in comments, or in strings and character constants. The implementation assumes that source files use the UTF-8 representation, in which 16-bit Unicode characters are represented as sequences of one, two, or three bytes.
Comments begin with the # character and extend to the end of the line. Comments are ignored.
An identifier is a sequence of letters and digits of which the first is a letter. Letters are the Unicode characters a through z and A through Z, together with the underscore character, and all Unicode characters with encoded values greater than 160 (A0 hexadecimal, the beginning of the range corresponding to Latin-1).
Only the first 256 characters in an identifier are significant.
The following identifiers are reserved for use as keywords, and may not be used otherwise:
adt alt array big break byte case chan con continue cyclic do else exit fn for hd if implement import include int len list load module nil of or pick real ref return self spawn string tagof tl to type while
There are several kinds of constants for denoting values of the basic types.
Integer constants have type int or big. They can be represented in several ways.
Decimal integer constants consist of a sequence of decimal digits. A constant with an explicit radix consists of a decimal radix followed by R or r followed by the digits of the number. The radix is between 2 and 36 inclusive; digits above 10 in the number are expressed using letters A to Z or a to z. For example, 16r20 has value 32.
The type of a decimal or explicit-radix number is big if its value exceeds 231-1, otherwise it is int.
Character constants consist of a single Unicode character enclosed within single-quote characters '. Inside the quotes the following escape sequences represent special characters:
\' single quote \" double quote \\ backslash \t tab \n newline \r carriage return \b backspace \a alert character (bell) \v vertical tab \udddd Unicode character named by 4 hexadecimal digits \0 NUL
Real constants consist of a sequence of decimal digits containing one period . and optionally followed by e or E and then by a possibly signed integer. If there is an explicit exponent, the period is not required. Real constants have type real.
String constants are sequences of Unicode characters contained in double quotes. They cannot extend across source lines. The same escape sequences listed above for character constants are usable within string constants. Strings have type string.
The constant nil denotes a reference to nothing. It may be used where an object of a reference type is expected; otherwise uninitialized values of reference type start off with this value, it can be assigned to reference objects, and reference types can be tested for equality with it. (The keyword has other uses as well.)
The operators are
+ - * / % & | ^ == < > <= >= != << >> && || <- :: = += -= *= /= %= &= |= ^= <<= >>= := ~ ++ -- !
: ; ( ) { } [ ] , . -> =>
In this manual, Limbo syntax is described by a modified BNF in which syntactic categories are named in an italic font, and literals in typewriter font. Alternative productions are listed on separate lines, and an optional symbol is indicated with the subscript ``opt.''
Limbo has three kinds of objects. Data objects exist in the storage associated with a module; they can be manipulated by arithmetic operations, assignment, selection of component entities, and other concrete operations. Each data object has a type that determines what can be stored in it and what operations are applicable.
The second kind of object is the function. Functions are characterized by the types of the arguments they accept and the values they return, and are associated with the modules in which they are defined. Their names can be made visible in their module's declaration, or they can be encapsulated within the adt (abstract data types) of their modules, or they can exist privately within their module.
Finally, Limbo programs are organized into modules: a named collection of constants, abstract data types, data, and functions made available by that module. A module declaration displays the members visible to other modules; the module's implementation defines both the publicly visible members and its private parts, including the data objects it uses. A module that wishes to use the facilities of another includes its declaration in order to understand what it exports, but before using them it explicitly loads the new module.
Limbo has several basic types, some built-in higher abstractions, and other ways of composing new types. In declarations and some other places, constructions naming a type are used. The syntax is:
type: data-type function-type
The syntax of data types is
data-type: byte int big real string tuple-type array of data-type list of data-type chan of data-type adt-type ref adt-type module-type module-qualified-type type-name data-type-list: data-type data-type-list , data-type
The five basic data types are denoted by byte, int, big, real, and string.
Bytes are unsigned 8-bit quantities.
Integers (int) are 32-bit signed quantities represented in two's complement notation. Large integers (big) are 64-bit signed quantities represented in two's complement notation.
Real numbers (real) are 64-bit quantities represented in the IEEE long floating notation.
The byte, int, big, and real types are collectively called arithmetic types.
Strings are rows of Unicode characters. They may be concatenated and extended character-by-character. When a string is indexed with a single subscript, it yields an integer with the Unicode encoding of the character; when it is indexed by a range, it yields another string.
The tuple type, denoted
tuple-type: ( data-type-list )
The array type describes a dynamically-sized row of objects, all of the same type; it is indexed starting from 0. An array type is denoted by
array of data-type
A list is a sequence of like-typed objects; its denotation is
list of data-type
A channel, whose type is written
chan of data-type
chan of (int, string)
c <-= (123, "Hello");
An abstract data type or adt is an object that can contain data objects of several different types and declare functions that operate on them. The syntax for declaring an adt is given later. Once an adt has been declared, the identifier associated with it becomes a data-type name.
adt-type: identifier module-qualified-type
There is also a ref adt type representing a reference (pointer) to an adt. It is denoted
ref adt-type
A module type name is an identifier:
module-type: identifier
When an adt is declared within a module declaration, the type name of that adt is not generally visible to the rest of the program unless a specific import request is given (see §6.6, §10 below). Without such a request, when adt objects implemented by a module are declared by a client of that module, the adt type name is qualified:
module-qualified-type: identifier -> identifier
Finally, data types may be named, using a type declaration; this is discussed in §6.4 below.
type-name: identifier
A function type characterizes the arguments and return value of a function. The syntax is
function-type: fn function-arg-ret function-arg-ret: ( formal-arg-listopt ) ( formal-arg-listopt ) : data-type formal-arg-list: formal-arg formal-arg-list , formal-arg formal-arg: nil-or-D-list : type nil-or-D : self refopt identifier nil-or-D : self identifier * nil-or-D-list: nil-or-D nil-or-D-list , nil-or-D nil-or-D: identifier nil
fn (nil: int, nil: int): int fn (radius: int, angle: int): int fn (radius, angle: int): int
fn (nil: string)
The self keyword has a specialized use within adt declarations. It may be used only for the first argument of a function declared within an adt; its meaning is discussed in §6.3 below.
The star character * may be given as the last argument in a function type. It declares that the function is variadic; during a call, actual arguments at its position and following are passed in a manner unspecified by the language. For example, the type of the print function of the Sys module is
fn (s: string, *): int
Limbo source programs that implement modules are stored in files, conventionally named with the suffix .b. Each such file begins with a single implement directive naming the type of the module being implemented, followed by a sequence of declarations. Other files, conventionally named with the suffix .m, contain declarations for things obtainable from other modules. These files are incorporated by an include declaration in the implementation modules that need them. At the top level, a program consists of a sequence of declarations. The syntax is
program: implement identifier ; top-declaration-sequence top-declaration-sequence: top-declaration top-declaration-sequence top-declaration top-declaration: declaration identifier-list := expression ; identifier-list = expression ; ( identifier-list ) := expression ; module-declaration function-definition adt-declaration
Declarations are used both at the top level (outside of functions) and also inside functions and module declarations. Some styles of declaration are allowed only in certain of these places, but all will be discussed together.
Declarations take several forms:
declaration: identifier-list : type ; identifier-list : type = expression ; identifier-list : con expression ; identifier-list : import identifier ; identifier-list : type type ; include string-constant ; identifier-list: identifier identifier-list , identifier expression-list: expression expression-list , expression
These forms constitute the basic way to declare and initialize data:
identifier-list : type ; identifier-list : type = expression ;
For example,
i, j: int = 1; r, s: real = 1.0;
Another kind of declaration is a shorthand. In either of
identifier := expression ; ( identifier-list ) := expression ;
x: int = 1;
x := 1;
(p, q) := (1, 2.1);
The con declaration
identifier-list : con expression ;
Seven: con 3+4;
The identifier iota has a special meaning in the expression in a con declaration. It is equivalent to the integer constant 0 when evaluating the expression for the first (leftmost) identifier declared, 1 for the second, and so on numerically. For example, the declaration
M0, M1, M2, M3, M4: con (1<<iota);
The identifier iota is not reserved except inside the expression of the con declaration.
An adt or abstract data type contains data objects and functions that operate on them. The syntax is
adt-declaration: identifier : adt { adt-member-listopt } ; adt-member-list: adt-member adt-member-list adt-member adt-member: identifier-list : cyclicopt data-type ; identifier-list : con expression ; identifier-list : function-type ; pick { pick-member-list }
Point: adt { x, y: int; add: fn (p: Point, q: Point): Point; eq: fn (p: Point, q: Point): int; };
r, s: Point; xcoord: int; ... xcoord = s.x; r = r.add(r, s);
As this example indicates, adt members are accessed by mentioning an object with the adt type, a dot, and then the name of the member; the details will be discussed in §8.13 below. A special syntactic indulgence is available for functions declared within an adt: frequently such a function receives as an argument the same object used to access it (that is, the object before the dot). In the example just above, r was both the object being operated on and the first argument to the add function. If the first formal argument of a function declared in an adt is marked with the self keyword, then in any calls to the function, the adt object is implicitly passed to the function, and is not mentioned explicitly in the actual argument list at the call site. For example, in
Rect: adt { min, max: Point; contains: fn(r: self Rect, p: Point): int; }; r1: Rect; p1: Point; ... if (r1.contains(p1)) ...
If self is specified in the declaration of a function, it must also be specified in the definition as well. For example, contains would be defined
Rect.contains(r: self Rect, p: Point) { . . . }
The adt type in Limbo does not provide control over the visibility of its individual members; if any are accessible, all are.
Constant adt members follow the same rules as ordinary constants (§6.2).
The cyclic modifier will be discussed in §11.1.
An adt which contains a pick member is known as a pick adt. A pick adt is Limbo's version of a discriminated union. An adt can only contain one pick member and it must be the last component of the adt. Each identifier enumerated in the pick-tag-list names a variant type of the pick adt. The syntax is
pick-member-list: pick-tag-list => pick-member-list pick-tag-list => pick-member-list identifier-list : cyclicopt data-type ;
pick-tag-list: identifier pick-tag-list or identifier
The pick-member-list contains a set of data members for each pick-tag-list. These data members are specific to those variants of the pick adt enumerated in the pick-tag-list. The adt data members found outside of the pick are common to all variants of the adt. A pick adt can only be used as a ref adt and can only be initialized from a value of one of its variants. For example, if Constant is a pick adt and Constant.Real is one of its variant types then
c : ref Constant = ref Constant.Real("pi", 3.1);
The type declaration
identifier-list : type data-type ;
A module declaration collects and packages declarations of adt, functions, constants and simple types, and creates an interface with a name that serves to identify the type of the module. The syntax is
module-declaration: identifier : module { mod-member-listopt } ; mod-member-list: mod-member mod-member-list mod-member mod-member: identifier-list : function-type ; identifier-list : data-type ; adt-declaration ; identifier-list : con expression ; identifier-list : type type ;
Linear: module { setflags: fn (flag: int); TRUNCATE: con 1; Vector: adt { v: array of real; add: fn (v1: self Vector, v2: Vector): Vector; cross: fn (v1: self Vector, v2: Vector): Vector; dot: fn (v1: self Vector, v2: Vector); make: fn (a: array of real): Vector; }; Matrix: adt { m: array of array of real; add: fn (m1: self Matrix, m2: Matrix): Matrix; mul: fn (m1: self Matrix, m2: Matrix): Matrix; make: fn (a: array of array of real): Matrix; }; };
linearmodule: Linear;
linearmodule = load Linear "/usr/dmr/limbo/linear.dis"; if (linearmodule == nil) { sys->print("Can't load Linear\n"); exit; }
To initialize data declared as part of a module declaration, an assignment expression may be used at the top level. For example:
implement testmod; testmod: module { num: int; }; . . . num = 5;
These declarations take the form
identifier-list : import identifier ;
The string following the include keyword names a file, which is inserted into the program's text at that point. The included text is treated like text literally present. Conventionally, included files declare module interfaces and are named with the suffix .m. The directories to be searched for included files may be specified to the Limbo compiler command. Include files may be nested.
All executable code is supplied as part of a function definition. The syntax is
function-definition: function-name-part function-arg-ret { statements } function-name-part: identifier function-name-part . identifier
add_one(a: int): int { return a+1; }
Functions that are declared within an adt use the qualified form of definition:
Point: adt { x, y: int; add: fn (p: Point, q: Point): Point; eq: fn (p: Point, q: Point): int; } . . . Point.add(p: Point, q: Point): Point { return Point(p.x+q.x, p.y+q.y); }
Expressions in Limbo resemble those of C, although some of the operators are different. The most salient difference between Limbo's expression semantics and those of C is that Limbo has no automatic coercions between types; in Limbo every type conversion is explicit.
The basic elements of expressions are terms:
term: identifier constant real-constant string-constant nil ( expression-list ) term . identifier term -> term term ( expression-listopt ) term [ expression ] term [ expression : expression ] term [ expression : ] term ++ term --
. -> () [] ++ --
The first five kinds of term are constants and identifiers. Constants have a type indicated by their syntax. An identifier used in an expression is often a previously declared data object with a particular data type; when used as a term in an expression it denotes the value stored in the object, and the term has the declared object's type. Sometimes, as discussed below, identifiers used in expressions are type names, function names, or module identifiers.
A comma-separated list of expressions enclosed in parentheses is a term. If a single expression is present in the list, the type and value are those of the expression; the parentheses affect only the binding of operators in the expression of which the term is a part. If there is more than one expression in the list, the value is a tuple. The member types and values are taken from those of the expressions.
A term of the form
term . identifier
A term of the form
term -> term
An example using an abridged version of an example above: given
Linear: module { setflags: fn(flag: int); TRUNCATE: con 1; Vector: adt { make: fn(v: array of real): Vector; v: array of real; }; };
lin := load Linear "/dis/linear.dis"; a: array of real; v1: lin->Vector; v2: Linear->Vector; lin->setflags(Linear->TRUNCATE); v1 = lin->(Linear->Vector).make(a); v1 = lin->v1.make(a); v1 = lin->v1.add(v1); v1.v = nil;
When calling a function associated with an adt of another module, it is necessary to identify both the module and the adt as well as the function. The two calls to the make function illustrate two ways of doing this. In the first,
v1 = lin->(Linear->Vector).make(a);
v1 = lin->v1.make(a);
v1 = lin->Vector.make(a); # Wrong v1 = lin->Linear->Vector.make(a); # Wrong
Using import makes the code less verbose:
lin := load Linear "/usr/dmr/limbo/linear.dis"; Vector, TRUNCATE, setflags: import lin; a: array of real; v1: Vector; v2: Vector; setflags(TRUNCATE); v1 = Vector.make(a); v1 = v1.make(a); v1 = v1.add(v1); v1.v = nil;
The interpretation of an expression in the form
term ( expression-listopt )
A plain identifier as the term names a function defined in the current module or imported into it. A term qualified by using the selection operator . specifies a function member of an adt; a term using -> specifies a function defined in another module.
Function calls in Limbo create a copy of each argument of value type, and the execution of a function cannot affect the value of the corresponding actual argument. For arguments of reference type, execution of the function may affect the value of the object to which the reference refers, although it cannot change the argument itself. The actual arguments to a function are evaluated in an unspecified order, although any side effects caused by argument evaluation occur before the function is called.
Function calls may be directly or indirectly recursive; objects declared within each function are distinct from those in their dynamic predecessors.
Functions (§4.3, §7) may either return a value of a specified type, or return no value. If a function returns a value, it has the specified type. A call to a function that returns no value may appear only as the sole expression in a statement (§9.1).
In a term of the form
term [ expression ]
It is erroneous to refer to a nonexisting part of an array or string. (A single exception to this rule, discussed in §8.4.1 below, allows extending a string by assigning a character at its end.)
In a term of the form
term [ expression : expression ]
Thus, for both arrays and strings, the number of elements in a[e1:e2] is equal to e2-e1.
A slice of the form a[e:] means a[e:len a].
When a string slice is assigned to another string or passed as an argument, a copy of its value is made.
A slice of an array produces a reference to the designated subarray; a change to an element of either the original array or the slice is reflected in the other.
In general, slice expressions cannot be the subject of assignments. However, as a special case, an array slice expression of the form a[e1:] may be assigned to. This is discussed in §8.4.1.
The following example shows how slices can be used to accomplish what would need to be done with pointer arithmetic in C:
fd := sys->open( ... ); want := 1024; buf := array[want] of byte; b := buf[0:]; while (want>0) { got := sys->read(fd, b, want); if (got<=0) break; b = b[got:]; want -= got; }
A term of the form
term ++
The term
term --
Monadic expressions are expressions with monadic operators, together with a few more specialized notations:
monadic-expression: term monadic-operator monadic-expression array [ expression ] of data-type array [ expressionopt ] of { init-list } list of { expression-list } chan of data-type data-type monadic-expression monadic-operator: one of + - ! ~ ref * ++ -- <- hd tl len
The - operator produces the negative of its operand, which must have an arithmetic type. The type of the result is the same as the type of its operand.
The + operator has no effect; it is supplied only for symmetry. However, its argument must have an arithmetic type and the type of the result is the same.
The ! operator yields the int value 1 if its operand has the value 0, and yields 0 otherwise. The operand must have type int.
The ~ operator yields the 1's complement of its operand, which must have type int or byte. The type of the result is the same as that of its operand.
If e is an expression of an adt type, then ref e is an expression of ref adt type whose value refers to (points to) an anonymous object with value e. The ref operator differs from the unary & operator of C; it makes a new object and returns a reference to it, rather than generating a reference to an existing object.
If e is an expression of type ref adt, then * e is the value of the adt itself. The value of e must not be nil.
For example, in
Point: adt { ... }; p: Point; pp: ref Point; p = Point(1, 2); pp = ref p; # pp is a new Point; *pp has value (1, 2) p = Point(3, 4); # This makes *pp differ from p *pp = Point(4, 5); # This does not affect p
A monadic expression of the form
++ monadic-expression
The term
-- monadic-expression
The operand of the hd operator must be a non-empty list. The value is the first member of the list and has that member's type.
The operand of the tl operator must be a non-empty list. The value is the tail of the list, that is, the part of the list after its first member. The tail of a list with one member is nil.
The operand of the len operator is a string, an array, or a list. The value is an int giving the number of elements currently in the item.
The operand of the tagof operator is a monadic expression of type ref adt that refers to a pick adt. or the type name of a pick adt or one of its variants. The value is an int giving a unique value for each of the variants and for the pick adt type itself.
The operand of the communication operator <- has type chan of sometype. The value of the expression is the first unread object previously sent over that channel, and has the type associated with the channel. If the channel is empty, the program delays until something is sent.
As a special case, the operand of <- may have type array of chan of sometype. In this case, all of the channels in the array are tested; one is fairly selected from those that have data. The expression yields a tuple of type (int, sometype ); its first member gives the index of the channel from which data was read, and its second member is the value read from the channel. If no member of the array has data ready, the expression delays.
Communication channels are treated more fully in §9.8 and §9.13 below with the discussion of the alt and spawn statements.
In the expressions
array [ expression ] of data-type array [ expressionopt ] of { init-list ,opt }
init-list: element init-list , element element: expression expression => expression * => expression
If an element of the form * =>e2 is present, all members of the array not otherwise initialized are set to the value e2. The expression e2 is evaluated for each subscript position, but in an undefined order. For example,
arr := array[3] of { * => array[3] of { * => 1 } };
If the expression giving the size of the array is omitted, its size is taken from the largest subscript of a member explicitly initialized. It is erroneous to initialize a member twice.
The value of an expression
list of { expression-list }
The value of
chan of data-type
ch: chan of int; # just declares, sets ch to nil . . . ch = chan of int; # creates the channel and assigns it
An expression of the form
data-type monadic-expression
In arithmetic casts, the named type must be one of byte, int, big, or real, and the monadic-expression must have arithmetic type. For example,
byte 10
Here the named data type is string. In a first form, the monadic expression has arithmetic type (byte, int, big, or real) and the value is a string containing the decimal representation of the value, which may be either positive or negative. A real operand is converted as if by format %g, and if the result is converted back to real, the original value will be recovered exactly.
In a second form, the monadic expression has type array of byte. The value is a new string containing the Unicode characters obtained by interpreting the bytes in the array as a UTF-8 representation of that string. (UTF-8 is a representation of 16-bit Unicode characters as one, two, or three bytes.) The result of the conversion is undefined if the byte array ends within a multi-byte UTF-8 sequence.
In a first form, the monadic expression is a string, and the named type is an arithmetic type. The value is obtained by converting the string to that type. Initial white space is ignored; after a possible sign, conversion ceases at the first character not part of a number.
In a second form, the named type is array of byte and the monadic-expression is a string. The value is a new array of bytes containing the UTF-8 representation of the Unicode characters in the string. For example,
s := "Ångström"; a := array of byte s; s = string a;
Here the named type is that of an adt or ref adt, and the monadic expression is a comma-separated list of expressions within parentheses. The value of the expression is an instance of an adt of the named type whose data members are initialized with the members of the list, or whose single data member is initialized with the parenthesized expression. In case the type is ref adt, the value is a reference to the new instance of the adt.
The expressions in the list, read in order, correspond with the data members of the adt read in order; their types and number must agree. Placement of any function members of the adt is ignored. For example,
Point: adt { x: int; eq: fn (p: Point): int; y: int; }; . . . p: Point; p = Point(1, 2);
p := Point(1, 2);
Binary expressions are either monadic expressions, or have two operands and an infix operator; the syntax is
binary-expression: monadic-expression binary-expression binary-operator binary-expression binary-operator: one of * / % + - << >> < > <= >= == != & ^ | :: && ||
* / % + - << >> < > <= >= == != & ^ | :: && ||
The *, /, and % operators respectively accomplish multiplication, division, and remainder. The operands must be of identical arithmetic type, and the result has that same type. The remainder operator does not apply to type real. If overflow or division by 0 occurs, the result is undefined. The absolute value of a%b is less than the absolute value of b; (a/b)*b + a%b is always equal to a; and a%b is non-negative if a and b are.
The + and - operators respectively accomplish addition and subtraction of arithmetic operands of identical type; the result has the same type. The behavior on overflow or underflow is undefined. The + operator may also be applied to strings; the result is a string that is the concatenation of the operands.
The shift operators are << and >>. The left operand may be big, int, or byte; the right operand is int. The type of the value is the same as its left operand. The value of the right operand must be non-negative and smaller than the number of bits in the left operand. For the left-shift operator <<, the fill bits are 0; for the right-shift operator >>, the fill bits are a copy of the sign for the int case, and 0 for the byte case.
The relational operators are < (less than), > (greater than), <= (less than or equal), >= (greater than or equal), == (equal to), != (not equal to). The first four operators, which generate orderings, apply only to arithmetic types and to strings; the types of their operands must be identical, except that a string may be compared to nil. Comparison on strings is lexicographic over the Unicode character set.
The equality operators == and != accept operands of arithmetic, string, and reference types. In general, the operands must have identical type, but reference types and strings may be compared for identity with nil. Equality for reference types occurs when the operands refer to the same object, or when both are nil. An uninitialized string, or one set to nil, is identical to the empty string denoted "" for all the relational operators.
The value of any comparison is the int value 1 if the stated relation is true, 0 if it is false.
The logical operators & (and), ^ (exclusive or) and | (inclusive or) require operands of the same type, which must be byte, int, or big. The result has the same type and its value is obtained by applying the operation bitwise.
The concatenation operator :: takes a object of any data type as its left operand and a list as its right operand. The list's underlying type must be the same as the type of the left operand. The result is a new list with the left operand tacked onto the front:
hd (a :: l)
The logical and operator && first evaluates its left operand. If the result is zero, then the value of the whole expression is the int value 0. Otherwise the right operand is evaluated; if the result is zero, the value of the whole expression is again 0; otherwise it is 1. The operands must have the same arithmetic type.
The logical or operator || first evaluates its left operand. If the result is non-zero, then the value of the whole expression is the int value 1. Otherwise the right operand is evaluated; if the result is non-zero, the value of the whole expression is again 1; otherwise it is 0. The operands must have the same arithmetic type.
The remaining syntax for expressions is
expression: binary-expression lvalue-expression assignment-operator expression ( lvalue-expression-list ) = expression send-expression declare-expression load-expression assignment-operator: one of = &= |= ^= <<= >>= += -= *= /= %=
lvalue-expression: identifier nil term [ expression ] term [ expression : ] term . identifier ( lvalue-expression-list ) * monadic-expression lvalue-expression-list: lvalue lvalue-expression-list , lvalue
In general, the types of the left and right operands must be the same; this type must be a data type. The value of an assignment is its new left operand. All the assignment operators associate right-to-left.
In the ordinary assignment with =, the value of the right side is assigned to the object on the left. For simple assignment only, the left operand may be a parenthesized list of lvalues and the right operand either a tuple or an adt whose data members correspond in number and type to the lvalues in the list. The members of the tuple, or the data members of the adt, are assigned in sequence to lvalues in the list. For example,
p: Point; x, y: int; (x, y) = p;
If the left operand of a simple assignment is an adt and the right side is a tuple, then the assignment assigns the members of the tuple to the adt data members; these must correspond in number and type with the members of the tuple.
The constant nil may be assigned to an lvalue of any reference type. This lvalue will compare equal to nil until it is subsequently reassigned. In the Inferno implementation of Limbo, such an assignment also triggers the removal of the object referred to unless other references to it remain.
The left operand of an assignment may be the constant nil to indicate that a value is discarded. This applies in particular to any of the lvalues in a tuple appearing on the left; to extend the examples above,
(x, nil) = p;
A special consideration applies to strings. If an int containing a Unicode character is assigned to a subscripted string, the subscript is normally required to lie within the string. As a special case, the subscript's value may be equal to the length of the string (that is, just beyond its end); in this case, the character is appended to the string, and the string's length increases by 1.
A final special case applies to array slices in the form e1[e2:]. Such expressions may lie on the left of =. The right side must be an array of the same type as e1, and its length must be less than or equal to (len e1)-e2. In this case, the elements in the array on the right replace the elements of e1 starting at position e2. The length of the array is unchanged.
A compound assignment with op= is interpreted in terms of the plain assignment;
e1 op= e2;
e1 = (e1) op (e2);
A send-expression takes the form
send-expression: lvalue-expression <- = expression
e1 <- = e2
A declare-expression is an assignment that also declares identifiers on its left:
declare-expression: lvalue-expression := expression
The value and type of a declare-expression are the same as those of the expression.
A load-expression has the form
load-expression: load identifier expression
Execution of load brings the file containing the module into local memory and dynamically type-checks its interface: the run-time system ascertains that the declarations exported by the module are compatible with the module declaration visible in the scope of the load operator (see §11.2). In the scope of a module declaration, the types and constants exported by the module may be referred to without a handle, but the functions and data exported by the module (directly at its top level, or within its adt) may be called only using a valid handle acquired by the load operator.
The value of load is nil if the attempt to load fails, either because the file containing the module can not be found, or because the found module does not export the specified interface.
Each evaluation of load creates a separate instance of the specified module; it does not share data with any other instance.
In several places a constant expression is required. Such an expression contains operands that are identifiers previously declared with con, or int, big, real, or string constants. These may be connected by any of the following operators:
+ - * / % & | ^ == < > <= >= != << >> && || ~ !
Expressions in Limbo are not reordered by the compiler; values are computed in accordance with the parse of the expression. However there is no guarantee of temporal evaluation order for expressions with side effects, except in the following circumstances: function arguments are fully evaluated before the function is called; the logical operators && and || have fully defined order of evaluation, as explained above. All side effects from an expression in one statement are completed before the next statement is begun.
In an expression containing a constant subexpression (in the sense of §8.5), the constant subexpression is evaluated at compile-time with all exceptions ignored.
Underflow, overflow, and zero-divide conditions during integer arithmetic produce undefined results.
The real arithmetic of Limbo is all performed in IEEE double precision, although denormalized numbers may not be supported. By default, invalid operations, zero-divide, overflow, and underflow during real arithmetic are fatal; inexact-result is quiet. The default rounding mode is round-to-nearest-even. A set of routines in the Math library module permits independent control of these modes within each thread.
The executable code within a function definition consists of a sequence of statements and declarations. As discussed in the Scope section §11 below, declarations become effective at the place they appear. Statements are executed in sequence except as discussed below. In particular, the optional labels on some of the statements are used with break and continue to exit from or re-execute the labeled statement.
statements: (empty) statements declaration statements statement statement: expression ; ; { statements } if ( expression ) statement if ( expression ) statement else statement labelopt while ( expressionopt ) statement labelopt do statement while ( expressionopt ) ; labelopt for ( expressionopt ; expressionopt ; expressionopt ) statement labelopt case expression { qual-statement-sequence } labelopt alt { qual-statement-sequence } labelopt pick identifier := expression { pqual-statement-sequence } break identifieropt ; continue identifieropt ; return expressionopt ; spawn term ( expression-listopt ) ; exit ;
label: identifier :
Expression statements consist of an expression followed by a semicolon:
expression ;
The null statement consists of a lone semicolon. It is most useful for supplying an empty body to a looping statement with internal side effects.
Blocks are statements enclosed in {} characters.
{ statements }
The conditional statement takes two forms:
if ( expression ) statement if ( expression ) statement else statement
The simple looping statements are
labelopt while ( expressionopt ) statement labelopt do statement while ( expressionopt ) ;
The for statement has the form
labelopt for ( expression-1opt ; expression-2opt ; expression-3opt ) statement
expression-1 ; while ( expression-2 ) { statement expression-3 ; }
The case statement transfers control to one of several places depending on the value of an expression:
labelopt case expression { qual-statement-sequence }
qual-statement-sequence: qual-list => qual-statement-sequence qual-list => qual-statement-sequence statement qual-statement-sequence declaration qual-list: qualifier qual-list or qualifier qualifier: expression expression to expression *
The case statement is executed by comparing the expression at its head with the constants in the qualifiers. The test is for equality in the case of simple constant qualifiers; in range qualifiers, the test determines whether the expression is greater than or equal to the first constant and less than or equal to the second.
None of the ranges or constants may overlap. If no qualifier is selected and there is a * qualifier, then that qualifier is selected.
Once a qualifier is selected, control passes to the set of statements headed by that qualifier. When control reaches the end of that set of statements, control passes to the end of the case statement. If no qualifier is selected, the case statement is skipped.
Each qualifier and the statements following it up to the next qualifier together form a separate scope, like a block; declarations within this scope disappear at the next qualifier (or at the end of the statement.)
As an example, this fragment separates small numbers by the initial letter of their spelling:
case i { 1 or 8 => sys->print("Begins with a vowel\n)"; 0 or 2 to 7 or 9 => sys->print("Begins with a consonant\n"); * => sys->print("Sorry, didn't understand\n"); }
The alt statement transfers control to one of several groups of statements depending on the readiness of communication channels. Its syntax resembles that of case:
labelopt alt { qual-statement-sequence }
outchan := chan of string; inchan := chan of int; alt { i := <-inchan => sys->print("Received %d\n", i); outchan <- = "message" => sys->print("Sent the message\n"); }
If a qualifier of the form * is present, then the statement does not block; if no channel is ready the statements associated with * are executed.
If two communication operators are present in the same qualifier expression, only the leftmost one is tested by alt. If two or more alt statements referring to the same receive (or send) channel are executed in different threads, the requests are queued; when the channel becomes unblocked, the thread that executed alt first is activated.
As with case, each qualifier and the statements following it up to the next qualifier together form a separate scope, like a block; declarations within this scope disappear at the next qualifier (or at the end of the statement.) Thus, in the example above, the scope of i in the arm
i := <-inchan => sys->print("Received %d\n", i);
As mentioned in the specification of the channel receive operator <- in §8.2.8, that operator can take an array of channels as an argument. This notation serves as a kind of simplified alt in which all the channels have the same type and are treated similarly. In this variant, the value of the communication expression is a tuple containing the index of the channel over which a communication was received and the value received. For example, in
a: array [2] of chan of string; a[0] = chan of string; a[1] = chan of string; . . . (i, s) := <- a; # s has now has the string from channel a[i]
During execution of an alt, the expressions in the qualifiers are evaluated in an undefined order, and in particular subexpressions may be evaluated before the channels are tested for readiness. Therefore qualifying expressions should not invoke side effects, and should avoid subparts that might delay execution. For example, in the qualifiers
ch <- = getchar() => # Bad idea ich <- = next++ => # Bad idea
The pick statement transfers control to one of several groups of statements depending upon the resulting variant type of a pick adt expression. The syntax resembles that of case:
labelopt pick identifier := expression { pqual-statement-sequence }
pqual-statement-sequence: pqual-list => pqual-statement-sequence pqual-list => pqual-statement-sequence statement pqual-statement-sequence declaration pqual-list: pqualifier pqual-list or pqualifier pqualifier: identifier *
Once a qualifier is selected, control passes to the set of statements headed by that qualifier. When control reaches the end of that set of statements, control passes to the end of the pick statement. If no qualifier is selected, the pick statement is skipped.
Each qualifier and the statements following it up to the next qualifier together form a separate scope, like a block; declarations within this scope disappear at the next qualifier (or at the end of the statement.)
The identifier and expression given in the pick statement are used to bind a new variable to a pick adt reference expression, and within the statements associated with the selected qualifier the variable can be used as if it were of the corresponding variant type.
As an example, given a pick adt of the following form:
Constant: adt { name: string; pick { Str or Pstring => s: string; Real => r: real; } };
printconst(c: ref Constant) { sys->print("%s: ", c.name); pick x := c { Str => sys->print("%s\n", x.s); Pstring => sys->print("[%s]\n", x.s); Real => sys->print("%f\n", x.r); }; }
The break statement
break identifieropt ;
The continue statement
continue identifieropt ;
Similarly, execution of continue with an identifier transfers control to the end of the enclosing while, do, or for labeled with the same identifier.
The return statement,
return expressionopt ;
f, g: fn(a: int); f(a: int) { . . . return g(a+1); }
f(a: int) { . . . g(a+1); return; }
Running off the end of a function is equivalent to return with no expression.
The spawn statement creates a new thread of control. It has the form
spawn term ( expression-listopt ) ;
The exit statement
exit ;
As discussed above, modules present constants, functions, and types in their interface. Their names may be the same as names in other modules or of local objects or types within a module that uses another. Name clashes are avoided because references to the entities presented by a module are qualified by the module type name or an object of that module type.
For example, after the module and variable declarations
M: module { One: con 1; Thing: adt { t: int; f: fn(); }; g: fn(); }; m: M;
th1: M->Thing; th2: m->Thing;
m->g(); m->th1.f();
The import declaration
identifier-list : import identifier ;
One, Thing: import M;
th: Thing;
g, Thing: import m;
g();
m->g();
th: Thing; th.f();
th: M.Thing; m->th.f();
implement Mod; . . . Mod: module { . . . };
The scope of an identifier is the lexical range of a program throughout which the identifier means a particular type of, or instance of, an object. The same identifier may be associated with several different objects in different parts of the same program.
The names of members of an adt occupy a separate, nonconflicting space from other identifiers; they are declared in a syntactically distinct position, and are always used in a distinguishable way, namely after the . selection operator. Although the same scope rules apply to adt members as to other identifiers, their names may coincide with other entities in the same scope.
Similarly, the names of constants, functions, and adt appearing within a module declaration are ordinarily qualified either with the name of the module or with a module variable using the -> notation. As discussed above, the import declaration lifts these names into the current scope.
Identifiers declared in a top-declaration (§5) have scope that lasts from the declaration throughout the remainder of the file in which it occurs, unless it is overridden by a redeclaration of that name within an inner scope. Each function definition, and each block within a function, introduces a new scope. A name declared within the block or function (including a formal argument name of a function) has a scope that begins at the completion of its declaration and lasts until the end of the block or function. If an already-declared identifier is redeclared within such an inner scope, the declaration previously in force is used in any initialization expression that is part of the new declaration.
As discussed above, within case alt and pick, each qualifier and the statements following it form an inner scope just like a block.
The scope of a label is restricted to the labeled statement, and label names may coincide with those of other entities in the same scope.
In general, names must be declared before they are used.
The first exception to this rule is that a function local to a module need not have a declaration at all; it is sufficient to give its definition, and that definition may appear anywhere in the module.
The general rule implies that no adt may contain, as a member, an adt not previously declared (including an instance of itself). A second exception to this rule applies to ref adt types. An adt may contain a member whose type is a ref to itself, or to another adt even if the second adt has not yet been declared. Unless a special notation is used, such references are restricted: all mutual or self references among adt are checked statically throughout all the adt visible in a module to determine which members refer to other adt. Any member of an adt of ref adt type that refers directly, or indirectly through a chain of references, back to its own underlying type may not be assigned to individually; it can gain a value only by an assignment to the adt as a whole. For example, in
Tree: adt { l: ref Tree; r: ref Tree; t: ref Ntree; }; Ntree: adt { t: ref Tree; }; t1 := Tree(nil, nil, nil); # OK t2 := Tree(ref t1, ref t1, nil); # OK t1 = Tree(ref t1, ref t2, nil); # OK t1.l = ... ; # not OK nt := ref Ntree(nil); # OK nt.t = ... # not OK
These restrictions suffice to prevent the creation of circular data structures. Limbo implementations guarantee to destroy all data objects not involved in such circularity immediately after they become non-referenced by active tasks, whether because their names go out of scope or because they are assigned new values. This property has visible effect because certain system resources, like windows and file descriptors, can be seen outside the program. In particular, if a reference to such a resource is held only within an adt, then that resource too is destroyed when the adt is.
The default rules are burdensome because they impede the construction even of harmless structures like trees. Therefore an escape is provided: using the word cyclic before the type in an adt member removes the circular-reference restriction for that member. For example,
Tree: adt { l: cyclic ref Tree; r: cyclic ref Tree; t: ref Ntree; }; Ntree: adt { t: cyclic ref Tree; }; t1 := Tree(nil, nil, nil); # OK t2 := Tree(ref t1, ref t1, nil); # OK t1 = Tree(ref t1, ref t2, nil); # OK t1.l = ... ; # OK now nt := ref Ntree(nil); # OK nt.t = ... # OK now
In an assignment and in passing an actual argument to a function, the types of the target and the expression being assigned or passed must be equal (with certain exceptions, e.g. assignment of nil to a reference type). When a function is defined, its type must be equal to the type of a function with the same name if one is in scope. Type equality is determined as follows.
Two basic types are equal if and only if they are identical.
Two tuple types are equal if and only if they are composed of equal types in the same order.
Two array types are equal if and only if they are arrays of equal types. The size of an array is not part of its type.
Two list types are equal if and only if they are composed of equal types.
Two channel types are equal if and only if they transmit equal types.
Two adt types are equal if and only if their data members have the same names and correspondingly equal types, including any cyclic attribute. The order of member declaration is insignificant, and constant and function members of an adt do not enter into the comparison, nor does the name of the adt type itself. In particular, with the declarations
A: adt { x: ref B; }; B: adt { x: ref A; };
Two ref adt types are equal if and only if they are references to equal adt types.
Two module types are equal if and only if their data and function members have the same names and correspondingly equal types; the order of their mention is insignificant. Constant members and type members do not enter into the comparison.
Two function types are equal if and only if their return values have the same type and their argument lists have correspondingly equal types. Any self attributes given to arguments much match. Names given to arguments do not enter into the comparison.
A type name has the same type as the type from which it was constructed.
When a module is loaded, the module stored in the file system must have a type that is compatible with the type mentioned in the load expression. The type of the stored module type is compatible with the mentioned type if and only if all data members of the two types are equal in name and type, and all adt or functions actually mentioned by the program executing load have names and types equal to corresponding members of the stored module.
Because Limbo was designed for the Inferno environment, several of these examples consist of simplified versions of already simple Inferno applications in a prototype Inferno implementation. Some appreciation for the resources available in this environment should become evident, but its full description is available elsewhere; the discussion here will focus on language features. However, several of the programs use facilities from the module Sys, which provides an interface to a file system and its methods resembling those of Unix or Plan 9, as well as other useful library facilities.
Some of the programs are annotated with line numbers; they are there only for descriptive purposes.
This version of a shell program reads from a keyboard and executes `commands' typed by the user. Its own interface has the type of a Command module, and that is the type of the things it executes. In particular, it can call modules like the hello example at the beginning of the paper.
1 implement Command; 2 include "sys.m"; 3 include "draw.m"; 4 sys: Sys; 5 stdin: ref Sys->FD; 6 Command: module 7 { 8 init: fn(nil: ref Draw->Context, nil: list of string); 9 };
10 init(ctx: ref Draw->Context, nil: list of string) 11 { 12 13 14 buf := array[256] of byte; 15 sys = load Sys Sys->PATH; 16 stdin = sys->fildes(0); 17 for(;;) { 18 sys->print("$ "); 19 n := sys->read(stdin, buf, len buf); 20 if(n <= 0) 21 break; 22 (nw, arg) := sys->tokenize(string buf[0:n], " \t\n"); 23 if(nw != 0) 24 exec(ctx, arg); 25 } 26 }
Local variables are declared on lines 12-14; line 15 loads the Sys module and stores a handle for it in the variable sys. Line 16 creates an FD for the standard input by calling the fildes function of the Sys module using the -> operator; the notation modhandle->func(...) specifies a call to the function named func in the module currently referred to by modhandle. (In general there can be several modules of the same type and name active, and there can also be unrelated modules containing identically named functions. The import declaration, described in §6.6 above, can be used to abbreviate the references when names do not clash.)
The loop on lines 17-25 prints a prompt (line 18), reads a line from the standard input (line 19), parses it into tokens (line 22), and executes the command.
The function call sys->tokenize is worth discussing as an example of style. It takes two strings as arguments. The characters in the second string are interpreted as separators of tokens in the first string. It returns a tuple whose first member is the number of tokens found, and whose second is a list of strings containing the tokens found: its declaration is
tokenize: fn (s: string, sep: string): (int, list of string);
The sys->read routine gathers an array of bytes into buf. Thus the expression for the first argument of sys->tokenize converts this array to a string by slicing the array with [0:n], using the actual number of bytes gathered by the read, and using a cast.
At lines 23-24, if there were any words found, exec is called:
27 exec(ctx: ref Draw->Context, args: list of string) 28 { 29 c: Command; 30 cmd, file: string; 31 cmd = hd args; 32 file = cmd + ".dis"; 33 c = load Command file; 34 if(c == nil) 35 c = load Command "/dis/"+file; 36 if(c == nil) { 37 sys->print("%s: not found\n", cmd); 38 return; 39 } 40 c->init(ctx, args); 41 }
If either attempt to get a handle to the named module succeeds, c will contain a valid handle to it; line 40 calls its init function, passing it the whole argument list. When it returns, the exec function returns, and the main loop resumes.
This example shows two instances of a module for interfacing to a TV remote control; one is for the real remote, which in this case is connected to a serial port on a set-top box, and the other is simulated for testing programs running on a regular operating system. The techniques of special interest are the dynamic use of modules and the communication using a channel.
The module is used by creating a channel and passing it to the module's init function, which returns a success/error indicator and starts an asynchronous process to read the remote control. The user of the module executes a receive on the channel whenever it wishes to accept a button-push.
The (abridged) module declaration is
Ir: module { # Codes buttons on IR remote control Zero: con 0; One: con 1; . . . Mute: con 23; Error: con 9999; init: fn(chan of int): int; PATH: con "/dis/ir.dis"; SIMPATH: con "/dis/irsim.h"; };
implement Ir; include "ir.m"; include "sys.m"; FD, Dir: import Sys; sys: Sys; init(keys: chan of int): int { cfd, dfd: ref FD; sys = load Sys Sys->PATH; cfd = sys->open("/dev/eia1ctl", sys->OWRITE); if(cfd == nil) return -1; sys->fprint(cfd, "b9600"); dfd = sys->open("/dev/eia1", sys->OREAD); cfd = nil; spawn reader(keys, dfd); return 0; }
reader(keys: chan of int, dfd: ref FD) { n, ta, tb: int; dir: Dir; b1:= array[1] of byte; b2:= array[1] of byte; # find the number of bytes already # queued and flush that many (n, dir) = sys->fstat(dfd); if(n >= 0 && dir.length > 0) { while(dir.length) { n = sys->read(dfd, array[dir.length] of byte, dir.length); if(n < 0) break; dir.length -= n; } }
loop: for(;;) { n = sys->read(dfd, b1, len b1); if(n <= 0) break; ta = sys->millisec(); # Button pushes are pairs of characters # that arrive closer together than # 200 ms. Longer than that is likely # to be noise. for(;;) { n = sys->read(dfd, b2, 1); if(n <= 0) break loop; tb = sys->millisec(); if(tb - ta <= 200) break; ta = tb; b1[0] = b2[0]; } # map the character pair; the significant # bits are the lowest 5. case ((int b1[0]&16r1f)<<5) | (int b2[0]&16r1f) { 975 => n = Ir->Zero; 479 => n = Ir->One; . . . 791 => n = Ir->Mute; * => n = Ir->Error; } # found a button-push; send the value keys <-= n; } keys <-= Ir->Error; }
Here is another implementation of the same interface. Its init function performs the same kind of initialization as the other version, but using the operating system's keyboard files /dev/cons and /dev/consctl. In the Inferno environment, operations corresponding to the Unix `stty' primitive are accomplished by writing messages to a control file associated with the file that handles the data.
implement Ir; include "ir.m"; include "sys.m"; FD: import Sys; sys: Sys; cctlfd: ref FD; init(keys: chan of int): int { dfd: ref FD; sys = load Sys Sys->PATH; cctlfd = sys->open("/dev/consctl", sys->OWRITE); if(cctlfd == nil) return -1; sys->write(cctlfd, array of byte "rawon", 5); dfd = sys->open("/dev/cons", sys->OREAD); if(dfd == nil) return -1; spawn reader(keys, dfd); return 0; }
The reader function for this module has the same structure as the first example, but doesn't have to worry about a noisy infrared detector:
reader(keys: chan of int, dfd: ref FD) { n: int; b:= array[1] of byte; for(;;) { n = sys->read(dfd, b, 1); if(n != 1) break; case int b[0] { '0' => n = Ir->Zero; '1' => n = Ir->One; . . . 16r7f => n = Ir->Mute; * => n = Ir->Error; } keys <-= n; } keys <-= Ir->Error; }
implement Irtest; include "sys.m"; include "draw.m"; FD: import Sys; include "ir.m"; Irtest: module { init: fn(nil: ref Draw->Context, nil: list of string); }; ir: Ir; sys: Sys;
init(nil: ref Draw->Context, nil: list of string) { c: int; stderr: ref FD; irchan := chan of int; sys = load Sys Sys->PATH; stderr = sys->fildes(2); # If the real IR remote application can # be found, use it, otherwise use the simulator: ir = load Ir Ir->PATH; if(ir == nil) ir = load Ir Ir->SIMPATH; if(ir == nil) { # %r format code means the last system error string sys->fprint(stderr, "load ir: %r\n"); return; } if(ir->init(irchan) != 0) { sys->fprint(stderr, "Ir.init: %r\n"); return; } names := array[] of { "Zero", "One", . . . "Mute", }; for(;;) { c = <-irchan; if(c == ir->Error) sys->print("Error %d\n", c); else sys->print("%s\n", names[c]); } }
movie(entry: ref Dbinfo, cc: chan of int) { i: int; m: Mpeg; b: ref Image; m = load Mpeg Mpeg->PATH; if (m == nil) return; # make a place on the screen w := screen.window(screen.image.r); mr := chan of string; s := m->play(w, 1, w.r, entry.movie, mr); if(s != "") return; # wait for the end of the movie # while watching for button pushes for(;;) { alt { <-mr => return; i = <-cc => case i { Ir->Select => m->ctl("stop"); Ir->Up or Ir->Dn => m->ctl("pause"); } } } }
Statically allocated storage within a module is accessible to all the functions of that module, and there is no explicit mechanism in Limbo for synchronizing concurrent updates to this storage from several tasks. However, it is straightforward to build a variety of concurrency-control mechanisms by using channel communications.
An example is a module that implements a Monitor abstract data type. Each instance of Monitor has a lock and an unlock operation; calling lock delays if another task holds the lock; calling unlock releases the lock and enables any other task attempting to execute lock.
implement Mon; Mon: module { Monitor: adt { create: fn(): Monitor; lock: fn(m: self Monitor); unlock: fn(m: self Monitor); ch: chan of int; }; };
Monitor.create(): Monitor { m := Monitor(chan of int); spawn lockproc(m.ch); return m; }
Monitor.lock(m: self Monitor) { m.ch <- = 0; }
Monitor.unlock(m: self Monitor) { <- m.ch; }
lockproc(ch: chan of int) { for (;;) { <- ch; # wait for someone to lock ch <- = 0; # wait for someone to unlock } }
mp: Mon; Monitor: import mp; mp = load Mon "..."; l := Monitor.create(); l.lock(); # region of code to be protected; # only one thread can execute here at once. l.unlock();
Limbo channels are unbuffered; a sender blocks until there is a receiver. This example shows a way to make a buffered channel of strings from an unbuffered channel. It is written as a module whose bufchan function takes a chan of string and a size as argument, and returns a new channel; it creates an asynchronous task that accepts input from the argument channel and saves up to size strings, meanwhile trying to send them to its user.
implement Bufchan; Bufchan: module { bufchan: fn(c: chan of string, size: int): chan of string; }; xfer(oldchan, newchan: chan of string, size: int) { temp := array[size] of string; fp := 0; # first string in buffer n := 0; # number of strings in buffer dummy := chan of string; sendch, recvch: chan of string; s: string; for (;;) { sendch = recvch = dummy; if (n > 0) sendch = newchan; if (n < size) recvch = oldchan; alt { s = <-recvch => temp[(fp+n)%size] = s; n++; sendch <- = temp[fp] => temp[fp++] = nil; n--; if (fp>=size) fp -= size; } } }
bufchan(oldchan: chan of string, size: int): chan of string { newchan := chan of string; spawn xfer(oldchan, newchan, size); return newchan; }
The module could be used in the following way:
Bufchan: module { PATH: con "/appl/lib/bufchan.dis"; bufchan: fn(c: chan of string, size: int): chan of string; }; bufc := load Bufchan Bufchan->PATH; sourcech := chan of string; # ... (here, hand off sourcech to a process that # reads strings from it and copies them to ch) ch: chan of string = bufc->bufchan(sourcech, 10); s := <- ch;
This section summarizes the grammar of Limbo above the lexical level; constants and identifiers are left undefined.
program: implement identifier ; top-declaration-sequence
top-declaration-sequence: top-declaration top-declaration-sequence top-declaration
top-declaration: declaration identifier-list := expression ; identifier-list = expression ; ( identifier-list ) := expression ; module-declaration function-definition adt-declaration
declaration: identifier-list : type ; identifier-list : type = expression ; identifier-list : con expression ; identifier-list : import identifier ; identifier-list : type type ; include string-constant ;
identifier-list: identifier identifier-list , identifier
expression-list: expression expression-list , expression
type: data-type function-type
data-type: byte int big real string tuple-type array of data-type list of data-type chan of data-type adt-type ref adt-type module-type module-qualified-type type-name
tuple-type: ( data-type-list )
data-type-list: data-type data-type-list , data-type
adt-type: identifier module-qualified-type
module-type: identifier
module-qualified-type: identifier -> identifier
type-name: identifier
function-type: fn function-arg-ret
function-arg-ret: ( formal-arg-listopt ) ( formal-arg-listopt ) : data-type
formal-arg-list: formal-arg formal-arg-list , formal-arg
formal-arg: nil-or-D-list : type nil-or-D : self refopt identifier nil-or-D : self identifier *
nil-or-D-list: nil-or-D nil-or-D-list , nil-or-D
nil-or-D: identifier nil
module-declaration: identifier : module { mod-member-listopt } ;
mod-member-list: mod-member mod-member-list mod-member
mod-member: identifier-list : function-type ; identifier-list : data-type ; adt-declaration ; identifier-list : con expression ; identifier-list : type type ;
adt-declaration: identifier : adt { adt-member-listopt } ;
adt-member-list: adt-member adt-member-list adt-member
adt-member: identifier-list : cyclicopt data-type ; identifier-list : con expression ; identifier-list : function-type ; pick { pick-member-list }
pick-member-list: pick-tag-list => pick-member-list pick-tag-list => pick-member-list identifier-list : cyclicopt data-type ;
pick-tag-list: identifier pick-tag-list or identifier
function-definition: function-name-part function-arg-ret { statements }
function-name-part: identifier function-name-part . identifier
statements: (empty) statements declaration statements statement
statement: expression ; ; { statements } if ( expression ) statement if ( expression ) statement else statement labelopt while ( expressionopt ) statement labelopt do statement while ( expressionopt ) ; labelopt for ( expressionopt ; expressionopt ; expressionopt ) statement labelopt case expression { qual-statement-sequence } labelopt alt { qual-statement-sequence } labelopt pick identifier := expression { pqual-statement-sequence } break identifieropt ; continue identifieropt ; return expressionopt ; spawn term ( expression-listopt ) ; exit ;
label: identifier :
qual-statement-sequence: qual-list => qual-statement-sequence qual-list => qual-statement-sequence statement qual-statement-sequence declaration
qual-list: qualifier qual-list or qualifier
qualifier: expression expression to expression *
pqual-statement-sequence: pqual-list => pqual-statement-sequence pqual-list => pqual-statement-sequence statement pqual-statement-sequence declaration
pqual-list: pqualifier pqual-list or pqualifier
pqualifier: identifier *
expression: binary-expression lvalue-expression assignment-operator expression ( lvalue-expression-list ) = expression send-expression declare-expression load-expression
binary-expression: monadic-expression binary-expression binary-operator binary-expression
binary-operator: one of * / % + - << >> < > <= >= == != & ^ | :: && ||
assignment-operator: one of = &= |= ^= <<= >>= += -= *= /= %=
lvalue-expression: identifier nil term [ expression ] term [ expression : ] term . identifier ( lvalue-expression-list ) * monadic-expression
lvalue-expression-list: lvalue lvalue-expression-list , lvalue
expression: term monadic-operator monadic-expression array [ expression ] of data-type array [ expressionopt ] of { init-list } list of { expression-list } chan of data-type data-type monadic-expression
term: identifier constant real-constant string-constant nil ( expression-list ) term . identifier term -> term term ( expression-listopt ) term [ expression ] term [ expression : expression ] term [ expression : ] term ++ term --
monadic-operator: one of + - ! ~ ref * ++ -- <- hd tl len tagof
init-list: element init-list , element
element: expression expression => expression * => expression
send-expression: lvalue-expression <- = expression
declare-expression: lvalue-expression := expression
load-expression: load identifier expression