Language reference (0.3)
lpp targets Linux on x86-64. Source files are UTF-8 (string literals are bytes).
Types
| Type | Size | Where | Notes |
|---|---|---|---|
int | 8 | anywhere | signed 64-bit, wraps on overflow |
u8, u32 | 1, 4 | variables, fields, payloads | storage types: a store keeps the low 8/32 bits; reading gives an int |
str | 16 | anywhere | read-only byte slice (pointer + length); string literals are str |
[]u8 | 16 | anywhere | writable byte slice; accepted where a str is expected |
[N]u8 | N | variables (var), fields, payloads | zeroed; 1 ≤ N ≤ 1,048,576; its name gives a []u8 (or a str if the owner is immutable) |
struct | sum of fields, aligned | anywhere | value type, copied on assignment and when passed |
enum | 8 + largest payload | anywhere | tagged union, used with match |
&T | 8 | parameters only | reference to a struct or enum owned by the caller |
fn(T, ...) -> int | 8 | anywhere | function value |
Character literals like 'a' are int. Escapes: \n \r \t \0 \\ \' \" \xNN. Integer literals are decimal or 0x hex (hex may use all 64 bits: 0xFFFFFFFFFFFFFFFF is -1).
Declarations
let x = 5; // immutable
var y: int; // mutable, zeroed
var b: [256]u8; // stack array, zeroed
var flags: u8 = 300;// stores 44
let s: str = "hi";
A name can be declared once per block; an inner block can shadow an outer name. Everything starts zeroed, including structs.
Structs
struct Point { x: int, y: int }
struct Rect { min: Point, max: Point, label: str, tag: u8, buf: [16]u8, on_click: fn(int) -> int }
var r: Rect; // all fields zero
r.max.x = 3;
r.label = "box";
let copy = r; // copies the whole struct
copy(r.buf, "abc"); // array fields are []u8 slices of the struct's memory
There are no struct literals: declare, then set fields. A struct can't contain itself by value.
Enums and match
enum Shape { Circle(int), Rect(Size), Label(str), Empty }
let s = Shape.Circle(2);
match s {
Circle(r) => { ... } // r is an immutable copy of the payload
Shape.Rect(sz) => { ... } // the enum name may qualify the variant
Label(_text) => { }
Empty => { }
}
A match on an enum must name every variant or end with _ => { }. A match on an int uses number arms (200 => { }, -1 => { }) and must end with _. An arm after _ is an error. Each variant has at most one payload; use a struct for more.
References
&T is a parameter type for a struct or enum the caller owns. The callee reads it and can modify its fields.
fn grow(r: &Rect, by: int) -> int { r.max.x = r.max.x + by; return 0; }
grow(&r, 1); // r must be a 'var' (or itself a &T parameter: grow(&r, 1) passes it on)
References can't be stored in variables or fields, and slices can't be written through a reference (r.label = local_buffer; is an error). That rule is what keeps every slice from outliving the frame it points into, without a borrow checker.
Function values
fn add(a: int, b: int) -> int { return a + b; }
fn apply(f: fn(int, int) -> int, x: int) -> int { return f(x, x); }
apply(add, 3);
let g = add; g(1, 2);
op.apply(2, 5); // call a function stored in a field
A function name used without a call is its value. Calling a zeroed function value (a field that was never set) traps.
Expressions
| Operators (loosest first) | Notes | ||
|---|---|---|---|
| `\ | \ | ` | short-circuit, gives 1 or 0 |
&& | short-circuit, gives 1 or 0 | ||
== != < <= > >= | give 1 or 0; write a < b && b < c, not a < b < c | ||
| `+ - \ | ^` | `\ | and ^ bind like +` (as in Go) |
* / % << >> & | & binds like *: a & m == 0 is (a & m) == 0 | ||
unary - ! ~ & | & only as a call argument for a &T parameter | ||
postfix s[i], s[a..b], x.field, f(args) |
+ - *wrap./ %truncate toward zero. A zero divisor traps.MIN / -1=MINandMIN % -1= 0.<< >>need a count of 0..63, or they trap.>>is arithmetic (it keeps the sign).- Any non-zero
intis true.
Statements
if c { } else if d { } else { }, while c { }, match x { ... }, break;, continue;, return e;, x = e;, x.f = e;, s[i] = e; (only through []u8, stores the low byte). There is no for.
Functions and modules
use io; // loads std/io.lpp
fn add(a: int, b: int) -> int { return a + b; }
- Parameters are immutable and have distinct names. Their types are
int,str,[]u8, a struct or enum (copied in),&T, orfn(...). Useintfor small integers (u8/u32are storage types). - Functions return
int, and the-> intcan be left out. A slice or struct can't be returned; pass a[]u8buffer or a&Tto fill. - One global namespace for functions and types. A program needs
fn main() -> int, and its return value becomes the exit status. - Every loaded function is type-checked. Only functions reachable from
main(by call or as values) are emitted.
Builtins
| Builtin | Type | Meaning |
|---|---|---|
len(s) | int | slice length |
copy(dst: []u8, src: str) | int | copies all of src to the start of dst and returns len(src). Traps if src is longer. Overlap is handled like memmove. |
argc() | int | number of program arguments, including the program name |
arg(i) | str | argument i, bounds-checked; argument strings live for the whole process |
syscall(n, ...), addr(x) | int | std/sys.lpp only; addr takes a slice or a struct |
Runtime checks
A failed check prints one of these to stderr and exits with status 101:
index or slice out of boundsdivision by zerocopy source longer than destinationshift count outside 0..63call through an unset function value
Programs ignore SIGPIPE: writing to a closed pipe or socket returns -32 (EPIPE) instead of killing the process.
Compile errors
Errors show file:line:col, the source line and a caret:
lpp: app.lpp:3:16: expected int, found str
return x + "s";
^
Limits
| Limit | Value | Outcome |
|---|---|---|
| array size | 1 MiB | compile error |
| stack frame per function, struct size | 4 MiB | compile error |
| stack depth | the process stack limit (ulimit -s, usually 8 MiB) | SIGSEGV (exit 139); pages are touched one by one, so it never skips the guard page or corrupts memory |
| expression depth, block and else-if nesting | 256 | compile error |
| source file | 16 MiB | compile error |
| modules | 256 | compile error |
| fields per struct, variants per enum | 256 | compile error |
Trusted code
std/sys.lpp starts with trusted; and may use syscall and addr. The compiler refuses trusted; in any other file. The std directory is trusted input: an explicit LPP_STD must contain sys.lpp (otherwise it is an error, never a silent fallback), and pointing it elsewhere means trusting that sys.lpp.