Project Announcement: Onion, a functional scripting language with an immutability-first design, built in Rust
⚓ Rust 📅 2025-08-04 👤 surdeus 👁️ 10Hi everyone,
I'm excited to announce a personal project I've been working on: Onion, a new programming language built from the ground up entirely in Rust.
What is Onion? 
Onion is a modern, functional programming language designed around a few core principles: immutability-first, an expressive and safe function system, and a layered concurrency model.
Instead of just listing features, I'd like to walk you through some of the core design decisions that make Onion different, and how they were implemented in Rust.
- GitHub Repository:
https://github.com/sjrsjz/onion-lang
First, a glimpse of what Onion code looks like, showcasing its function composition and stream processing:
@required "stdlib"; // use comptime `required` function to declare a external variable
// Lazy filtering with the | operator
evens := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | (x?) -> x % 2 == 0;
// Membership checks are evaluated lazily
stdlib.io.println("Is 4 an even number?", 4 in evens); // true
stdlib.io.println("Is 5 an even number?", 5 in evens); // false
// To get a concrete array, use .collect()
collected_evens := evens.collect();
stdlib.io.println("Collected evens:", collected_evens);
// Stream processing with the |> operator (Mapping)
squared := [1, 2, 3] |> (x?) -> x * x;
// The stream is not lazy, so we can directly use it
stdlib.io.println("Squared numbers:", squared);
Core Design Pillars & The Rust Implementation
1. Immutability-First by Design
This is the most fundamental principle. In Onion, all values are fundamentally immutable. This is not just a convention; it's enforced by the VM's core object model.
-
The
OnionObjectEnum: The design is directly visible in the centralOnionObjectenum, which relies heavily onArc<T>for cheap, safe sharing of data. There are no&mut Tfields anywhere in the core types.// The core object enum in the VM pub enum OnionObject { Integer(i64), Float(f64), String(Arc<str>), // All heap-allocated values are shared-immutable Tuple(Arc<OnionTuple>), // ... Mut(GCArcWeak<OnionObjectCell>), // The *only* source of mutability } -
The
mutKeyword's True Nature: So how does mutation work? Themutkeyword doesn't make an object mutable. Instead,mut xcreates anOnionObject::Mut, which is an immutable, garbage-collected smart pointer to a single, mutableOnionObjectCell. The only possible "mutation" is to make this pointer point to a completely newOnionObject.As the source code comment states:
"If we need to 'mutate' an object, it is IMPOSSIBLE to mutate the object itself, we can only let the Mut object point to a new object."
This brings a Rust-like discipline to a dynamic language, preventing entire classes of bugs caused by shared mutable state.
2. Expressive and Safe Function System
Onion's function system is designed for flexibility and safety, taking inspiration from functional languages like Lisp and Haskell.
-
Flexible Invocation Syntax: Parentheses are optional for function calls.
f(x)andf xare treated as equivalent, allowing for a cleaner, more compositional style. -
Parameter Constraints (Guards): This is where safety comes in. You can attach a "guard" function to any parameter. The VM will execute this guard at runtime before entering the function body. Think of it as a form of runtime-validated trait bounds or dynamic contracts.
// Define a guard function Positive := (x?) -> x > 0; // OR "x" -> x > 0 OR (x => true) -> x > 0 // Apply it to a parameter. The syntax `x?` is sugar for a guard that always returns true. add_positive := (a => Positive, b => Positive) -> a + b; add_positive(5, 3); // OK // add_positive(-1, 5); // Fails at runtime with a constraint violation error -
Automatic Tuple Unpacking & Nested Constraints: If a function is defined with tuple-like parameters, passing a tuple as an argument will cause the VM to automatically unpack it into the corresponding parameter slots. This, combined with the fact that guards can be nested, allows for powerful and descriptive function signatures.
// A function expecting an integer and a nested tuple of two integers f := (x => Positive, (y => Positive, z => Positive)) -> x + y + z; // These calls are valid: f(1, (2, 3)); // Result: 6 // This call is also valid due to tuple unpacking packaged_args := (1, (2, 3)); f(packaged_args); // Result: 6
3. A Unique Prototypal Object Model
Instead of classes, Onion uses a simple but powerful prototypal object model built from composing Tuples and Pairs (key: value).
-
Inheritance via
:: You link an object to its prototype using the:operator:my_instance : my_prototype. -
How
selfWorks: This is a key differentiator. In Onion,selfisn't a magic keyword that appears at call time. When you access a function on an instance (e.g.,instance.method), the VM creates a newLambdaobject on the fly. This new object pairs the original function's definition with a reference to theinstance, which becomesself.This mechanism is explicit in the
OnionObject::Lambdatype definition:Lambda((Arc<OnionLambdaDefinition>, Arc<OnionObject>)), // (definition, self_object)This means
selfis lexically captured at the moment of access, leading to more predictable behavior.
4. First-Class Metaprogramming
Onion has a compile-time macro system that allows you to directly manipulate the Abstract Syntax Tree (AST). This isn't just string substitution; it's structural code generation.
-
Example: You can use
@to execute Onion code at compile time. For example, build a lambda.// @ast lets you build Abstract Syntax Trees programmatically lambda := @ast.lambda_def(false, ()) << ( ("x", "y"), // Parameters ast.operation("+") << ( // Body ast.variable("x"), ast.variable("y") ) ); // The `lambda` variable is now a function equivalent to `(x?, y?) -> x + y` stdlib.io.println(lambda(10, 20)); // Prints 30
5. The Layered Concurrency Model
This builds upon the foundation of immutability. Because data is fundamentally non-mutable, sharing it across threads is inherently safer.
- GIL-Free Multi-threading: The
launchkeyword usesstd::thread::spawnto create a new OS thread. Each thread gets a completely new and isolated VM context and GC instance. This is a simple but powerful approach that entirely sidesteps the need for a GIL. - Cooperative Async Scheduling: The
async/spawnsystem uses a priority-based scheduler, which I'll detail below.
A Deeper Dive: The Scheduler Implementation
For those interested in the VM's internals, the execution flow is built upon one central trait: Runnable.
The Core Abstractions: Runnable and StepResult
Everything that can be executed implements the Runnable trait. Its step method returns a StepResult enum, which dictates the VM's next action (Continue, NewRunnable, Return, SpawnRunnable, Error).
1. The Synchronous Scheduler: A Classic Call Stack
The standard Scheduler is simple: it manages a Vec<Box<dyn Runnable>> which acts as a call stack. NewRunnable pushes to the stack, and Return pops from it, passing the result to the new top of the stack via the receive method. It's a classic state machine implementation of a function call system.
2. The AsyncScheduler: A Priority-Based Event Loop
This is where things get interesting. The AsyncScheduler manages a VecDeque<Task>.
- Cooperative & Non-blocking: It iterates through the queue, stepping each task. It never waits.
- Priority & Exponential Backoff: Each task has a
priority. The scheduling frequency is based on this priority (step % (2^(priority+1) - 1)).- When a task returns
RuntimeError::Pending(e.g., waiting onvalueof), its priority is lowered. This is an elegant exponential backoff: the scheduler wastes fewer cycles on tasks that are just waiting. - When a task makes progress, its priority is reset to the highest level.
- When a task returns
This layered model, implemented with Rust's traits and enums, allows Onion to handle everything from simple function calls to complex, non-blocking concurrent workloads within a single, coherent framework.
What I'm Looking For
This project has been a solo journey so far, and I've reached the point where I would love to get external perspectives. I'm particularly interested in feedback on:
- Language Design: What are your initial thoughts on the immutability-first philosophy, the function system, and the prototypal object model? Do these ideas resonate?
- VM Implementation: Are there any glaring anti-patterns or potential performance bottlenecks in the VM code? I'm especially curious about feedback on the GC, object model, and scheduling logic.
- Contribution: The project is open for contributions! If you're interested in compilers, VMs, or language design, I would be thrilled to have you join.
Thank you for taking the time to read this. I'm excited to hear what you think and to continue this journey with the wisdom of the Rust community.
4 posts - 3 participants
🏷️ Rust_feed