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.

OperatorsDescriptionPrecedenceAssociativity
- ~ !Negate, Compliment, Logical Not12Right
* / %Multiply, Divide, Modulo11Left
+Add, Subtract10Left
<< >>Left Shift, Right Shift9Left
&Bitwise And8Left
^Bitwise Xor7Left
|Bitwise Or6Left
> >= < <= = !=Comparison5Left
||Logical Or4Left
&&Logical And3Left
<. .>Left Compose, Right Compose2Left
<$ $>Left Apply, Right Apply1Left

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 reckeyword 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 xand 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 ;)