AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Effect-Oriented JVM-based Language
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Token-efficient effect-oriented language. Runs on JVM (Java 21+). Tree-walk interpreter with nREPL server.README 전문 보기 ↗
Token-efficient effect-oriented language.
Runs on JVM (Java 21+). Tree-walk interpreter with nREPL server.
./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
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
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
./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.
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 returnsfn 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 handlereffect, 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 clockpre, post (function-level), in, out (module-boundary with blame)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.)proto, implscope, fork, par, race, timeoutstd.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 effectstd.term's Term effect: raw mode, decoded key/mouse events, resize, ; mockable, so a TUI is testable with no screenAll docs live in docs/:
irij-lang-spec.org — full language specification (source of truth)phase-*.md — implementation notes per phasearchitecture.md — file map and code structureparser-gotchas.md — known parser edge casesinternals/ — how each subsystem works, including internals/quint.mdsrc/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
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.
;; doc-comment paragraph (if
any), kind, and source line.CompletionItemKind per decl kind.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 (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.
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.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.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.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:
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 (…).=>. The 0-arity imperative form is => alone on its line — NOT _ => (that's a match-arm wildcard).=> 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.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.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".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).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.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).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.
wcwidthprintln "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 itcap 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[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/; 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/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)cond (first condition that holds) and case (equality dispatch) for multi-way choices without else if chainsmod, use, pub, qualified namesClass/method for statics, obj#method for instance members (auto-reflection, java.lang auto-imported); every Java site needs ::: JVM, checked at compile timeirij build packages self-contained runnable JARseditors/emacs/irij-nrepl.elirij-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/interpreterValuesBuiltinsEnvironmentEffectSystemJavaInteropdev.irij.runtime