Show HN: Bolt – A super-fast, statically-typed scripting language written in C
253 beariish 92 8/10/2025, 5:53:09 PM github.com ↗
I've built many interpreters over the years, and Bolt represents my attempt at building the scripting language I always wanted. This is the first public release, 0.1.0!
I've felt like most embedded languages have been moving towards safety and typing over years, with things like Python type hints, the explosive popularity of typescript, and even typing in Luau, which powers one of the largest scripted evironments in the world.
Bolt attempts to harness this directly in the lagnauge rather than as a preprocessing step, and reap benefits in terms of both safety and performance.
I intend to be publishing toys and examples of applications embedding Bolt over the coming few weeks, but be sure to check out the examples and the programming guide in the repo if you're interested!
I also love the focus on performance. I'm curious if you've considered using a tail call design for the interpreter. I've found this to be the best way to get good code out of the compiler: https://blog.reverberate.org/2021/04/21/musttail-efficient-i... Unfortunately it's not portable to MSVC.
In that article I show that this technique was able to match Mike Pall's hand-coded assembly for one example he gave of LuaJIT's interpreter. Mike later linked to the article as a new take for how to optimize interpreters: https://github.com/LuaJIT/LuaJIT/issues/716#issuecomment-854...
Python 3.14 also added support for this style of interpreter dispatch and got a modest performance win from it: https://blog.reverberate.org/2025/02/10/tail-call-updates.ht...
(Whether the blob uses computed gotos or loop-switch is less important these days, because Clang [but not GCC] is often smart enough to actually replicate your dispatch in the loop-switch case, avoiding the indirect branch prediction problem that in the past meant computed gotos were preferable. You do need to verify that this optimization actually happens, though, because it can be temperamental sometimes[1].)
By contrast, tail calls with the most important interprerer variables turned into function arguments (that are few enough to fit into registers per the ABI—remember to use regparm or fastcall on x86-32) give the compiler the opportunity to allocate registers for each bytecode’s body separately. This usually allows it to do a much better job, even if putting the cold path out of line is still advisable. (Somehow I’ve never thought to check if it would be helpful to also mark those functions preserve_none on Clang. Seems likely that it would be.)
[1] https://blog.nelhage.com/post/cpython-tail-call/
http://www.emulators.com/docs/nx25_nostradamus.htm
The VM I've been poking at is I/O bound so the difference (probably) isn't even measurable over the overhead of reading a file. I went with a pure 'musttail' implementation but didn't do any sort of performance measurements so who knows if it's better or not.
> import abs, epsilon from math
IMHO it's wrong to put the imported symbols first, because the same symbol could come from two different libraries and mean different things. So the library name is pretty important, and putting it last (and burying it after a potentially long list of imported symbols) just feels wrong.
I get that it has a more natural-language vibe this way, but put there's a really good reason that most of the languages I know that put the package/module name first:
With Typescript being the notable exception:Though I almost never manually type out imports manually anymore.
See https://guide.elm-lang.org/webapps/modules (scroll down to "Using Modules") for examples
Whereas the `import abs from math` often means you type `import` have no auto-complete for what comes next, maybe type ` from math` then cursor back to after the import to get auto-completion hints.
It's very similar to the arguments about how the SQL syntax is backwards for good auto-complete and a lot of people prefer things like PRQL or C# LINQ that take an approach like `from someTable where color = 'Red' select name` (rather than `select name from someTable where color = 'Red'`).
Put the category first so it makes it easy to skim and sort dependencies. You're never going to organise your dependencies based on what the individual functions, types or sub-packages are called, and sorting based on something that ends up in a more or less random place at the end of a line just seems obtuse.
"In case of conflict or convenience, you can give modules an alias as well."
Let's say I have two modules, "telnet" and "ssh", and both have a "connect" function. When I read "import connect, (long list of other imports here)" I don't know which connect it is, and I might form the wrong mental connection, which I then have to revise when I start to read the module name.
No comments yet
That said, somehow I do not believe it is faster than LuaJIT. We will see.
Just checked with nbody:
I did spot some poor code in the Bolt version of nbody that can be changed (the usage of `.each()` in the hot loop is creating loads of temporary iterators, that's the memory difference.)
luajit -joff does perform better even with this change, but I observe closer to 15% than a 2x difference
Compile to register bytecode is legitimate as a strategy but its not the fast one, as the author knows, so probably shouldn't be branding the language as fast at this point.
It might be a fast language. Hard to tell from a superficial look, depends on how the type system, alias analysis and concurrency models interact. It's not a fast implementation at this point.
> This means functions do not need to dynamically capture their imports, avoiding closure invocations, and are able to linearly address them in the import array instead of making some kind of environment lookup.
That is suspect, could be burning function identifiers into the bytecode directly, not emitting lookups in a table.
Likewise the switch on the end of each instruction is probably the wrong thing, take a look at a function per op, forced tailcalls, with the interpreter state in the argument registers of the machine function call. There's some good notes on that from some wasm interpreters, and some context on why from luajit if you go looking.
Embedded interpreters are that designed to be embedded into a c/c++ program (often a game) as a scripting language. They typically have as few dependencies as possible, try to be lightweight and focus on making it really easy to interopt between contexts.
The comparison hits many of the major languages for this usecase. Though it probably should have included mono's interpreter mode, even if nobody really uses it since mono got AoT
What about memory management/ownership? This would imply that everything must be copy by value in each function callsite, right? How to use references/pointers? Are they supported?
I like the matchers which look similar to Rust, but I dislike the error handling because it is neither implicit, and neither explicit, and therefore will be painful to debug in larger codebases I'd imagine.
Do you know about Koka? I don't like its syntax choices much but I think that an effect based error type system might integrate nicely with your design choices, especially with matchers as consumers.
[1] https://koka-lang.github.io/koka/doc/index.html
Functions do have a return signature.
It looks like the author chose to show off the feature of return type inference in the short example README code, rather than the explicit case.
https://github.com/Beariish/bolt/blob/main/doc/Bolt%20Progra...
Bolt: a language with in-built data-race freedom! (recent discussion: https://news.ycombinator.com/item?id=23122973) - https://github.com/mukul-rathi/bolt
Bolt: A programming language for rapid application development - https://github.com/boltlang/Bolt
BOLT: a programming language that was desinged for begining programmers who have never seen code before in their life - https://sourceforge.net/projects/boltprogramming/files
I'm very much in favor of authors choosing unique names for programming languages because there's still plenty of good names up for grabs without having to step on someone's toes. If the project is dead, that's one thing; the data-race one was a research project and hasn't had any activity in 5 years. BOLT last modified in 2014.
But beariish/bolt and boltlang/bolt were started in the same year and are still under active development. With boltlang/bolt obviously snagging the namespace first, I think they should have claim to the name for now. That said, neither seems to have registered any domains, so whoever gets bolt-lang.org/com/net first will probably have an easier time defending a claim.
We read here a couple days ago about Q which is compiled. Bolt claims to "plow through code at over 500kloc/thread/second". Q claims to compile in milliseconds--so fast that you can treat it like a script.
Bolt and Q are both newborns. Perhaps you could include each other in your benchmarks to give each other a little publicity.
My main concern about a new language is not performance, syntax, or features, but long term support and community.
At this point it is too early to know. Even JavaScript took like 20 years to catch on
make me instantly lost interest in the language.
so it seems to me you're hung up on the current limitations of the language server, which is an implementation detail.
if what you want is actually important, the syntax should have been `import yyy.xxx as xxx` or similar, with optional `as xxx`, instead of a Cobol inspired syntax detail to remember.
1. Ask - the author is very much available, right here in this comment section they made specifically for such a prospect. 2. Contribute - Code the change you wish to see in the world. Follow the OP’s example, and do something about it.
Dynamically typed languages have more difficulties with autocomplete in general, Bolt is statically typed so you shouldn't automatically assume the same difficulties carry over.
As for writeups, I'm working on putting out some material about the creation of Bolt and my learnings now that it's out there.
On the feature side, is there any support for breakpoints or a debugging server, and if not is it planned?
I noticed that `let`-declared variables seem to be mutable. I'd strongly recommend against that. Add a `var` keyword.
https://github.com/Beariish/bolt/blob/0.1.0/doc/Bolt%20Progr...
Usually I feel like that's bare minimum before I'd like to try and play around with a language
Quickly scanned the programming guide - but wasn't able to find it. Did i miss a section?
https://github.com/Beariish/bolt/blob/main/examples/error_ha...
But I think OP was asking about whatever the hell this means by "native error callback" https://github.com/Beariish/bolt/blob/0.1.0/doc/Bolt%20Stand...
(https://www.compuphase.com/pawn/pawn.htm)
Is it deterministic like Lua?