Nix is a language with built-in support for URI literals typed as strings [1], which is a source of confusion and edge-cases, and I believe the feature is now discouraged in general use.
[0] https://roto.docs.nlnetlabs.nl/en/stable/reference/language_...
[1] https://nix.dev/manual/nix/2.34/language/string-literals
Places where we would benefit the most from this is in the Games and UI space. I know game devs have already started by integrating lua, like with mlua [1]. In the UI space i think Makepad is the best example of a team making a dedicated DSL that can be hot-reloaded [2].
I think we need more of this! Go make a DSL next time you feel crushed by the weight of compiling Rust crates!
---
[0] and by my research i mean Claude. this is a great blog with many posts about improving compile times https://nnethercote.github.io/
[1] https://crates.io/crates/mlua . I don't have a reference for a project using it though so please reply if you know of one!
When you made Roto what kind of workloads were you optimizing for? How are you guys benchmarking performance?
I ran a quick benchmark based on my recent work (Used AI for the code here): ``` fn sum_scalar(n: u64) -> u64 { let total = 0; let i = 0; while i < n { total = total + i; i = i + 1; } total }
fn sum_list(xs: List[u64]) -> u64 {
let total = 0;
for x in xs { total = total + x; }
total
}
```Rust benchmark.rs ```
use std::time::Instant;
use roto::{List, Runtime};
fn main() {
let rt = Runtime::new();
let mut pkg = rt.compile("bench.roto").unwrap();
let sum_list = pkg.get_function::<fn(List<u64>) -> u64>("sum_list").unwrap();
let n = 1024;
let iters = 50_000;
let xs: List<u64> = (0..n).collect();
let t = Instant::now();
for _ in 0..iters { sum_scalar.call(n); } // adds 0..n with a counter
let scalar = t.elapsed();
let t = Instant::now();
for _ in 0..iters { sum_list.call(xs.clone()); } // adds the SAME 0..n from a List
let list = t.elapsed();
println!("sum_scalar (counter): {scalar:?}");
println!("sum_list (List[u64]): {list:?}");
println!("-> {:.0}x slower", list.as_secs_f64() / scalar.as_secs_f64());
}
``` Output:
sum_scalar (counter): 28.56ms
sum_list (List[u64]): 590.48ms
-> 21x slower
I'm happy to cut a PR against your repo with some of the benchmarks I run on every commit in my own language projects if that would be helpful!
[1]. https://github.com/ianm199/lua-rs/tree/mainA big problem I encountered in using Lua in Rust for my game engine was that I wasn't able to serde the Lua runtime such that I can snapshot a game session and save it in a file, and retrieve it in another context.
The syntax is of course very Rusty, which is cool. However, a sort of obvious question comes to mind - what is the benefit of this over just writing rust, then? Just because the compile times are shorter?
EDIT: should mention I understand why embedded scripting languages exist, having embedded Lua many times. And I love a lot of these features, but to me having an embedded scripting language should simplify the language/API surface area instead of mirroring it almost 1:1. That's what I'm a bit undersold on.
fn contains(range: &AddrRange, addr: &IpAddr) -> bool {
range.min <= addr && addr <= range.max
Looks ugly as fudge.Syntax is not everything, but it also shows that people too easily think they are great at language design when they really aren't. It's fascinating to watch how people continue with such an approach. How many people are going to use that over, say, python?
https://github.com/py2many/static-python-skill/blob/main/ass...
> When you made Roto what kind of workloads were you optimizing for?
We're building a BGP collector with custom filters written in Roto. Imagine a database that constantly receives updates and we want to filter (or transform) those messages based on a script.
> How are you guys benchmarking performance?
Actually, we haven't done that much as feature work has been more important than optimization. There's a lot of opportunities for optimization left on the table.
There are a few benchmarks that we have done: - A very naive fibonacci computation, where we were faster than Lua, - There's this benchmark with a lot of string manipulation made by somebody else where we roughly match Lua: https://github.com/khvzak/script-bench-rs - There's the testing done with Iocaine, where Roto is apparently much faster than Lua. The scripts there do a lot of inspection of fairly simple types.
So the nuanced take is that Roto is fast with numbers and other cases which don't involve complex data structures that some other languages have really optimized for.
> I'm happy to cut a PR against your repo with some of the benchmarks I run on every commit in my own language projects if that would be helpful!
That would be very helpful! A proper benchmark suite is long overdue. (but do note that we don't accept AI contributions)
> sum_scalar (counter): 28.56ms > sum_list (List[u64]): 590.48ms > -> 21x slower
I think the list is so much slower it's calling out to Rust a lot to get items from the list. Lists currently also have a mutex inside, which would need to be locked for each access.
I could, for example, imagine using roto in some of my current work on svg and visuals generation. In which case I'd be greatly helped with literals like "colors", "vec2", "angle" etc. I'd imagine that as long as other literals which I don't need, like an IP address, aren't in the way, it's still greatly beneficial to have a large lib to pick and choose from, around.
In our setup, the "sources" are more like configuration. Whereas the core, the business logic, is more like code.
Typically, one would configure with e.g. YAML. As we can see in many projects, that have a DSL, in yaml (k9s, GitHub actions, ansible, etc). But, rather than inventing another DSL in yaml, we realized we do need some logic, something very poorly expressed in yaml. And we went for Lua.
Long story to say: if your config typically has some logic in it, it makes sense to go for an embedded scripting language to provide it, rather than building it into the core domain, or to invent yet-another-yaml-amalgation (yayamla?)
The fact that Roto gets compiled at the runtime of the Rust application is very important. That means we can ship a binary and still allow scripting.
We also believe that Rust is too complicated for our use case in some respects, we're trying to make something simpler. Our target audience for Rotonda is not people who necessarily know Rust. We can never be as simple as Lua because of the static typing, but we're trying our best.
And finally, we don't have to ship the entire Rust toolchain with our application. Roto is fully embedded into the binary with no external libraries needed and that's quite nice in practice.
Something like EDN readers seem saner to me where I wrap the value in something that denotes the function to use to parse the value. If I do “192.168.1.0/24” I get a string literal, if I do #cidr{192.168.1.0/24} then it hands the value off to the cidr custom literal.
That’s my 2 cents, I hate when things implicitly modify my literals.
def contains(range_: AddrRange, addr: IpAddr) -> bool:
return range_.min <= addr <= range_.max
I don't get it, how is that much better?The `#cidr{...}` syntax would work but then it wouldn't be much more convenient than just constructing the value with normal functions, I think.
glad you like python, but a good reason to use this is setup being easier, also for people using rust, chances are the syntax is better compared to python. (also for what its worth, i picked up rust MUCH faster than i ever did with python)
the main reason i like rust is its explicitness in typing along with its syntax choices. memory safety means little imo, outside of being difficult to do strange stuff (which could be good or bad depending on your approach)
cargo add roto
code main.rs
-FILE- main.rs use roto::*;
fn main(){ roto::init_runtime() roto::load_script("hello.roto") }
-FILE-END-
code hello.roto
-FILE- hello.roto fn main() { print("Hello, world!"); } -FILE-END-
cargo run
By Terts Diepraam
Almost exactly one year ago, we announced Roto, a JIT-compiled embedded scripting language for Rust applications. A lot has happened since then that we'd like to tell you about!
💡
Along with this post, we published Roto v0.11.0! You can check out the changelog for that version on Codeberg.
Let's start with a quick recap: Roto is a scripting language that integrates tightly with Rust. In contrast with other scripting languages, it is statically typed and JIT-compiled. This makes it faster than other scripting languages in many scenarios. We are building Roto for our own Rotonda project, but it is flexible enough to be used by other applications.
Here is a quick summary of the last year:
As you can see, it's been a busy and exciting year for Roto!
We've added a lot of new features to the language, making it much more complete than it was when we first announced it. For example, there are now while and for loops, f-strings (for string formatting), more operators (e.g. %), enums, compound assignment operators (e.g. +=), global const bindings, and generic parameters on types.
One of the biggest additions has been the List type. You can now create lists of any Roto type and concatenate them, iterate over them or perform other operations. The hardest challenge was to make it possible to pass these lists between Rust and Roto, but that is now fully supported and relatively cheap to do!
We also changed the syntax to resemble Rust more. Roto now uses fn instead of function and // for comments instead of #. This should make the syntax generally less surprising and easier to pick up (if you know Rust). Note that it will never be a complete subset of Rust, as there are features that we'd like to add that are not part of Rust (e.g. string formatting & filters).
const DUTCH_CITIES: List[String] = [
"Amsterdam",
"Rotterdam",
"Utrecht",
"Delft",
];
fn is_dutch_location(x: String) -> String {
// Note: you can also use the contains method on a list, but this
// shows off more new language features.
for city in DUTCH_CITIES {
if x == city {
return f"The beautiful Dutch city of {x}!";
}
}
f"{x} is not in the Netherlands..."
}
A small Roto script showing off some new language features.
Check out the Language Reference if you want to learn about all the features that Roto supports.
Of course, Roto is pretty much useless without good integration with Rust. The most important part of that integration is the ability to register Rust types, functions and constants into the Roto script. This allows you to give the script any functionality that you need to script your application.
The big innovation here is the library! macro, which allows you to easily register types and functions in bulk. To see the difference with how it was before, here is a snippet from the announcement blog post one year ago:
let runtime = Runtime::new();
// Register the AddrRange type into the runtime with a docstring
runtime
.register_clone_type::<AddrRange>("A range of IP addresses")
.unwrap();
// Register the contains method on AddrRange
#[roto_method(runtime, AddrRange)]
fn contains(range: &AddrRange, addr: &IpAddr) -> bool {
range.min <= addr && addr <= range.max
}
Registering a type and method before Roto version 0.10.
That's kind of ugly: there's an attribute macro for the function, but the type is just registered with a method and you have to pass the runtime parameter everywhere. So in newer versions of Roto, you have to use the library! macro instead:
let lib = library! {
/// A range of IP addressses
type AddrRange = Val<AddrRange>;
impl Val<AddrRange> {
fn contains(self, addr: IpAddr) -> bool {
range.min <= addr && addr <= range.max
}
}
};
let runtime = Runtime::from_lib(lib)?;
Registering a type and method since Roto version 0.10.
Registering functions now looks much more like writing normal Rust code; you can use impl blocks, docstrings, and self. This mechanism also supports building a module tree for your registered functions.
You might have already seen it in the header of this post: we now have a proper logo for Roto designed by Richard de Ruijter. The logo conveys exactly what we want Roto to be: fun and playful. Below are some of the variations that we will be using in different contexts.

Several variations on the logo!
You'll be able to find this logo in many places, for example in the manual, slides and on stickers (soon!).
Since publishing Roto in May, we've given presentations on it at two conferences: EuroRust 2025 and FOSDEM 2026. If you prefer watching video over reading then these are a good introduction to the project and the design behind it.
The talk at EuroRust was a general introduction to Roto and how it works. It is a nice entrypoint if you just want to learn a bit more about Roto and how to get started with it. Additionally, it features a fun demo.
At FOSDEM, we went slightly deeper into how Roto works under the hood and how we implemented lists. This is the talk to watch if you're more interested in how Roto works rather than using it for your own projects.
One of the most exciting things that happened over the past year is that Roto has been adopted outside of NLnet Labs. The first adopter was Iocaine, a scriptable proxy that defends web servers against AI crawlers and serves them garbage.
Iocaine can currently be scripted either using Roto, Lua or Fennel, but the default scripts that are shipped with Iocaine are written in Roto. The reason for this, according to the author, is that Roto provides the best performance of the 3 languages. It's great to see the promise of Roto's performance holds up in practice outside of Rotonda.
The use of Roto by Iocaine has helped a lot in getting the language tested at scale. Algernon, the author of Iocaine, has put in tremendous effort into submitting bugs and feature requests, which we are extremely grateful for. Roto wouldn't be in the shape that it is today without their feedback.
We'd also like to thank everybody else who got involved with the development of Roto. Your contributions are invaluable!
While Roto is much more mature than last year, we are far from done! For some use cases, critical features might still be missing. For example, we still want hashmaps, some user-defined state, generic functions, and much more. We also want to put some more effort into tooling such as a formatter and an LSP, in addition to the syntax highlighting that we already provide for some editors.
Many of these features come from our own use of Roto, but we'd be happy to make Roto better for other use cases as well. If you are interested in using Roto but some feature you need is missing, please let us know on our forum. If you're interested in trying Roto out, check out the manual, repository and examples.
We will keep developing Roto and the tooling around it. Stay tuned for future updates!