The L language
L language reference manual in PDF format
Overview
What is L
The L programming language, along with its compiler, library and the simple web page scripting built on top of them, is a long-term hobby of mine. However it is not a toy programing language - it has the features you'd expect from a language in the vein of Java or C# and it compiles to either bitcode or optimized native code, via LLVM
Like any other programming language, L reflects the ideas of its author - in this case me - about what makes a good programming language. I've used a few different programming languages over the years, starting with ZX81 BASIC and Z80 machine code and then working my way though more BASIC, Pascal1, Modula-2, C, Ada, Smalltalk2, C++, Java, C# and VB.Net and I've created a number of languages of my own both for fun (such as a peculiar macro assembler written in BASICA for the IBM PC and an object orientated Forth) and occasionally for work (including a screen-scraping scripting language for the IBM 3270 PC and an interpreter for a simple application scripting language)
L takes the aspects I like from all languages I've used or toyed with. The result is an object orientated language with a strong and static type system. The syntax is influenced by Algol-68 and the object model is much like Java. The library relies extensively on templates, which are taken from C++ (although L's templates are much simpler than those in C++). All objects are stored on a garbage collected heap - life is too short for manual memory management - and finalizers and/or finally blocks can be used to dispose of anything that requires additional cleanup. The approach to run time checks and exceptions in general is to not allow anything dangerous but to allow checks to be bypassed without fuss by those determined to shoot themselves in the foot.
1,2Borland Turbo Pascal and Digitalk SmalltalkV were life altering products for the geek in me
Lexical structure
Characters
L supports only 7-bit ASCII source (this will probably change to UTF-8 in a future version)
White space
White space delimits some tokens but is otherwise ignored
Newlines
Newlines (cast char(10)) are treated as white space and are ignored except to terminate line comments. Carriage returns (cast char(13)) are not valid within an L program and will result in a parse error
Comments
L has two kinds of comments: line comments and C-style comments
Line comments
Line comments are introduced with // and are terminated by the next newline character. The contents of the comment are ignored
C-style comments
C-style comments are introduced with /* and are terminated by */. C-style comments do not nest
Keywords
bool, break, case, cast, catch, char, class, const, continue, default, do, elif, else, end, enum, esac, false, fi, finally, for, foreach, get, if, import, int, is, isa, long, namespace, native, new, null, od, pragma, private, proc, protected, ptr, public ref, return, set, static, struct, super, then, this, throw, true, try, use, var, void, while, word, yrt
Symbols
[] = && ! || & | ~ ^ : , + - / % * . { } ( ) [ ] ; == >= > <= < != =~ !~
Literals
Integer literals
Integer literals can be either base ten or base 16. Integer literals default to type int. A suffix of 'l' or 'L' makes a literal of type long and a suffix of 'w' or 'W' makes a literal of type word
integer_literal = decimal_literal | hex_literal
decimal_literal = ((['0'-'9']+)(['W' 'w' 'L' 'l' 'C' 'c'])?)
hex_literal = (('0'['x' 'X'](['A'-'F' 'a'-'f' '0'-'9'])+)(['W' 'w' 'L' 'l' 'C' 'c'])?)
String literals
Data types
Primitive data types
L has a small number of primitive types. There are no unsigned integer types and no floating point types at all
bool
Values of the bool type hold one of two values whose literals are true and false. The default initialize value for booleans is false.
byte
byte type hold signed 8-bit integers. There are no literal values of this type and the default initialize value is cast byte(0)
byte b = cast byte(0x1F);
char
Values of the char type also hold signed 8-bit integers. Characters may become unsigned in a future version. Literal characters are enclosed in single quotes. The default initialize value is cast char(0)
char c = 'X';
int
Values of the int type hold signed 32-bit integers. Literals are decimal or hexadecimal integers in C syntax.
int i = 123;
long
Values of the long type hold signed 64-bit integers
long l = 200000000000L;
word
Values of the word type hold a signed integer of at least pointer width.
word w = cast word(apointer);
basetype :
'int'
| 'long'
| 'word'
| 'bool'
| 'char'
| 'byte'
| 'void'
Constructed types
Arrays
An array type is written "type[]"
int a, b; // both a and b have type int[]
Pointers
A pointer type is written "type ptr"
// declare a pointer to an integer
int ptr p;
// declare a pointer to a pointer to an array of integers
int[] ptr q;
Pointer arithmetic is not allowed in sandboxed mode.
References
A reference type is written as "type ref"
// declare a reference to an integer
int ref p;
Refence types may only appear in a method or proc argument list. The only way to initialize a variable of reference type is to pass a variable to a method expecting a reference parameter. At all other times references are automatically de-referenced
complex_type:
: type '[]'
| type 'ptr'
| type 'ref'
| generic_type
Type conversions
Implicit conversions
L will reinterpret object types as any compatible object type in assignments and when passing arguments to methods:
- Instances of a class may be treated as instances of any super class of that class (up-casted)
- Objects may be treated as any interface they implement
- Instances of any interface may be treated as instances of System.Object
- Array types may be treated as the corresponding specialization of System.Array
and vice-versa - Proc types may be treated as the corresponding specialization of System.Proc
and vice-versa
No other implicit conversions are performed. In particular there are no implicit type conversions between different integer types or between integer and character types.
Explicit conversions
Explicit type conversions are performed with a cast:
- Widening casts between integer types sign extend.
- Narrowing casts between integer types do not check for overflow: values not representable in the target type are silently truncated.
- Down-casts between class types are runtime checked to ensure the casted value is an instance of the target type.
- Casts from non-class types to class types are unchecked and are not allowed in sandbox mode
int i = cast int('A')
char c = cast char(65)
cast : 'cast' type '(' expression ')'
Type inference
L has a very simple type inference mechanism. You can declare local variables as type var and the compiler figures out the type from the initializer, which must be present
var i = 0; // i is an integer.
Type inference saves some typing for long winded template type names
var v = new Generic.Vector();
untyped_declaration : 'var' identifier_list ';'
Classes
The root of the L class hierarchy is System.Object. All classes inherit either directly or indirectly from System.Object.
L also has structs, which are similar to classes but do not support inheritance, and interfaces, which specify a protocol that any class can inherit
Names and scopes
Namespaces
An L namespace defines a named scope. Symbols within namespaces must qualified with the namespace name(s) when accessed from outside the namespace. Namespaces can be nested within other namespaces.
namespace N is class C; class D; si
Outside namespace N classes C and D must be referred to as N.C and N.D
namespace M is
class E is
N.C c;
N.D d;
si
si
The individual symbols from other namespaces can be made accessible without qualification via use Namespace.Class
namespace O is
use N.C;
use N.D;
class F is
C c;
D d;
si
si
A use applies only to the single namespace block it appears in. System classes are in the System namespace, collection template classes are in the Generic namespace and the rest of the run time library is in Util
namespace :
'namespace' name 'is' 'si'
| 'namespace' name 'is' class_list 'si'
use : 'use' name ';'
Scopes
Types, fields and methods occupy the same space and so it is an error to declare two symbols with the same name in the same scope even if they are of different kinds (this is different from Java where fields and methods are in different namespaces). Redefining symbols in different scopes outside of method scope is permitted but the compiler will issue a warning. Resolution of symbols by name proceeds from innermost scope outwards to global scope.
L generates a new scope for each block statement. It is an error to redefine a symbol previously defined in a method that is still in scope:
if i == 0 then
// new scope here:
var j = 1;
while i < 10 do
// new scope here but
// var j = 0;
// would not be legal here since j is already defined
// in an enclosing scope
od
else
// new scope here, this is allowed as previous definition
// of j is not in scope here:
var j = -1;
fi
Classes, methods and variables
When a variable is declared of class type it holds either a reference to an instance of that class or a class derived from it or a null reference. Objects are constructed with 'new'. Method calls are virtual. Static variables exist from program start until program exit. Instance variables exist from object construction until the owning object is garbage collected.
Variables
Variables hold a value - either directly or a reference. Variables of primitive type hold the value directly and assignments to the variable change its value. Variables of array or class type hold a reference to their value and assignment to the variable causes the reference to point to the new value. Variables that hold a reference may also take the value null (reference to no value).
Classes
Inheritance
L supports single class inheritance and a class heirarchy with a single root, System.Object. A class definition indicates what class it inherits from via isa.
class Thing // inherits from System.Object by default
...
si
class SpecialThing isa Thing is // explicitly inherits from Thing
...
si
In addition to inheriting from its super class, an L class can inherit from any number of interfaces with do.
interface Action is
void doSomething();
si
class Actor do Action is // inherit from System.Object and also Action interface
void doSomething() is
...
si
si
Access
The default access for methods, enumerations and classes is public. The default access for fields is private.
class C isa System.Object is
int a; // implicitly private
protected int b; // accessible from subclasses
public c; // accessible from anywhere
void init(int a, int b, int c ) is // implicitly public
this.a = a;
this.b = b;
this.c = c;
si
si
Templates
Template are defined with formal type parameters listed in angle brackets. A concrete specialization of a template class is made whenever a new unique combination of arguments is encountered
There are no constraints on what types may be substituted for a given template's formal parameters but if the resulting specialization relies on operations not supported by an actual type argument then that specialization will not compile and an error will be issued
class D<T,U> is
T a;
U b;
void init(T a, U b) is
this.a = a;
this.b = b;
si
si
...
var d = new D<System.String,int[]>("Hello World", {1, 2, 3});
Class definition grammar
class_specifiers :
access_specifiers
|
class :
class_specifiers 'class' identifier generic implements class_body
| class_specifiers 'class' identifier generic 'isa' name generic_super implements class_body
implements :
'do' type_list
|
generic :
'<' plain_identifier_list '>'
|
generic_super :
'<' type_list '>'
class_body :
'is' 'si'
| 'is' class_body_declarations 'si'
declarations :
span class=SpellE>class_body_declaration
| class_body_declarations class_body_declaration
class_body_declaration :
field_declaration
| method_declaration
| native_declaration
| access_specifiers field_declaration
| access_specifiers method_declaration
| access_specifiers native_declaration
| enumeration
| pragma
Methods
Methods can be defined only within classes. Methods are public by default
Normal methods
normal_method_declaration : type identifier declare_arguments method_body
declare_arguments :
'(' declare_argument_list ')'
| '(' ')'
declare_argument_list :
argument_declaration
| declare_argument_list ',' argument_declaration
argument_declaration : type identifier
Default return value
Reference parameters
Parameters can be passed by reference:
int swap(int ref a, int ref b) is int t = a; a = b; b = t; si ... int x = 10, y = 20; swap(x, y);
Accessors
Accessors, also called properties, setters, getters or mutators, are members of a class that appear to be fields but which, when accessed, call methods to get or set the value.
class C is
int value;
void init();
// readable int accessor C.Value:
get int Value is
return value;
si
// assignable int accessor C.Value:
set int Value = v is
value = v;
si
si
...
var c = new C();
c.Value = 123; // calls set int Value accessor with v = 123
var i = c.Value; // calls get int Value accessor and assigns its return value to i
There is no requirement for get and set accessors to be paired and they can be overridden, overloaded and made public/protected/private separately. Accessors are instance methods by default declared instance or class (static).
accessor_declaration :
'get' type identifier method_body
| 'set' type identifier '=' identifier method_body
Indexers
L indexers are a mechanism for making instances of a class respond to array syntax, which is useful for classes implementing collections.
// a class that can be accessed as an array of integers
// indexed by letters starting with 'A':
class A is
int[] value;
void init(int size) is
value = new int[size];
si
// a readable int indexer with index type char:
get int[char index] is
return value[cast int(index - 'A')];
si
// an assignable int indexer with index type char
set int[char index] = v is
value[cast int(index - 'A')] = v;
si
si
...
var a = new A(26);
// call A's set indexer with index = 'A', v = 123:
a['A'] = 123;
// call A's set indexer with index = 'Z', v = 456:
a['Z'] = 456;
// call A's get indexer with index = 'M':
int i = a['M'];
Indexers can be overloaded and there is no restriction on the type of the indexer or its index. Like accessors, get and set indexers are independent and need occur in pairs.
indexer_declaration :
'get' type '[' argument_declaration ']' 'is' method_body
| 'set' type '[' argument_declaration ']' '=' identifier method_body
Indexed Accessors
Indexed Accessors combine an accessor and an indexer to giv accessors that behave like arrays.
class B is
int[] values;
void init(int size) is
values = new int[size];
si
set int Value[int i] = v is
values[i] = v;
si
get int Value[int i] is
return values[i];
si
si
...
var b = new B(10);
b.Value[0] = 123;
var i = b.Value[3];
indexed_accessor_declaration :
'get' type identifier '[' argument_declaration ']' method_body
| 'set' type identifier '[' argument_declaration ']' '=' identifier method_body
Literal procedures
Enumerations
Enumerations define a new type with an associated set of symbolic values. Enumeration values can be initialized to integers or left uninitialized, in which case they are assigned ascending values.
enum Suits is Hearts, Diamonds, Clubs, Spades si enum BitMask is A = 0x1, B = 0x2, C = 0x4, D = 0x8 si
Each enumeration forms a named scope and so its members must always be qualified with their enumeration name
var s = Suits.Diamonds; var bm = BitMask.A;
Enumerations are strongly typed and can hold only their defined values. Their values can though be explicitly cast to and from int. Variables of enumeration type that are not explicitly initialized are implicitly initialized to zero, which may not correspond to any member if the members have explicit values.
enumeration :
class_specifiers 'enum' identifier 'is' identifier_list 'si'
| class_specifiers 'enum' identifier 'is' 'si'
Statements
Assignment
Assignments set a variable or accessor to the result of evaluating an expression.
j = 10; i = i + 1;
Method call
A call to any regular method is a valid statement. If the method returns a value it is discarded. Accessors and indexers are not methods in this context.
Std.out.flush();
Variable definition
A variable definition associates a name with a data type and a storage location, which may be in the heap, in static data or on the stack depending on where and how the variable is defined.
Variable definitions can appear within class bodies and within method bodies. Initializers are allowed only for static (class) variables and local variables - instance variables should be initialized in a constructor.
Instance variables are private by default.
Variables are always initialized - if no explicit initializer is given for a variable then it receives the default initial value for its type.
class C is
int a; // instance variable cannot have an initializer
static int b = 123; // initializer OK
void test() is
var c = 456; // initializer OK, type inferred from initializer
si
si
field_declaration : type identifier_list ';'
untyped_declaration : 'var' identifier_list ';'
local_declaration :
untyped_declaration
| field_declaration
Constant definition
A constant definition is written as a variable definition with an access type of const. Constants must always be initialized and can only be of primitive type.
const int MAX = 1024;
If-else
The L if-else statement conditionally executes a statement list if the supplied condition evaluates to true. The optional else clause is executed if the condition evaluates to false. The condition must evaluate to a value of type bool.
if c > d then IO.Std.err.println( "c > d" ); fi if a == b then IO.Std.err.println( "a == b" ); elif a != b then IO.Std.err.println( "a != b" ); else throw new System.Exception( "excluded middle" ); fi
if_statement :
'if' expression 'then' block_statement else_statement 'fi'
| 'if' expression 'then' block_statement 'fi'
else_statement :
'else' block_statement
| 'elif' expression 'then' block_statement
While
The L while statement repeatedly executes a statement list until the supplied condition evaluates to false. The condition is tested before the statement is executed. The condition must evaluate to a value of type bool.
while i < 10 do i = i + 1; od
while_statement : 'while' expression 'do' block_statement 'od'
Do
The L do statement repeatedly executes a statement list. The loop can be exited via the break statement.
do i = i + 1; if i > 10 then break; fi od
do_statement : 'do' block_statement 'od'
For
The L for statement is a loop with an initializer, a condition and an increment any or all of which can be omitted. The initializer can be a local variable definition in which case the declared variable is valid only within the loop (however it scope, like all local variables is still method wide). The condition must evaluate to bool
for int i = 0; i < 10; i = i + 1 do IO.Std.err.println( "i is: " + i ); od
for_statement :
'for' within_for_statement expression ';' very_simple_statement 'do' block_statement 'od'
| 'for' within_for_statement ';' very_simple_statement 'do' block_statement 'od'
| 'for' within_for_statement expression ';' 'do' block_statement 'od'
to-line| 'for' within_for_statement ';' 'do' block_statement 'od'
Case
The case statement selects a statement list to execute by comparing the value of an expression to a list of possible values and then executing the statement list associated with the matching value. If no value matches then a default statement list is executed instead, if present.
int v; case v is 1, 2, 3: IO.Std.err.println( "v is 1, 2 or 3" ); is 3, 4, 5: IO.Std.err.println( "v is 4, 5 or 6" ); default: IO.Std.err.println( "v is something else" ); esac
statement :
'case' expression case_list 'esac'
| 'case' expression 'esac'
case_list :
case
| case_list case
| default
| case_list default
case :
'is' expression_list ':' block_statement
default :
default ':' block_statement
Foreach
The foreach statement loops over a list of values supplied by an iterator. At the start of each iteration the loop index variable is assigned the next value produced by the iterator until no more values are available.
The iterator should either inherit from the Generic.Iterator foreach_statement :
'foreach'
type identifier
';'
expression
'do'
block_statement
'od'
|
'foreach'
type identifier
';'
expression
'do'
block_statement
'od'
The break
statement breaks out of loops and case statements and the continue
statement returns control flow to the head of loops. Loops and case statements
can be labelled and break and continue can reference these labels to break out of
nested loop or case statements A break
or a continue
statement within the body of a try statement is not supported and will result
in an error message The return statement jumps to the end of the method,
executing any finally statements encountered on the way. If the return type of
the method is not void then the return statement must return a value. If the
method return type is void then no value can be returned. The try/catch/finally statement handles thrown exceptions. Catch clauses receive
exceptions thrown within the try block and the finally statement is always executed irrespective of whether any exceptions
are caught or pass through all catch causes uncaught.
try ... code ... catch SomeExceptionClass e ... handle SomeExceptionClass ... catch OtherExceptionClass e
... handle OtherExceptionClass ... finally ... always executed ... yrt There is a maximum of one active exception per thread.
Catching an exception stops exception propagation. Throwing a new exception
within a finally handler replaces the active exception with the newly thrown
exception. Returning a value within an exception handler
cancels the active exception L does not insist on a particular order for catches but if a
more general catch is placed before a more specific one then the second catch
may never receive an exception: Each catch block forms a scope including the exception variable definition so each
exception variable is valid only within its catch statement.
Exceptions are thrown with the throw statement. Only objects
that inherit from System.Exception may be thrown The import statement imports code from other L files or from
library files into the L program. All classes defined in an imported file (and all the files
that file imports) are visible throughout the program irrespective of where an
import statement appears. A given file is imported only once regardless of how many
import statements request it. The search order for imports is:
the current directory, any library directories specified on the compiler
command line and then the standard library directories. Importing a library or an object file is an unsafe operation
and is not allowed in sandboxed mode. A number of different sorts of value are possible in L
expressions: "this is an L string" a subset of C-style escapes are supported including: \n \t \x 'x' `Hello World`
A procedure reference is a value of a proc type
x y IO.Std.err Enumeration.MEMBER Test<int>.Length method(a + b);
IO.Std.err.println("hello world"); Sort<int>.sort(list) a[i + 1] All array accesses are bounds
checked at run time and an out of bounds index results in a System.ArrayBoundsException
being thrown new Generic.Vector<int>(30) [p + 3] Pointer dereference is an unsafe operation and is not permitted in sandbox mode (x + y) L does not auto box values of
primitive types but values can be explicitly boxed using the .box
attribute. The .boxattribute
can also be applied to values of class type, in which case it simply returns
the value unchanged. This can be useful in template classes as values of any
type can be boxed this way to obtain an instance of some sub-class of System.Object. Values of primitive types
implement a small set of methods that give them a subset of the protocol of
System.Object including
toString(),
toInt() and
hash()
The null value null is
compatible for equality comparisons with any value whose type is an object, an
array or a pointer. It is compatible with any type in assignments where it
gives the assigned variable its implicit initialization value. L also defines some special fields that give access to
attributes of values they're applied to: .length,
when applied to arrays returns the number of elements in the array .address,
when applied to a variable returns a pointer to its storage location (note for
objects this is the address of the reference to the object, which might not be
what you expect - for the object's address simply cast the reference to a
pointer) when applied to an array returns a
pointer to its storage location of the first element of the array. This pointer
is suitable for passing to C code .sieof, when applied to a type returns the
number of bytes of storage a variable of that type occupies (for class types
this returns the size of the reference to an object of the class type - i.e.
one machine word) .struct, when applied to an object returns a
pointer to the first field in the object. This pointer is suitable for passing
to C code that expects a C struct .none,
when applied to a returns the implicit initialization value for that type.
Unlike null,
which is merely compatible with other types, the
T.noneattribute is
a genuine value of type T and can appear anywhere a literal or value of type T
is allowed. .box,
when applied to a basic type returns a value of object type representing the
same value. The class of the resulting object depends on the type of the boxed
value: Signed integer operations: Operator precedence is: L operators == and
!= compare for
reference equality on objects, arrays and pointers. L has an additional pair of operators,
=~
and
!~, for testing equal and not-equal that operate exactly like
== and
!= on all non object types, arrays and on null references but
behave specially with non-null object operands. These operators are
intended to be overridden to provide a value equality operation
(see below) L supports operator overloading of dyadic operators where one
or both operands is an object. Operators are created by defining operator methods
with standard names. Operator method calls are resolved by looking in both object
operands for the best matching method. Overloadable operators are:
v = new Generic.Vector
Break/Continue
outer: foreach var i; 0..9 do
foreach var j; 0..9 do
if i * j == k then
break outer;
fi
od
od
Return
int f(x) is
return x * 2;
si
void p() is
return;
si
Try/catch/finally
try
int[] a = new a[10];
// throws ArrayBoundsException which is a
// sub-class of System.Exception
a[-1] = 1;
catch System.Exception se
// this will also catch ArrayBoundsException
catch System.ArrayBoundsException
// this catch will never see an exception
yrt
Throw
class BadArgumentException isa System.Exception is
void init( System.String message ) is
super.init( message );
si
si
...
if i < 0 then
throw new BadArgumentException( "i < 0" );
fi
Import
// search the import path for a file named 'stream.l' and make all types
// defined in this file available to this program:
import stream;
// link against the PostgreSQL client library libsq:
import "sq";
// link an external object file produced by another compiler:
import "test.o";
Expressions
Values
Literals
10 // int
123L // long
-27l // long
1000W // word
0xFF10w // word
10c // char
true
false
{1, 2, 3, 4, 5} // type is int[] because all elements are integers
{"A", "B", "C" + "D", null} // type is String[]
{1, 2, "A", "B"} // error, because no one type is assignable from all elements
new String[] { "A" + "B", "C" + "D" } // force a specific type
Unqualified variable or accessor names
Qualified variable or accessor names
Unqualified method calls
Qualified method calls
Array elements
New object creation
Pointer dereferences
Parenthesised expressions:
Boxed values
Method calls on values of primitive types
Null value
Proc references
Attributes
Length
int[] a = new int[10];
IO.Std.err.println( a.length ); // prints 10
Address
int i = 123;
int ptr p = i.address;
int[] a = new int[10];
int ptr p = a.address;
Sizeof
Struct
None
var i = int.none;
var o = System.Object.none;
Box
int i = 123;
System.Int oi = i.box; // .box applied to an int constructs an equivalent System.Int object
Operators
Arithmetic
Precedence
System.String s = "test";
System.String t = "test";
// s and t reference two distinct (although equal in value)
// objects:
if s == t then
IO.Std.err.println( "not reached" );
fi
System.String s = "test";
System.String t = "test";
// s and t have equal values:
if s =~ t then
IO.Std.err.println( "reached" );
fi
Operator overloading
class N is
int v;
N operator+( N n ) is
return new N( v + n.v );
si
N operator=~( N n ) is
// note: compiler ensures opEquals only called if
// both this and n are non-null:
return v == n.v;
si
int operator>( N n ) is
// compiler calls this for any inequality operation.
// we must return negative, zero or positive depending
// on ordering of this and n:
return v n.v;
si
si
...
N n, m, o;
n = m + o; // calls m.operator+(o)
if n > m then // calls m.operator>(o)
...
fi
]