A compiled language for the JVM with actors, channels, and native Java interop.
Siyo compiles directly to JVM bytecode. No intermediate language, no transpilation — your code runs on the same VM as Java and Kotlin, with full access to the Java ecosystem.
It ships with an actor model, Go-style channels, closures, pattern matching, structs, and a growing standard library. The compiler is ~19K lines of Java and includes both a bytecode backend (ASM) and a tree-walking interpreter for debugging.
A result that is one shape or another, and a match that must cover it:
type Config = Loaded(map) | Missing(string)
fn load(path: string) -> Config {
if !io.exists(path) { return Missing(path) }
match json.parse(io.readFile(path)) {
Parsed(settings) => Loaded(settings),
Invalid(why) => Missing(path + ": " + why),
}
}
fn main() {
match load("app.json") {
Loaded(settings) => println("port " + toString(settings["port"])),
Missing(why) => println("no config: " + why),
}
}Leaving out either arm is a compile error, not a surprise at run time.
Concurrent key-value store with actors:
actor Store { data: map }
impl Store {
fn new() -> Store { Store { data: map() } }
fn set(self, key: string, value: string) -> string {
self.data.set(key, value)
"OK"
}
fn get(self, key: string) -> string {
if self.data.has(key) { return toString(self.data.get(key)) }
"(nil)"
}
}
mut store = spawn Store.new()
println(store.set("lang", "siyo")) // OK
println(store.get("lang")) // siyo
send store.set("async", "fire-and-forget")REST API with SQLite (runs on JVM):
import java "java.sql.DriverManager"
import java "java.net.ServerSocket"
fn dbInsert(name: string, email: string) -> int {
mut conn = DriverManager.getConnection("jdbc:sqlite:app.db")
mut stmt = conn.createStatement()
stmt.execute("INSERT INTO users (name, email) VALUES ('" + name + "','" + email + "')")
mut id = stmt.executeQuery("SELECT last_insert_rowid()").getInt(1)
stmt.close()
conn.close()
id
}
mut server = ServerSocket.new(3000)
println("Listening on :3000")
// ... handle requests with full HTTP parsingClosures and higher-order functions:
fn makeMultiplier(factor: int) -> fn(int) -> int {
return fn(x: int) -> int { x * factor }
}
imut triple = makeMultiplier(3)
imut times10 = makeMultiplier(10)
println(toString(triple(7))) // 21
println(toString(times10(7))) // 70
mut words = ["banana", "apple", "cherry"]
sort(words, fn(a: string, b: string) -> int {
if a < b { return -1 }
if a > b { return 1 }
return 0
})
// ["apple", "banana", "cherry"]Structured concurrency with channels:
imut results = channel(10)
scope {
spawn {
for i in range(0, 100) {
results.send(i * i)
}
results.close()
}
spawn {
for val in results {
println(toString(val))
}
}
}
// all threads joined automatically at scope endLinux / macOS:
curl -fsSL https://raw.githubusercontent.com/urunsiyabend/SiyoCompiler/master/install.sh | shWindows (PowerShell):
irm https://raw.githubusercontent.com/urunsiyabend/SiyoCompiler/master/install.ps1 | iexNo Java installation required — a bundled JRE is included.
Build from source
Requires Java 21+ and Maven 3.9+.
git clone https://github.com/urunsiyabend/SiyoCompiler.git
cd SiyoCompiler
mvn package -DskipTests
export PATH="$PWD/bin:$PATH"Uninstall:
rm -rf ~/.siyo # Linux/macOS — also remove PATH lines from .bashrc/.zshrc# Create a new project
siyoc new my-app
cd my-app
# Run project (reads siyo.toml, resolves deps, runs main)
siyoc run
# Run a single file
siyoc run hello.siyo
# Compile to .class file
siyoc compile hello.siyo
# Interpret (tree-walking, useful for debugging)
siyoc interpret hello.siyo
# REPL
siyoc repl
# With external JARs on the classpath
siyoc -cp lib/sqlite-jdbc.jar run server.siyosiyoc new creates a project with automatic dependency management:
# siyo.toml
[project]
name = "my-app"
version = "0.7.0"
main = "src/main.siyo"
[dependencies]
"org.xerial:sqlite-jdbc" = "3.45.0"Dependencies are downloaded from Maven Central on first siyoc run and cached in ~/.siyo/cache/. Imports resolve relative to src/ when a siyo.toml exists.
| Feature | Description |
|---|---|
| Types | int, long, bool, float, string, arrays, maps, structs, enums, sum types, null |
| Literals | 42, 0xFF, 9000000000L, 3.14, "text", [1, 2], {"k": 1} |
| Variables | mut x = 10, imut y = "constant", x += 1 |
| Control flow | if/else, while, for, for x in collection, break, continue |
| Functions | Typed params, return types, implicit return, recursion, forward refs |
| Closures | fn(x: int) -> int { x * 2 }, factory pattern, and a mutable local a closure captures is shared with the enclosing scope |
| Structs | struct Point { x: int, y: int }, field access, mutation, pass-by-ref |
| Enums | enum Direction { N, E, S, W } |
| Sum types | type Result = Ok(int) | Err(string), recursive types, equality, printing |
| Pattern matching | match r { Ok(v) => v, Err(m) => 0 } — destructures a variant, checked for exhaustiveness |
| Function types | fn(int) -> int is checked: arity, parameter and return types, and the call's own result type |
| Generics | Array<int>, Map<string, int>, Set<int>, type Option<T> = Some(T) | None, fn identity<T>(x: T) -> T |
| Interfaces | interface Shape { fn area() -> int } with impl Shape for Square, dispatched on the struct at run time |
| Visibility | pub marks what a module exports; a module that marks nothing exports everything |
| Module aliasing | import "std/math" as m |
| Reflection | fields(v), field(v, name), setField(v, name, x), toMap(v), typeName(v) |
| Error handling | try { ... } catch e { ... }, throw raises any value, catch e: Failure binds it with a declared type, error("msg"), try-as-expression |
| Concurrency | scope/spawn, channels (buffered & unbuffered), for msg in ch |
| Actors | actor Store (actor struct Store is accepted for compatibility), spawn Actor.new(...), sync calls, async send |
| Java interop | import java "java.net.Socket", Java types in signatures, constructors, static & instance methods, overload resolution |
| Modules | import "file" |
| String interpolation | "Hello, $name! You are $age years old." / "${expr}" |
Conversion: toString, toInt, toDouble, toFloat, toLong, parseInt, parseFloat, parseLong
Strings: len, substring, contains, indexOf, startsWith, endsWith, replace, trim, toUpper, toLower, split, chr, ord
Arrays: push, pop, removeAt, sort, range, map, filter, reduce, forEach
Collections: map, set, channel
I/O: print, println, input, error
Other: random, httpGet, httpPost, canRead
The repository includes 29 example programs and 2 multi-file projects:
| Category | Examples |
|---|---|
| Algorithms | Bubble sort, binary search, linked list, stack |
| Applications | Todo app, calculator, grade calculator, guessing game |
| Data processing | CSV reader, JSON parser, word counter |
| Networking | REST API (with SQLite), HTTP client/server, NIO server |
| Java interop | File I/O, JDBC, sockets |
| Concurrency | SiyoDB (actor-based TCP database), chat server |
siyoc run examples/fizzbuzz.siyo
siyoc run examples/json_parser.siyo
siyoc -cp lib/sqlite-jdbc.jar run examples/rest_api_v2.siyoSource (.siyo) → Lexer → Parser → Binder → Lowerer ─┬→ Emitter → JVM Bytecode
└→ Evaluator (interpreter)
Both the bytecode compiler and interpreter share the same frontend (lexer through lowerer), guaranteeing identical semantics. The compiler emits standard .class files using the ASM library, targeting Java 21's virtual threads for concurrency.
| Component | Role |
|---|---|
| Lexer | Tokenizes source into a stream of tokens |
| Parser | Builds a concrete syntax tree |
| Binder | Resolves names, types, scopes; desugars for-in and channel iteration |
| Lowerer | Rewrites control flow (loops → labels + gotos, break/continue → jumps) |
| Emitter | Generates JVM bytecode via ASM (classes, methods, fields, exception tables) |
| Evaluator | Tree-walking interpreter (reference implementation) |
src/main/java/
├── Main.java Entry point & CLI
└── codeanalysis/
├── syntax/ Lexer, Parser, AST nodes
├── binding/ Binder, BoundScope, TypeResolver
├── lowering/ Lowerer (control flow rewriting)
├── emitting/ ASM bytecode emitter
├── project/ TomlParser, SiyoProject, DependencyResolver
├── Evaluator.java Interpreter
├── SiyoActor.java Actor runtime (mailbox, event loop)
├── SiyoChannel.java Channel (SynchronousQueue / LinkedBlockingQueue)
├── SiyoRuntime.java Compiled-code runtime helpers
├── BuiltinFunctions.java 37 stdlib functions
└── ... Types, diagnostics, symbols
src/test/java/ 1565 tests
examples/ 29 standalone examples
projects/ Multi-file projects (siyodb, chat)
mvn test2184 tests across 25 suites — lexer, parser, parser statements, parser recovery, binder, evaluator, compilation (bytecode-vs-interpreter parity), module regression, module scope, module visibility, language semantics, sum types, generics, interfaces, error handling, ergonomics, function types, higher-order builtins, numeric literals, Java boundary, examples smoke, stdlib, syntax rules, and source text handling. The compilation test suite verifies that every program produces identical output in both the bytecode and interpreter paths.
- GRAMMAR.md — Complete language grammar, type system, and built-in reference
- RELEASE_NOTES_0.7.0.md — What changed in 0.7.0, and why
- RELEASE_NOTES_0.6.0.md — What changed in 0.6.0, and why
- FUTURE.md — Roadmap from 0.7.0 through 1.0.0
- docs/ACTOR_DESIGN.md — Actor model design rationale
These are tracked for future releases — see FUTURE.md:
- A struct is not generic:
struct Box<T>is not accepted, though a sum type, a function and a container all take type arguments. - An interface declares methods and nothing else — no default bodies, no interface that requires another, and an interface cannot bound a type parameter.
ascasts a Java object; it does not convert between numeric types. Widening is implicit and narrowing is written withtoInt.- A struct literal with no fields (
Empty { }) is not recognised, and a map literal written as a function body's tail value (fn f() -> map { {"a": 1} }) is read as a block. - No package manager, formatter, or LSP server.
This is an experimental language project. Before opening a pull request, please read CONTRIBUTING.md. You can also browse open good first issues.
This project is provided as-is for educational and experimental purposes.
