8000 GitHub - aldebogdanov/irij: Effect-Oriented JVM-based Language · GitHub
[go: up one dir, main page]

Skip to content
aldebogdanovPublic

About

Effect-Oriented JVM-based Language

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Irij

Token-efficient effect-oriented language.

Runs on JVM (Java 21+). Tree-walk interpreter with nREPL server.

Quick Start

./gradlew shadowJar
java -jar build/libs/irij.jar examples/hello.irj

Or install to PATH:

./gradlew install    # installs to ~/.local/bin/irij
irij examples/hello.irj

Usage

irij                          # launch REPL
irij file.irj                 # run a file
irij file.irj --port 9090     # args after the file reach the program (env-args)
irij test                     # run tests/ directory
irij audit                    # where the program reaches Java, against irij.toml [trust]
irij audit --fix              # add to [trust] what the program needs (build/publish require [trust])
irij install                  # fetch seeds and [jvm] Java libraries (pinned in irij.lock)
irij deps                     # the Java libraries asked for and the jars they resolve to
irij --nrepl-server            # start nREPL (port 7888)
irij --nrepl-server=9000       # nREPL on custom port
irij --parse-only file.irj    # check syntax only
irij --ast file.irj            # dump AST

Examples

irij examples/hello.irj       # hello world
irij examples/basics.irj      # bindings, functions, patterns
irij examples/collections.irj # vectors, maps, pipelines
irij examples/effects.irj     # algebraic effects deep dive
irij examples/walkthrough.irj # full language tour (all phases)
irij examples/compiled.irj    # bytecode compiler MVP demo (see: irij compile)
irij examples/terminal.irj    # raw-mode terminal (Term effect) — needs a tty
irij examples/quint.irj       # model-based testing against a Quint spec — needs `quint`
(cd examples/java-libs && irij run main.irj)  # a Maven library through a cap ([jvm], irij.lock)
(cd examples/java-sources && irij run main.irj)  # the project's own Java (java/) as a cap provider

Tests

./gradlew test                # 791 Java unit tests, one line per test
./gradlew test -Pverbose      # a block per test: its output, full stack traces
./gradlew test -q             # only failures and the summary
irij test                     # 643 integration tests (tests/*.irj)
irij test -v                  # … listing every test, not only failures

Both are coloured on a terminal and plain when piped; for irij, NO_COLOR=1 turns colour off and FORCE_COLOR=1 on.

Language Features

  • Specs — spec declares structure with certification tags; runtime validation at boundaries — variant payloads and applied parametric specs ((Maybe Int)) included; a name nothing declares is a compile error; builtin Result a e (Ok a | Err e) for what try returns
  • Mandatory effect tracking — fn name ::: Console declares effects; unannotated = pure — a with counts too: installing handler loud :: Log ::: Console needs Console in the fn's row, so a pure fn can't print, read the clock or reach Java through a handler
  • Algebraic effects & handlers — effect, handler, with, resume, on-failure; the Console, Random and Time builtins (println, read-line, random-int, now-ms, sleep, …) perform their effect, so a test handler captures output and fixes the dice and the clock
  • Contracts — pre, post (function-level), in, out (module-boundary with blame)
  • Model-based testing — model bank :: "spec/bank.qnt" :pure 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; 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.)
  • Protocols — type-dispatched methods: proto, impl
  • Structured concurrency — scope, fork, par, race, timeout
  • Child processes — std.proc's Proc effect: run a command to completion (stdin, timeout, both streams) or start one and talk to it a line at a time; mockable like every effect
  • Terminal apps — std.term's Term effect: raw mode, decoded key/mouse events, resize, wcwidth; mockable, so a TUI is testable with no screen
  • Unambiguous output — println "a b" prints a b, but a Str inside a collection, variant or record prints quoted and escaped, as source writes it: #["a b" "c"], {name= "Ada"}, #[(Ok 1) (Err "e")]; inspect gives that form for any value, a Str included, and the REPL echoes it
  • Java behind caps, confined by trust — a cap binds a Java class to an effect, and a call through it performs that effect, so code that installs default-db declares ::: Db, not JVM; irij.toml's [trust] names the modules that may declare caps or write Java at all, the compiler refuses the rest, and irij audit lists every site
  • Java libraries — irij.toml's [jvm] names Maven coordinates ("org.apache.commons:commons-text" = "1.12.0"); they're fetched with their dependencies from Maven Central, pinned with sha256 in irij.lock, loaded child-first so a library keeps its own versions, and shipped whole inside irij build's JAR. Seeds can bring libraries too, if [trust] jars names them. See examples/java-libs/
  • Java sources — a project or seed keeps its own Java in java/; Irij compiles it (against the [jvm] libraries and the JDK, cached in ~/.irij/java) and a cap binds it. That's how a library wraps an object API — a driver, a client — behind static methods over plain values, so its handler performs its own effect instead of JVM. A seed's sources compile only where [trust] java names it. See examples/java-sources/
  • Databases — std.jdbc's Db effect talks to any database with a JDBC driver: db-open "jdbc:sqlite:app.db" with "org.xerial:sqlite-jdbc" in [jvm], or PostgreSQL, MySQL, H2… Irij bundles no driver (std.db, SQLite-only and built in, was renamed)
  • Pattern matching — destructuring, guards, spread, ADTs; cond (first condition that holds) and case (equality dispatch) for multi-way choices without else if chains
  • Module system — mod, use, pub, qualified names
  • Java interop — Clojure-style Class/method for statics, obj#method for instance members (auto-reflection, java.lang auto-imported); every Java site needs ::: JVM, checked at compile time
  • Bytecode compiler — the only execution model since v0.6.20: ASM-emitted JVM classfiles with state-machine effect lowering, self-TCO, invokedynamic hot-redef; irij build packages self-contained runnable JARs
  • nREPL — Emacs integration via editors/emacs/irij-nrepl.el
  • Editors — Emacs irij-mode (highlighting, offside indentation, LSP wiring; see docs/emacs.md) a VS Code extension in editors/vscode/ (highlighting, LSP, REPL; see docs/vscode.md), and a TextMate grammar for Shiki / Linguist / Zed in editors/textmate/

