Yet another Erlang/Gleam? Same ideas — on a lightweight VM of our own.
TinyActor is a lightweight Actor language + VM
Functional, type-safe, Erlang-style actor concurrency on a lightweight bytecode VM — embeddable in C with easy interop.
// An ADT shapes the message; match destructures it by pattern
type Msg { Add(a, b); Mul(a, b) }
fn worker() {
match recv() {
Add(a, b) -> print(a + b) // Add(20, 22) arrives → 42
Mul(a, b) -> print(a * b)
}
}
fn main() {
let pid = spawn(fn { worker() }) // concurrency: spawn an actor
send(pid, Add(20, 22)) // messaging: the message is an ADT value
}
Why another language?
There are already plenty of languages. TinyActor doesn’t stack features — it stands on a few trade-offs:
Actors
The actor model is one of the most correct ways to do concurrency. Concurrency is a first-class citizen — spawn / send / recv are syntax, with no middle layer between you and concurrent code.
Type safety
A functional language where type safety makes code more robust: Hindley-Milner inference and exhaustive pattern matching catch errors at compile time.
C interop
TA is the main language, C is the glue: C only implements base libraries and OS interop, and embedding is seamless. The opposite of Lua, where Lua serves C.
Quick Start
Save the example above as hello.ta and run:
./tinyactor run hello.ta
It prints 42. type defines the message as an ADT, match destructures it, and spawn / send / recv handle concurrency — type safety and concurrency in one screen.
The tinyactor script resolves the VM and bootstrap compiler relative to itself, so it can be called from outside the checkout:
/path/to/tinyactor/tinyactor run /path/to/project/main.ta
Set TAVM=/path/to/tavm to override the VM location. Run make tavm and make bootstrap to produce the runtime files.
Documentation
- Design Overview
- TA Language Specification
- Layered Type Model
- Generic ADT Design
- Design Decisions
- Improvement Plan
- C Module Authoring Guide
- Typecheck Performance Analysis
Project Status
- Self-hosting: the compiler (lexer / parser / typecheck / codegen) is written in TA itself, compiling to bytecode that runs on its own VM
- Type safety: Hindley-Milner inference + generic ADTs + exhaustive pattern matching
- Modules & built-ins:
import/pub, with built-innet/http/bufio/list/str/fmt/math - Tests: 7 suites all passing — basic / actor / gc / module / compiler / bootstrap / example
- Roadmap: supervisor → hot reload → persistence → distributed → stdlib → performance → DX
Full roadmap: ROADMAP.md.
FAQ
Isn't a bytecode VM slow?
Yes, a bytecode VM is slower than native code. But "slow" is relative: Python is one of the most popular languages today, and interpreter performance hasn't stopped its rise. For the vast majority of use cases, bytecode VM performance is more than enough.
Aren't languages with GC bad?
No. GC dramatically lowers the mental burden of writing code, and pauses have many solutions: Java pushes GC to the limit — incremental / generational / concurrent collection, every engineering trick in the book; Go has long emphasized low-latency GC. But those are global GCs. TinyActor takes a different route: per-actor GC. There is no global stop-the-world — an actor's GC pause only affects that actor. When each actor's heap is small enough, a single GC is fast enough, and pauses stop being a problem. Combined with preemptive fair scheduling, the whole system can achieve good soft real-time behavior.
Why no REPL?
A REPL is great for humans writing code — fast feedback boosts productivity. But in the age of AI writing code, a REPL doesn't meaningfully speed up AI. It's on the roadmap, just not a priority.
How does it compare to Erlang?
Same actor model — TinyActor is a minimal Erlang: the whole VM is on the order of ~4000 lines of C. At the language level Erlang is untyped; TinyActor leans harder into type safety: Hindley-Milner inference + exhaustive pattern matching. Of course, OTP and the distributed ecosystem are Erlang's moat — TinyActor isn't competing on that turf.
How about Go?
Different concurrency models: Go implements CSP, TinyActor is actor-based, and both are excellent at concurrency. Different positioning too: Go is a versatile standalone language; TinyActor is a lightweight high-level language that leans on C for infrastructure.
How about Lua?
Both sit on top of C as high-level languages. Lua is designed to be embedded in C; TinyActor calls into C. TinyActor handles threads at the VM layer and exposes an actor abstraction upward; Lua leaves that layer to the user.
How about Scheme?
TinyActor's surface syntax is parsed into a Lisp-form internal AST — a kernel as small as Scheme: no macros, no continuations. The low level is untyped; type checking happens in the surface syntax.
Why not just use Lisp syntax?
Because TA's primary user is AI. AI doesn't like writing ))))))) — all those matching parens are error-prone in editor tooling. It doesn't like Python's indentation ruler either — it's just used to it after writing so much Python. And it likes YAML even less.
Why does C interop matter so much?
TinyActor sits on a bytecode VM, which sits on C and the OS. You need a boundary that hides details going up and communicates going down. C is the best infrastructure layer here — the universal language that most precisely describes hardware abstraction. As long as modern operating systems are built on C, this layer stays stable.
Still not convinced?
There's an HTTP server implementation (lib/serve.ta). In a real benchmark against Go, TA reached 1/4 of Go's performance — an interpreter compared to a compiled language.
Are you kidding me?
Nope. The entire project was written by AI — if you don't believe me, go ask the AI whether it lied to you.