Readit News logoReadit News
Posted by u/beariish 4 months ago
Show HN: Bolt – A super-fast, statically-typed scripting language written in Cgithub.com/Beariish/bolt...
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!

haberman · 4 months ago
I love the concept -- I've often wished that lean languages like Lua had more support for static typing, especially given the potential performance benefits.

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

beariish · 4 months ago
I did experiment with a few different dispatch methods before settling on the one in Bolt now, though not with tailcalls specifically. The approach I landed on was largely chosen cause it in my testing competes with computed goto solutions while also compiling on msvc, but I'm absolutely open to try other things out.
UncleEntity · 4 months ago
From my research into the subject the easiest way to implement it would be a 'musttail' macro which falls back to a trampoline for compilers which don't support it. The problem then becomes having the function call overhead (assuming the compiler can't figure out what's going on and do tail-call optimizations anyway) on the unsupported systems with each and every opcode which is probably slower than just a Big Old Switch -- which, apparently, modern compilers are pretty good at optimizing.

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.

mananaysiempre · 4 months ago
There’s one thing that tail calls do that no other approach to interpreters outside assembly really can, and that is decent register allocation. Current compilers only ever try to allocate registers for a function at a time, and somehow that invariably leads them to do a bad job when given a large blob of a single intepreter function. This is especially true if you don’t isolate your cold paths into separate functions marked uninlineable (and preferably preserve_all or the like). Just look at the assembly and you’ll usually find that it sucks.

(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/

nolist_policy · 4 months ago
Take look at the Nostradamus Distributor:

http://www.emulators.com/docs/nx25_nostradamus.htm

debugnik · 4 months ago
You may be interested in Luau, which is the gradually-typed dialect of Lua maintained by Roblox. The game Alan Wake 2 also used it for level scripting.
ngrilly · 4 months ago
The author seems to be aware as Luau is mentioned in the performance section of the documentation.
summerwant · 4 months ago
I see lua, do you know terralang?
perlgeek · 4 months ago
I like 99% of this, and the thing I don't like is in the very first line of the example:

> 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:

    import packageName.member; // java
    from package import symbol; # python
    use Module 'symbol'; # perl
    
With Typescript being the notable exception:

    import { pi as π } from "./maths.js";t

jasonjmcghee · 4 months ago
Also autocomplete.

Though I almost never manually type out imports manually anymore.

bbkane · 4 months ago
I really like the way Elm does it, from "wide" (package) to "narrow" (symbol). I suspect this also helps language server implementation.

See https://guide.elm-lang.org/webapps/modules (scroll down to "Using Modules") for examples

beariish · 4 months ago
Do you think approaching the way typescript does it for Bolt is a reasonable compromise here? Bolt already supports full-module renames like

    import math as not_math
So supporting something along the lines of

    import abs as absolute, sqrt as square_root from math
Would be farily simple to accomplish.

WorldMaker · 4 months ago
The OP seems to be asking for the Python order of the import statement because it allows for simpler auto-completion when typing it:

    from math import square_root as sqrt, abs as absolute
    from math import * as not_math
In a format like this, your language service can open up `math` immediately after the `from math` and start auto-completing the various types inside math on the other side of the `import`.

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'`).

cess11 · 4 months ago
Why?

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.

pepa65 · 4 months ago
Or: `import math with abs as absolute, sqrt as square_root`
vhodges · 4 months ago
According to the Programming Guide, it supports aliases for imports

"In case of conflict or convenience, you can give modules an alias as well."

perlgeek · 4 months ago
This isn't about conflict, it's about how humans read it.

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.

Tokumei-no-hito · 4 months ago
can't the compiler process it in reverse?
masklinn · 4 months ago
The compiler doesn’t care either way, this is for the human reader’s benefit.
MobiusHorizons · 4 months ago
FYI "the embedded scene" is likely to be interpreted as "embedded systems" rather than "embedded interpreters" even by people who know about embedded interpreters, especially since all the languages you give as an example have been attempted for use on those targets (micropython, lua, and even typescript)
beariish · 4 months ago
That's a good point, thank you. I've made a small edit to clarify.
RossBencina · 4 months ago
True. I misread it as being for embedded, especially with the term "real-time" in the mix. Then later when there was no ARM or RISC-V support I became very confused.
conaclos · 4 months ago
I was quite excited by the description and then I noted that Bolt heavily relies on double floating point numbers. I am quite disappointed because this doesn't allow me to use Bolt in my context: embedded systems where floating point numbers are rarely supported... So I realized that I misinterpreted `embedded`.
devmor · 4 months ago
Same here! It's very cool but my ideal use case would be on a limited ISA architecture like ESP32.
nativeit · 4 months ago
Bolt doesn’t support ARM or RISC. There’s some comments above re: the confusion with the term “embedded” and “real time”.
megapoliss · 4 months ago
Run some examples, and it looks like this "High-performance, real-time optimized, super-fast" language is

  ~ 10 times slower than luajit
  ~ 3 times slower than lua 5.4

jeroenhd · 4 months ago
Not bad for version 0.1.0. lua(jit) is no slowpoke and has had decades of performance improvements.

Deleted Comment

johnisgood · 4 months ago
With this and https://github.com/Beariish/bolt/blob/main/doc/Bolt%20Perfor..., it is indeed confusing without testing it out myself.

That said, somehow I do not believe it is faster than LuaJIT. We will see.

megapoliss · 4 months ago
I used brainfuck interpreter https://github.com/Beariish/bolt/blob/main/examples/bf.bolt vs lua / luajit implementations from https://github.com/kostya/benchmarks

Just checked with nbody:

  - still 10 times slower than luajit
  - 2 times slower than luajit -joff
  - but 20% faster than lua 5.4
  - but uses 47 Mb RAM vs 2.5 Mb for lua/luajit

amai · 4 months ago
Where do you get these numbers from? Looking at https://github.com/Beariish/bolt/blob/main/doc/Bolt%20Perfor... doesn’t seem to support these numbers.
ModernMech · 4 months ago
They at least clarified it by saying "outperforming other languages in its class". It's a slow class so the bar is low.
JonChesterfield · 4 months ago
Outperforming languages in its class is doing some heavy lifting here. Missing comparison to wasm interpreter, any of the java or dot net interpreters, the MLs, any lisps etc.

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.

phire · 4 months ago
The class is "embeddable interpreted scripting language", which is not quite the same thing as just an interpreter.

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

banginghead · 4 months ago
This looks so familiar that it got me thinking: who is collating all of the languages that are being invented? I must see two dozen a year on HN. I'm not dissing OP, but I've seen so many languages I'm not sure if I'm having deja vu, or vuja de.
cookiengineer · 4 months ago
If functions don't have a return signature, does that mean everything must be satisfied in the compilation step?

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

driggs · 4 months ago
> If functions don't have a return signature, does that mean everything must be satisfied in the compilation step?

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

zygentoma · 4 months ago
Oh, not OP, but I love Koka. I should play around with it again thanks for reminding me!