lppHomeLanguageDesignProgressRoadmapChangelogDeploy

Design

Goal

A full web stack where every byte and every cycle has a reason. Measured with make bench (bench/results, i7-10700K, Linux 7.2): examples/web.lpp is a 27 KB binary; one worker uses 52 KB PSS, and four workers use 88 KB PSS in total after serving about 46k requests/s. The compiler peaks at 3–4 MB.

Pipeline

.lpp source -> lex.rs -> ast.rs (parser) -> gen.rs (types, layout, check + x86-64 codegen) -> driver.rs (modules, reachability, ELF64)

The compiler encodes machine instructions itself and writes a single PT_LOAD segment (read + execute; nothing in it is ever written). Strings live right after the code. There are no sections, no symbols, no relocations and no dynamic loader.

Why it is memory safe

  1. No pointers. Slices are a pointer plus a length the program cannot change. &T references exist only as parameters: they can't be stored, and slices can't be written through them.
  2. Every access is checked. s[i] and s[a..b] compare against the length with unsigned comparisons, so negative values trap too.
  3. Nothing outlives its frame. A slice can point at a string literal or argument (static), at memory owned by a caller (through a parameter), or at an array in the current frame. Functions return only int, there are no globals or heap, references can't be stored, and slices can't be stored through a reference. So a slice can only ever move to the same frame or to callees, which end first.
  4. No uninitialised memory. Arrays and variables start at zero.
  5. One trusted module. syscall and addr exist only in std/sys.lpp, and every wrapper passes the slice's own length to the kernel. Each wrapper states its contract at the top of the file.
  6. Every function is checked, including ones that are never called, so an unused function cannot hide an unsafe or ill-typed body.
  7. The stack is grown page by page. Each prologue touches every new page, so a big frame or deep recursion ends in a clean SIGSEGV at the guard page, never in a write past it.

Calling convention

The caller pushes arguments left to right (a slice is pushed as two words), the callee uses rbp frames, results come back in rax, and the caller pops the arguments. It's simple to debug with objdump -D -b binary -mi386:x86-64.

Trade-offs in 0.1

HTTP server

The lpp runner

lpp file.lpp caches binaries in a per-user directory ($XDG_CACHE_HOME/lpp, mode 0700, owner checked). A cached binary is run only if it is a regular file you own with exactly the expected bytes. All outputs are written to an exclusively created temporary file, given an explicit mode (so the umask can't strip execute permission), and renamed into place. lpp build refuses an output path that is one of the program's source files.

The compiler runs on a thread with a 64 MiB stack, on top of the depth limits. LPP_STATS=1 prints its own peak RSS (VmHWM, which resets on exec), which make bench uses.