Ten Years of Brat

Okay, it’s a little crazy that this silly little language has been under development for ten years!

There isn’t much to say, really, except it’s been a fun and educational (and sometimes frustrating) ten years of tinkering on this bratty language.

Recent Updates

Now for some recent news.

Brat works on OSX again.

The bootstrapping binary (“minibrat”) only worked on Linux. Now there is one for OSX, too (“minibrat-osx”). OSX has also been added to the TravisCI build to help ensure future compatibility.

Parsing is faster.

I spent a significant amount of time focused on making the parser go faster. It is still slow relative to industrial parsers, but much faster than it was!

There isn’t a lot of Brat code to test the parser on, but I’m seeing a 40-60% reduction in parse times.

Faster method name conversion.

Brat escapes quite a few symbols so they are usuable in variable/function names. When calling a method like has_method?, the string passed in must be escaped first.

This can be a bit slow, since it uses regular expressions which requires calling out to the Oniguruma regular expression library.

Now Brat caches these conversions and first attempts a native Lua pattern match before falling back to Oniguruma.

(Some future work involves automatically using native Lua patterns for Brat regular expressions, where possible.)

Symbols are back.

Symbols (:example) are immutable strings again. Fun!

What’s Next?

Who knows? That is what is great about hobbies. You can pick them up and put them down as you like!

Small Implementation Change in Conditionals

Brat is pretty much all objects and method calls. There are no special keywords and only a few special characters. So true?/false?/null? are just methods and branches are just arguments to those methods.

Brat is also very eager, so the only way to delay evaluation is to put code inside of functions.

If one writes code like this:

true? something, do_this, else_do_that

Brat will execute something, do_this, and else_do_that then pass the results as arguments to true?. That’s probably not what we want!

Instead, we have to do this:

true? something, { do_this }{ else_do_that }

A while back we inlined branches if they were (inlineable) functions. This led to a pretty decent performance improvement.

But what about code like this?

true? something, "0""1"

In this case, the two strings will be generated regardless of the condition, as they are just arguments to the true? method. This is obviously wasteful. But putting the branches inside functions is also a bit cumbersome:

true? something
  { "0" }
  { "1" }

Feels a bit like overkill just to avoid allocating a string.

To address this, when the branches are any simple value (string, number, array, etc.), Brat will generate the same kind of inline if constructs as it would for inlineable functions.

Some Numbers?

Let’s look at some luatrace summaries.

Before

1000.times { i |
  true? i > 500"1""0"
}
TRACE SUMMARY
=============
Trace Status                       Traces       Bytecodes           Lines
------------                       ------       ---------           -----
Success                          6 ( 75%)      836 (  6%)      139 ( 52%)
loop unroll limit reached        2 ( 25%)    12271 ( 93%)      128 ( 47%)
------------------------- --------------- --------------- ---------------
Total                            8 (100%)    13107 (100%)      267 (100%)
========================= =============== =============== ===============

Before, Using Functions

1000.times { i |
  true? i > 500{ "1" }{ "0" }
}
    TRACE SUMMARY
    =============
    Trace Status           Traces       Bytecodes           Lines
    ------------           ------       ---------           -----
    Success              4 (100%)      379 (100%)      106 (100%)
    ------------  --------------- --------------- ---------------
    Total                4 (100%)      379 (100%)      106 (100%)
    ============  =============== =============== ===============

After

    TRACE SUMMARY
    =============
    Trace Status           Traces       Bytecodes           Lines
    ------------           ------       ---------           -----
    Success              4 (100%)      379 (100%)      103 (100%)
    ------------  --------------- --------------- ---------------
    Total                4 (100%)      379 (100%)      103 (100%)
    ============  =============== =============== ===============

What does this all mean?

Starting with the “Before” results, we see 6 traces were compiled by the JIT, but 2 hit problems that caused the JIT process to stop. Also, notice the large number of bytecodes affected in the “Before” results - only 836 successfully compiled, 12,271 did not.

Compare to using functions for the conditional branches. In the “Before, Using Functions”, all traces are compiled and it’s only 379 bytecodes. This is because the true? function call is gone and replaced with an if statement with inlined branches instead of function calls.

Now, in the “After” results, we see it lines up with the result from wrapping the branches in functions, except we don’t have to do that extra bit of work or worry about a performance hit. Hooray!

How much faster?

For the code above,

Before: 0.020s

After: 0.010s

While that is a 50% improvement, it’s probably not going to make any particular program go that much faster. Instead, it just removes that little bit of performance concern with simple arguments to true?/false?/null?.

What Happened in 2017?

Oops, 2017 went by without a blog post! What happened?

Not a lot…

  • The interactive Brat shell works again!
  • More unboxed operations on numbers
  • Single-quoted backslashes finally parse correctly
  • More tracking of types during compilation
  • Symbols (:blah) are now immutable, again
  • Create prototype objects on demand
  • This works: x.y = {}()
  • Fix nested comments and semicolons in comments
  • Updated LuaJIT

Overall, Brat is another year older, and a little bit faster!

More posts…

Fork me on GitHub