Documentation

All docs live in docs/:

  • irij-lang-spec.org — full language specification (source of truth)
  • phase-*.md — implementation notes per phase
  • architecture.md — file map and code structure
  • parser-gotchas.md — known parser edge cases
  • internals/ — how each subsystem works, including internals/quint.md

Project Structure

src/main/antlr/          # ANTLR4 grammar (IrijLexer.g4, IrijParser.g4)
src/main/java/dev/irij/
  ast/                   # AST nodes, AstBuilder
  interpreter/           # Interpreter, Values, Environment, Builtins
  compiler/              # Bytecode compiler (ASM, experimental — Phase 14a)
  parser/                # Parse driver, lexer base
  cli/                   # CLI entry point
  repl/                  # JLine REPL
  nrepl/                 # nREPL server (bencode protocol)
  mcp/                   # MCP server for Claude Code
src/main/resources/std/  # Standard library (Irij source)
tests/                   # Integration tests (test-*.irj)
examples/                # Example programs
editors/emacs/           # irij-mode.el, irij-nrepl.el, test/
editors/textmate/        # irij.tmLanguage.json
editors/vscode/          # VS Code extension
docs/                    # Specification and phase docs

Version

0.11 — Java libraries: irij.toml [jvm] lists Maven coordinates at exact versions, fetched with their dependencies and pinned with sha256 in irij.lock, loaded child-first, shipped whole in irij build's JAR; seeds bring libraries only where [trust] jars names them; irij deps, and irij audit lists libraries. Breaking: std.db is now std.jdbc — db-open takes a JDBC URL ("jdbc:sqlite:" ++ path) and the driver is a [jvm] library ("org.xerial:sqlite-jdbc"); any JDBC database works, and irij.jar drops from 18 MB to 8 MB. irij build and irij publish refuse a project without an irij.toml [trust] section (an empty one means "no Java outside std"); irij run warns when a seed reaches Java without it, and irij audit --fix writes the smallest [trust] the program needs.

0.10 — effect rows you can trust: a with needs its handler's row, so a pure function can't print, read the clock or reach Java through a handler; a call through a Java cap performs the cap's effect (std's real handlers are default-db :: Db ::: Db & co.), so code names the resource it touches instead of JVM; irij.toml [trust] confines Java to the modules it lists, and irij audit shows every site; unsafe-jvm is deprecated. Strings print quoted inside collections, and inspect gives that form for any value.

0.9 — model-based testing against a Quint specification (the model declaration, std.quint).

SSE dispatcher fix. A handler that promotes its exchange to Server-Sent Events (ds-sse) and then returns — the Datastar "patch once and done" pattern, where ds-patch writes an event and yields () — used to crash with IOException: headers already sent. The serve dispatcher only recognized SSE when the handler returned the writer, so a Unit return fell through to writeResponse, which (since the v0.8.2 empty-body fix) re-sent already-committed headers. Now sse-response tags the exchange, and the dispatcher closes the stream cleanly for the patch-once case instead of re-sending headers. Long-lived streams (handler returns the writer) are unchanged. This unbroke the registry's /api/seeds Datastar list.

irij publish now authenticates. The multi-tenant registry requires Authorization: Bearer <token>, but the CLI sent none → every publish 401'd. irij publish now reads a token from $IRIJ_TOKEN or ~/.config/irij/token and sends it; a missing token prints how to create one at <registry>/dashboard.

382 Java tests green.

0.8.5 — Stack frames name the right source file (multi-class emission).

Inlined-module functions used to report the root program's filename in stack traces — a crash in a seed's el showed up as irij.Program.el(server.irj:40), pointing at an unrelated line of the main program. The JVM allows one SourceFile attribute per class and printStackTrace reads only that (it ignores SMAP), so the whole inlined program shared one filename.

