Skip to content

About

Siyo is a compiled JVM programming language with actors, channels, structured concurrency, and native Java interoperability.

Topics

Resources

Contributing

Stars

12 stars

Watchers

0 watching

Forks

Repository files navigation

The Siyo Programming Language

Siyo

A compiled language for the JVM with actors, channels, and native Java interop.

CI Java 21+ v0.7.0 Tests


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.

What it looks like

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 parsing

Closures 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 end

Install

Linux / macOS:

curl -fsSL https://raw.githubusercontent.com/urunsiyabend/SiyoCompiler/master/install.sh | sh

Windows (PowerShell):

irm https://raw.githubusercontent.com/urunsiyabend/SiyoCompiler/master/install.ps1 | iex

No 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

Usage

# 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.siyo

Project structure

siyoc 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.

Language Features

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}"

Standard Library — 41 built-in functions

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

Examples

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.siyo

How it works

Source (.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)

Project layout

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)

Testing

mvn test

2184 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.

Documentation

Known limitations (0.7.0)

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.
  • as casts a Java object; it does not convert between numeric types. Widening is implicit and narrowing is written with toInt.
  • 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.

Contributing

This is an experimental language project. Before opening a pull request, please read CONTRIBUTING.md. You can also browse open good first issues.

License

This project is provided as-is for educational and experimental purposes.

About

Siyo is a compiled JVM programming language with actors, channels, structured concurrency, and native Java interoperability.

Topics

Resources

Contributing

Stars

12 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages