roc/compiler/builtins
2020-10-13 00:55:19 +02:00
..
bitcode Cast i64 to f64 instead of using builtin bitcode 2020-10-12 17:59:20 -04:00
docs Add more docs files and check def exposition 2020-09-27 20:41:51 -03:00
src use a Dependencies struct to track what the dependencies between modules are and when they are met 2020-10-10 19:29:10 +02:00
Cargo.toml Clean up some Cargo.toml files 2020-05-03 21:37:59 -04:00
README.md Language clean up and uniqueness section in builtin readme 2020-09-09 12:14:23 -04:00

So you want to add a builtin?

Builtins are the functions and modules that are implicitly imported into every module. All of them compile down to llvm, but some are implemented directly as llvm and others in terms of intermediate functions. Either way, making a new builtin means touching many files. Lets make it easy for you and just list out which modules you need to visit to make a builtin. Here is what it takes:

module/src/symbol.rs

Towards the bottom of symbol.rs there is a define_builtins! macro being used that takes many modules and function names. The first level (List, Int ..) is the module name, and the second level is the function or value name (reverse, mod ..). If you wanted to add a Int function called addTwo go to 2 Int: "Int" => { and inside that case add to the bottom 38 INT_ADD_TWO: "addTwo" (assuming there are 37 existing ones).

Some of these have # inside their name (first#list, #lt ..). This is a trick we are doing to hide implementation details from Roc programmers. To a Roc programmer, a name with # in it is invalid, because # means everything after it is parsed to a comment. We are constructing these functions manually, so we are circumventing the parsing step and dont have such restrictions. We get to make functions and values with # which as a consequence are not accessible to Roc programmers. Roc programmers simply cannot reference them.

But we can use these values and some of these are necessary for implementing builtins. For example, List.get returns tags, and it is not easy for us to create tags when composing LLVM. What is easier however, is:

  • ..writing List.#getUnsafe that has the dangerous signature of List elem, Int -> elem in LLVM
  • ..writing List elem, Int -> Result elem [ OutOfBounds ]* in a type safe way that uses getUnsafe internally, only after it checks if the elem at Int index exists.

can/src/builtins.rs

Right at the top of this module is a function called builtin_defs. All this is doing is mapping the Symbol defined in module/src/symbol.rs to its implementation. Some of the builtins are quite complex, such as list_get. What makes list_get is that it returns tags, and in order to return tags it first has to defer to lower-level functions via an if statement.

Lets look at List.repeat : elem, Int -> List elem, which is more straight-forward, and points directly to its lower level implementation:

fn list_repeat(symbol: Symbol, var_store: &mut VarStore) -> Def {
    let elem_var = var_store.fresh();
    let len_var = var_store.fresh();
    let list_var = var_store.fresh();

    let body = RunLowLevel {
        op: LowLevel::ListRepeat,
        args: vec![
            (elem_var, Var(Symbol::ARG_1)),
            (len_var, Var(Symbol::ARG_2)),
        ],
        ret_var: list_var,
    };

    defn(
        symbol,
        vec![(elem_var, Symbol::ARG_1), (len_var, Symbol::ARG_2)],
        var_store,
        body,
        list_var,
    )
}

In these builtin definitions you will need to allocate for and list the arguments. For List.repeat, the arguments are the elem_var and the len_var. So in both the body and defn we list these arguments in a vector, with the Symobl::ARG_1 adn Symvol::ARG_2 designating which argument is which.

Since List.repeat is implemented entirely as low level functions, its body is a RunLowLevel, and the op is LowLevel::ListRepeat. Lets talk about LowLevel in the next section.

Connecting the definition to the implementation

module/src/low_level.rs

This LowLevel thing connects the builtin defined in this module to its implementation. Its referenced in can/src/builtins.rs and it is used in gen/src/llvm/build.rs.

Bottom level LLVM values and functions

gen/src/llvm/build.rs

This is where bottom-level functions that need to be written as LLVM are created. If the function leads to a tag thats a good sign it should not be written here in build.rs. If its simple fundamental stuff like INT_ADD then it certainly should be written here.

Letting the compiler know these functions exist

builtins/src/std.rs

Its one thing to actually write these functions, its another thing to let the Roc compiler know they exist as part of the standard library. You have to tell the compiler "Hey, this function exists, and it has this type signature". That happens in std.rs.

Specifying the uniqueness of a function

builtins/src/unique.rs

One of the cool things about Roc is that it evaluates if a value in memory is shared between scopes or if it is used in just one place. If the value is used in one place then it is 'unique', and it therefore can be mutated in place. For a value created by a function, the uniqueness of the output is determined in part by the uniqueness of the input arguments. For example List.single : elem -> List elem can return a unique list if the elem is also unique.

We have to define the uniqueness constraints of a function just like we have to define a type signature. That is what happens in unique.rs. This can be tricky so it would be a good step to ask for help on if it is confusing.

Mistakes that are easy to make!!

When implementing a new builtin, it is often easy to copy and paste the implementation for an existing builtin. This can take you quite far since many builtins are very similar, but it also risks forgetting to change one small part of what you copy and pasted and losing a lot of time later on when you cant figure out why things dont work. So, speaking from experience, even if you are copying an existing builtin, try and implement it manually without copying and pasting. Two recent instances of this (as of September 7th, 2020):

  • List.keepIf did not work for a long time because in builtins its LowLevel was ListMap. This was because I copy and pasted the List.map implementation in `builtins.rs
  • List.walkRight had mysterious memory bugs for a little while because in unique.rs its return type was list_type(flex(b)) instead of flex(b) since it was copy and pasted from List.keepIf.