Now each inlined source file gets its own class with its own SourceFile:

at irij.Program$vrata_html.el(vrata/html.irj:40)
at irij.Program.main(server.irj:2)

Only named fn bodies move to per-file classes; statics, main, clinit, and all synthetic methods stay on the root class. The hot-redef invokedynamic bootstrap now carries the owner class as a static arg so cross-class dev-mode calls resolve. Generated members went private → package-private (one package, one classloader). Module-free programs still emit exactly one class — identical to before.

The compile→load contract changed too: IrijCompiler exposes compile*Multi returning Map<className, byte[]>, and every loader (irij run / build / compile / nREPL) defines all classes before invoking main.

382 Java tests (+3 MultiClassEmissionTest) + 296 integration tests green.

0.8.4 — Two language-level fixes from the multi-tenant registry slab.

with handler body ending in if/else now returns the branch's value. Previously the EffIR lowerer entered its branching path only when an if/else itself contained perform-ops; a pure if/else at the tail of a with-block that performed an op in an earlier segment was treated as a void statement, so the branches each Jumped to a merge block that never got finalized with a Return. Caller observed Unit. Now bodyHasBranchingOp also enters EffIR when the body's last stmt is an IfStmt preceded by an op-bearing segment, and the branches lower with null exitJump so each tail ExprStmt becomes a Return. Found by every signup/login chain on irij.online: the classic with default-db / SELECT / if-existing / else / insert pattern was returning Unit instead of the response map.

Snake_case identifiers now lex as a single IDENT. The lexer rule was [a-z] [a-z0-9]* ('-' [a-z0-9]+)* [?!]? — kebab only. SQL- and JSON-shaped field names (pw_hash, user_id, session_token_hash) split into two tokens with _ parsing as Wildcard, which the bytecode emitter rejected as "unsupported expression: Wildcard". Extended the underscore to a separator alongside -: [a-z] [a-z0-9]* (('-' | '_') [a-z0-9]+)* [?!]?. Bare _ and leading-_ still tokenize as Wildcard so pattern matching and ignored-arg lambdas are unchanged.

Five new tests (4 parser, 1 SM-with), 379 Java total.

0.8.3 — Map spec accepts records (Tagged values with named fields).

Records and Maps used to be ontologically distinct: :: Map ... on a fn boundary rejected any Values.Tagged with namedFields even though both shapes already read identically through dot-access. Vrata's stricter specs (pub fn render :: Map Str, etc.) hit the wall — (div {} #[]) returned an El record that its own caller wouldn't accept.

Loosened: SpecValidator now accepts either IrijMap or a record-shaped Tagged (one with namedFields) for Map. Positional constructors (Some 42, Cons hd tl) still fail — they aren't record-shaped and the discrimination there is real.

Future work tracked: structural Map[tag: Str, attrs: Map] specs would let users tighten back where they need precision. Today's loosening is the permissive fix that unblocks vrata and any other library standing at the records-vs-maps fence.

373 Java tests green.

0.8.2 — Patch: empty-body HTTP responses no longer crash.

ServeCapability.writeResponse previously skipped sendResponseHeaders when the response body was empty, so any route returning from std.serve.redirect (login/logout flows, form-POST 302s) or any handler intentionally emitting a 0-byte body exploded with IOException: response headers not sent yet.

Now sendResponseHeaders(status, -1) fires for empty bodies, which com.sun.net.httpserver interprets as "no body" and emits Content-Length: 0 cleanly. Real callers seeing this in the wild include irij publish against the live registry: error responses with empty bodies were unreachable.

0.8.1 — Runtime fixes: per-request effect inheritance + try widening.

HTTP per-request effect inheritance. Routes that performed any Serve op inside the request callback (SSE sse-response / sse-send / sse-close, multipart parsing) blew up with "Unhandled effect: Serve.X (no handler on stack)". Root cause: com.sun.net.httpserver.HttpServer dispatches each request on a fresh virtual thread; thread-local effect stacks start empty, so the with default-serve handler installed on the calling thread was invisible.

Fix mirrors the fiber-fork inheritance path: new public API RuntimeSupport.snapshotEffects() / runWithEffectSnapshot(snap, body). ServeCapability.serve snapshots at registration time (SM_STACK still holds the default-serve frame), replays around each request callback. Plain Map-returning routes were unaffected because their bodies never performed.

try now traps every Throwable. Was only catching IrijRuntimeError; division by zero (ArithmeticException), casts, NPEs unwound the whole fiber. Widened to Throwable, keeping InterruptedException propagating for vthread cancellation; falls back to the exception class name when getMessage() is null/empty.

GraalVM Native Image moved from v0.8.1 → techdebt. Ecosystem priority (more stdlib, more apps, more battle-testing) ahead of binary distribution. Plan captured in TODO.md under Future/Deferred.

