Where contracts meet effects

A JVM language with algebraic effects, runtime specs, and property-based contracts

v0.9.231JVMVirtual ThreadsANTLR4

Algebraic Effects

First-class effect system with deep handlers. Mock any side effect in tests. State-machine bytecode lowering for hot perform-loops.

Parametric Effect Rows

Higher-order fns declare row variables: fold :: (Fn):eff _ _ _ ::: eff. The callback's effect row binds eff and propagates into the caller's row at each call site. Compile-time enforcement, no runtime erasure.

Runtime Specs

Malli-inspired specs replace types. Certify values at construction, validate at boundaries. Spec-annotated pub fns are mandatory; lint enforces it.

Contracts

Pre/post conditions on functions, in/out contracts at module boundaries. Blame tracking — pre-failure accuses the caller, post-failure accuses the implementation.

Structured Concurrency

scope/fork/await, par, race, timeout. Java virtual threads with auto-cancellation. Spawned fibers inherit effect row + session namespace.

Terminal UI

std.term gives raw mode, decoded key/mouse/resize events and wcwidth as an effect — so a mock handler makes a whole TUI assertable as a string, with no tty. The uzor seed builds cell buffers, layout, widgets and an app loop on top.

Pipeline Operators

|> pipeline, @ map, /? filter, /+ fold. >> composition, ~ apply-to-rest.

Module System & Packages

use mod.name :open / :as alias / { selective }. Git deps via irij.toml. Pub/private exports. Effect-gated APIs.

Bytecode-only Execution

irij build emits a self-contained JAR. State-machine handler lowering is the single execution path — no interpreter, no threaded fallback, no ambiguity.

Hot Redef + Java Interop

REPL-driven fn redefinition via invokedynamic + MutableCallSite. Java classes via Class/member, dot-access, ::: JVM capability tracking.

Model-Based Testing

A model declaration binds a Quint specification to the code implementing it — one clause per action, with the spec's nondet picks bound by name. Quint generates traces, Irij replays them and fails at the first diverging step; Apalache proves invariants. Generating needs the quint binary, replaying a committed trace needs nothing, because the CLI is reached through an effect.

Install

Irij runs on the JVM. All you need is a JDK 21+ (any distribution) and git — the build fetches everything else. Linux, macOS and WSL are supported; on Windows use gradlew.bat instead of ./gradlew.

Build and install to PATH

## clone the repo
git clone https://github.com/aldebogdanov/irij.git
cd irij

## build + install the launcher
./gradlew install

This writes the runtime to ~/.local/lib/irij.jar and an irij launcher script to ~/.local/bin/irij. Make sure that directory is on your PATH:

## add to ~/.bashrc or ~/.zshrc
export PATH="$HOME/.local/bin:$PATH"

Prefer not to install? ./gradlew shadowJar alone leaves a runnable JAR at build/libs/irij.jar — run it with java -jar build/libs/irij.jar file.irj.

Verify and take it for a spin

irij --version                ## engine version
irij                          ## interactive REPL

echo 'println "Hello, Irij!"' > hello.irj
irij hello.irj

Everyday commands

irij file.irj                 ## run a source file
irij test                     ## run tests/test-*.irj
irij install                  ## fetch seeds from irij.toml
irij build                    ## package a self-contained JAR
irij publish                  ## publish a seed to the registry
irij lsp                      ## LSP server for editors
irij --nrepl-server           ## nREPL on port 7888
irij help                     ## full command list

Emacs users: editors/emacs/irij-mode.el and irij-nrepl.el ship in the repo.

Code Examples

Specs & Pattern Matching

spec Shape
  Circle Float
  Rect Float Float

fn area :: Shape Float
  Circle r   => 3.14159 * r * r
  Rect w h   => w * h

println (area (Circle 5.0))
;; 78.53975

Algebraic Effects

effect Logger
  log :: Str -> ()

handler console :: Logger ::: Console
  log msg =>
    println msg
    resume ()

with console
  log "Starting..."
  log "Done!"

Parametric Effect Rows

;; Row-variable `:eff` binds the callback's effects
;; and propagates them to the caller's effect row.
fn fold :: (Fn):eff _ Vec _ ::: eff
  (f acc xs -> ...)

;; Pure use — no extra row at the call site.
total := fold (+) 0 #[1 2 3 4 5]

;; Effectful callback — Console flows in automatically.
fn shout ::: Console
  =>
  sum := fold (acc n -> println n ; acc + n) 0 #[1 2 3]
  println sum

Contracts (pre / post / in / out)

fn withdraw :: Account Int Account
  pre  (account amt -> amt > 0)
  post (result -> result.balance >= 0)
  (account amt ->
    {...account balance= account.balance - amt})

;; Blame-tracking: pre-fail accuses caller,
;; post-fail accuses the fn itself.

Pipelines & Seq Ops

nums := #[1 2 3 4 5 6 7 8 9 10]

result := nums
  |> /? (n -> n > 3)
  |> @ (n -> n * n)
  |> /+

println result
;; 355

Structured Concurrency

scope s
  a := s.fork (-> fetch "users")
  b := s.fork (-> fetch "posts")
  merge (await a) (await b)

;; Composable combinators
sum := par (+) (-> slow-calc 1) (-> slow-calc 2)
winner := race (-> slow) (-> fast)
ok := timeout 1000 (-> work)

Functions

;; Three fn body styles:

fn greet ::: Console            ;; imperative
  => name
  msg := "Hello, " ++ name
  println msg
  msg

fn add (x y -> x + y)           ;; lambda

fn fib
  0 => 1                        ;; match arms
  n => n * fib (n - 1)

Collections & Records

