RSS Amplifier

Kirsle.net · May 1, 2026

RiveScript in Rust

0
Sign in to vote or save

kirsle.net

Over the past couple of weeks, I decided I would get serious again about learning how to program in Rust and I have ported my chatbot scripting language, RiveScript over to Rust.

You can check it out on GitHub at https://github.com/aichaos/rivescript-rs

The Rust port is fully 'feature complete' to a similar extent that the JavaScript and Go ports are, including some optional features (kept in other Rust crates) that are similar to the extras that the JS and Go ports come with:

  • rivescript-js enables RiveScript object macros written in JavaScript to work (by using the Rust boa crate for a JavaScript interpreter written in native Rust).
  • rivescript-redis is an optional plugin for User Variables to be stored proactively in a Redis cache rather than the default in-memory store (and to demonstrate how the user variable session trait works; I leave it for now as an exercise for the reader to implement SQL storage or anything else you want instead of Redis -- pull requests welcome!)

What even is RiveScript?

RiveScript is a scripting language for chatbots that I had invented in the early 2000's.

It can be used to program the classic sort of "canned responses" chatbot that was especially popular back then, where your chatbot needed to anticipate all of the questions a user could possibly ask up front and give a pre-programmed response back. It has nothing at all in common with modern LLMs such as ChatGPT, though, RiveScript could still be useful in 2026 when paired with an LLM, so that you can "hard-code" responses to important questions and guarantee that your chatbot answers them how you would like, before falling back to an LLM to handle everything else.

RiveScript itself is a lightweight library that just handles the chatbot scripting language, designed to be very easy for chatbot developers to integrate it into their custom programs. (For example, RiveScript doesn't come with LLM fallback support, but it's very easy to write a custom program of your own that could do that).

A simple example of its syntax looks like this:

+ hello bot
- Hello, human!

See its official website RiveScript.com for details!

The .rs file extension comes full circle

The Rust port of RiveScript is interesting because RiveScript, when it first released in 2005 with the original Perl implementation, it used the file extension .rs to contain your chatbot's response data.

In 2013, a Rust language developer contacted me about changing my file extension because the upcoming Rust language used .rs for its source files, and RiveScript was causing some complications on GitHub and in code editors when it was being mistaken for Rust code.

So I had changed the official file extension to .rive but most of the RiveScript ports would read files with both .rs and .rive extensions (for backwards compatibility).

This is the first new official port of RiveScript I have written since 2015, so the Rust port will only consider .rive file extensions to be valid by default when loading your chatbot's brain.

RiveScript is how I learned so many programming languages

The first proper programming language that I taught myself was Perl, and I taught myself to program specifically because I wanted to run these kind of chatbots on AOL Instant Messenger back in the day, and some Perl code to do so was the first thing I stumbled upon.

So, the initial version of RiveScript was written in Perl, as I used Perl for everything back then.

But, Perl was already starting to lose popularity even back then, and I wanted more people to be able to use RiveScript and make their own fun chatbots. So I personally ported RiveScript into several other programming languages: Java, JavaScript, Python and Go.

RiveScript.java was my first ever Java project, RiveScript.py was my first Python project, and the Go port was one of my first Go projects!

RiveScript made for a really useful library for learning the ropes with a new programming language, as a proper RiveScript port involves touching so many different features of a programming language: filesystem access, regular expressions, lists and hash maps, classes and interfaces.

I was given a very good tour of Rust during the development of this project!

Interesting things learned

The infamous Rust borrow checker was interesting as compared to the other programming languages.

For those who aren't familiar: Rust as a programming language doesn't have a 'garbage collector' to automatically reclaim memory when your variables go out of scope (the way that Python, JavaScript, Go, Perl, etc. all do), but also, unlike C or C++ which also have no garbage collector, in Rust you don't need to worry about freeing your own memory yourself either. In a C program, errors around freeing memory lead to all kinds of problems and vulnerabilities when it's done incorrectly: your program can either have a 'memory leak' if you fail to free your memory, or a security bug if you free memory incorrectly and then use it again later in your program.

Rust avoids all of those pitfalls of C with its borrow checker. You basically need to prove, at compile time, that all of your variables are being handled correctly: where were they declared, which function has access to them, which parts of the code are able to write to them? And when you have satisfied the compiler and your program actually builds, then you know confidently that it doesn't have memory leaks or memory-related vulnerabilities; Rust manages the lifetime and memory of your variables automatically, freeing their memory when it knows they are no longer reachable anywhere, and so on.

Coming from other programming languages like Go, where it's totally OK to pass a mutable pointer to my master RiveScript struct all over the place, in Rust I had to be a lot more deliberate about when the master RiveScript struct is mutable, who has access to it, what they can do with it, and so on.

