Lambda Prelude separates flexible iteration from production deployment. The interpreter walks the AST for quick feedback; build produces a real native binary by lowering Lambda Prelude source to OCaml source and handing it to ocamlopt.

There is no custom machine-code backend and no bytecode VM. The compiler maps Lambda Prelude syntax, types, and the actor model onto OCaml types, functions, records, closures, and effects. Code generation, optimisation, linking, and binary layout are delegated to the OCaml toolchain.

Pipeline

.lp source
  ├── lexer / parser
  ├── type inference           (HM with protocols, row variables)
  ├── AOT lowering             (AST → OCaml source)
  ├── ocamlopt                 (native code generation)
  └── linker                   (AOT runtime)
.exe

The language implementation owns Lambda Prelude-specific semantics — types, protocols, dispatch, actor primitives. Binary production is left to the mature OCaml side.

lambda-prelude build script.lp --out binary
lambda-prelude build script.lp --emit            # print the lowered OCaml to stdout
lambda-prelude build script.lp --out script.ml   # write the lowered OCaml to a file

The AOT Binary is the Semantic Record

When the interpreter and the AOT binary disagree, the AOT binary is canonical. The interpreter is a development tool — fast to extend, useful for prototyping a feature before it is lowered to AOT — but the AOT path is what the language commits to. Interpreter behaviour that does not match AOT is treated as an interpreter bug.

This places Lambda Prelude in the same family as:

The opposite tradition — interpreter as the canonical execution mode — belongs to dynamically typed languages such as Python, Ruby, and Lua, and to image-based Smalltalk. Lambda Prelude looks Smalltalk-like at the surface but is statically typed and AOT-native at the core, so it follows the static / compiled tradition.

Byte-exact regression: most example programs are run under both the interpreter and the AOT binary, and the harness rejects any output divergence.

The generated binary

lambda-prelude build produces a native executable. It lowers Lambda Prelude source to OCaml source and compiles it natively with ocamlopt. The Lambda Prelude runtime — the value representation, the actor scheduler, the builtin methods, and the method-dispatch path — is compiled into the resulting binary. Sends whose receiver class is statically known become direct function calls; only the sends that aren't (reflection / remote / plugin classes) go through runtime method dispatch.

There is no separate "fully static" build mode and no fallback toggle: each call site is lowered to the best form available, with both kinds coexisting in the same binary.

The earlier standalone native code generator — which required every send to resolve at compile time — has been retired. Measurement showed its only advantage was tight CPU-bound instance-dispatch loops, orthogonal to Lambda Prelude's I/O-bound service positioning, while it could not build the natural form of the core workloads (actors, reflective RPC).

Four Categories of Dispatch

Lambda Prelude is statically typed, but not every send is required to resolve at compile time. Four categories of dispatch coexist by design — categories 1-3 are semantic boundaries the language commits to; category 4 is engineering state that shrinks over time as inference strengthens:

  1. Static dispatch. The receiver class is known at compile time and the send is lowered to a direct OCaml call. The common case. Hindley-Milner inference covers protocol-bound polymorphism and actor row variables; type annotations are optional.
  2. Reflection. perform:, Reflect, className, atField:put: operate on selector / class names at runtime, so they are always dynamic.
  3. Distribution. Remote at:for:id: returns a proxy; sends to the proxy are serialised across the wire, and the remote side is an ordinary Lambda Prelude class. Given a literal class — Remote at: url for: #Class id: — the proxy carries that class in its type, so a send is type-checked against the target class's protocol at compile time and an unknown selector is a compile-time error. Dispatch still goes over the wire — AOT never lowers a proxy send to a direct local call. A non-literal for:, which cannot name a class at compile time, stays dynamically typed as before.
  4. Runtime dispatch. Sends whose receiver class inference cannot pin down (polymorphic methods on opaque types, plugin classes, dynamic JSON payloads) are routed through the runtime dispatcher the binary links via lib/eval.ml.

Execution Mode Matrix

CapabilityinterpreterAOT
Actor basics (spawn, send, FIFO receive, mailbox)yesyes
Selective receive, receive timeoutyesyes
Future, ask:, future timeoutyesyes
Link, monitor, trap-exits, supervisionyesyes
Bounded socket I/Oyesyes
HTTP server / HTTP clientyesyes
TLS sockets (Tls.Engine driven)yesyes
JSON, JSON-RPC, TERIOS RPC over HTTPyesyes
SQLite (builtin), file / OS / subprocess / reflectionyesyes
PostgreSQL / MariaDB / MySQL connections (Dynlink plugins)yesyes
Redis / Valkey (RESP2 wire, Dynlink plugin)yesyes
TORM 2-way SQL template + DAO macroyesyes
Multi-domain spawn, cross-domain inbox, work-stealingyesyes

The AOT binary has the same runtime compiled in, so it gets the full multi-domain work-stealing scheduler and loads the same Dynlink plugins; the two columns are identical by construction.

Cross-Platform Build

The output is a native binary built directly from Lambda Prelude source, suitable for containers, Windows development environments, and Linux deployment targets.

Both targets are exercised before features are considered complete. A feature that only works on one is treated as incomplete.

Inspecting the Generated Code

--emit writes the lowered OCaml source to stdout; --out <file>.ml writes it to a file. This is the standard tool when investigating what AOT actually does with a piece of code:

lambda-prelude build examples/15_counter_actor.lp --emit | less
lambda-prelude build examples/15_counter_actor.lp --out counter.ml

The result is ordinary OCaml — type-correct, type-annotated where useful, and laid out to read like the input source rather than a flattened IR.