Lua processing with darklua

I wanted to share a project I’ve been working from time to time since February. It’s called darklua.

Disclaimer: I’m French so there may be some grammar issues. Feel free to message me so I can correct them :books: :books: :books:

Background

What initially started as an obfuscator turned into something a little more useful: a Lua processor/compiler/transformer (honestly I don’t know how to call it). As I created the system that was able to transform the Lua code, I realized that there was more potential than I had anticipated.

What Is It?

darklua is a command line tool made in Rust. What does it mean to you? To keep it simple: it is fast and there is less chances of bugs.

If you’re unfamiliar with command line tools, it’s fine! Leave a comment and I (or probably many others) can help! There are probably good existing resources out there to get you started.

What It Does

darklua takes a file that contains Lua code, executes a customizable stack of rules and then generate a new file with the new processed code. The real features come from the individual rules that modifies your code.

As I mentioned earlier, this project started as an obfuscator, so there are a bunch of rules that do not modify the behavior of the code, but rather just makes it different on a syntax level. They’re not what really makes darklua cool.

One feature that makes darklua interesting is that it is capable of evaluating code. That means that complex rules can be created. Here is a really simple example of the compute_expression rule that would take this code as input:

return 2 + 2

And produce this:

return 4

It is also able to understand if control structures. In concrete, there is a rule called remove_unused_if_branch that modifies if statements when possible. Given this:

if true then
    print('condition is true')
else
    print('condition is false')
end

darklua would generate this code:

do
    print('condition is true')
end

However, the compute_expression rule alone is not that great, but using it with the inject_global_value starts to make things useful. This rule will replace a global variable with a given value. For example, you could tell darklua to replace all variables named MY_GAME_FEATURE_IS_ON with true or false. Let’s say we would have the following code somewhere in our code base:

if MY_GAME_FEATURE_IS_ON then
    callNewFeature()
end

When darklua applies the replace rule with a value of true, it becomes:

if true then
    callNewFeature()
end

And after applying the if rule:

do
    callNewFeature()
end

If we would have decided to replace the variable with false, the generated code would have been different and the callNewFeature function would not have been called. That shows that darklua is able to do feature flagging statically.

You can find the current set of possible rules here.

Example With Roact

Installation

I have not uploaded any pre-built binaries, so right now you’ll need to have Rust and cargo installed. Then, you can install it with the following command:

cargo install darklua

Process Roact

Open a command prompt and clone the Roact repository.

git clone https://github.com/Roblox/roact.git
cd roact

Then, execute the default set of rules by running:

darklua process src processed-src

The process command takes the input as a first argument, and generates the new code at the given output. For this example, the src/init.lua will be generated to processed-src/init.lua:

See generated code
local a,b,c,d,e,f=require(script.GlobalConfig),require(script.createReconciler),
require(script.createReconcilerCompat),require(script.RobloxRenderer),require(
script.strict),require(script.Binding)local g=b(d)local h=c(g)local i=e{
Component=require(script.Component),createElement=require(script.createElement),
createFragment=require(script.createFragment),oneChild=require(script.oneChild),
PureComponent=require(script.PureComponent),None=require(script.None),Portal=
require(script.Portal),createRef=require(script.createRef),createBinding=f.
create,joinBindings=f.join,createContext=require(script.createContext),Change=
require(script.PropMarkers.Change),Children=require(script.PropMarkers.Children)
,Event=require(script.PropMarkers.Event),Ref=require(script.PropMarkers.Ref),
mount=g.mountVirtualTree,unmount=g.unmountVirtualTree,update=g.updateVirtualTree
,reify=h.reify,teardown=h.teardown,reconcile=h.reconcile,setGlobalConfig=a.set,
UNSTABLE={}}return i

You can specify the more readable format with the following command:

darklua process src processed-src --format readable

Which would output the same src/init.lua file this way:

See generated code`
local a, b, c, d, e, f = require(script.GlobalConfig), require(script.createReconciler), require(script.createReconcilerCompat), require(script.RobloxRenderer), require(script.strict), require(script.Binding)
local g = b(d)
local h = c(g)
local i = e{
    Component = require(script.Component),
    createElement = require(script.createElement),
    createFragment = require(script.createFragment),
    oneChild = require(script.oneChild),
    PureComponent = require(script.PureComponent),
    None = require(script.None),
    Portal = require(script.Portal),
    createRef = require(script.createRef),
    createBinding = f.create,
    joinBindings = f.join,
    createContext = require(script.createContext),
    Change = require(script.PropMarkers.Change),
    Children = require(script.PropMarkers.Children),
    Event = require(script.PropMarkers.Event),
    Ref = require(script.PropMarkers.Ref),
    mount = g.mountVirtualTree,
    unmount = g.unmountVirtualTree,
    update = g.updateVirtualTree,
    reify = h.reify,
    teardown = h.teardown,
    reconcile = h.reconcile,
    setGlobalConfig = a.set,
    UNSTABLE = {},
}

return i

You can find more info about the commands and arguments in the readme.md. There is also a section about the configuration file that darklua uses to know what rules to apply. If you do try to use it, let me know anything you wish you would have found in the documentation.

What’s Next?

There are mostly two pillars in darklua: the set of rules and the Lua evaluation system. I would like to improve the Lua evaluator a lot, since it allows existing rules to be applied better and to make more complex rules, like to statically evaluate constant functions or inline small functions. You can see a few ideas on the issues page (feel free to submit yours!).

Can You Use It?

I’m confident that it can be used because I made sure to test it well enough. Since darklua is generating code, it was important to me that it was not never breaking anything. That’s why I’m automatically testing darklua with Roact, Rodux, Roact-Rodux and t. For each of these projects, the code is processed with different set of rules, and their unit-tests are ran to see if everything still pass.

If you have an open-source project (in Lua) with unit-tests, I would be happy to add it to the test suite!

Conclusion

You may notice that I did not go to much into the details of the command line interface, because at this point I’m seeking more feedback than users. I don’t really expect people to use it, but I could use any comments and ideas!

I also felt that it was time to present it because I recently added a new feature that makes it a lot more ready to be used in real life. I added the possibility to generate human readable code (version 0.4). That is useful when testing and debugging, since error messages and line numbers make more sense than with the dense Lua code generator (or as my brother called it the first time he saw the generated code: sausage code simulator).

I also wanted to share the project to see if anyone would have any awesome idea that could improve it! Feel free to open an issue and propose your idea, leave a comment or send me a direct message somewhere!

9 Likes

This looks like an awesome Lua optimising processor - out of interest, have you done any benchmarks of script efficiency before and after darklua processing?

Not yet! I need to implement more actual optimizing rules, because right now the rules don’t really change the actual Lua bytecode that much. It’s definitely something that will exist in the future.

One thing you’re making me think of is that I could use luac to compare the generated bytecode to see how much a file would have actually changed. With that, I would have an idea about how much real modification darklua did. Then, it is easier to find meaningful candidates for benchmarks and avoid testing on syntax sugars.

Right now, my strategy is only to get the evaluation system as best as I can so that it can compute as much things as possible. Besides that, if you have any ideas about what darklua could do to optimize stuff let me know!

1 Like