AIR-J is an AI-first, JVM-targeting programming language whose primary artifact is a canonical, typed, effect-tracked intermediate representation.
It is meant to be written, transformed, checked, and lowered by software agents, not optimized for direct human authorship.
The mission, taken directly from the project design notes, is to:
- reduce ambiguity
- reduce representation variance
- reduce re-analysis cost
- make transformation legality mechanically checkable
- preserve direct access to the JVM ecosystem
Principle: one meaning, one representation.
AIR-J is not trying to be a nicer Java, Kotlin, or Clojure for humans. The persisted form is a canonical s-expression tree with:
- explicit imports and exports
- explicit types
- explicit effects
- explicit control flow
- explicit mutation
- explicit Java interop
- direct lowering to JVM bytecode
That makes AIR-J closer to a writable canonical IR than to a human-oriented surface language.
For the authoritative design and language contract, see:
The implementation is written in Clojure and targets the JVM directly.
Implemented areas include:
- module parsing and normalization
- explicit types and effects
- records, enums, and tagged unions
- contracts and invariants
- canonical primitive operators for
Int,Bool,Float, andDouble - string and text-sequence primitives
- explicit stdin/stdout operations
- explicit Java interop
- host-backed modules for callback-oriented JVM frameworks such as Processing
- canonical standard modules for
airj/core,airj/bytes,airj/env,airj/file,airj/json, andairj/process - JVM lowering and bytecode emission
AIR-J is already far enough along to compile and run nontrivial programs, but it is still evolving toward a fuller AI-oriented language/runtime.
The main entrypoint is airj.cli.
Examples:
clj -M -m airj.cli parse path/to/program.airj
clj -M -m airj.cli normalize path/to/program.airj
clj -M -m airj.cli check path/to/program.airj
clj -M -m airj.cli lower path/to/program.airj
clj -M -m airj.cli build path/to/program.airj target/classes
clj -M -m airj.cli run path/to/program.airj arg1 arg2Two execution modes matter:
buildwrites.classfilesruninvokes the exported AIR-Jmain
The generated JVM main wrapper behaves like this:
- if AIR-J
mainreturnsInt, that becomes the JVM process exit code - other return values are not printed automatically by the generated JVM wrapper
- program output should be done explicitly with
io/printorio/println
For boundary-heavy programs, the canonical rule is:
- raw boundary ops may carry explicit host effects and
Foreign.Throw *-resultwrapper functions should convert recoverable host failures into(Result _ Diagnostic)Diagnostic.messageshould stay stable and machine-orientedDiagnostic.detailshould identify the relevant input, path, or context
AIR-J keeps host interaction in a small canonical module surface:
airj/core: canonical carriers such asDiagnostic,Interchange,Option, andResultairj/bytes: raw byte values and UTF-8 conversionairj/env: explicit environment readsairj/file: canonical filesystem boundaryairj/json: canonical JSON interchange boundaryairj/process: canonical subprocess boundaryairj/test: canonical test outcomes and assertionsairj/test-runner: AIR-J-native test summary and exit-code logic
These modules exist to reduce representation drift. They are the default machine-facing boundary, and raw Java interop should be reserved for intentional foreign integration.
The canonical AIR-J test shape is:
- reusable suite modules export uniquely named suite functions
- explicit runnable test-root modules export a zero-arg
testsfunction returning(Seq TestOutcome) - root
maindelegates toairj/test-runner.runorairj/test-runner.run-json - the resulting root module can be built and run like any other AIR-J jar
- bootstrap CLI testing expects that exact root-module shape; individual exported test functions are not a second supported style
clj -M -m airj.cli test --json ...returns one canonical summary artifact with:modulepassedfailederroredoutcomes
See examples/HTW/README.md for a complete AIR-J-native test jar example.
See examples/ToolWorkflow/README.md for a non-game example that emits and then consumes the canonical JSON test artifact.
See examples/Contracts/README.md for a contract-heavy example that uses invariants plus requires/ensures.
See examples/Ledger/README.md for a contract-heavy ledger example with explicit text and JSON test roots.
See examples/Thermostat/README.md for a non-financial safety-controller example with contract failure tests.
See examples/Wiki/README.md for a pure AIR-J wiki domain example mapped from the non-HTTP wiki acceptance features.
The repo uses Speclj, coverage, CRAP, mutation testing, and dependency analysis.
Core commands:
clj -M:check-structure spec
clj -M:spec
clj -M:cov
clj -M:crap
clj -M:mutate src/.../file.clj --scan
clj -M:mutate src/.../file.clj --max-workers 3
clj -M:check-dependenciesThe stricter project workflow and pinned toolchain are documented in AGENTS.md.
This program prints its own output and returns 0.
(module example/hello
(imports)
(export main)
(fn main
(params (args StringSeq))
(returns Int)
(effects (Stdout.Write))
(requires true)
(ensures true)
(seq
(io/println "Hello, world!")
0)))Run it:
clj -M -m airj.cli run hello.airjBuild it:
clj -M -m airj.cli build hello.airj target/classesIf the program only uses pure computation and direct Java interop, the generated classes can usually be run directly from the output directory:
java -cp target/classes example.helloThis is a small AIR-J command-line program that reads its first argument, computes the prime factors, prints them, and returns 0.
(module example/prime-factors
(imports)
(export divides? append-factor main)
(fn divides?
(params (n Int) (divisor Int))
(returns Bool)
(effects ())
(requires true)
(ensures true)
(int-eq
(int-mod (local n) (local divisor))
0))
(fn append-factor
(params (acc String) (first? Bool) (factor Int))
(returns String)
(effects ())
(requires true)
(ensures true)
(if
(local first?)
(int->string (local factor))
(string-concat
(string-concat (local acc) " ")
(int->string (local factor)))))
(fn main
(params (args StringSeq))
(returns Int)
(effects (Foreign.Throw Stdout.Write))
(requires (int-gt (seq-length (local args)) 0))
(ensures true)
(let ((input
(string->int
(seq-get (local args) 0))))
(loop ((n (local input))
(divisor 2)
(acc "")
(first? true))
(if
(int-eq (local n) 1)
(seq
(io/println (local acc))
0)
(if
(call (local divides?) (local n) (local divisor))
(recur
(int-div (local n) (local divisor))
(local divisor)
(call (local append-factor)
(local acc)
(local first?)
(local divisor))
false)
(recur
(local n)
(int-add (local divisor) 1)
(local acc)
(local first?))))))))Run it:
clj -M -m airj.cli run prime_factors.airj 294Expected program output:
2 3 7 7
AIR-J can optionally emit a class that extends a Java host superclass:
(module example/hosted
(host java.util.ArrayList)
(imports
(java java.util.ArrayList))
(export snapshot)
(fn snapshot
(params (self (Java java.util.ArrayList)))
(returns Int)
(effects (Foreign.Throw))
(requires true)
(ensures true)
(java/call
(local self)
size
(signature () Int))))This is the compatibility hook used for Processing-style callback frameworks and similar JVM libraries.
The canonical development path is still:
clj -M -m airj.cli run path/to/program.airj ...For built classes, AIR-J has two runtime shapes:
- self-contained generated classes that only need the output directory on the classpath
- generated classes that rely on AIR-J runtime helpers such as JSON, file, process, or host support
For the second case, launch with the built output directory plus the current AIR-J runtime classpath:
java -cp "target/classes:$(clj -Spath)" example.tool input.json output.jsonThat keeps the persisted AIR-J program canonical while making the host/runtime dependency boundary explicit.
Java already has collections, numbers, strings, and framework APIs. AIR-J still needs its own canonical surface because the project goal is not raw capability. It is canonicality for agents.
If AIR-J exposed Java’s full representation freedom as the default, agents would have to choose between many equivalent encodings:
ArrayListvsLinkedListHashMapvsLinkedHashMapOptionalvs sentinel values- exceptions vs explicit result values
That would directly work against the mission in notes.md. AIR-J therefore prefers a smaller, explicit, canonical machine vocabulary and only uses raw Java APIs when interop is intentionally requested.
- notes.md: design intent and rationale
- formal-v0-spec.md: normative persisted-language contract
- AGENTS.md: implementation workflow, checks, and toolchain pins