Onion is a statically typed language for turning messy external data into checked, reversible tools. It runs on the JVM and calls Java directly.
Most languages hand you a String at the boundary and wish you luck. Onion asks you to
describe the boundary once — and derives the parser, the printer, the failure channel and
the command-line interface from that one description.
record Access(ip: String, method: String, path: String, status: Int)
shape common = re"(\S+) (\w+) (\S+) (\d+)"
def main(path: String, min: Int = 400): void {
val each = file(path).eachLine(Access::common())
foreach a: Access in Outcome::values(each) {
if a.status() >= min { println(a.status() + " " + a.path()) }
}
foreach d: Defect in Outcome::defects(each) {
println("line " + d.origin().line() + ": " + d.expected())
}
}
One declaration gives you parsing, printing, and a CLI with --min and --help derived
from main's signature.
The last two lines are the point. A thousand-line log with five corrupted lines gives you 995 rows and the five you could not read, each with its line number — where almost every other tool hands back 995 rows and no indication the other five ever existed.
Properties can be checked by the compiler, at build time:
record Pt(x: Int, y: Int)
shape text = re"(-?\d+),(-?\d+)"
law roundtrip(p: Pt) { Pt::text().parse(Pt::text().print(p)).get() == p }
That law really runs — over generated samples, during onionc. It is deliberately not on
the Access shape above: \S+ cannot match an empty string, so printing an Access with
an empty ip produces text that does not parse back. The round trip holds on the data a
shape can actually represent, and saying which is the difference between a reversible
language and one that claims to be. Writing the law on Access is how the sentence you
just read got corrected.
Install the latest release with one command (requires Java 17+):
curl -fsSL https://raw.githubusercontent.com/onion-lang/onion/develop/install.sh | shThis downloads onion.jar from the latest GitHub Release and installs the
onion, onionc and onion-repl commands into ~/.local/bin.
Once installed, run scripts directly — shebang scripts work too:
echo 'println("hello")' > hello.on
onion hello.on#!/usr/bin/env onion
println("Hello, " + args[0] + "!")
Or start a multi-file project instead of a single script:
onion new hello
cd hello
onion run
onion testSee the Project CLI guide for the manifest, source layout, build cache, and test conventions.
Local val / var declarations can omit types when an initializer is present, and control-flow constructs such as if are expressions.
val name = "Onion"
val label = if name.length > 0 { "ok" } else { "empty" }
val xs = [1, 2, 3] // List literal
val ages = ["alice": 12, "bob": 34] // Map literal (insertion-ordered)
val double: Int -> Int = x -> x * 2 // lambdas: bare params, expression bodies
foreach i: Int in 0..<xs.size() { // ranges: a..b inclusive, a..<b exclusive
println("xs[#{i}] = #{xs.get(i)}") // interpolation (nested strings OK)
}
val cmp: Comparator[Integer] = (a, b) -> (b as Int) - (a as Int) // SAM conversion
Collections::sort(mutable, cmp)
val branch = Proc::run("git", "branch", "--show-current") // run external commands
val opts = Args::parse(args) // CLI flags & options
foreach f: String in Files::glob(".", "*.on") { ... } // glob
// onion --watch script.on -> rerun on every save
def add(x: Int, y: Int): Int = x + y
val double: Int -> Int = (x) -> { return x * 2; }
val inc = (x: Int) -> { return x + 1; }
println(double(21))
See Shapes for the whole story: named shapes per
record, Outcome instead of null, per-line reads that keep what failed, and canPrint()
answering whether a shape is reversible rather than the printing method silently not
existing.
Kotlin-style null safety, including nullable-aware generics:
val name: String? = lookup()
val len = name?.length() ?: 0 // safe call + Elvis
val first = rows?[0] // safe indexing
val sure: String = name!! // non-null assertion (throws on null)
if name != null {
println(name.length()) // smart cast: name is String here
}
class Box[T] { ... } // bare [T] accepts String? too;
new Box[String?](name) // deref inside the body needs ?. / checks
record Pair[A, B](first: A, second: B)
val (a, b) = new Pair[String, Integer]("x", 42) // destructuring
enum Planet(mass: Double) {
MERCURY(3.3e23),
EARTH(5.97e24)
public:
def heavierThan(other: Planet): Boolean = this.mass() > other.mass()
}
class Point(val x: Int, val y: Int) { // primary constructor
public:
def dist(): Int = this.x * this.x + this.y * this.y
}
sealed interface Shape {}
record Circle(r: Int) conforms Shape
record Rect(w: Int, h: Int) conforms Shape
select shape { // exhaustiveness-checked
case Circle(r): println("circle " + r)
case Rect(w, h): println(w * h)
}
trait Numeric[T] {
def plus(a: T, b: T): T
def zero(): T
}
instance Numeric[Int] {
def plus(a: Int, b: Int): Int = a + b
def zero(): Int = 0
}
def sum[T: Numeric](xs: List[T]): T { ... }
trait / instance with dictionary passing for constrained generics.
Onion supports Haskell-style do notation for composing monadic operations (Option, Result, Future, etc.):
// Async computation with do notation
val result: Future[Int] = do[Future] {
x <- Future::async(() -> { return fetchUser(); })
y <- Future::async(() -> { return fetchData(x); })
ret x + y
}
// Option chaining
val user: Option[String] = do[Option] {
id <- lookupId("alice")
profile <- loadProfile(id)
ret profile.name
}
Methods accepting a function as the last parameter can use trailing lambda syntax:
// Traditional call
list.map((x: Int) -> { return x * 2; })
// With trailing lambda
list.map { x => x * 2 }
// Multiple arguments + trailing lambda
future.onComplete(onSuccess, onFailure) { result =>
println("Done: " + result)
}
Built-in Future[T] type for async operations:
val future: Future[String] = Future::async(() -> {
return Http::get("https://api.example.com/data");
})
future.map((data: String) -> { return parseJson(data); })
.onSuccess((result: Object) -> { println(result); })
.onFailure((error: Throwable) -> { println("Error: " + error); })
Onion collapses the boilerplate layers a script usually needs. Scheme-prefixed
raw literals turn a shape into a value; prefix"raw" desugars to prefix("raw")
for any identifier, so re/file/http are built in and you can define your
own prefix just by defining a function of that name:
val p = re"\d+-\d+" // compiled Pattern (raw: no \\ escaping)
val rows = file"data.csv".csvRows() // read + RFC 4180 parse, header-mapped
def sql(q: String): String = "[SQL] " + q // a user-defined prefix...
sql"SELECT * FROM t" // ...used as sql("SELECT * FROM t")
A record can derive a typed parser straight from a regex shape, and the pipeline
operator |> threads a value through functions:
record Hit(ip: String, method: String, path: String, status: Int)
from re"(\S+) (\w+) (\S+) (\d+)"
Hit::parseAll(file"access.log".text()) // List[Hit], bad lines skipped
.filter { h => h.status() >= 500 }
.groupBy { h => h.path() }
|> println
A top-level main with typed parameters derives its whole command-line interface
(required params are positional, defaulted params become --flags, usage is
generated on error):
def main(name: String, count: Int = 3, loud: Boolean = false): void { ... }
// $ onion greet.on world --count 5 --loud
Onion was originally written in Java, and has been rewritten in Scala completely except the parser, which uses JavaCC.
The post-parse compiler is now an explicit pipeline:
Parsing -> Rewriting -> Typing -> TailCallOptimization
-> MutualRecursionOptimization -> TypedAstCodeGeneration -> backend.asm.AsmBackend
Typing.scalais the orchestration layer for header/outline/body/duplication passes, with per-unit state held intyping.session.TypingSession/TypingUnitContext.- Type name resolution now lives in
onion.compiler.typing.NameResolver, withNameMapperretained only as a compatibility facade. TypedGenerating.scalais now just a public legacy facade overonion.compiler.codegen.legacy.TypedGeneratingBridge; the main pipeline targetsonion.compiler.codegen.TypedAstCodeGenerationandonion.compiler.backend.asm.AsmBackend.OnionCompilerdelegates execution and timing toonion.compiler.pipeline.CompilerPipeline, which can emit phase-by-phase compile profiles.
onionc [options] source files...- -classpath Set classpath of source files in compilation.
- -encoding Set encoding of source files.
- -d Set output directory of results.
- -maxErrorReport Set the maximum number of compilation errors reported.
- --dump-ast Print parsed AST to stderr.
- --dump-typed-ast Print typed AST summary to stderr.
- --profile-compile Emit a phase-by-phase compile profile.
- --profile-format <text|json> Set compile profile format.
- --profile-output Write the profile to stderr, stdout, or a file.
- --warn <off|on|error> Set warning level.
- --Wno Suppress warnings (e.g., W0001,unused-parameter).
onionc compiles source files into class files in the directorys corresponding to module names
of source files rooted by "-d" option. If "-d" is not specified, the value of "-d" is specified as the current directory.
For example, if source files which module name is "org.onion_lang" is compiled, class files are generated under:
- Unix-like OS : org/onion_lang
- Windows: org\onion_lang
onion [options] source files... [command line arguments]
- -classpath classpath of source files in compilation.
- -encoding encoding of source files.
- -maxErrorReport the maximum number of compilation errors reported.
- --dump-ast Print parsed AST to stderr.
- --dump-typed-ast Print typed AST summary to stderr.
- --profile-compile Emit a phase-by-phase compile profile.
- --profile-format <text|json> Set compile profile format.
- --profile-output Write the profile to stderr, stdout, or a file.
- --warn <off|on|error> Set warning level.
- --Wno Suppress warnings (e.g., W0001,unused-parameter).
onion new <name> scaffolds a convention-over-configuration project with a
manifest, src/, and tests/; onion build, onion run [-- args...],
onion test, and onion clean then work from the project root or any
nested directory. See the
Project CLI guide for the full
manifest, layout, cache, and test conventions.
Use onion repl, onion-repl, or sbt repl to start the interactive REPL. The promoted REPL supports multi-line input, persistent resN bindings, :load, :time, :classpath, :type, :typed, :ast, and :bytecode.
onion compiles source files into in-memory class files and execute them. The entry point is:
- A main method if there is an explicit class definition and it have the main method.
- The main method of the class on the top.
- Otherwise, the first top-level declaration or expression.
- The compiler enforces a no-crash / no-miscompile bar (mutation fuzzer, crash-reproducer corpus, codegen-correctness tests); if you find a crash or miscompilation, please file a minimal repro.
- Generics are erasure-based: no reified type info, and type arguments are invariant (no variance or wildcards).
- Tail-call optimization covers direct and mutual self-recursion (not general continuation-passing style).
- Diagnostics are still improving; some errors may be reported later in the pipeline than ideal.