Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lamb

Node.js 20 or newer zero dependencies fifteen tests passing

Lamb running inside a terminal

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.

Install

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 link

The 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 lambda

Quick start

Passing 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.ml

Selecting 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 assembly

Standard 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

Commands

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.

Tagged words

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 value

Tagged runtime layout inside a terminal

The 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 --json

Pipeline

The 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

Lambda optimization inside a terminal

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.

Tagged bytecode inside a terminal

Type inference

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 true

The 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 6

Closure conversion

The 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 35

Selecting 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.

Diagnostics

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

Grammar

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.

JSON traces

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.json

Every 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.

References

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.

About

Compiler for a subset of OCaml expressions that exposes deterministic IR's

Topics

Resources

Stars

16 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages