RLC

This document describes the architecture of RLC, the rulebook compiler, and of the other tools of the RLC suite. The intent is to provide a high level description of how the various parts of Rulebook fit into a product built on top of it, as well as a description of RLC command line facilities. More exhaustive descriptions of all tools can be found in the reference.

RLC is a LLVM based compiler, it ingest one or more rulebook files and various possible outputs depending on the user request.

digraph RLC_Compiler {
    /*––Layout––*/
    rankdir = LR;
    nodesep = 0.6;
    ranksep = 1;

    /*––Global node / edge styling––*/
    node [shape=box, style=filled, color="#dddddd",
          fontname="Helvetica", fontsize=10];
    edge [arrowsize=0.7];

    /*––Input & compiler––*/
    rl_file [label="file.rl",
             shape=note, style="filled,rounded",
             color="#8ecae6", fontcolor="#0a3045"];
    rlc     [label="rlc",
             shape=box3d, color="#219ebc", fontcolor="white"];

    /*––Outputs, all kept on the same vertical line––*/
    subgraph cluster_outputs { rank=same;
        executable [label="Executable"];
        shared     [label="Shared Library"];
        static     [label="Static Library"];
        header     [label="header.h"];
        godot      [label="Godot-CPP Module"];
        csharp     [label="C# Wrapper"];
        python     [label="Python Wrapper"];
    }

    /*––Edges––*/
    rl_file -> rlc;

    rlc -> executable;
    rlc -> shared  ;
    rlc -> static ;
    rlc -> header;
    rlc -> godot;
    rlc -> csharp  ;
    rlc -> python;
}

RLC produces:

  • native executable, pretty much the same as the output of a c/cpp file compiled with clang.

  • native libraries to reuse in other languages.

  • a wrapper to use the library in that language.

rlc-lsp and autocomplete

rlc-lsp is a language server for the Rulebook language. It allows users to get autocomplete in their ide.

Notice that rlc-lsp must be in PATH, so if you are using the PIP package of rulebook, you must start your editor from the shell that has already enabled the virtual environment that installed rl_language or rl_language_core.

VSCODE

vscode has a plugin available in the plugin store called rl-lsp and rl-language that enables autocomplete and syntax highlighting.

VIM with YouCompleteMe

On vim, if you use YouCompleteMe you can get access to automplete by adding the followings to your .vimrc file.

au BufRead,BufNewFile *.rl set filetype=rl

let g:ycm_language_server = [ {
            \    'name': 'rulebook',
            \     'cmdline': [ 'rlc-lsp', '--stdio' ],
            \     'filetypes': [ 'rl' ]
            \   }]

rlc-test

rlc-test runs all functions with no arguments found in the input file that return a bool called test_* .

fun test_return_success() -> Bool:
    return true
rlc-test file.rl

This program is only available in a pip installation.

rlc-random

rlc-random runs random actions on a program with a finite amount of actions, and does until it reaches the end of the program. It prints the selected actions.

This tool is usefull as a command line utility to quickly test a interactive program, or to produce a trace usefull to some other program.

To be usable with rlc-random, a program just requires that to have a entry point with the following signature act play() -> Game, and that all action statements that appear in play are enumerable.

This program is only available in a pip installation.

rlc-learn

rlc-learn uses a off-the-shelf implmentation of PPO to maximize some metric in a given Rulebook program. This tool is intended to be used to perform sanity checks by those that wish to roll out their own machine learning algorithm, or by those that do not have knowledge of reinforcement learning and wish to use a acceptable off-the-shelf implementation. A basic tutorial is shown here.

This program is only available in a pip installation.

rlc-play

Given a Rulebook program file.rl, and the network obtained from rlc-learn file.rl, rlc-play generates a playout of that environment from start to finish according to the probabilities assigned by the network. A basic tutorial is shown here.

This program is only available in a pip installation.

rlc-probs

rlc-probs uses the network generated by rlc-learn to display on screen the actions available to the network in a given state of the input interactive program, and their respective probabilities. This tool is intended to be use to inspect the decision making skills of a trained network, beside gaining more insight than observing a single decision at the time.

A basic tutorial is shown here.

This program is only available in a pip installation.

rlc-action

Applies a execution trace to a rulebook program, and then prints the final result. This tool is usefull to check if a trace is valid, if the traced program is correct.

This program is only available in a pip installation.

Tools for compiler people

This section of the document talks about the internal components of RLC, and is meant for compiler developers, not users of the language. If you are confused about what this means and you are wondering if this section is for your, it is not, altough you can still read it if you are interested in knowing what goes on under rlc hood.

RLC internals