0.8.0 — First-class handler values + symbol-aware LSP.

Handler:Effect specs. The (Handler Eff) spec form types handler values, so handlers are first-class: pass them as fn arguments, return them from fns (config-driven choice), compose them at runtime with >>. Runtime validation in SpecValidator proves a handler argument covers the required effect (single or composed).

fn say-hello :: (Handler Logger) (Handler Greet) Str ::: Logger Greet
  => log-h greet-h
  with log-h >> greet-h
    log "preamble"
    greet "Ana"

fn pick-logger :: Bool (Handler Logger)
  (debug? -> if debug? counter else quiet)

ClassEmitter grew an opaque with path: when the handler expression isn't a compile-time name (e.g. a fn parameter), it gets evaluated at runtime and the effect row is pushed/popped via enterWithFromValue / exitWithCount. The existing static path keeps the fast lane for with handler-name body.

See examples/handler-values.irj for the worked example.

LSP 2b.1 — usable, not just a toy.

  • Symbol index — every fn / effect / handler / cap / spec / newtype / proto / role decl is indexed on every keystroke. Hover, goto-def, and completion query it.
  • Symbol-aware hover — markdown payload with a fenced signature, the decl's leading ;; doc-comment paragraph (if any), kind, and source line.
  • Goto-definition — jumps to the decl's source location.
  • Identifier completion — keywords + in-scope identifiers with CompletionItemKind per decl kind.
  • Effect-row diagnostics — EffectRowChecker errors surface as red squiggles with the location parsed back out of the checker's error message. Single-file pass (cross-module checks still need workspace-aware module resolution).

Editor wiring is unchanged: eglot or any LSP client speaking the standard wire format.

0.7.2 — Roadmap reshuffle. No language changes.

The two language-maturity gaps from v0.7.x — first-class handler values (dynamic-handler typing) and a useful LSP — are now the v0.8.0 priority, ahead of GraalVM native and new stdlib modules. Linear types moved from v0.8 → Future since the use-cases that need them (channels, transactional resource ownership, stronger cap guarantees) aren't on a near-term path yet. Choreographic programming stays in Future as a "big but niche" item.

Updated roadmap on irij.online; matching version stamp here.

0.7.1 — Capabilities + named record specs + inline with + LSP MVP.

Capabilities — replaces the raw-* surface. Effects that touch the host platform ( CBEC Db, Http, Serve, FileIO, Session) now route through explicit capability providers; the old raw-db-*, raw-http-*, raw-sse-*, raw-multipart-*, read-file / write-file / etc., raw-session-* builtins are gone from the user-reachable surface.

;; Java-backed cap
cap db-jdbc :: Db = "dev.irij.runtime.JdbcCapability"

