Skip to main content

Introduction

Lea is a pipe-oriented functional programming language with a tree-walk interpreter written in TypeScript.

let numbers = [1, 2, 3, 4, 5]

let sumOfSquares = numbers
/> filter((x) -> x > 2)
/> map((x) -> x * x)
/> reduce(0, (acc, x) -> acc + x)

sumOfSquares /> print -- 50

Features

Lea provides the following features:

  • Pipes — Left-to-right data flow with the /> operator
  • Functions — First-class functions with optional type annotations
  • Decorators — Composable function modifiers (#log, #memo, #retry(3))
  • Records — Object literals with member access
  • Contexts — Dependency injection system
  • Async/await — Promise-based asynchronous execution

Quick start

# Clone and install
git clone https://github.com/mcclowes/lea.git
cd lea
npm install

# Run a file
npm run lea example.lea

# Interactive REPL
npm run repl

Or use npx without installing:

npx lea-lang hello.lea

Syntax overview

Bindings

let x = 10              -- Immutable
maybe counter = 0 -- Mutable

Functions

let double = (x) -> x * 2
let add = (a, b) -> a + b

-- Type annotations (trailing :: syntax)
let typed = (x) -> x * 2 :: Int :> Int

-- Multi-statement bodies
let process = (x) ->
let y = x * 2
let z = y + 1
z

Pipes

16 /> sqrt              -- sqrt(16) = 4
5 /> add(3) -- add(5, 3) - value becomes first arg
5 /> add(3, _) -- add(3, 5) - placeholder controls position

-- Chain operations
[1, 2, 3, 4, 5]
/> filter((x) -> x > 2)
/> map((x) -> x * x)
/> print

Records

let user = { name: "Max", age: 99 }
user.name /> print -- "Max"

let nested = { data: { value: 42 } }
nested.data.value /> print

Decorators

Apply modifiers after the function body:

let logged = (x) -> x * 2 #log
let cached = (x) -> expensiveOp(x) #memo
let resilient = (x) -> riskyOp(x) #retry(3) #timeout(1000)

Lea provides the following built-in decorators:

DecoratorDescription
#logLog inputs and outputs
#memoCache results
#timeLog execution time
#retry(n)Retry on failure
#timeout(ms)Fail if time limit exceeded
#validateRuntime type checking
#pureWarn on side effects
#asyncMark as async
#traceDeep call logging

Context system

Use contexts for dependency injection:

-- Define context with default
context Logger = { log: (msg) -> print("[DEFAULT] " ++ msg) }

-- Override in scope
provide Logger { log: (msg) -> print("[PROD] " ++ msg) }

-- Attach to function
let greet = (name) ->
@Logger
Logger.log("Hello " ++ name)

"World" /> greet -- "[PROD] Hello World"

Async/Await

let fetchData = () -> delay(100) #async
await fetchData() /> print

Architecture

Source -> Lexer -> Tokens -> Parser -> AST -> Interpreter -> Result
  • src/lexer.ts - Tokenization
  • src/parser.ts - Recursive descent parser
  • src/ast.ts - AST node definitions
  • src/interpreter.ts - Tree-walk interpreter
  • src/repl.ts - Interactive REPL

License

MIT