vec := #[1 2 3]
set := #{:a :b :c}
tup := #(42 "hello")
person := {name= "Alice" age= 30}

;; Immutable update
older := {...person age= 31}

;; Mutable opt-in
counter :! 0
counter <- counter + 1

Protocols

proto Show a
  show :: a -> Str

impl Show for Int
  show := (n -> to-str n)

impl Show for Vector
  show := (v -> "[" ++ (join ", " (@ to-str v)) ++ "]")

println (show 42)
println (show #[1 2 3])

Model-Based Testing

;; a Quint spec generates the traces; Irij replays them
model bank :: "spec/bank.qnt" :pure
  start                  => {balances= {alice= 0}}
  deposit  st who amount => {...st balances= (credit st.balances who amount)}
  overdraft st _ _       => {...st last-error= "no funds"}

r1 := test "bank conforms" (-> qcheck bank {traces= 50})
;; diverged at step 4, action deposit
;;   expected {alice= 11}  actual {alice= 12}

Roadmap

Irij is in active development. Current status:

donePhase 0-2 Parser, AST, REPL, CLIdonePhase 3 Algebraic effects with deep handlersdonePhase 4 Module system + standard librarydonePhase 5 Structured concurrency (virtual threads)donePhase 6 Contracts, blame trackingdonePhase 7 Effect row declarations + enforcementdonePhase 8 Runtime specs, validation, lintdonePhase 9 Package management (deps.irj, git deps)donePhase 10 I/O: JSON, FileIO, HTTP client + serverdonePhase 11 Database effect (SQLite, std.db)donePhase 12 Package registry app + PlaygrounddonePhase 13 Java interop (Class/member, dot-access)donePhase 14 Bytecode compiler: state-machine effects, hot-redef, JVM capabilitydonev0.5.0 Effect-row soundness; full bytecode spec parity; pre/post contracts in bytecodedonev0.5.x Bytecode-mode parity for the full irij.online website; Irij source line numbers in JVM stack tracesdonev0.6.0 Parametric effect rows ((Fn):eff / (_ -> Bool):eff / ::: eff)donev0.6.13 Interpreter package + threaded handler lowering removed. Single execution model: state-machine bytecode.donev0.6.15 Spawn inherits namespace + per-session stdout (Playground cross-eval fn refs + spawn output capture). SM lowering: composed handlers, destructure pre-op binds.donev0.7.0 std.auth seed (sha256, hmac, random-token); SM-lowering correctness gaps closed; dev.irij.interpreter renamed to dev.irij.runtime; release auto-tagdonev0.7.1 Capabilities replace raw-* surface (Db, Http, Serve, FileIO, Session); Irij-record caps; named record specs with row params; inline `with do …`; LSP MVP (irij lsp)donev0.8.0 Handler:Effect specs (first-class handler values, dynamic-handler typing); LSP 2b.1 (symbol-aware hover, goto-def, identifier completion, effect-row diagnostics)donev0.8.1 Per-request HTTP handlers inherit caller's effect stack (SSE / multipart now work); `try` traps every Throwable (division by zero, casts, NPEs); GraalVM deferred to techdebtdonev0.8.2 Patch: empty-body HTTP responses send -1 length cleanly (redirects, 204s, errors from `irij publish`)donev0.8.3 `Map` spec accepts records (Tagged with namedFields); vrata-style `El`/`Node` flow through `:: Map` boundaries without ceremonydonev0.8.4 `with` + tail `if/else` returns the branch value (EffIR lowering); snake_case identifiers lex as one IDENT (SQL/JSON field names)donev0.8.5 Multi-class emission: inlined-module fns compile into per-source-file classes, so JVM stack frames name the right .irj instead of the root programdonev0.8.6 SSE "patch once" responses (a handler that patches and returns no longer crashes); `irij publish` Bearer authdonev0.8.7 `with` blocks whose tail is a block-form if/else return the branch value in every shape (EffIR + pure/SM paths)donestd.term Terminal effect: raw mode, decoded key/mouse/resize events, wcwidth, alt-screen. JLine-backed cap; escape decoding sits outside it so the grammar is testable with no tty. `mock-term` makes a whole TUI assertable as a string.doneLanguage fixes `party` replaces `role` as the choreography keyword (frees `user.role`); integer division/remainder are `quo`/`rem` (the old `mod` builtin was unreachable — the module keyword owned the name); `&&`/`||` short-circuit; product specs validate field types and are closed; `\e` string escapedoneMulti-tenant registry Users, sessions, API tokens, per-owner ownership; auth-gated publish; Datastar dashboard with token CRUDdonechereda Queue effect + SQLite cap; at-least-once with optional dedup keys; first opt-in seednextchereda-redis Redis backend for chereda; opt-in jedis dep gated by seed importdonebutterfly Reactive signal graph in Irij: signals, cached computes with automatic dependency tracking, watchers, batching, snapshot/restore time traveldoneuzor Terminal UI toolkit on std.term: cell buffers with per-line diffing, constraint layout, widgets, an app loop, mouse, overlays and themes — the rendering path is pure, so a whole TUI is tested without a terminaldonev0.9.0 Model-based testing: the `model` declaration binds a Quint specification to the code implementing it, `std.quint` replays generated traces and `verify` proves invariants with Apalache. Generation is an effect, so a committed trace replays in CI with no Quint installed. Replaces the `law` property testing removed in v0.6.12 — sampling is not proof, and the tool that samples also proves.plannedFuture Linear types (capability + channel safety); channels (send/recv/select); choreographic programming; refinement types; content-addressed code; multi-resume effects (search/nondet/probabilistic); GraalVM native image