Introduction
Lamb is a simple small (WIP) functional programming language. If you are using this, it's probably because I asked you to be a tester, and handed you this book!
This book serves as a guide to lamb and its fluffy oddities.
Syntax
Lamb's syntax most closely resembles that of the Haskell-like languages, but introduces a more familiar C-style like feel through its use of blocks and brace delimiters.
Scripts are stored in .lb
or .lamb
files. These files are then fed from the lexer to the parser, and from the parser to the compiler to be turned into bytecode.
Comments
Lamb only features lime comments, and these begin with a --
:
-- This is a line comment
Reserved Words
Lamb doesn't feature many reserved words:
fn case if elif else return struct union struct from import let def
So... just don't go using these for variable names.
Identifiers
Identifiers in Lamb are able to be a series of numbers, letters or underscores. Ergo, the following are possible identifiers:
x
_
X_123
x_123
_x_123
Identifiers are case sensitive in Lamb, though choosing between camelCase and snake_case is a matter of personal preference.
Top-Level Definitions
Top level definitions are bindings written in the outermost scope, and they begin with the def
keyword. Top level definitions are required to have a type annotation.
def answer: int = 42;
def greeting: list[usv] = "hello there";
Blocks
Blocks in Lamb are denoted by {
}
and are allowed to contain any series of statements followed by an expression. In fact, blocks themselves are expressions, and when the final item in a block is a statement, they evaluate to nil
. Blocks can also contain local definitions, denoted by the let
keyword. Locals don't need type ascriptions.
def block: list[usv] = {
print("hello");
print(" world");
println(" I am writing from Lamb!");
let local := "And the value of this block is this string!";
local
};
Function Syntax
Function definitions in Lamb take the form fn(<args>) -> <expr>
. This allows them to either be in the short form, like:
def inc := fn(x) -> x + 1;
or the more familiar form (from the perspective of a C developer):
def inc := fn(x) -> {
x + 1
};
Because blocks are just expressions, you can omit a return statement in a block if you would like, though you can of course still use one:
def inc := fn(x) -> {
return x + 1;
};
Precedence and Associativity
Most operators in Lamb follow C, with the exception that the bit operators have higher precedence than the comparison operators.
Operators | Description | Precedence | Associativity |
---|---|---|---|
- ~ ! | Negate, Compliment, Logical Not | 12 | Right |
* / % | Multiply, Divide, Modulo | 11 | Left |
+ | Add, Subtract | 10 | Left |
<< >> | Left Shift, Right Shift | 9 | Left |
& | Bitwise And | 8 | Left |
^ | Bitwise Xor | 7 | Left |
| | Bitwise Or | 6 | Left |
> >= < <= = != | Comparison | 5 | Left |
|| | Logical Or | 4 | Left |
&& | Logical And | 3 | Left |
<. .> | Left Compose, Right Compose | 2 | Left |
<$ $> | Left Apply, Right Apply | 1 | Left |
Values
This chapter will go through some of the types available in Lamb.
Nil
nil
is the simplest value in Lamb as it is both a type, as well as a value. nil
is the default value of a block that did not end with an expression, or a function that doesn't explicitly return a value. It's similar to void
from C except that it is also a value.
assert_eq(nil, {});
assert_eq(nil, if true {});
assert_eq(nil, case 1 {});
assert_eq(nil, println(""));
Boolean
Booleans like in other languages are either of the value true
or false
.
assert_eq(true, !false);
assert_eq(false, !true);
Numbers
Unlike most other languages of this size, Lamb makes a hard distinction between floating point numbers and integers. They cannot be used interchangeably, and attempting to do so will result in a type error. Numbers look just as in other languages:
-- Integer Literals
0b123 -- Binary
0x123 -- Hex
0123 -- Octal
123 -- Decimal
-- Float Literals
1.2
1.2e3
Char
A char
is a unicode scalar value and therefor 4 bytes in length. Because they are unicode scalar values, they may not be the item that most people expect. For example, é
is actually two characters, and as such would be two char
in Lamb., namely '\u{0065}'
and '\u{0301}'
'h'
'e'
'l'
'l'
'o'
Escaping
Escaping in Lamb is currently done with the :
character, so to represent a newline char in Lamb, one must write: ':n'
.
'::' -- Results in :
':n' -- Results in \n
':r' -- Results in \r
':t' -- Results in \t
':'' -- Results in '
Strings
A string is simply a list of char
and can be constructed using the double-quote character:
"This is a string"
"This is also a string"
Functions
Functions are just like other values, and can be passed around as variables to be used. They follow the syntax specified in the previous chapter: [rec] fn(<args>) -> <expr>
. The rec
keyword is optional and denotes that a function is recursive,and that the name should be visible within the body of the function.
-- This will fail because the function is not marked as recursive and so `fib` is
-- not defined
let fib := fn(i) -> case i {
0 | 1 -> 1,
_ -> fib(i - 1) + fib(i - 2),
};
-- Now it will work!
let fib := rec fn(i) -> case i {
0 | 1 -> 1,
_ -> fib(i - 1) + fib(i - 2),
};
Functions in Lamb are able to capture items from outside their scope, and because of this are actually closures. If an item cannot be located in the current scope, Lamb will walk backwards until it can find an item with the same name. If it cannot, it will assume the value is a global variable.
Array
An array is a collection of any other kind of values. Like in other languages, those values are required to share a type. Thus, the following are all valid arrays:
[1, 2, 3]
[fn(i) -> i + 1, fn(i) -> i - 1]
To access an element in an array, you can use the postfix index operator []
, like in C:
my_array[0]
my_array[{ x := 1; x - 1}]
my_array[if cond { 0 } else { 1 }]
Control Flow
Unlike many other scripting languages, Lamb doesn't have a concept of "truthy". As a consequence, values must be converted to booleans before being used in places where a boolean is expected, like if
conditions.
If Expressions
In Lamb, the simplest control flow is an if
expression. They look like this:
if <cond-1> {
<body-1>
} elif <cond-2> {
<body-2>
} else {
<body-3>
}
Unlike in many languages, there are no parenthesis requires around the conditions, so if true { } else { }
is syntactically correct.
An if
expression evaluates to the final value expression of the branch that is taken. If there is no else
branch, and none of the branches are taken, the expression evaluates to nil
.
Case Expressions
When looking to test if a variable is one of a series of values, one could write:
if val = 1 || val = 7 {
...
} elif val = 4 || value = 8 {
...
} else {
...
}
But if we want to extend this to arrays it gets messy:
-- Check if an array's first two elements are 1 and 2
if len(arr) > 2 && arr[0] = 1 && arr[1] = 2 {
...
} else {
...
}
Lamb offers a more concise way to do this through pattern matching with case
:
case val {
1 | 7 -> {}
4 | 8 -> {}
_ -> {}
}
case arr {
[1, 2, ..] -> print("Has 1 then 2"),
_ -> print("Doesn't have 1 then 2"),
}
Patterns can be arbitrarily nested and you can even bind patterns to values. For example, to sum an array, one could write:
let sum := rec fn(xs) -> case xs {
[x, rest @ ..] -> x + sum(rest),
[] -> 0,
};
sum([1,2,3,4]);
This function looks at the structure of the array, and if the array contains at least one element, assigns the first to x
and assigns the rest of the array the value rest
.
The syntax for a case
expression is roughly:
case <value> {
(<pattern> -> <body>)*
}
If <body>
is a block expression, no ,
is necessary. Otherwise, it is required to put a ,
.
Similarly to if
, a case
expression evaluates to the value of the branch that is taken, or nil
if no arms are taken.
Loops
Ah loops... yeah... we don't do that here. Loops can be imitated through the use of recursion, so be prepared to have to rid your mind of loops ;)