This leads to a few interesting quirks that the Rust port of RiveScript has compared to the others:

In RiveScript (generally), there is a feature called 'Object Macros' where you can write custom program code to add dynamic behavior to your bot. For example, you can let a user ask "what is the weather like in Los Angeles?" and have the bot go and fetch that information from a weather API using a custom Object Macro function.

In all RiveScript ports, object macros are given two parameters: a pointer to the master RiveScript struct, and a string array of parameters given to the macro function.

The *RiveScript pointer is sent so that the function can get or set user variables or potentially mutate the inner state of your chatbot. In most programming languages, it's fine to pass mutable pointers around all willy nilly - the languages depend on the programmer to ensure any mutations are done safely (e.g., by protecting internal hash maps with a mutex). In Rust, you can not give a mutable pointer to multiple parts of your program at the same time.

So in the Rust port, object macro functions receive a 'Proxy' object instead: it exposes a subset of useful function calls from RiveScript that an object macro would commonly want to use, but it is not the literal same RiveScript struct that is running your chatbot. Reading user variables will pass through and read them from RiveScript, but setting user variables will have the Proxy keep a 'staged' HashMap of the pending changes. (If you set and then read the variable, you get back the recently updated 'staged' copy instead of the one from upstream).

After the object macro function returns, RiveScript takes the staged writes and commits them properly to storage.

Uncovering a decades-old RiveScript bug

As I was building and testing the Rust port, I came across a decades-old bug in RiveScript that affected the JavaScript, Python and Go ports!

In RiveScript, responses from your chatbot are grouped into "topics", where the default topic is named "random" and whenever a user is within a topic, the only triggers their messages may match are the ones written for that topic.

Here is a cheeky example of how topics could be used to 'punish' a user for using foul language with your chatbot:

+ [*] (list|of|bad|words|like|f bombs|goes|here) [*]
- Watch your mouth! I will make you apologize for using harsh language before we
  ^ can continue chatting any more. {topic=apology}
> topic apology
    // While in this topic, only these triggers are available to match.
    // The '*' trigger matches all possible messages when a more specific
    // trigger failed to do so.
    + *
    - Nope, apologize first before we can chat.
    - Say you're sorry so we can move on.
    - You offended me earlier, apologize so we can chat properly again.
    + [*] (sorry|i apologize) [*]
    - OK, I'll forgive you. Just don't let it happen again! {topic=random}
< topic

Anyway, in RiveScript it is possible for topics to "include" or "inherit" the triggers that belong to another topic.

> topic my_topic includes another_topic inherits a_third_topic
    // ...
< topic

The intended behavior is:

  • When a topic includes another, then the full set of triggers in the current topic and the included one are treated as if they all belonged to the current topic. That is, they all get sorted in the same pool of triggers (most specific trigger first).
  • When a topic inherits another, the inherited topic's triggers are all treated as lower priority for matching than the current topic's are. So for example, if the current topic had a catch-all trigger * then the inherited topic's triggers would be unmatchable, since they're all a lower priority than the included topic.

The default example RiveScript brain that ships with all ports comes with an RPG demo that implements a basic text-based role playing game, featuring multiple scenes or 'rooms' and it uses these topic inheritance features to have common 'base commands' that are available in all rooms of the game.

However, in most ports of RiveScript (in JavaScript, Go and Python for sure), the RPG demo gets stuck at a certain point because of a subtle nuance in how topic includes were handled.

I had created a minimal test case that demonstrated where the bug happens:

> topic global
    + help
    - good luck
< topic
> topic mars includes global
    + breathe
    - can not breathe
< topic
> topic puzzle inherits mars
    + north
    - wrong
    + west
    - wrong
< topic
> topic puzzle1 includes puzzle
    + west
    - right!
< topic

When the user was in the puzzle1 topic and they typed "west", they ended up matching the "wrong" response from the > topic puzzle inherits mars topic, because puzzle1 includes puzzle and they had a duplicate trigger in both.

I was a bit confused to see this example was broken across 3 different ports of RiveScript. I thought: "surely, this demo did work at one time, back when I first added it!"

The original Perl port of RiveScript had the demo working correctly, which confirmed my suspicion.

