Lamb is a terminal-first compiler observatory that accepts a focused OCaml expression subset and prints deterministic representations spanning parsed source through Typedtree and Lambda before closure conversion exposes Clambda and machine lowering produces Cmm.
The tool keeps its parser and inference engine independent from browser APIs or native compiler libraries which makes every trace reproducible on any current Node.js installation without package downloads or external services.
OCaml turns Typedtree into Lambda then lowers through Clambda and Cmm before emitting native code while representing most integers as tagged machine words. Casually reading the compiler backend reveals an absurdly well-engineered language.
The repository contains no runtime dependencies and requires Node.js 20 or newer which means installation only needs a clone followed by an optional global link that exposes lamb from any directory.
git clone https://github.com/yourname/lamb.git
cd lamb
npm linkThe executable can also run directly from its checkout when a global command is unnecessary because the repository preserves its executable permission and declares the correct Node.js interpreter through its shebang.
./lamb sample.ml --stage lambdaPassing a source file with no stage selection prints the complete modeled lowering sequence and includes the inferred result type alongside syntax-node totals and useful descriptive metadata for every generated representation.
lamb sample.mlSelecting one stage keeps larger compiler investigations readable because Lamb still prints the complete pipeline index while limiting the detailed terminal output block to only the specifically requested intermediate representation.
lamb sample.ml --stage typedtree
lamb sample.ml --stage lambda
lamb sample.ml --stage clambda
lamb sample.ml --stage cmm
lamb sample.ml --stage assemblyStandard input works without additional flags so generated expressions and editor selections can move directly into Lamb through ordinary shell pipelines while diagnostics continue reporting positions against the virtual stdin source.
printf 'let x = 20 in x + 22\n' | lamb --stage cmm| invocation | behavior |
|---|---|
lamb file.ml |
Print every modeled representation |
lamb file.ml --stage lambda |
Print one selected representation |
command | lamb |
Compile source received through standard input |
lamb --word 42 |
Inspect a tagged machine word |
lamb file.ml --json |
Export a complete structured trace |
lamb --list |
List accepted stage names |
lamb --help |
Print command documentation |
The --no-color option removes ANSI control sequences for logs and snapshots while the conventional NO_COLOR environment variable provides the same behavior across scripts that standardize terminal output globally by default.
OCaml native integers store their signed payload after a one-bit left shift and reserve the least significant bit as an immediate marker which lets the runtime distinguish integers from aligned pointers cheaply.
$ lamb --word 42
Lamb / tagged word
OCaml int 42
Encoded 85
Hex 0x0000000000000055
Bits 00000000 00000000 00000000 00000000 00000000 00000000 00000000 01010101
Low bit 1 · immediate valueThe probe uses arbitrary precision arithmetic before truncating only the hexadecimal and binary displays to 64 bits which preserves correct signed encoding even when an entered decimal value exceeds native JavaScript integer precision.
Machine-readable mode returns the original integer and encoded machine word alongside fixed-width hexadecimal and binary forms plus the decoded low-bit tag classification without terminal formatting or unnecessary explanatory display labels.
lamb --word -42 --jsonThe traditional no-Flambda path motivates Lamb’s stage vocabulary while each printed form remains intentionally compact and stable so educational traces do not depend upon undocumented constructor changes between actual compiler releases.
| stage | model | information exposed |
|---|---|---|
| Source | Parsed OCaml expression | Binding structure and operator precedence |
| Typedtree | Typed expression nodes | Unified types and recursive status |
| Lambda | Functional intermediate form | Primitive operations and applications |
| Clambda | Closure-converted form | Captured environments and generic calls |
| Cmm | Machine-oriented control form | Tagged constants and explicit allocation |
| Assembly | Representative x86-64 sketch | Shifts and arithmetic instructions |
The stage index highlights the selected representation with restrained ANSI color while dimming surrounding stages which preserves useful context without printing decorative panels or requiring an interactive full-screen terminal interface.
Lamb implements Hindley–Milner unification with occurs checking and level-based generalization so pure let-bound functions can receive fresh type variables at each use while recursive bindings acquire provisional types during body inference.
let identity = fun value -> value in
let number = identity 42 in
identity trueThe preceding expression returns bool because the generalized identity binding instantiates independently for the first integer application and subsequent boolean application without allowing either use to constrain the other statically.
Recursive functions become available inside their own bodies before unification fixes the parameter and result types which permits ordinary numeric recursion while rejecting inconsistent branch results and infinite self-application types.
let rec sum n =
if n = 0 then 0
else n + sum (n - 1)
in
sum 6The Clambda representation computes lexical free variables for every function then places those names inside an explicit environment list which reveals the state that must survive after control leaves the defining scope.
let offset = 7 in
let add = fun value -> value + offset in
add 35Selecting Clambda for this specimen displays environment [offset] because the generated runtime closure needs the surrounding value while its explicit parameter remains available directly through the ordinary function calling convention.
Parser and inference failures return concise diagnostics with source names and one-based positions followed by the relevant source line and a caret spanning the smallest expression range associated with the failure.
error: type int is incompatible with bool
stdin:1:1
1 │ if true then 1 else false
│ ^^^^^^^^^^^^^^^^^^^^^^^^^
Usage errors and compilation errors use distinct process statuses so automated shell scripts can reliably separate invalid invocation from rejected OCaml while successful traces and tagged-word probes always return zero.
| status | meaning |
|---|---|
0 |
Successful trace or inspection |
1 |
Source parsing or type inference failure |
2 |
Invalid invocation or unavailable input |
The supported fragment remains small enough for every lowering to stay legible while covering nested comments and precedence alongside higher-order functions polymorphic bindings recursive functions conditionals comparisons boolean operators and tagged integer arithmetic.
expression ::= let | function | conditional | infix
let ::= "let" ["rec"] name {name} "=" expression "in" expression
function ::= "fun" name {name} "->" expression
conditional ::= "if" expression "then" expression "else" expression
infix ::= application {operator application}
application ::= atom {atom}
atom ::= integer | boolean | name | "()" | "(" expression ")"
operator ::= "+" | "-" | "*" | "/" | "=" | "<>" | "<" | ">"
| "<=" | ">=" | "&&" | "||"
Function parameters following a let-bound name become nested unary functions which directly mirrors OCaml’s curried semantics and keeps function application left associative throughout parsing and every subsequent compiler lowering stage.
Structured output contains the input name and inferred type alongside the syntax-node total and requested compiler stages which lets editor integrations consume Lamb without scraping ANSI output or intermediate-form headers.
lamb sample.ml --stage cmm --json > trace.jsonEvery stage record includes its stable key and display name plus phase description and explanatory note alongside the exact multiline output and calculated line total used by the terminal renderer.
The OCaml compiler backend guide describes untyped Lambda and native code generation while the memory representation guide documents immediate values and low-bit classification.
The upstream runtime header mlvalues.h defines Is_long and Is_block alongside the canonical Val_long and Long_val conversion macros modeled by the tagged-word command.