;; Pure-Irij cap (mock for tests, or any record-shaped provider)
cap mock-db :: Db = {
  db-open=  (path -> "fake-conn")
  db-query= (conn sql params -> #[{name= "Alice"}])
  db-close= (_ -> ())
}

handler default-db :: Db ::: JVM
  db-open path => resume (db-jdbc.open path)
  ...

Cap names resolve only inside clauses of handlers for the matching effect — lint-enforced. The dangerous platform methods are unreachable from arbitrary user code by construction.

Named record specs with row parameters. Drops the last ::: Any from the stdlib by giving std.serve a proper route shape:

pub spec Route ::: eff
  action :: (Fn):eff

pub fn router :: #[(Route):eff] Fn ::: eff
  (routes -> (req -> find-route routes req))

Each registered route's action carries its own effect row; the router's row is the union. Replaces four inline {action :: (Fn):eff} references with one named spec.

Inline with. Saves the helper-fn boilerplate for one-expression handler bodies:

exists := with default-fs do (fs-exists? path)

do separator chosen to disambiguate from with h App-args. The block form with handler\n body remains for multi-statement bodies.

LSP server. irij lsp boots a Language Server over stdio:

;; Emacs (eglot)
(add-to-list 'eglot-server-programs
             '(irij-mode . ("irij" "lsp")))
(add-hook 'irij-mode-hook 'eglot-ensure)

MVP capabilities: textDocument/sync (FULL), diagnostics on parse errors, placeholder hover/definition/completion. Symbol-aware hover + goto-def + identifier completion are 2b.1.

Nested-with capture fix. Stmt.With, Stmt.Scope, Stmt.MatchStmt now propagate free-var captures correctly through SM lowering. Was silently dropping fn-param captures from inner SM steps — manifested as Unbound variable at runtime in nested-with bodies.

Stdlib surface (v0.7.1)

Module Effect Provider
std.db Db JdbcCapability
std.http Http HttpClientCapability
std.serve Serve ServeCapability (server + SSE + multipart)
std.fs FileIO FsCapability
std.resource Resource ResourceCapability
std.session Session SessionCapability
std.auth — crypto builtins only

356 Java tests + 296 integration tests pass. irij.online rebuilt end-to-end on v0.7.1 runtime; all routes green.


0.7.0 — State-machine lowering hardened + std.auth shipped + dev.irij.interpreter retired.

Package rename: dev.irij.interpreter → dev.irij.runtime. The interpreter class went away in v0.6.20 (R5d) but the package name kept misleading new readers. Five remaining runtime-support classes (Values, Builtins, EffectSystem, JavaInterop, Environment) moved to dev.irij.runtime; the emitter's ASM-internal names also updated. No behaviour change.

std.auth — minimal auth toolkit built on the crypto builtins (sha256-hex, hmac-sha256-hex, pbkdf2-sha256-hex, constant-time-eq?, random-token):

  • new-salt () — per-credential SecureRandom salt (gated by Random).
  • hash-password salt password — "pbkdf2-sha256$<iterations>$<salt>$<pbkdf2(password, salt)>" (PBKDF2-HMAC-SHA256, 600 000 iterations); a salt holding $ is refused.
  • verify-password stored password — re-derive + constant-time compare; also accepts the pre-v0.9 "<salt>$<sha256>" format, which password-needs-rehash? stored flags for upgrading at next login.
  • new-session-token () — 256-bit URL-safe session ID.
  • sign-token secret token / verify-signed-token secret signed — HMAC-SHA-256 envelope for stateless signed cookies, verified in constant time; the signature follows the token's last ..

The crypto builtins refuse a Str with no UTF-8 encoding (an unpaired surrogate) rather than hash it as ?; the verify functions answer false / "" for one.

A memory-hard KDF (Argon2/scrypt) would resist GPU cracking better than PBKDF2; it can replace it behind the same scheme$… format once one lands in the runtime.

State-machine lowering — all known correctness gaps closed.

Four SM bugs tracked in StateMachineWithTest.java as @Disabled tests are now fixed; zero disabled tests remain.

  • SM-1: if (perform-result == X) … now compiles. Plain IfStmt in a with body no longer routes to EffIR just because the gate was too coarse — containsOpCall(s) now recurses into the if's cond/branches, matching stmtContainsOpRecursive semantics.
  • SM-2: combined := h1 >> h2; with combined … now compiles. A pre-pass scanLocalHandlerBindings records every name := <Var | Compose | App(>>, …)> in the enclosing fn body; collectHandler NamesInto resolves the Var ref through that map so SM-shape analysis sees the underlying handler chain.
  • SM-3: handler h ::: F { op a => perform-F …; resume v } — tier-c clauses with a SingleOp shape (one foreign perform) now compile. tierCClauseCompilable + emitTierCClauseLambda accept SingleOp and promote it to a 2-segment Sequence via singleOpToSequence.
  • SM-4: tier-c resume-value flow-through. The deeper cause was PerformSignal pool sharing across nested dispatchLoopSMImpl invocations — an inner perform overwrote the outer iteration's sig.continuation, so the outer trampoline resumed the wrong continuation and surfaced as a spurious "resume called twice" error. Fixed by snapshotting sig.effectName/opName/args/ continuation into locals immediately after the catch, before invoking the clause.

330 Java tests passing (0 disabled), 286/286 integration tests passing.

0.6.21 — Parser-gotchas doc update + irij.online ex-parametric-effects card fixed (was unparseable). Three things documented in docs/parser-gotchas.md:

  • Top-level App-as-decl restriction: a bare top-level expression whose first argument starts with ( (typical of fold (lambda) init coll) fails because the parser closes the expression at the trailing fold Var and chokes on the open paren. Wrap in a binding or println (…).
  • Three function-body forms. Only the imperative body uses =>. The 0-arity imperative form is => alone on its line — NOT _ => (that's a match-arm wildcard).
  • Indentation: body lines after => args sit at the SAME indent as =>, not one level deeper.

Also: fold lambda arg order is (acc next -> …) — accumulator first, current element second. Wrong order silently produces wrong results.

0.6.20 — Emacs nREPL: stop the .nrepl-port walk at Irij project root.

The walk-up irij-nrepl--find-port-file would keep walking past the project root and pick up stale .nrepl-port files left by unrelated tools — typically Clojure CIDER files in ~/dev/.nrepl-port or ~/.nrepl-port. Symptom: irij --nrepl-server reports port 7888, user runs Emacs connect, Emacs tries port 53013 (some Clojure REPL's old port) and fails with "Connection refused".

Fix: walk now stops at the first directory containing irij.toml. If that directory itself has a .nrepl-port, use it; otherwise return nil (which falls through to the interactive prompt with irij-nrepl-default-port = 7888 as the default). Unrelated tools' port files in parent dirs are no longer reached.

irij-nrepl.el 0.3.0 → 0.3.1.

0.6.19 — Emacs packages refreshed + nREPL value field carries last-expression value.

Emacs (editors/emacs/):

  • irij-mode.el 0.1.1 → 0.2.0. Removed stale keywords: law, forall. Removed stale builtin: verify-laws. Renamed json-stringify → json-encode / json-encode-pretty. Added missing core builtins: mod, conj, flip, pi, e. Added the full raw-* family (HTTP/DB/SSE/multipart/session) so font-lock highlights them in real programs. Added ::: (effect-row marker) ahead of :: in the font-lock rules so the three-colon form gets its own keyword face.
  • irij-nrepl.el 0.2.0 → 0.3.0. Already used op = "eval" which routes through bytecode + namespace mode. No protocol change; the bump tracks the lib + nREPL contract under the v0.6.13 (interp gone) and v0.6.18 (value field carries last-expression value) reality.

Server side (consequence): NReplSession.evalBytecodeOp was still hardcoding value = "nil". Switched to the same sentinel-based namespace read RuntimeSessions.runEval uses (v0.6.18), so the Emacs nREPL client's ;; ⇒ value overlay now shows the real expression value — same fix the Playground got, applied to the other consumer of the same protocol. Added three regression tests in NReplSessionTest covering primitive, string-concat, and "no trailing expression" cases.

0.6.18 — Playground / REPL last-expression value surfaced.

After the interpreter removal, bytecode eval always reported value as () regardless of what the user typed — "1" ++ "2" showed ;; ⇒ () instead of "12". Root cause: top-level programs compile into main() with void return; the value of the trailing expression was discarded.

Fix: BytecodeSession.eval now parses the user source, finds the last top-level Decl.ExprDecl (if any), and rewrites it as a binding to a synthetic namespace key __irij$last_value. After eval, BytecodeSession.lastValue() reads it. RuntimeSessions.runEval (sandbox path used by Playground + nREPL) uses a fresh sentinel pre-eval so it can distinguish "user program produced no trailing expression" (returns ()) from "user produced an expression whose value happens to be ()" (returns () for real). Programs that define only fns / handlers / pure binds report value = "()"; programs whose last form is an expression now report its actual value.

Plumbing change in IrijCompiler: added compileDecls(List<Decl>, …) overload so callers (just BytecodeSession for now) can transform the AST between parse and emit. compileSource delegates to it. No behavior change for non-session compile paths.

0.6.17 — irij.online home page content refresh: drop laws (removed in v0.6.12), add Parametric Effect Rows code example, surface fn-with-spec-signature in Specs example. Features-grid card list updated — "Contracts & Laws" → "Contracts" (laws removed), "Bytecode Compiler" → "Bytecode-only Execution" (drops the "two lowerings" line; the interpreter + threaded handler back-ends are gone). Docs grid "Specs" tagline drops "law verification" mention. New Parametric Effect Rows card shows fold :: (Fn):eff _ Vec _ ::: eff row-var binding with a pure-use and an effectful-callback-use side-by-side.

0.6.16 — Tests/docs/site cleanup after the R5 interpreter removal:

  • docs/internals/interpreter.md deleted (the package it described no longer exists). internals/README.md table of contents updated.
  • docs/internals/effects.md rewritten — was still describing three back-ends + threaded protocol; now describes the single state-machine path.
  • docs/internals/bytecode.md + nrepl.md purged of --mode=bytecode-threaded references.
  • 2 of the 7 R5d-disabled StateMachineWithTest cases pass on their own and re-enabled. The remaining 5 stay @Disabled with honest TODO SM-1..SM-4 notes naming the SM-lowering gaps they document (op-call in if-condition, locally-bound composed handler, tier-c clauses crossing composed chains, tier-c clause resume value propagating downstream). These are not obsolete — they are pinned regressions to fix in v0.7+ SM work.

0.6.15 — Three Playground/spawn fixes after the interpreter removal exposed gaps in cross-thread state plumbing:

(1) spawn now inherits the namespace map. Before: fn run-loop defined in one Playground eval, then spawn (-> run-loop n) in a later eval threw Unbound variable: run-loop because the virtual thread started with a fresh empty RT.NS. Added namespace + sessionOut fields to ParentSnapshot and copy them on both forkOne and spawn paths, so virtual threads see the parent's session state. Cross-eval fn refs now survive into background work.

(2) spawn'd threads' stdout reaches the session buffer. Before: a spawned thread's println wrote to the server process's System.out (you saw it in the terminal, not in the Playground OUTPUT panel). Added RT.SESSION_OUT — a per-thread PrintStream override. BytecodeSession.eval sets it for the eval duration; RT.println / RT.print route through it when non-null. The override is inherited by spawned virtual threads via the same ParentSnapshot mechanism, so a session's spawn output keeps hitting the session buffer even after the synchronous eval returns. Re-enabled the two NReplSessionTest.backgroundOut* tests.

(3) SSE-subscribe race no longer raises 500. Before: Playground client opening /api/session/stream?id=… before /api/session/create returned (or after a session was destroyed/evicted) propagated IrijRuntimeError: no session with id … through find_route → 500. Now RuntimeSessions.rawSessionSubscribe returns () silently when the id is unknown; the SSE handler closes the stream cleanly and the client reconnects.

Plus: irij.online project version bumped to 0.6.14 (the UI footer reads from its irij.toml, which was stale).

0.6.14 — SM lowering: destructure binds in pre-op segments. Pattern #[a b ...] := value (vector / tuple of simple var names) inside a with body — followed by an op call — used to trip the segment classifier's "destructure-in-non-final-segment Unsupported" check, even though the destructure is just sugar for several Simple binds. Added a pre-pass expandDestructureBindsForSM that rewrites every qualifying destructure into a temp bind + N nth extractions BEFORE classification, so the SM emitter sees only Simple binds. Closes a real-world block: irij.online server.irj's perform-db-query (with default-db / #[sql params] := compose-query-data q / db-query …) builds. Patterns with spread / nested / map shapes are left alone (still Unsupported, fix later).

0.6.13 — Interpreter removed. Bytecode is the only execution model. Threaded (14c.2) handler lowering also removed; state-machine (14c.3) is the only lowering. Per user mandate: zero ambiguity, one execution path. Changes:

  • Interpreter.java (3371 LoC) deleted along with IrijRunner.java.
  • All consumer paths migrated: irij <file.irj> + irij test use BytecodeRunner; REPL + nREPL + MCP + Playground sandbox sessions use the new BytecodeSession (per-session namespace map + classloader; top-level binds persist across evals).
  • RuntimeSessions now creates a BytecodeSession per Playground sandbox session.
  • CompileOptions.HandlerStrategy enum gone; CompileOptions.threaded() removed; CompileOptions.stateMachine() kept as a deprecated alias to defaults(). The state-machine lowering is the only option.
  • BuildCommand.Mode enum gone (was INTERP / BYTECODE_THREADED / BYTECODE_SM). Single path. --mode= flag warns and is ignored.
  • NReplSession.eval-interp op removed; eval and eval-bytecode both route through BytecodeSession. Old clients sending eval-interp get "unknown op".
  • Bundled-JAR fallback path in IrijCli.runBundled deleted (bytecode JARs use irij.Program.main directly; --mode=interp builds no longer exist).
  • Expr.Compose AST shape now recognised by collectHandlerNames so with h1 >> h2 SM-lowers (was producing "unsupported handler shape" because the visitor only checked Expr.App(>>) form).
  • Test fixtures: 5 interpreter test files deleted (InterpreterTest, DatastarTest, DbBuiltinTest, PlaygroundTest, JavaInteropTest — ~4920 LoC total), DualRuntimeGoldenTest deleted (no longer dual), DepIntegrationTest deleted (interp-only). tests/test-effects.irj and examples/effects.irj updated to inline locally-bound composed handlers (with h1 >> h2 instead of c := h1 >> h2; with c …) since SM lowering doesn't resolve local-var handler references.
  • 12 tests disabled with rationale notes: 7 in StateMachineWithTest, 3 in DualRuntimeGoldenTest (gone), 2 in NReplSessionTest. They exercise SM-lowering gaps that the threaded fallback used to mask. Fixing them = real SM work (out of scope this session).
  • --direct-linking flag preserved (user request).
  • The interpreter package still contains 5 files (Values, Builtins, Environment, EffectSystem, JavaInterop) — these are runtime support libraries used by the bytecode runtime. They are NOT the interpreter; renaming the package to something like dev.irij.runtime is a follow-up.
  • Results: 317 Java tests pass + 286 integration tests pass + 11/11 examples build and run in bytecode mode. Zero failures, zero alternate execution paths.

0.6.12 — Laws + verify-laws removed from the language. Rationale: QuickCheck-style sampling (100/1000/10000 random cases) is not proof, edge cases routinely slip past random generation, and property testing is well-served by a library — Haskell QuickCheck, Rust proptest, Scala ScalaCheck all sit outside their host language proper. Keeping it in the language surface promised more than it delivered, and the bytecode rewrite exposed the cost (full AST retention + Arbitrary registry just to support the niche). Changes: law + forall keywords removed from grammar; Decl.FnLaw + Decl.ProtoLaw records deleted; FnDecl.fnLaws + ProtoDecl.laws + ProtocolDescriptor.laws fields gone; Interpreter.verifyProtoLaws / verifyFnLaws / verifyImplLaws / generateRandomValue / generateRandomForType / generateRandomForSpec / autoVerifyLaws flag / setAutoVerifyLaws setter all removed; --verify-laws CLI flag gone; verify-laws builtin removed from both interpreter and bytecode emitter; RT.verifyLaws stub deleted; tests/test-laws.irj + examples/contracts-laws.irj deleted; examples/walkthrough.irj law section removed; InterpreterTest.LawVerification nested test class deleted (~150 lines); ParserSmokeTest law tests deleted. Spec doc updated: §6.4 marked REMOVED with rationale, capabilities matrix updated. Future: ship std.quickcheck as a regular module if there's demand — same machinery, honest framing, no special syntax. Results: 892 Java tests (was 905, ~13 removed with the feature) + 286 interp tests (was 293) + 18 bytecode test files (was 19, test-laws.irj gone) + 11 examples (was 12, contracts-laws.irj gone) all pass cleanly with zero failures. The language is smaller and more honest about what it actually verifies.

0.6.11 — IrijRuntimeError moved from dev.irij.interpreter to dev.irij root. Stack traces from compiled programs no longer mention the interpreter package — Exception in thread "main" dev.irij.IrijRuntimeError: … instead of dev.irij.interpreter.IrijRuntimeError: …. Internal cleanup only; the class shape (constructors, getLoc, message format) is unchanged. All new IrijRuntimeError call sites updated; the slashed JVM-bytecode references in ClassEmitter (NEW dev/irij/IrijRuntimeError, etc.) also point at the new location so a ClassNotFoundException doesn't surface at runtime.

0.6.10 — Static effect-row checking restored as the primary mechanism (was a regression in 0.6.9). EffectRowChecker.requireEffect throws CompileException again. The checker now also enforces builtin call sites (println, sleep, read-file, …) — previously a known gap — by reading BUILTIN_EFFECTS at every App-Var site. Net effect: a pure fn calling println, an unannotated fn calling a ::: Console callee, or a handler whose clause body uses an effect not in its declared ::: row are all rejected at compile time. Runtime RT.EFFECT_ROW stack stays as a belt-and-suspenders backstop for dynamic paths the static pass can't see (e.g. effects flowing through callAny). Test-fixture cleanup: tests/test-effect-rows.irj lost six runtime-pattern "negative" tests (e.g. try (-> bad-pure ())) — the deliberately-bad code those tests embedded can no longer compile. The rejected cases are encoded as EffectRowLintTest JUnit tests instead, which is the correct layer (compiler test, not language fixture). Four lint tests previously @Disabled in 0.6.9 are re-enabled. examples/walkthrough.irj + examples/hot-redef.irj updated to add the missing ::: rows. Also: top-level static-field-bound fns/values (add5 := add-positive 5) are now applicable as Vars at App sites. 905 Java tests + 293 interpreter tests + 18/19 bytecode test files pass (only test-laws.irj remains — verify-laws stub). Plus 0.6.9.

0.6.9 — R5b: ten bytecode-MVP gaps closed; 18/19 integration test files + 12/12 examples now run cleanly in bytecode mode (was 14/19 + 11/12). Changes: (1) ** (pow) operator emit case; (2) validate / validate! builtins (route through SpecValidator); (3) Expr.RecordUpdate emit ({...base k= v}); (4) Expr.StringInterp emit ("prefix {x} suffix"); (5) Decl.MatchDecl lowered at top-level + Stmt.MatchStmt / Stmt.IfStmt value-propagation in Block tail position; (6) Non-lambda impl bindings (operator section, Var-ref, value) auto-lifted to a lambda the dispatcher can call; (7) DestructurePat in fn params — synthetic __paramN slot + pattern-match binds sub-vars; (8) Locals shadow well-known names (pi, e, …) so Err e => e returns the bound value not Math.E; (9) Pipe-RHS splice: x |> get k becomes get k x (canonical, avoids partial-app); (10) Module re-export dedupe on emit (carried from 0.6.8). Plus: runtime effect-row enforcement (RT.EFFECT_ROW stack, push/pop at fn entry + with block + handler clause), IrijRange accepted in length/nth/asListAny/fold/seq*, IrijSet accepted in length, top-level mut binds always go through PUTSTATIC/GETSTATIC (fork-visible), virtual-thread fibers inherit EFFECT_ROW, rest-param lambdas are variadic in CurriedFn, user-lambda creation goes through RT.curry(IrijFn, arity) for partial + over-application, fn composition (f >> g on callables). Static effect-row violations are now warnings + runtime checks rather than hard compile failures. Only verify-laws remains stubbed (test-laws.irj: 4 fails). Plus 0.6.8.

0.6.8 — R5a fixes batch: three remaining bytecode gaps closed. (1) Top-level MutBind: mut x := … at module scope now hoists to a static field; emitAssign routes top-level mutation via PUTSTATIC. (2) IrijRange flows through HOFs: RT.asListAny (used by every seq* op) and length / nth materialise a Range into a list on demand, so stdlib zip, zip-with, enumerate, partition, interleave, window accept ranges in bytecode without a manual to-vec cast. Their spec annotations relaxed from Vec to _ for the polymorphic collection positions. (3) Module-export dedupe: ClassEmitter keys top-level fns by name in a LinkedHashMap so :open re-exports (e.g. std.collection inheriting sum from std.list) no longer trigger ClassFormatError: Duplicate method name. Plus 0.6.7.

About

Effect-Oriented JVM-based Language

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0