RLC is a LLVM and MLIR based compiler. It has a custom recursive descent parser, and a custom AST built on top of MLIR, called the RLC Dialect.

digraph RLC_Pipeline_V {
    rankdir = TB;           /* vertical (top-to-bottom) layout          */
    nodesep = 0.6;
    ranksep = 1;
    dpi     = 100;          /* higher resolution                        */

    node [shape=box, style=filled, color="#dddddd",
          fontname="Helvetica", fontsize=11];
    edge [arrowsize=0.7];

    /* main pipeline */
    lexer                 [label="Lexer"];
    parser                [label="Parser"];
    unchecked_ast         [label="Unchecked AST"];
    typechecked_ast       [label="Typechecked AST"];
    implicit_instantiation[label="Implicit Instantiation"];
    flattened             [label="Flattened"];
    action_rewriting      [label="Action Rewriting"];
    llvm_ir               [label="LLVM IR"];
    file_a                [label="file.a"];
    linker                [label="Linker"];
    lib_so                [label="lib.so"];

    /* auxiliary */
    libruntime [label="libruntime", shape=note,
                style="filled,rounded", color="#8ecae6",
                fontcolor="#0a3045"];
    wrappers   [label="Wrappers"];

    /* sequential flow */
    lexer  -> parser
    parser -> unchecked_ast
    unchecked_ast -> typechecked_ast
    typechecked_ast -> implicit_instantiation
    implicit_instantiation -> flattened
    flattened -> action_rewriting
    action_rewriting -> llvm_ir
    llvm_ir -> file_a
    file_a  -> linker
    linker  -> lib_so

    /* extra edges */
    libruntime -> linker ;
    implicit_instantiation -> wrappers;
}

Lexer

The lexer of RLC takes the input file and turns them into token to be consumed by the parser. There is nothing fancy here except that since the language has semantical whitespaces, the lexer must keep track of the indentation.

You can see the token stream with rlc --token file.rl

Parser

The parser is a recursive descent handwritten parser, the parser takes the tokens, and creates on the fly the unchecked AST.

Unchecked AST

Before typechecking the AST of the language is unchecked, it means that almost every non trivial operation is marked as having unkown type.

You can see the token stream with rlc --unchecked file.rl

Typechecked AST

The typechecked IR knows all types of all expressions, except for templates, which have been typechecked, but are not expanded yet.

Ideally, all static errors in the input program should be discovered during typechecking. In practice this is true for every error except errors relating to drop, init and assign when they are used inside templates.

You can see the token stream with rlc --type-checked file.rl

Implicit instantiation

During the implicit instantiation step templates end up being generated, and implicit init, drop and assign functions emitted.

After the implicit step, no object of the module can be a template, and all non external declared entities must have been discovered.

You can see the token stream with rlc --after-implicit file.rl

Wrappers for other languages are emitted after this step, when all concrete functions are accounted for.

Flattened

During the flattening step all control flow constructs are removed and replaced with jumps.

You can see the token stream with rlc --flattened file.rl

Action rewriting

During action rewriting all Action Functions act $name() -> $Type: end up being replaced with the functions they declare. This includes:

  • fun $name() -> $Type:

  • fun can_$name() -> Bool: if the action function $name has a precondition.

  • fun $act($Type, args...) for each action statement called $act in $name

  • fun can_$act($Type, args...) -> Bool for each action statement called $act in $name

  • fun is_done($Type) -> Bool

After action rewrting the module contains only types and functions.

Lowering to LLVM IR

The module is translated to LLVM IR to be optimized and compiled.

You can see the token stream with rlc --ir file.rl to see the LLVM ir. You can use rlc --ir file.rl -O2 if you want to see the optimized IR.

Making IR more readable

When priting the RLC IR you can pass --hide-positions and --hide-dl to omit the module data layout and the module debug positions. That makes the IR slightly more readable.

MLIR

I know you are looking for a explanation of how MLIR works since MLIR documentation is arcane. Good luck!

Once you figure it out: RLC uses MLIR as a mere intermediate rappresentation generator, and as a way to convert to LLVM IR. We use nothing about Dialect interoperability or tensorial stuff. Every MLIR operation we use(except when converting to LLVM IR) is custom made for RLC.

In practice what we get out MLIR is:

  • the pipeline infrastructure

  • the declarative way of creating new operation types

  • the textual serializers for MLIR

  • mlir-opt which runs single passes of mlir

  • some basic type interfaces

  • MLIR dataflow analisys

Running single RLC passes

If you want to run a RLC pass in isolation after a particular step of the rlc pipeline, say the typechecking step, you can run

rlc --type-checked file.rl -o - | rlc-opt --PASS_NAME