Every technical blog eventually faces the same problem. Code examples written in Python read strangely to systems programmers; Go examples look alien to TypeScript developers; C examples require a mental translation step for anyone who has spent the last five years in managed languages. The result is friction at precisely the moment when the text needs the reader’s full attention.
Laila Lang is a pseudo-language designed to solve this. It is not intended to be compiled or executed; it is intended to be read. Its syntax is assembled from the parts of four real languages that contribute the most clarity per character, namely Rust, C, TypeScript, and Lua. No piece of Laila Lang syntax is arbitrary; each choice has a source and a reason.
This document is the complete specification. It defines the lexical structure, the type system, the evaluation semantics, the standard library, and a reference grammar. Where a construct has a deliberately defined behavior (for example the result type of an operator, or whether two values compare equal), that behavior is stated explicitly so that examples across the blog never depend on the reader guessing.
0. Design Principles
Laila Lang optimizes for one thing, reading clarity for an audience that knows some mainstream language but not necessarily the same one. Five principles follow from that goal.
Immutability by default. Variables are immutable unless explicitly declared mutable. This is Rust’s contribution, and it matters; when you see let x = 42, you know x will not change. When you see let mut x = 42, the mutation is a signal, not a surprise.
Expressions over statements. Most constructs that produce a value can appear anywhere a value is expected. if is an expression, match is an expression, and a block’s last expression is its value without a return keyword. This reduces syntactic noise.
Explicit types where they carry information, inferred where they do not. Laila Lang has full type inference. Writing let n: u16 = port conveys that port is bounded and unsigned; writing let n = 42 conveys that the type is obvious. Both are valid.
One obvious way. Where two notations would mean the same thing, Laila Lang keeps the one most readers already recognize and drops the other. There is one string-concatenation operator (+), one length query (.len()), and one null-like value (nil). Redundancy is noise, and noise is the enemy of a reading language. The one deliberate exception is absence, which has both a lightweight spelling (T | nil) and an explicit one (Option<T>); the exception is admitted precisely because the two are one underlying value viewed two ways (Section 2), not two competing concepts, and because signatures benefit from the louder form while locals benefit from the quieter one.
Absence is explicit. Laila Lang has exactly one null-like value, nil, borrowed from Lua. There is no null and no undefined. Absence is expressed either as a bare optional (T | nil) for lightweight cases or as Option<T> when a function signature should advertise that a value may be missing. Recoverable failure is a value, Result<T>, never a silently discarded code.
Why a pseudo-language at all
A fair objection is that the world does not need another language, not even a fake one. Why not write every example in Python, or in Rust, and let readers translate?
The answer is that translation is exactly the cost we are trying to remove. A reader fluent in Go who meets a Python decorator, or a Python developer who meets Rust lifetimes, spends part of their attention decoding syntax that is incidental to the point being made. Multiply that across a long post and the incidental load competes with the real idea. A pseudo-language pays a small fixed cost (the reader learns one short dialect, this document) in exchange for removing the per-example translation tax for the rest of the blog.
The design target is clarity per character. Every token should either carry meaning the reader needs or be invisible. That is why the syntax is borrowed rather than invented; borrowed notation is already familiar to some part of the audience, and familiarity is readability. The four sources were chosen because each is the clearest known expression of a specific idea.
- Rust contributes the safety-and-intent vocabulary (
let/mut,match,Result, references, enums with data) because it makes a program’s intent visible in its source rather than hidden in convention. - C contributes the low-level vocabulary (raw pointers, fixed-size arrays, bit operations) because posts about how machines actually work must be able to talk about memory as it physically is.
- TypeScript contributes union types, structural-style interface declarations, and
async/awaitbecause those are the most widely recognized modern forms of those ideas. (Interface conformance is still explicit rather than structural, see Section 9.) - Lua contributes a few small, high-clarity touches (
nil, multiple return values, floor division) because they read cleanly and carry almost no baggage.
Where two sources offered the same idea, the version most readers already recognize won, and the other was dropped to keep the dialect small. The guiding rule for the rest of this document is simple. When two designs are equally expressive, the more readable one wins; when readability ties, the one that prevents a silent mistake wins.
1. Lexical Structure
Comments
// Single-line comment.
/*
Multi-line comment.
Used for longer explanations or temporarily disabling code.
*/Documentation comments use /// and support Markdown.
/// Parses a port number from a string.
/// Returns `Err` if the string is not a valid integer in `[1, 65535]`.
fn parse_port(s: str) -> Result<u16> { ... }Tokens and Whitespace
Source text is UTF-8. Whitespace separates tokens and is otherwise insignificant, except that a newline can terminate a statement (see Semicolons). Tokens are keywords, identifiers, literals, operators, and delimiters. The lexer is greedy; it always consumes the longest valid token.
Semicolons
Semicolons are optional. A newline ends a statement when the text so far already forms a complete expression; if the line ends mid-expression (for example on an operator or an open bracket) the statement continues onto the next line. Use a semicolon when you put multiple expressions on one line, or when the grammar would otherwise be ambiguous.
let x = 1
let y = 2
let a = 1; let b = 2 // two bindings on one lineKeywords
The following identifiers are reserved and cannot be used as names.
let mut const fn return if else match loop while for in
break continue struct enum interface impl for type mod use pub
async await move spawn try catch finally unsafe as self Self true false nilIdentifiers and Casing Conventions
| Kind | Convention | Examples |
|---|---|---|
| Variable, function, module | snake_case | retry_count, parse_frame, net::http |
| Type, struct, enum, interface | PascalCase | TcpStream, ParseError, Shape |
| Constant | SCREAMING_SNAKE_CASE | MAX_RETRIES, DEFAULT_PORT |
Literals
42 // integer, defaults to i32
42u8 // integer with type suffix
0xFF 0o17 0b1010 // hex, octal, binary
1_000_000 // underscores as digit separators
3.14 1.0e-9 2.5f32 // float, f64 by default
true false // bool
'a' '€' '😀' // char, exactly one Unicode scalar value
"hello" // str
f"x={x}" // formatted str (see Section 13)
nil // the absence valueA char literal contains exactly one Unicode scalar value. There is no empty char literal ''; the empty case is represented with Option<char> or an empty str.
Design rationale
Optional semicolons. A statement ends at a newline, so beginners can write one statement per line and never think about terminators. This is safe (and free of the subtle bugs that automatic semicolon insertion causes in some languages) because Laila Lang ends a statement at a newline only when the text so far already forms a complete expression; if a line ends with an operator or an open bracket, the statement continues onto the next line. Advanced readers will recognize this as the same rule that makes newline handling predictable in Go and Swift, which is why explicit semicolons are needed only to put two statements on one line or to resolve genuinely ambiguous grammar.
Why not significant whitespace. Indentation-as-syntax reads cleanly for simple code but becomes ambiguous in the dense, deeply nested snippets common in systems writing (a closure inside a match inside a loop). Braces make nesting unambiguous on the page, and on the page is where this language lives.
Casing as a free namespace. PascalCase for types and snake_case for values let a reader tell what kind of thing a bare identifier is with no surrounding context. Seeing Frame you know it is a type; seeing frame you know it is a value. The compiler does not need this, but the reader does, and reader cost is what this language minimizes. SCREAMING_SNAKE_CASE carries the same instant signal for “this never changes.”
2. Types
Primitive Types
| Type | Description | Literal examples |
|---|---|---|
bool | Boolean | true, false |
i8, i16, i32, i64 | Signed integers (8 to 64 bit) | -1, 2147483647 |
u8, u16, u32, u64 | Unsigned integers (8 to 64 bit) | 0, 255, 65535 |
isize, usize | Platform-pointer-sized signed/unsigned | -1, 1024 |
f32, f64 | IEEE 754 floating point | 3.14, 1.0e-9 |
char | Unicode scalar value | 'a', '€', '😀' |
str | UTF-8 string slice | "hello" |
byte | Alias for u8 | 0xFF, 42 |
nil | The absence value; its own type | nil |
() | Unit, the empty tuple | () |
! | Never, the type of expressions that do not return | (no literal) |
When a numeric literal has no annotation, i32 and f64 are the defaults. Type suffixes are available when needed, for example 42u8 and 3.14f32.
Compound Types
// Tuple, fixed length, heterogeneous
let pair: (str, i32) = ("alice", 30)
// Fixed-size array, homogeneous, size is part of the type (from C/Rust)
let buf: [u8; 16] = [0; 16] // 16 zero bytes
// Slice, a borrowed view into a contiguous sequence, does not own its data
let window: &[i32] = &arr[1..4]
// Growable list, heap-allocated, variable length (analogous to Rust's Vec<T>)
let xs: List<i32> = [1, 2, 3]
// Map (analogous to Rust's HashMap<K, V>)
let m: Map<str, i32> = { "alpha": 1, "beta": 2 }
// Set
let s: Set<str> = { "a", "b" }The three sequence forms are deliberately distinct in type notation and in meaning. The value literal [1, 2, 3] is shared between them and resolves to the right form from the binding’s type (it builds a List<T> by default and coerces to [T; N] when the type asks for it).
| Form | Meaning | Owns data | Resizable |
|---|---|---|---|
[T; N] | Fixed-size array, length N in the type | yes | no |
&[T] | Slice, a borrowed view | no | no |
List<T> | Growable list on the heap | yes | yes |
A list literal [a, b, c] constructs a List<T> by default and coerces to [T; N] when the binding’s type requires it.
Optionals, nil, and Option<T>
Laila Lang offers two ways to express absence, and they share a single underlying value.
A bare optional is a union with nil, written T | nil. It is the lightweight form, used for local variables and fields where the absence case is obvious from context. The ??, ?., and narrowing operators (Section 4) work directly on bare optionals.
let port: u16 | nil = nil
let host: str | nil = lookup(name)An Option<T> is the explicit form. It is an enum with two variants, Some(T) and None. Use it in function signatures and data structures where the possibility of absence should be advertised by the type itself, and where the method API (map, unwrap_or, ?) is convenient.
fn first<T>(xs: &[T]) -> Option<T> {
if xs.len() == 0 { None } else { Some(xs[0]) }
}The relationship between the two forms is defined exactly, so examples never depend on guessing.
- At the value level there are only two cases, present or absent. The absent case is the single value
nil.Noneis the name that theOption<T>enum gives to that same absent value;Noneandnilare equal (None == nilistrue). - The present case is
xitself for a bare optional, andSome(x)for anOption<T>.Some(x) == Some(y)istrueexactly whenx == y. - A bare
nilliteral has typenil, which unifies into anyT | niland into anyOption<T>(asNone). Conversion betweenT | nilandOption<T>is implicit at binding and call sites; the two are different presentations of the same data, not different data.
In short, nil is the value, None is the typed name Option<T> gives that value, and Some/the bare present value carry the contents.
Union Types (from TypeScript)
fn accepts(value: str | i64 | bool) { ... }A union type is satisfied by any value whose type is listed in the union. The compiler requires narrowing before the value is used as a specific member.
fn format(v: str | i64) -> str {
match v {
s: str => s,
n: i64 => str(n),
}
}T | nil is the special case of a union with the nil type, and the optional operators (??, ?.) are defined on exactly that case.
Type Aliases
type Addr = str
type Port = u16
type Payload = List<u8>
type Handler = fn(Request) -> async Result<Response>Type Inference Rules
- A binding without an annotation takes the type of its initializer.
- An unsuffixed integer literal defaults to
i32; an unsuffixed float literal defaults tof64. - A list literal infers
List<T>whereTis the unified type of its elements, unless the context demands[T; N]or&[T]. - There is no implicit numeric widening or narrowing; mixed-type arithmetic requires an explicit
ascast (Section 4). - Generic type parameters are inferred from argument types at the call site when unambiguous, and may be supplied explicitly with the turbofish
::<T>.
Design rationale
The full integer zoo. A managed-language reader may wonder why there are ten integer types. The reason is that a large fraction of this blog is about systems, protocols, and cryptography, where the width of a number is part of its meaning. A TLS record length field is a u16 because the protocol says so, and writing it as a generic int would erase information the post is specifically about. usize and isize are kept separate because they are pointer-sized; their width depends on the target machine, and folding them into u64 would hide that dependency. For posts where width does not matter, inference and the i32/f64 defaults keep the noise away.
Why i32 and f64 are the defaults. They are the types most readers expect an unannotated number to be, matching Rust, C’s int, and the IEEE double. A default should surprise no one.
Why no implicit numeric conversion. Many languages silently widen or narrow numbers, which is convenient right up until a u16 length wraps around or a large i64 quietly loses precision as f32. Requiring an explicit as makes every such conversion visible at the exact point it happens, which is both safer and, for a teaching text, more honest about what the hardware does. The cost is a few extra as tokens, a fair price in prose meant to be studied.
Why two ways to say “absent.” This is the most debated choice, so the reasoning deserves to be plain. A bare optional (T | nil) is the lightest possible notation for “maybe missing,” ideal for a local where the reader can see the whole story at once. But a signature returning str | nil advertises absence less loudly than one returning Option<str>, and signatures are read far more often than they are written. So both exist, the bare form for ergonomics and Option<T> for self-documenting APIs. The decisive choice is that they are the same value underneath (see above), so there is no conversion ceremony and no second kind of null. This sidesteps what Tony Hoare called his “billion dollar mistake,” an absent value the type system never forces you to consider, by keeping absence visible in the type while still feeling lightweight.
Why unions need narrowing. A union str | i64 says a value is one of several types but not which, so using it as a specific type before checking would be a category error. Requiring a match (or an if let) to narrow is what makes unions safe; it is the discipline a careful reader applies mentally, made explicit and checked by the compiler.
Side by side
Absence, from Lua’s nil.
-- Lua
local host = lookup(name) -- nil if not found
if host == nil then host = "localhost" end// La3
let host = lookup(name) ?? "localhost" // host: strLua gives a single nil. La3 keeps that one absence value and adds a type-level wrapper (Option<T>) plus the ?? operator for the default, so absence can be either lightweight or advertised in a signature.
Union types, from TypeScript.
// TypeScript
function format(v: string | number): string {
return typeof v === "string" ? v : String(v);
}// La3
fn format(v: str | i64) -> str {
match v {
s: str => s,
n: i64 => str(n),
}
}Both spell the union A | B. La3 narrows with match instead of typeof, and the compiler checks that the narrowing is exhaustive.
Option<T>, from Rust.
// Rust
fn first<T: Clone>(xs: &[T]) -> Option<T> {
if xs.is_empty() { None } else { Some(xs[0].clone()) }
}// La3
fn first<T>(xs: &[T]) -> Option<T> {
if xs.len() == 0 { None } else { Some(xs[0]) }
}Nearly identical. La3 leaves the Clone bound off the surface because example code does not dwell on ownership unless the post is specifically about it.
3. Variables
Variables are immutable by default. This is Rust’s rule, deliberately kept.
let x = 42 // immutable; type inferred as i32
let mut y = 10 // mutable
y = y + 1 // allowed: y is declared mut
const MAX: u32 = 65535 // compile-time constant; type annotation requiredType annotations follow the identifier with :.
let port: u16 = 8080
let ratio: f64 = successful as f64 / total as f64Shadowing rebinds the same name, optionally to a different type. The old binding is gone; there is no mutation.
let input = read_line() // str
let input = input.trim() // str, shadowed by trimmed version
let input = input.parse::<i64>()? // now i64Destructuring unpacks compound values in a binding.
let (a, b) = (1, 2)
let (ok, err) = try_operation()
// Struct destructuring
let Point { x, y } = origin
// List head/tail (spread)
let [head, ..tail] = itemsCasting uses the as keyword for numeric conversions.
let n: i64 = 300
let b: u8 = n as u8 // truncates to 44
let f: f64 = n as f64 // losslessScoping and Lifetime
Bindings are block-scoped and live from their declaration to the end of the enclosing block. A binding may be shadowed by a later let in the same or a nested block. References (Section 11) may not outlive the value they point to, a rule the compiler enforces.
Design rationale
Immutability by default. When a binding cannot change, a reader can hold its value in mind for the rest of the scope without rechecking, which removes a whole class of “where did this get modified” questions. That is why let is immutable and mut is the marked, visible exception. In concurrent code the same guarantee is what makes sharing safe; an immutable value can be read from many threads with no coordination. The cost is one keyword on the genuinely mutable bindings, which doubles as documentation.
Shadowing is not mutation. Rebinding a name with a new let creates a fresh binding; the old value still exists for anything that captured it. This is why a parse pipeline (read_line, then trim, then parse) is allowed without mut, the reader sees a value transformed through stages, each stage its own immutable binding, rather than one mutable variable changing type underfoot. The distinction matters to advanced readers because a closure that captured the earlier binding keeps the earlier value.
Why as for casts. The same reasoning as the no-implicit-conversion rule; a cast can lose data through truncation, sign change, or lost precision, so it is spelled out where it happens. n as u8 truncating 300 to 44 is surprising only when it is invisible; written explicitly, it is a documented choice.
Side by side
Immutability and shadowing, from Rust.
// Rust
let x = 42;
let mut y = 10;
y += 1;
let input = read_line();
let input = input.trim(); // shadowing, a new binding// La3
let x = 42
let mut y = 10
y += 1
let input = read_line()
let input = input.trim() // shadowing, a new bindingThe model is Rust’s verbatim; La3 only drops the semicolons.
4. Operators
Arithmetic
| Operator | Meaning | Notes |
|---|---|---|
+ - * | Add, subtract, multiply | Integer or float; operands must share a type |
/ | Division | Integer division for integer operands (truncates toward zero) |
% | Remainder | Sign matches the left operand |
** | Exponentiation | Always produces f64; cast the result when an integer is needed, for example (2 ** 10) as i32 |
// | Floor division | Rounds toward negative infinity (from Lua) |
Mixed-type arithmetic requires an explicit cast.
let x: i32 = 7
let y: f64 = x as f64 / 3.0 // 2.333...Comparison
==, !=, <, >, <=, >= produce bool. Equality on primitives compares by value; equality on references compares the pointed-to values; equality on user types dispatches to a custom eq method when defined. nil == nil is true, nil == None is true, and Some(a) == Some(b) is a == b.
Logical
&&, ||, ! are short-circuit and always produce bool. && evaluates its right operand only when the left is true; || evaluates its right operand only when the left is false. Unlike Lua, they do not return one of their operands; the result is always boolean.
if connected && !buffer.is_empty() { flush(buffer) }
let valid = port > 0 && port <= 65535Optional Operators
Two operators act on bare optionals (T | nil). They make the common “use a value if present, otherwise fall back or stop” patterns explicit without the Lua trick of overloading &&/||.
The null-coalescing operator ?? returns its left operand when it is non-nil, otherwise its right operand.
// opts.port: u16 | nil
let port = opts.port ?? 443 // u16: opts.port if present, else 443The optional-chaining operator ?. accesses a field or method only when the receiver is non-nil, and short-circuits to nil otherwise.
// user: User | nil
let name = user?.name // str | nil: nil if user is nil, else user.name
let city = user?.address?.city // chains; nil if any link is nilThese operate on bare optionals (T | nil), not on Option<T> wrappers. For Option<T>, use map, unwrap_or, and the ? operator (Section 8).
Bitwise
&, |, ^, ~, <<, >> operate on integer types. ~ is bitwise NOT (from C), and ^ is XOR.
let flags: u8 = 0b0000_1101
let masked = flags & 0b0000_1111
let shifted = flags >> 2Ranges and String Concatenation
.. and ..= build ranges, 0..n (end exclusive) and 0..=n (end inclusive). The same .. token, in a struct literal, is the struct-update spread ..other (Section 6). These two uses never appear in the same syntactic position, so there is no ambiguity.
String concatenation uses +. There is exactly one concatenation operator.
let s = "Hello, " + name + "!"Length
The length of a string (in bytes), list, array, map, or set is .len(). There is exactly one length query.
let n = items.len()
let empty = items.len() == 0 // or items.is_empty()Reference and Dereference
These appear in the memory model sections.
let r = &x // take a reference to x
let v = *r // dereference r to get its valueOperator Precedence
Highest to lowest, following Rust and C conventions. Operators in the same row share precedence and associate left to right unless noted.
- Postfix
.field,?., method call, indexing[],?(try) - Unary prefix
!,-,~,*,& **(right associative)*,/,%,//+,-<<,>>&^|..,..=(range)==,!=,<,>,<=,>=&&||??as- Assignment
=,+=,-=, and the other compound assignments (right associative)
Design rationale
Logical operators return bool, and why this changed. Lua’s and/or return one of their operands, so a or b yields a when a is truthy. That is elegant in Lua, but it is a trap in a language that otherwise looks like Rust and TypeScript, because a reader from those languages sees user && user.name and expects a boolean, not the value of user.name. Familiar-looking syntax that means something subtly different is the worst possible outcome for a reading language, so &&, ||, and ! always produce bool here, exactly as in C, Rust, Java, and TypeScript. The genuinely useful parts of the Lua idiom, “use this value if present, otherwise a default” and “reach inside only if it exists,” are given their own unambiguous operators, ?? and ?., taken from TypeScript where they already mean precisely that.
?? and ?. operate on bare optionals. x ?? d means “x if present, else d,” and a?.b means “b if a is present, else nil.” Both short-circuit, so the fallback and the access are evaluated only when needed. They are defined on T | nil rather than on Option<T> because they are the lightweight counterpart to the lightweight optional; the Option<T> wrapper has its own method vocabulary (unwrap_or, map, ?) for the same jobs, and keeping the two families separate means the question “does ?? work on an Option” never arises.
Why `always yieldsf64.** Exponentiation naturally produces non-integers (2 -1is0.5,2 0.5is irrational), and integer exponentiation overflows almost immediately. A type-preservingwould have to answer "what is the type of2 -1" with either a runtime error or a silent change of meaning, both worse than one clear rule. Soproducesf64always, and the rare integer power is written(2 10) as i32, which is explicit about rounding back to an integer. The cost is one cast in a minority of cases; the benefit is that the type ofa ** bnever depends on the values ofaandb`.
Why both / and // exist. Integer / truncates toward zero, which is what C and Rust do and what most readers expect. Floor division // rounds toward negative infinity, which is what you want when bucketing a possibly-negative index into fixed-size pages. They genuinely differ for negative operands (-7 / 2 is -3, -7 // 2 is -4), so both are kept; this is one of the few places the language carries two operators, because they do different things rather than the same thing two ways.
One concatenation operator, one length query. An earlier draft let you concatenate with both + and Lua’s .., and take a length with both #x and .len(). That is two ways to do each of two things, pure noise in a language whose whole purpose is reading clarity. + wins for concatenation because nearly every reader already reads + on strings as “join,” which frees .. to mean only ranges and struct spread. .len() wins for length because it is self-describing and discoverable (a reader can guess .len() exists; nobody guesses #), and three extra characters cost nothing in prose meant to be read slowly.
Why the precedence table looks the way it does. It follows the conventions a reader already carries from mathematics and from C/Rust, so that a + b * c and a == b && c parse the way intuition says. ** is right-associative and sits just below the prefix operators so that 2 ** 3 ** 2 is 2 ** 9, matching mathematical convention. ?? sits low, below comparison and the boolean operators, because it is usually the last step (compute something that might be nil, then supply a default). as sits lowest of the binary operators so that n as f64 / 2.0 casts before dividing, which is almost always the intent.
Why ~ and ! are different. C readers expect ~ for bitwise NOT and ! for logical NOT, and keeping them distinct means ~flags (flip every bit) and !done (negate a boolean) are never confused, which a single ! for both would invite.
Side by side
The logical-operator difference, Lua versus La3. This is the one place La3 deliberately departs from a source.
-- Lua: `and`/`or` return an operand, not a bool
local name = user and user.name -- user.name, or nil/false
local port = opts.port or 443 -- opts.port, or 443// La3: && and || are strictly bool; the value idioms get their own operators
let name = user?.name // str | nil
let port = opts.port ?? 443 // u16Lua overloads and/or to also do optional access and defaulting. La3 splits those into ?. and ?? so that &&/|| always mean “boolean,” which is what Rust and TypeScript readers expect.
Floor division, from Lua.
-- Lua
print(-7 // 2) -- -4// La3
io.println(str(-7 // 2)) // -4 (// floors; / would give -3)Optional chaining and coalescing, from TypeScript.
// TypeScript
const city = user?.address?.city ?? "unknown";// La3
let city = user?.address?.city ?? "unknown"Identical; these two operators are taken from TypeScript unchanged.
5. Functions
Basic Declaration
fn add(a: i32, b: i32) -> i32 {
a + b // last expression is the return value, no semicolon
}
fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
if x < lo { return lo } // early return is fine
if x > hi { return hi }
x
}
// No return value; the return type is () (unit)
fn log(msg: str) {
io.println("[LOG] " + msg)
}Multiple Return Values (from Lua)
A function can return a tuple, and the caller destructures it.
fn divmod(a: i64, b: i64) -> (i64, i64) {
(a / b, a % b)
}
let (quotient, remainder) = divmod(17, 5)Generics (from Rust/TypeScript)
Type parameters go after the function name, constrained by interfaces with :.
fn max<T: Ord>(a: T, b: T) -> T {
if a >= b { a } else { b }
}
fn first<T>(xs: &[T]) -> Option<T> {
if xs.len() == 0 { None } else { Some(xs[0]) }
}
fn zip<A, B>(xs: List<A>, ys: List<B>) -> List<(A, B)> {
let len = min(xs.len(), ys.len())
(0..len).map(|i| (xs[i], ys[i]))
}Variadic Parameters
fn sum(...args: i32) -> i32 {
args.reduce(0, |acc, x| acc + x)
}
let total = sum(1, 2, 3, 4, 5) // 15Closures (from Rust)
Closures capture variables from the enclosing scope. Types are inferred from usage. The syntax uses | pipes.
let double = |x| x * 2
let add = |a, b| a + b
let verbose = |name: str| -> str { "Hello, " + name }Closures capture by reference by default. move transfers ownership into the closure, which is necessary when the closure outlives the current scope, for example when it is passed to a new thread.
let threshold = 100
let exceeds = |x| x > threshold // borrows threshold
let base = compute_base()
let scaled = move |x| x * base // owns base; base is invalid here afterHigher-order collection methods.
let evens = [1..=10].filter(|x| x % 2 == 0)
let squares = evens.map(|x| x * x)
let total = squares.reduce(0, |acc, x| acc + x)
let sorted = records.sort_by(|a, b| a.timestamp < b.timestamp)
let grouped = entries.group_by(|e| e.category)Async Functions (from TypeScript)
An async fn returns a future. await suspends until the future resolves.
async fn fetch_json<T>(url: str) -> Result<T> {
let resp = await http.get(url)
resp.json::<T>()
}
async fn main() {
match await fetch_json::<Config>("https://config.example.com/v1") {
Ok(cfg) => apply(cfg),
Err(e) => io.println("config unavailable: " + e),
}
}Multiple futures can be awaited together; await all(...) resolves when all complete (Section 12).
async fn check_all(hosts: List<str>) -> Map<str, bool> {
let futures = hosts.map(|h| ping(h))
let results = await all(futures)
hosts.zip(results).collect()
}The ? Operator
? propagates failure. Applied to a Result, it returns Err(e) from the enclosing function when the value is Err(e), and evaluates to v when it is Ok(v). Applied to an Option, it returns None on None and evaluates to v on Some(v). It may appear only inside a function whose return type is a matching Result or Option (Section 8).
Design rationale
Blocks are expressions. The last expression of a block is its value, so let x = if cond { a } else { b } needs no temporary and no assignment in each branch. Beginners can ignore this and still write return; advanced readers get composability, since match, if, and {} can appear wherever a value is wanted. return still exists for early exit, which is clearer than forcing every guard clause into an expression.
Tuples for multiple returns. Returning (quotient, remainder) and destructuring at the call site is clearer than C-style out-parameters (which hide outputs in the argument list) or forcing a named struct for every multi-value return. The caller sees exactly how many values come back and names them on the spot.
Closures capture by reference, move to own. By default a closure borrows what it uses, which is cheap and is what you want for a closure passed to map and used immediately. But a closure that outlives the current scope (handed to a new thread, or stored for later) cannot safely borrow locals that are about to disappear, so move transfers ownership into it. Making move explicit means the reader can see, right at the closure, whether it borrowed or took ownership, which is exactly the information needed to reason about lifetimes and threads.
async fn returns a future. An async function does not run to completion when called; it returns a future that makes progress only when awaited. This models cooperative concurrency honestly (Section 12), and it is why await is visible at every suspension point rather than hidden.
The ? operator. Propagating errors by hand (matching every fallible call and returning the Err) buries the happy path under boilerplate. ? collapses that to one character at the call site, so the reader sees the normal flow with failure handling marked but not dominant. It is restricted to functions returning Result or Option because that is the only context where “return the error from here” has a meaning.
Side by side
Implicit return and ?, from Rust.
// Rust
fn read_config(path: &str) -> Result<Config, Error> {
let text = fs::read_to_string(path)?;
let cfg = serde_json::from_str(&text)?;
Ok(cfg)
}// La3
fn read_config(path: str) -> Result<Config> {
let text = fs.read(path)?
let cfg = json.decode::<Config>(text)?
Ok(cfg)
}Same ? propagation and same trailing-expression return. La3 fixes the error side into Result<T> (the error is always a message) so signatures stay short.
Multiple return values, from Lua.
-- Lua
local function divmod(a, b) return a // b, a % b end
local q, r = divmod(17, 5)// La3
fn divmod(a: i64, b: i64) -> (i64, i64) { (a // b, a % b) }
let (q, r) = divmod(17, 5)Lua returns several values natively; La3 expresses the same thing as one tuple, typed and destructured the same way.
Closures, from Rust.
// Rust
let double = |x| x * 2;
let scaled = move |x| x * base;// La3
let double = |x| x * 2
let scaled = move |x| x * baseAsync, from TypeScript.
// TypeScript
async function fetchJson<T>(url: string): Promise<T> {
const resp = await fetch(url);
return resp.json();
}// La3
async fn fetch_json<T>(url: str) -> Result<T> {
let resp = await http.get(url)
resp.json::<T>()
}async/await reads the same. La3 makes the fallible result explicit as Result<T> instead of relying on a thrown rejection.
6. Data Structures
Structs
struct Point {
x: f64,
y: f64,
}
struct TlsRecord {
content_type: u8,
version: u16,
length: u16,
payload: List<u8>,
}Construction uses TypeName { field: value } syntax. The struct-update spread ..other copies the remaining fields (from Rust).
let origin = Point { x: 0.0, y: 0.0 }
let moved = Point { x: 3.0, ..origin } // y: 0.0 copied from originMethods are defined in an impl block. self is the receiver, and associated functions with no receiver serve as constructors. Three receiver forms exist. self takes ownership, mut self takes ownership and allows the method to mutate its own local copy before returning it (the value-transform idiom), and &self / &mut self borrow without taking ownership (Section 11). mut self does not contradict immutability by default; the caller’s binding is still consumed, and what mutates is the method’s owned copy.
impl Point {
fn new(x: f64, y: f64) -> Point {
Point { x, y } // shorthand: field name == variable name
}
fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x
let dy = self.y - other.y
(dx*dx + dy*dy) ** 0.5
}
fn translate(mut self, dx: f64, dy: f64) -> Point {
self.x += dx
self.y += dy
self
}
}
let p = Point.new(3.0, 4.0)
let d = p.distance(&Point.new(0.0, 0.0)) // 5.0Enums (from Rust)
Variants can carry data, which distinguishes Laila Lang enums from C-style integer enums. This is the key thing Laila Lang borrows from Rust.
enum Direction { North, South, East, West }
enum IpAddr {
V4(u8, u8, u8, u8),
V6(str),
}
enum Shape {
Circle(f64),
Rect { width: f64, height: f64 },
Triangle(f64, f64, f64), // three sides
}
enum Result<T> {
Ok(T),
Err(str),
}
enum Option<T> {
Some(T),
None,
}Result<T> is an ordinary library enum spelled out here; nothing about it is special syntax. Option<T> has the shape of an ordinary enum, but the language adds one privileged rule on top of that shape. Its None variant is identified with the single absence value nil, so None == nil, and a bare T | nil and an Option<T> convert implicitly (Section 2). That rule is what lets the two absence spellings stay a single underlying value; it is the only place an enum receives special treatment, and it is deliberate rather than accidental.
Lists, Slices, and Arrays
// Fixed-size array, length is part of the type, stored on the stack (from C/Rust)
let buf: [u8; 512] = [0; 512]
// Slice, a borrowed view into an array or list, does not own data
fn process(data: &[u8]) { ... }
// Growable list, heap-allocated, variable length
let mut log: List<str> = []
log.push("entry one")
log.push("entry two")
log.pop() // removes and returns "entry two"
let len = log.len() // 1
let slice = log[0..1]Maps
let mut headers: Map<str, str> = {}
headers["Content-Type"] = "application/json"
headers["Authorization"] = "Bearer " + token
let ct = headers.get("Content-Type") // Option<str>
let ct = headers["Content-Type"] // str; panics if absent
headers.remove("Authorization")
for (key, val) in headers {
io.println(key + ": " + val)
}Sets
let mut seen: Set<str> = {}
seen.insert(ip)
let is_new = !seen.contains(ip)Design rationale
Methods live in impl, separate from the struct. Keeping the data layout (struct) apart from the behavior (impl) lets a reader see the shape of the data without scrolling past its methods, and it allows behavior to be added in more than one block (for instance one impl per interface). Construction uses Type { field: value } so every field is named at the call site, which keeps a constructor readable even with many fields and removes the positional-argument guessing that plagues long parameter lists.
Enums carry data, and why that is the headline feature. A C enum is just named integers. A Laila Lang enum is a sum type; each variant can carry its own data, so Shape is a Circle(f64) or a Rect { width, height } and nothing else. This lets the type make illegal states unrepresentable, a value that is “a circle with a radius” cannot also accidentally carry a width. Combined with exhaustive match (Section 7), the compiler then forces every site to handle every variant, so adding a variant later surfaces every place that must change. This pairing, sum types plus exhaustive matching, is the single most valuable idea borrowed from Rust, which is why Option and Result are themselves ordinary enums rather than built-in magic.
Three sequence types because the hardware has three situations. A fixed array [T; N] carries its length in its type and typically lives on the stack, which is what a protocol header or a hash output is. A slice &[T] is a borrowed window into someone else’s storage, owning nothing, which is what a function should take when it only needs to read. A List<T> owns a resizable heap buffer, which is what you build up at runtime. These are different runtime things, so they get different notations; collapsing them would hide exactly the allocation and ownership facts that systems posts are about. The growable form is spelled List<T> rather than [T] specifically because [T] reads as a slice to the largest expert audience (Rust readers), and notation that misleads the expert is worse than notation that is merely unfamiliar.
Braces for maps and sets. { "a": 1 } is a map and { "a", "b" } is a set, reusing the brace literal most readers associate with “a collection written out.” The form is disambiguated by its contents (key-value pairs versus bare values) and by the binding’s type, so {} is an empty map or set according to its annotation. This keeps collection literals visually uniform with struct literals while staying unambiguous in practice.
Side by side
Enums with data, from Rust.
// Rust
enum Shape {
Circle(f64),
Rect { width: f64, height: f64 },
}// La3
enum Shape {
Circle(f64),
Rect { width: f64, height: f64 },
}Identical; this is the central idea La3 takes from Rust. Contrast a C enum, whose variants are only integers and cannot carry data.
// C: a variant is just a name for an integer
enum Shape { CIRCLE, RECT }; // the radius and width live elsewhere, by conventionFixed-size arrays, from C.
// C
uint8_t buf[512] = {0};// La3
let buf: [u8; 512] = [0; 512]Same idea, the length is part of the type; La3 writes the size inside the type as [u8; 512].
Growable list, from Rust’s Vec.
// Rust
let mut log: Vec<String> = Vec::new();
log.push("entry".into());// La3
let mut log: List<str> = []
log.push("entry")La3 calls it List<T> rather than [T] so it never reads as a Rust slice.
7. Control Flow
if as Expression (from Rust)
// As a statement
if n < 0 {
io.println("negative")
} else if n == 0 {
io.println("zero")
} else {
io.println("positive")
}
// As an expression, both branches must return the same type
let label = if score >= 90 { "A" }
else if score >= 80 { "B" }
else { "F" }match (from Rust)
match is exhaustive; the compiler requires every possible value to be handled, either explicitly or via a _ wildcard. It is also an expression.
match code {
200 => io.println("OK"),
301 | 302 => io.println("redirect"),
404 => io.println("not found"),
500..=599 => io.println("server error"),
_ => io.println("unexpected: " + str(code)),
}
let description = match code {
200 => "OK",
404 => "Not Found",
_ => "Other",
}Pattern variants.
// Match a range
match byte {
0x00 => "null",
0x01..=0x1F => "control",
0x20..=0x7E => "printable ASCII",
0x7F => "DEL",
_ => "extended",
}
// Destructure an enum variant
match addr {
IpAddr.V4(a, b, c, d) => f"{a}.{b}.{c}.{d}",
IpAddr.V6(s) => s,
}
// Guard with `if`, the pattern only matches when the guard is true
match shape {
Shape.Circle(r) if r > 1000.0 => io.println("enormous circle"),
Shape.Circle(r) => io.println(f"circle r={r}"),
_ => io.println("other"),
}
// Bind the matched value with `@`
match n {
x @ 1..=12 => io.println(f"month {x}"),
_ => io.println("invalid month"),
}if let does single-variant matching without a full match.
if let Some(port) = config.port {
bind(port)
}
if let IpAddr.V4(a, b, c, d) = addr {
io.println(f"IPv4: {a}.{b}.{c}.{d}")
}while let loops as long as a pattern matches.
while let Some(item) = queue.pop() {
process(item)
}Loops
// loop, infinite, break with a value (from Rust)
let result = loop {
let line = io.read_line()
match parse(line) {
Ok(v) => break v, // loop evaluates to v
Err(_) => continue, // try again
}
}
// while
while !stream.is_closed() {
let chunk = stream.read_chunk()
buffer.append(chunk)
}
// for..in over any iterable
for packet in capture {
inspect(packet)
}
// Numeric range (end exclusive)
for i in 0..n {
arr[i] = compute(i)
}
// Inclusive range
for i in 1..=10 {
io.println(str(i))
}
// Enumerate, yields (index, value) pairs
for (i, item) in items.enumerate() {
io.println(f"{i}: {item}")
}Design rationale
if and match are expressions. Because they produce values, control flow composes; a value can be computed by branching with no mutable temporary, and the type checker verifies that every branch yields the same type. This is the same reasoning as “blocks are expressions” in Section 5, applied to branching.
Exhaustive match. The compiler rejects a match that does not cover every possible value. For beginners this catches the “I forgot the nil case” bug before it can run. For advanced readers it is a maintenance tool, adding a variant to an enum turns every now-incomplete match into a compile error that points straight at the code that must change, which is how large programs evolve safely. The _ wildcard is there when a catch-all is genuinely intended, so exhaustiveness is a help rather than a straitjacket.
if let and while let. A full match to handle one variant and ignore the rest is more ceremony than the situation deserves, so if let Some(x) = opt and while let Some(x) = queue.pop() are the ergonomic short forms. They read as “if this matches, bind and proceed,” which is exactly the intent.
loop with a break value. A dedicated infinite loop states “this repeats until something inside stops it” more honestly than while true, and allowing break value lets the loop be an expression that computes a result (retry until a parse succeeds, then yield the parsed value). The alternative, a mutable variable set inside the loop and read afterward, is precisely the mutable-state pattern the rest of the language avoids.
Side by side
match as an expression, from Rust.
// Rust
let description = match code {
200 => "OK",
404 => "Not Found",
_ => "Other",
};// La3
let description = match code {
200 => "OK",
404 => "Not Found",
_ => "Other",
}The same exhaustiveness and the same expression form; only the trailing semicolon differs.
8. Error Handling
Laila Lang has two error mechanisms with a clear division of labor. Result<T> models recoverable failure inside Laila Lang code, where the caller is expected to handle the outcome. try/catch models the boundary with external systems that signal failure by throwing. The rule is simple; if you write the failing function, return Result, and if you call into something that throws (I/O, network, FFI), wrap it in try/catch.
Result<T> (from Rust)
Result<T> is either Ok(value) or Err(message). Functions that can fail return a Result.
fn read_config(path: str) -> Result<Config> {
let text = fs.read(path)? // ? propagates Err immediately
let cfg = json.decode::<Config>(text)?
Ok(cfg)
}The ? operator (from Rust); if the value is Err(e), the enclosing function returns Err(e) immediately, and if it is Ok(v), the expression evaluates to v. It can appear only inside functions that return Result or Option.
Handling a Result.
match read_config("config.json") {
Ok(cfg) => apply(cfg),
Err(e) => {
io.println("failed to load config: " + e)
os.exit(1)
}
}Convenience methods avoid a verbose match for common patterns.
let cfg = read_config("config.json").unwrap() // panics on Err
let cfg = read_config("config.json").unwrap_or(default_config())
let cfg = read_config("config.json").unwrap_or_else(|_| make_default())
let n = parse_num(s).map(|v| v * 2) // Ok(v*2) or Err unchanged
let v = load(path).and_then(|raw| parse(raw)) // chain Result-returning fns
let msg = op().map_err(|e| "operation failed: " + e) // transform the errorOption<T>
let first: Option<str> = items.first()
if let Some(v) = first {
process(v)
}
let v = first.unwrap_or("default")
let v = first.map(|s| s.to_upper())
let v = first? // in an Option-returning function: return None if first is Nonetry / catch
When calling into external systems that use exception-style signaling, Laila Lang uses try/catch blocks. This is intentionally distinct from Result; it marks the boundary between Laila Lang’s value-based error model and external exception-based systems. Unlike Err(str), external exception objects carry a .message field and can be caught by named type.
try {
let conn = db.connect(dsn)
let rows = conn.query("SELECT id, name FROM users WHERE active = 1")
for row in rows {
process(row)
}
} catch e: ConnectionError {
io.println("database unreachable: " + e.message)
os.exit(1)
} catch e {
io.println("unexpected error: " + str(e))
} finally {
db.close()
}Design rationale
Two mechanisms, one boundary. Having both Result and try/catch looks redundant until you notice they describe two genuinely different situations. Result<T> is for failure the author of a function anticipates and the caller is expected to handle; the failure is part of the function’s contract, so it is part of its return type. try/catch is for the edge of the program where it meets systems that signal failure by throwing (operating-system calls, network libraries, foreign code), where there is no Result to inspect because the failure arrives as an exception. The rule that resolves every real case is mechanical. If you wrote the failing function, return Result; if you are calling something that throws, wrap it in try/catch. The two never compete for the same call, so the apparent redundancy is actually a clean division of labor between Laila Lang’s own value-based errors and the exception-based world outside it.
Why errors are values, and why ?. Representing a recoverable error as a returned Result rather than an unwound exception keeps the failure visible in the type, where it cannot be silently ignored; you must match it, unwrap it, or propagate it with ?. ? exists so that propagation, the common case, costs one character instead of a five-line match, keeping the happy path readable. Err(str) carries a human message rather than a typed error hierarchy because, in illustrative blog code, a message is what the reader needs to understand the failure; a production language would likely use a typed error, and a post on that topic is free to show one.
Side by side
try/catch, from TypeScript. (The Result/? comparison appears in Section 5.)
// TypeScript
try {
const conn = await db.connect(dsn);
for (const row of await conn.query(sql)) process(row);
} catch (e) {
console.error("db error:", e.message);
} finally {
db.close();
}// La3
try {
let conn = db.connect(dsn)
for row in conn.query(sql) { process(row) }
} catch e: ConnectionError {
io.eprintln("db error: " + e.message)
} finally {
db.close()
}The shape is TypeScript’s. La3 adds typed catch clauses and reserves the whole construct for the external, exception-throwing boundary, using Result everywhere inside its own code.
9. Interfaces and Trait Bounds
A Laila Lang interface describes a set of methods a type must implement. The declaration syntax follows TypeScript’s structural style, but satisfaction is nominal; a type satisfies an interface only when there is an explicit impl block for it, never merely by having methods with the right shapes. This combination (familiar declaration, explicit opt-in) is deliberate, it keeps the declaration readable while making conformance unambiguous at a glance.
interface Encode {
fn encode(self) -> List<u8>
}
interface Decode {
fn decode(raw: &[u8]) -> Result<Self>
}
// A combined bound
interface Codec: Encode + Decode {}Implementation.
struct Frame {
id: u16,
payload: List<u8>,
}
impl Encode for Frame {
fn encode(self) -> List<u8> {
let mut out: List<u8> = []
out.push((self.id >> 8) as u8)
out.push((self.id & 0xFF) as u8)
out.extend(&self.payload)
out
}
}A generic function constrained to an interface.
fn send<T: Encode>(conn: &mut TcpStream, value: T) -> Result<()> {
let bytes = value.encode()
conn.write_all(&bytes)
}Design rationale
Structural declaration, nominal conformance, and why the hybrid. The two mainstream models each get one thing right and one thing wrong for a reading language. Go’s structural interfaces (a type conforms automatically if it has the right methods) are wonderfully terse to declare, but they make conformance invisible; a reader cannot tell from a type whether it was meant to satisfy an interface or merely happens to. Rust’s traits are explicit (impl Trait for Type), so conformance is unmistakable, but the trait declaration itself is heavier. Laila Lang takes the readable half of each. The declaration looks like a TypeScript interface, a plain list of method shapes, and conformance is an explicit impl Encode for Frame block as in Rust. The result is that both questions a reader asks, “what does this interface require” and “does this type promise to satisfy it,” have answers visible on the page rather than inferred. The cost is one impl block per conformance, which is also where the methods are written, so it adds no real ceremony.
Side by side
The hybrid in one view, the declaration is TypeScript-shaped and the conformance is Rust-shaped.
// TypeScript: structural; a type conforms automatically if its methods match
interface Encode { encode(): Uint8Array }// Rust: nominal; conformance is an explicit impl
trait Encode { fn encode(&self) -> Vec<u8>; }
impl Encode for Frame { fn encode(&self) -> Vec<u8> { /* ... */ } }// La3: TypeScript-style declaration, Rust-style explicit impl
interface Encode { fn encode(self) -> List<u8> }
impl Encode for Frame { fn encode(self) -> List<u8> { ... } }La3 reads like the TypeScript declaration but requires the explicit impl like Rust, so conformance is never accidental.
10. Modules
use net::http
use net::dns
use crypto::{sha256, hmac_sha256}
use std::io::{println, read_file, write_file}
use std::collections::{Map, Set}Defining a module.
mod codec {
pub fn encode(frame: &Frame) -> List<u8> { ... } // pub = visible outside mod
pub fn decode(raw: &[u8]) -> Result<Frame> { ... }
fn checksum(data: &[u8]) -> u32 { ... } // private to mod
}
use codec::{encode, decode}Items are private to their module unless marked pub. Paths use :: to separate module segments and . to access fields, methods, and enum variants.
Design rationale
Private by default, pub to expose. A module’s surface area should be a deliberate choice, not the accidental sum of everything defined inside it. Defaulting to private lets a reader trust that a non-pub function is an internal detail with no callers outside the module, which makes the module easier to reason about and to change. pub then reads as “this is part of the contract.”
Two separators, :: and ., on purpose. Paths through the module and type namespace use :: (net::http, Type::method), while access into a value uses . (response.status, frame.encode()). Keeping them distinct lets a reader tell at a glance whether a::b is navigating the static structure of the program or a.b is reaching into a runtime value, a distinction a single dot would blur. This follows Rust, and it is one of the few places where a little extra punctuation buys real clarity.
Side by side
Imports, from Rust.
// Rust
use crypto::{sha256, hmac_sha256};
use std::collections::{HashMap, HashSet};// La3
use crypto::{sha256, hmac_sha256}
use std::collections::{Map, Set}The use path syntax and the :: separators are Rust’s; La3 renames the collections to Map/Set.
11. Memory Model
Most Laila Lang code does not need to think about memory. This section exists for posts about systems programming, operating system internals, and language implementation.
References (safe, from Rust)
A reference is a pointer with lifetime tracking. &T is a shared (read-only) reference, and &mut T is an exclusive (writable) reference. At any point a value has either any number of &T references or exactly one &mut T reference, never both.
fn sum(xs: &[i32]) -> i32 { // borrows xs, does not copy or own it
xs.reduce(0, |a, x| a + x)
}
fn double_in_place(x: &mut i32) {
*x *= 2 // * dereferences the reference
}
let mut n = 21
double_in_place(&mut n) // n is now 42Raw Pointers (from C)
Raw pointers are *T (read-only) and *mut T (writable). Unlike references, they carry no lifetime or aliasing guarantees. Dereferencing a raw pointer requires an unsafe block, a signal to the reader that the invariants are being maintained by human reasoning, not by the compiler.
let arr: [i32; 5] = [10, 20, 30, 40, 50]
let p: *i32 = &raw arr[0] // raw pointer to the first element
unsafe {
let third = *(p + 2) // pointer arithmetic: p + 2 advances by 2 × sizeof(i32) = 8 bytes
// third == 30
}&raw takes a raw pointer (distinct from &, which takes a safe reference). The compiler knows the pointed-to type and scales arithmetic accordingly; p + n advances the address by n * sizeof(T), not by n bytes. This is why going out of bounds is not automatically caught; the arithmetic produces a valid-looking address regardless of whether the target memory belongs to the current allocation.
Heap allocation.
// Allocate n bytes; returns a raw pointer to the first byte
let p: *mut u8 = alloc(n)
// Must be freed with the same size
dealloc(p, n)alloc and dealloc correspond to malloc/free in C or the raw allocator API in Rust. They appear in posts about heap internals; ordinary Laila Lang code uses List and Map, which manage memory automatically.
Pointer Arithmetic
let base: *i32 = &raw buf[0]
let elem: *i32 = base + 3 // address of buf[3]
// arr[i] and *(arr_ptr + i) are the same operation
let v1 = arr[3]
let v2 = unsafe { *(base + 3) }
// v1 == v2Design rationale
Two pointer-like things because there are two different promises. A reference (&T, &mut T) is the safe one; the compiler tracks how long it lives and enforces that a value has either many readers or one writer at a time, never both. That single rule, aliasing xor mutability, is what lets a reader (and the compiler) be sure a value is not being changed out from under another part of the code, and it is also what lets an optimizer reorder memory accesses safely. A raw pointer (*T, *mut T) makes none of those promises; it is just an address, exactly as in C, and it exists because posts about how memory physically works need to talk about addresses with the safety machinery out of the way.
Why &raw is separate from &. Taking a safe reference and taking a raw pointer are different acts with different guarantees, so they get different syntax; &x produces a tracked reference while &raw x produces a bare address. An earlier draft used & for both, which quietly implied that a raw pointer carried a reference’s guarantees. Separating them makes the loss of safety visible at the exact point it happens.
unsafe as a sign for the reader. Dereferencing a raw pointer is wrapped in unsafe { ... } not because the block does anything special at runtime, but because it marks the region where the compiler has stopped checking and a human is vouching for correctness. In a teaching text that marker is the whole point; it tells the reader “the invariants here are maintained by argument, not by the type system, so read carefully.”
Pointer arithmetic is scaled by type. p + n advances by n * sizeof(T), not n bytes, matching C, so p + 1 points at the next element rather than the next byte. This is why an out-of-bounds offset is not caught automatically; the arithmetic produces a plausible address whether or not it still belongs to the original allocation, which is precisely the hazard that posts about memory safety need to be able to show.
Side by side
Safe references, from Rust.
// Rust
fn double_in_place(x: &mut i32) { *x *= 2; }
let mut n = 21;
double_in_place(&mut n); // n == 42// La3
fn double_in_place(x: &mut i32) { *x *= 2 }
let mut n = 21
double_in_place(&mut n) // n == 42Raw pointers, from C.
// C
int arr[5] = {10, 20, 30, 40, 50};
int *p = &arr[0];
int third = *(p + 2); // 30// La3
let arr: [i32; 5] = [10, 20, 30, 40, 50]
let p: *i32 = &raw arr[0]
let third = unsafe { *(p + 2) } // 30The pointer arithmetic matches C exactly. La3 adds &raw to mark the unsafe address-taking and wraps the dereference in unsafe, neither of which C requires, because the whole point of those markers is to make the loss of safety visible to a reader.
12. Concurrency
Threads
let handle = spawn {
// runs concurrently in a new OS thread
let result = compute_heavy()
result
}
let value = handle.join() // blocks until thread completes; returns its valueMultiple threads communicating via channels.
let ch = channel<str>(capacity: 32) // buffered channel
spawn {
for packet in capture {
ch.send(inspect(packet))
}
ch.close()
}
for report in ch { // iterates until channel is closed
io.println(report)
}Async Concurrency
async/await is single-threaded concurrency; tasks are interleaved at await points, not run on parallel threads. Spawning the futures starts them; awaiting is where control can switch between them.
async fn resolve_all(hosts: List<str>) -> Map<str, Result<List<str>>> {
let jobs = hosts.map(|h| async { (h, await dns.resolve(h)) })
let pairs = await all(jobs)
pairs.collect()
}race resolves with the first future to complete.
async fn fetch_with_timeout(url: str, ms: u64) -> Result<Response> {
let fetching = http.get(url)
let timeout = async {
await os.sleep(ms)
Err("timeout after " + str(ms) + "ms")
}
await race(fetching, timeout)
}Design rationale
Threads and async are different tools, so both exist. spawn plus join is real parallelism, separate OS threads that can run at the same instant on different cores, which is what you want for CPU-bound work such as hashing or compression. async/await is concurrency without parallelism, a single thread interleaving many tasks at their await points, which is what you want for I/O-bound work where the program spends its time waiting on the network or disk rather than computing. Conflating them would hide the property that actually matters for a given post, whether these things run at the same time or merely take turns, so the language keeps both notations and uses them to mean exactly those two different things.
Channels for thread communication. Threads share work by sending values over a channel rather than by sharing mutable memory, which keeps the immutability-and-ownership story intact across thread boundaries; the value is handed off, not aliased. Iterating a channel until it closes (for x in ch) reads as a stream of results, which is usually what concurrent producers are.
all and race name the two common await patterns. Waiting for every future (all) and taking whichever finishes first (race) are the two things concurrent code almost always wants, so they are named primitives rather than something each post re-derives. race is also how a timeout is expressed, run the real work against a sleep and take whichever wins.
Side by side
Threads, from Rust.
// Rust
let handle = std::thread::spawn(|| compute_heavy());
let value = handle.join().unwrap();// La3
let handle = spawn { compute_heavy() }
let value = handle.join()Awaiting many futures, from TypeScript.
// TypeScript
const results = await Promise.all(hosts.map(h => ping(h)));// La3
let results = await all(hosts.map(|h| ping(h)))Promise.all becomes all, and the rest reads the same.
13. Standard Library
The standard library is a set of many small, fully independent modules rather than one monolithic runtime, and a program is understood to draw in only the modules it actually uses. The organizing rules behind that are spelled out at the end of this section. The following namespaces are available in any Laila Lang example without an explicit use. Signatures use -> for synchronous and -> async for asynchronous return types.
io
io.print(v: any)
io.println(v: any)
io.read_line() -> str
io.read_all() -> str
io.eprintln(v: any) // stderrfs
fs.read(path: str) -> Result<str>
fs.read_bytes(path: str) -> Result<List<u8>>
fs.write(path: str, content: str) -> Result<()>
fs.write_bytes(path: str, data: &[u8]) -> Result<()>
fs.open(path: str, mode: str) -> Result<File>
file.write(&[u8]) -> Result<()>
file.read_to_str() -> Result<str>
file.seek(offset: i64, from: SeekFrom) -> Result<u64>
file.close()net
http.get(url: str) -> async Result<Response>
http.post(url: str, body: str, headers: Map<str, str>) -> async Result<Response>
response.status: u16
response.headers: Map<str, str>
response.body: str
response.json::<T>() -> Result<T>
response.bytes() -> List<u8>
dns.resolve(host: str) -> async Result<List<str>>
dns.reverse(ip: str) -> async Result<str>
tcp.listen(addr: str, port: u16) -> Result<TcpListener>
tcp.connect(addr: str, port: u16) -> async Result<TcpStream>
stream.read(&mut [u8]) -> async Result<usize>
stream.write(&[u8]) -> async Result<()>
stream.close()str (methods on string values)
s.len() -> usize
s.is_empty() -> bool
s.contains(sub: str) -> bool
s.starts_with(prefix: str) -> bool
s.ends_with(suffix: str) -> bool
s.to_upper() -> str
s.to_lower() -> str
s.trim() -> str
s.trim_start() -> str
s.trim_end() -> str
s.split(sep: str) -> List<str>
s.split_once(sep: str) -> Option<(str, str)>
s.replace(from: str, to: str) -> str
s.repeat(n: usize) -> str
s.parse::<T>() -> Result<T>
s.as_bytes() -> &[u8]
s.chars() -> List<char>String formatting uses the f"..." prefix.
let msg = f"host={host} port={port} tls={tls}"
let hex = f"0x{byte:02x}" // lowercase hex, zero-padded to width 2
let pct = f"{ratio * 100:.1f}%" // one decimal place
let pad = f"{name:>20}" // right-aligned in a field of width 20List<T> (methods on lists)
xs.len() -> usize
xs.is_empty() -> bool
xs.push(v: T)
xs.pop() -> Option<T>
xs.first() -> Option<T>
xs.last() -> Option<T>
xs.contains(v: &T) -> bool
xs.extend(other: &[T])
xs.map(f: fn(T) -> U) -> List<U>
xs.filter(pred: fn(&T) -> bool) -> List<T>
xs.reduce(init: U, f: fn(U, T) -> U) -> U
xs.sort_by(cmp: fn(&T, &T) -> bool) -> List<T>
xs.group_by(key: fn(&T) -> K) -> Map<K, List<T>>
xs.enumerate() -> List<(usize, T)>
xs.zip(other: List<U>) -> List<(T, U)>bytes
bytes.from_hex(s: str) -> Result<List<u8>>
bytes.to_hex(b: &[u8]) -> str
bytes.from_base64(s: str) -> Result<List<u8>>
bytes.to_base64(b: &[u8]) -> str
bytes.compare(a: &[u8], b: &[u8]) -> bool // constant-timecrypto
crypto.sha256(data: &[u8]) -> [u8; 32]
crypto.sha512(data: &[u8]) -> [u8; 64]
crypto.sha3_256(data: &[u8]) -> [u8; 32]
crypto.hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32]
crypto.random_bytes(n: usize) -> List<u8>json
json.encode(value: any) -> Result<str>
json.decode::<T>(s: str) -> Result<T>
json.pretty(value: any) -> Result<str>os
os.args() -> List<str>
os.env(key: str) -> Option<str>
os.exit(code: i32) -> ! // ! means this function never returns
os.sleep(ms: u64) -> async ()
os.now() -> u64 // Unix timestamp in millisecondsmath
math.floor(x: f64) -> f64
math.ceil(x: f64) -> f64
math.round(x: f64) -> f64
math.abs(x: f64) -> f64
math.sqrt(x: f64) -> f64
math.log(x: f64) -> f64 // natural log
math.log2(x: f64) -> f64
math.sin(x: f64) -> f64
math.cos(x: f64) -> f64
math.pi: f64 // 3.14159265358979...
math.e: f64 // 2.71828182845904...
math.inf: f64Async primitives
// Resolves when all futures complete; returns a list of their results in order
all(futures: List<async T>) -> async List<T>
// Resolves with the result of whichever future finishes first
race(futures: ...async T) -> async Tall and race are top-level async primitives, available without a use statement.
Module independence and the two profiles
The standard library is deliberately not a single block of code that a program either takes whole or splits with a no_std switch. It is a set of many small modules, and a program draws in only the ones it actually uses. Three properties hold this together, and they are stated here so that example code can rely on them.
Independence is the invariant. Each module is self-contained. Used on its own, a module depends on nothing else in the library; there are no hard edges from one module to another. A post can show crypto in isolation and the reader can trust that nothing about crypto quietly drags in net or json.
Only what is used is present. A program pulls in the modules it touches and no others. Reaching for bytes.to_hex brings in bytes; it does not bring in math or fs. This is dead-code elimination at the granularity of a module, and it is what lets the same library serve both a one-line script and a full application without the script paying for the application’s surface.
Sharing is opportunistic, never structural. If a program happens to use two modules together, and one of them could be expressed more cheaply by reusing something the other already provides, the first may fold onto the second’s implementation and become smaller. The restriction that makes this safe is that it only ever removes duplication already present; it never creates a dependency in the alone case. A module that shares with a neighbour when the neighbour is there is still, used by itself, an island.
From these properties follow two profiles for the same library.
The granular profile is independence plus opportunistic sharing plus aggressive elimination of everything unused, the smallest possible surface. It is the profile to imagine when a post is about an embedded target, a bootloader, or a kernel, where every module that appears is one the code genuinely needs.
The monolithic profile is the default. Here the whole library is free to interdepend, tuned for ordinary programs where total size is not the constraint and convenience is.
The two profiles are required to agree on behaviour. The same program, read under the granular profile or the monolithic one, does exactly the same thing; only its size and internal layout differ. This is the load-bearing guarantee, because it lets a post discuss the granular profile for a systems audience without ever implying that the code would behave differently in an ordinary setting.
Design rationale
Why à-la-carte and not no_std. The familiar way to make a standard library fit a small target is to split it into a core that needs no operating system and an upper half that does, then ask embedded code to opt out of the upper half. That split is coarse; it draws one line through the whole library and forces every facility onto one side of it. The à-la-carte model draws no such line. Each facility is its own module, and the program’s actual usage rather than a global switch decides what is present. The reader of an embedded post then sees a library that contains exactly the modules in play, which is both smaller and more honest about what the code depends on.
Why independence is an invariant rather than a goal. If modules were merely “loosely coupled,” a reader could never be sure that a single module, shown alone, did not silently require another. Making independence a hard rule means a module shown in isolation is genuinely complete, which is precisely what a focused example needs. Opportunistic sharing is then admitted only because it can never violate that rule; it removes duplication when two modules already coexist and adds coupling in no case, so the property a reader relies on survives the optimization.
Why two profiles instead of one. A single library tuned for full applications would carry weight that an embedded post should not have to explain, and a single library tuned for the smallest target would make ordinary examples awkward. Two profiles let each kind of post show the library in the form that suits it, and the behavioural-identity guarantee keeps that from becoming two different languages. The granular profile is a lens for size-sensitive writing; the monolithic profile is the everyday default.
14. A Complete Example
To make the above concrete, here is a Laila Lang program that encodes and decodes a minimal TLS record, the kind of code that would appear in a post about network protocols.
use crypto::sha256
use bytes::{from_hex, to_hex}
const TLS_RECORD_HEADER_LEN: usize = 5
struct TlsRecord {
content_type: u8, // 20=ChangeCipherSpec 21=Alert 22=Handshake 23=AppData
version_major: u8,
version_minor: u8,
payload: List<u8>,
}
impl TlsRecord {
fn encode(self) -> List<u8> {
let length = self.payload.len() as u16
let mut out: List<u8> = []
out.push(self.content_type)
out.push(self.version_major)
out.push(self.version_minor)
out.push((length >> 8) as u8) // big-endian length
out.push((length & 0xFF) as u8)
out.extend(&self.payload)
out
}
fn decode(raw: &[u8]) -> Result<TlsRecord> {
if raw.len() < TLS_RECORD_HEADER_LEN {
return Err(f"too short: got {raw.len()} bytes, need at least {TLS_RECORD_HEADER_LEN}")
}
let length = ((raw[3] as u16) << 8) | (raw[4] as u16)
if raw.len() < TLS_RECORD_HEADER_LEN + length as usize {
return Err("truncated payload")
}
Ok(TlsRecord {
content_type: raw[0],
version_major: raw[1],
version_minor: raw[2],
payload: raw[TLS_RECORD_HEADER_LEN..TLS_RECORD_HEADER_LEN + length as usize],
})
}
}
fn main() -> Result<()> {
let record = TlsRecord {
content_type: 22, // Handshake
version_major: 3,
version_minor: 3, // TLS 1.2
payload: [0x01, 0x00, 0x00, 0x04, 0xDE, 0xAD, 0xBE, 0xEF],
}
let encoded = record.encode()
io.println(f"encoded: {to_hex(&encoded)}")
let decoded = TlsRecord.decode(&encoded)?
io.println(f"content_type: {decoded.content_type}")
io.println(f"payload: {to_hex(&decoded.payload)}")
Ok(())
}15. Reference Grammar
The following EBNF sketches the core grammar. It is descriptive rather than exhaustive; it captures the shapes that appear in blog examples. Terminals are in quotes, * is zero or more, + is one or more, ? is optional, and | separates alternatives.
program = item* ;
item = use_decl | mod_decl | fn_decl | struct_decl
| enum_decl | interface_decl | impl_decl | type_alias | const_decl ;
use_decl = "use" path ("::" "{" ident ("," ident)* "}")? ;
mod_decl = "pub"? "mod" ident "{" item* "}" ;
fn_decl = "pub"? "async"? "fn" ident generics? "(" params? ")"
("->" ret_type)? block ;
params = param ("," param)* ("," "..." ident ":" type)? ;
param = ("mut"? "self") | (ident ":" type) ;
generics = "<" generic ("," generic)* ">" ;
generic = ident (":" bound ("+" bound)*)? ;
struct_decl = "pub"? "struct" ident generics? "{" field ("," field)* ","? "}" ;
field = ident ":" type ;
enum_decl = "pub"? "enum" ident generics? "{" variant ("," variant)* ","? "}" ;
variant = ident ( "(" type ("," type)* ")" | "{" field ("," field)* "}" )? ;
interface_decl = "interface" ident (":" bound ("+" bound)*)?
"{" fn_sig* "}" ;
impl_decl = "impl" (ident "for")? type "{" fn_decl* "}" ;
type_alias = "type" ident "=" type ;
const_decl = "const" ident ":" type "=" expr ;
type = path generic_args?
| "&" "mut"? type (* reference *)
| "*" "mut"? type (* raw pointer *)
| "[" type ";" expr "]" (* fixed array *)
| "&" "[" type "]" (* slice *)
| "(" (type ("," type)*)? ")" (* tuple/unit *)
| type ("|" type)+ (* union *)
| "async"? type ;
generic_args = "<" type ("," type)* ">" ;
stmt = let_stmt | expr_stmt | item ;
let_stmt = "let" "mut"? pattern (":" type)? "=" expr ;
expr_stmt = expr ";"? ;
expr = literal | path | block | if_expr | match_expr
| loop_expr | while_expr | for_expr | closure | call
| unary expr | expr binop expr | expr "?" | "await" expr
| "spawn" block | "unsafe" block | try_expr ;
block = "{" stmt* expr? "}" ;
if_expr = "if" expr block ("else" (if_expr | block))? ;
match_expr = "match" expr "{" (pattern guard? "=>" expr ",")* "}" ;
guard = "if" expr ;
loop_expr = "loop" block ;
while_expr = "while" (expr | "let" pattern "=" expr) block ;
for_expr = "for" pattern "in" expr block ;
closure = "move"? "|" (ident (":" type)?)* "|" ("->" type)? (expr | block) ;
try_expr = "try" block ("catch" (ident (":" type)?)? block)* ("finally" block)? ;
pattern = "_" | literal | ident | "(" pattern ("," pattern)* ")"
| path "(" pattern ("," pattern)* ")"
| path "{" (ident ("," ident)*)? "}"
| "[" pattern ("," pattern)* ("," ".." ident)? "]"
| ident "@" pattern | range_pattern | ident ":" type ;
binop = "+" | "-" | "*" | "/" | "%" | "**" | "//"
| "==" | "!=" | "<" | ">" | "<=" | ">="
| "&&" | "||" | "??" | "&" | "|" | "^" | "<<" | ">>"
| ".." | "..=" | "as" ;
unary = "!" | "-" | "~" | "*" | "&" | "&raw" ;16. Summary of Sources
| Feature | Taken from | Notes |
|---|---|---|
let / let mut / const | Rust | Immutability by default |
Integer types i8…u64, usize | Rust and C | Full numeric range for low-level posts |
fn, ->, implicit return | Rust | Blocks are expressions |
Closures \|x\| and move | Rust | Captures, by-reference and move |
match with exhaustiveness | Rust | Pattern binding, guards, ranges |
if let, while let | Rust | Single-case matching idioms |
Result<T>, ? operator | Rust | Value-based error propagation |
loop { break value } | Rust | Loop as expression |
| Enums with data | Rust | Algebraic types |
impl, interface | Rust | Method dispatch, nominal conformance |
&T, &mut T, unsafe | Rust | Safe reference model |
Growable List<T> | Rust (Vec<T>) | Renamed so [T] never reads as a slice |
*T, *mut T, &raw, pointer arithmetic | C | Low-level memory sections |
alloc/dealloc | C | Heap illustration |
Fixed-size arrays [T; N] | C and Rust | Stack-allocated buffers |
~ bitwise NOT | C | Distinct from ! logical NOT |
async fn, await | TypeScript/JS | Async concurrency |
Promise.all → await all(...) | TypeScript/JS | Concurrent futures |
try/catch/finally | TypeScript/JS | External exception boundary |
Union types T \| U | TypeScript | Type narrowing in match |
?? null-coalescing, ?. optional chaining | TypeScript/JS | Defined on T \| nil |
| Interface declaration style | TypeScript | Structural shape, explicit impl to conform |
nil | Lua | Single null concept |
| Multiple return values | Lua | (a, b) = f() |
// floor division | Lua | Integer floor, distinct from / |
f"..." string interpolation | Python (format) | Readable in documentation |