lppHomeLanguageDesignProgressRoadmapChangelogDeploy

Language reference (0.3)

lpp targets Linux on x86-64. Source files are UTF-8 (string literals are bytes).

Types

TypeSizeWhereNotes
int8anywheresigned 64-bit, wraps on overflow
u8, u321, 4variables, fields, payloadsstorage types: a store keeps the low 8/32 bits; reading gives an int
str16anywhereread-only byte slice (pointer + length); string literals are str
[]u816anywherewritable byte slice; accepted where a str is expected
[N]u8Nvariables (var), fields, payloadszeroed; 1 ≤ N ≤ 1,048,576; its name gives a []u8 (or a str if the owner is immutable)
structsum of fields, alignedanywherevalue type, copied on assignment and when passed
enum8 + largest payloadanywheretagged union, used with match
&T8parameters onlyreference to a struct or enum owned by the caller
fn(T, ...) -> int8anywherefunction 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)

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; }

Builtins

BuiltinTypeMeaning
len(s)intslice length
copy(dst: []u8, src: str)intcopies all of src to the start of dst and returns len(src). Traps if src is longer. Overlap is handled like memmove.
argc()intnumber of program arguments, including the program name
arg(i)strargument i, bounds-checked; argument strings live for the whole process
syscall(n, ...), addr(x)intstd/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:

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

LimitValueOutcome
array size1 MiBcompile error
stack frame per function, struct size4 MiBcompile error
stack depththe 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 nesting256compile error
source file16 MiBcompile error
modules256compile error
fields per struct, variants per enum256compile 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.