The other ports of RiveScript had broken this example between 2012 and 2014, during the "v1.0" eras of the JavaScript and Python ports of RiveScript specifically.

  • In the original Perl implementation:

    When triggers are sorted in memory, only their text contents were sorted and stored on an array.

    The response data for those triggers were stored in a Hash Map, keyed by the trigger text and containing its responses.

    When the user's message matched a trigger on the sorted list, its details were looked up in the hash map and handled from there.

    However, this meant that 'duplicate' triggers would overwrite the key on the Hash Map. Sometimes, a 'duplicate' trigger is actually wanted, for example, if your chatbot has many "yes or no" questions that it could ask of the user, you may have many triggers to capture the user saying 'yes' but each trigger is linked to a different %Previous response from the bot.

    In the Perl port, duplicate triggers with %Previous don't function correctly because of the Hash Map.

  • In the JavaScript and Python ports, I had (at some point between 2012-14) refactored the bot so instead of the Hash Map it would keep an array of all triggers with their responses, so duplicates were allowed and the %Previous case worked as expected, but it introduced the bug with topic includes/inherits.

    The Go port (which was written the most recently) took inspiration from these ones and inherited the bug as well.

Now, the RPG Demo RiveScript code itself could have been rewritten to be more clear (using inheritance instead of includes to avoid the specific duplicate trigger), but given that the RPG Demo code existed for many years and it was once functional (still is for Perl, and once was for JavaScript and Python), I thought it best to actually fix RiveScript to make the demo work again.

So: when a topic 'includes' another and the two topics have an identical trigger, the first-party version belonging to the current topic will shadow the other one and take priority.

At the time of writing, the Rust port has this RPG Demo example working correctly, making the Rust port now even slightly more correct than the JavaScript, Python or Go ports!

I may update the other ports soon to backport the fix for them as well.

The future for RiveScript

The below is all speculative and I'm not sure it will happen, but here are some possible future plans for RiveScript.

Apart from bug fixes in the current ports, any major new development (if it happens) I will only be doing in the Rust port.

Let me explain.

While RiveScript made for a very useful project for me, for learning the ropes with a new programming language by porting over a native implementation of RiveScript, this became a double edged sword over time for RiveScript itself. The Python and JavaScript ports had proved especially popular, and they received feature requests and actual code contributions from the community on GitHub. But RiveScript had to be slow to evolve because I wanted all 5 ports to be more or less compatible in their functionality. I couldn't commit to adding a big new feature to RiveScript itself unless I was prepared to implement that feature 5 different times in 5 different programming languages.

Over the years I had sometimes thought about what a "RiveScript 3.0" might look like. The current RiveScript language has a few pain points, with its syntax and features on the edge case, that could be improved upon. But I no longer have the motivation to maintain 5 separate ports (now six separate ones, with Rust added) for such an ambitious project.

A couple of the brilliant features of Rust, though, are:

  1. It has no garbage collector, and compiles down to native machine code similar to a C or C++ program.
  2. And a Rust crate can actually be compiled as a C compatible shared object file (a .DLL or .so file).

Just about every programming language is able to link with C programs, so the above means that a library can be written in Rust and then have bindings created for JavaScript, Python, Perl, Go and so on which all use the Rust library via the C shared object API.

This is significantly better than my hopes for the Go programming language were: in Go, you can similarly compiled a Go program as a C shared object, however, because Go has a garbage collector, your C shared object brings along the full Go runtime environment. While a third-party program can link to a Go object, it can only do so one time, as adding a second Go DLL will cause the Go runtime to be added again and it will conflict with itself and blow up in your face. That all is not a problem with Rust crates compiled as C shared objects.

So if I find it a worthwhile endeavour to overhaul RiveScript, this is how I would basically want it to work:

  • It would be written only in Rust and then made available to the other languages via C bindings.
  • In the other languages, the library would be called rivescript3 and that would be the Rust binding (separate from the rivescript 2 library that already natively exists in some languages, which would still be around).
  • I'd want RiveScript 3 to be backwards compatible to still parse old files while supporting updated syntax for new ones.

Anyway, in the modern world with LLM chatbots like ChatGPT, I'm not sure the demand is so great for a classic "canned responses" type of chatbot anymore.

During all my years working on RiveScript, I saw two different waves of interest come and go around these kind of chatbots: they were booming in the early 2000's with AOL Instant Messenger chatbots such as SmarterChild, then it dulled down for a while until around 2012 when Facebook Bot Platform came about and people could program chatbots for FB Messenger (and Google/Microsoft had their versions). This was the era when the JavaScript and Python ports were especially popular. That hype again simmered down, and now we have LLMs which blow RiveScript completely out of the water.

One could still pair RiveScript with an LLM to get the best of both worlds (deterministic responses to important questions before falling back on the LLM), and RiveScript already would fit that niche well enough, so it may not be urgently needed to improve the quality of life for the RiveScript language itself.

Anyway, that's about all the news I have about RiveScript lately! Check out the Rust port if curious and let me know how well it works!

Copyright © 2026 Noah Petherbridge — All rights reserved
Hosted by DigitalOcean
Page generated in 0.0092s.

Read the original on kirsle.net

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.