Skip to content

A scripting language to plan your builds

The RSDP is very expressive, and allows you to precisely describe how any software should be built, on any platform. However, it only describes a binary format for that description, which can be hard to edit by hand, and is certainly not readable.

The Fix scripting language allows programmers to write simple descriptions of their build plans, in a textual format, that the Fix utility can then compile into their final binary form to be distributed.

If you are familiar with the Nix language, you may notice some similarities. However, the devil (as always) is in the details, and the two languages are actually quite different, both in scope and in their semantics.

Writing a Fix script

A Fix script is simply a text file whose name should end in .fix, that contains a single Fix expression. When running fix plan <name>, the interpreter will look for the file <name>.fix and evaluate its expression. If that expression produces a build plan, it will produce a binary representation of that plan and print its path to stdout.

Evaluation : How Fix scripts are "run"

Within this documentation, you will encounter many references to "evaluation". What this means precisely is that all Fix expressions can exist in one of two states : defined and evaluated. An expression that is parsed by the interpreter starts out as defined.

An expression that is only defined may not have a value (and indeed, often doesn't have one). Evaluation is the process by which an expression goes from having no value to having a value. This process only goes one way (an expression can never go back to being simply defined if it was evaluated), and is idempotent, which means that evaluating an expression twice is the same as evaluating it once.

This mode of operation is what allows Fix to provide a descriptive interface to building software, while avoiding needless computation if it isn't necessary.

Fix expressions

There are a few different kinds of expressions.

Simple values (integers, booleans and strings)

An simple value is an expression that evaluates to itself.

This is essentially what you'd expect. For example, writing 3 into a script will evaluate to the number 3, and "Hello" will evaluate to the text "Hello".

To allow for build scripts to be embedded within a Fix expression, you can also write multiline strings by enclosing them between ''' :

'''
    #include <stdio.h>
    int main(int argc, char* argv[]) {
        printf("Hello, world (from C, in Fix) !\n");
    }
'''

The Fix parser will remove as many spaces as are contained in the shortest common prefix of all the nonempty enclosed lines. Perhaps more clearly, Fix removes enough whitespace at the start of each line that at least one line begins with a nonblank character, while still preserving the structure and relative indentation across the whole block.

Lists

A list is a sequence of values. It is written [<item1> ... <itemN>], with spaces separating its elements. Lists can contains mixed types of elements, so that the list [1 "hello" [0]] is a valid list of three elements, the last one being itself a list of one element.

A list is one example of a structural expression. A structural expression is an expression that evaluates to itself, much like a simple value. However, contrary to a simple value, a structural expression can also contain other expressions. Evaluating a structural expression does not evaluate its subexpressions.

Essentially, when asked to tell what a list is, the Fix interpreter will respond with "yup, it's a list" and not look any further.

Dictionaries

Another kind of structural expression, a dictionary is an assocation between names and expressions. It is written as :

{
   name1 = value1;
   name2 = value2;
   ...
}

Given a dictionary d, you can get the value of a field <f> by writing d.<f> (for example d.name1 with the above dictionary).

You can also index a dictionary with an arbitrary expression <expr>, by writing d[<expr>] instead. Evaluating this expression will first evaluate <expr>, then interpret that value as a string to look for its correspondance in d. For example d["name1"] is equivalent to d.name1.

Dictionaries are useful for grouping related values. For example, the metadata of a build plan can be represented by a dictionary in the following way :

{
    author = {
        name = "Author Name";
        email = "author.name@domain.tld"
    };
    version = {
        major = 2;
        minor = 1;
        patch = 18;
    };
}

To make writing nested dictionaries easier, Fix also recognized a nested record syntax. Using that syntax, the above dictionary can equivalently be written as such :

{
    author.name = "Author Name";
    author.email = "author.name@domain.tld";

    version.major = 2;
    version.minor = 1;
    version.patch = 18;
}

The two formulations are entirely equivalent, and you can mix between them (in case of conflict, the later definition will override earlier ones) :

{
    author = {
        name = "Author Name";
        email = "not.used@old-email.tld";
    };
    author.email = "author.name@domain.tld";

    version = {
        major = 2;
        minor = 1;
    };
    version.patch = 18;
}

Functions

Like many other languages, and for the same purpose of avoiding repetition, Fix allows you to define functions. A function is defined in the following way :

fun <pattern>...: <body>

Functions can only be used in one way, by applying them to a arguments. For example :

def f = fun x y: [ "greetings" x y ];
f "hello" "goodbye"  # Will produce [ "greetings" "hello" "goodbye" ]

TODO keep defining functions

Environments and variables

Variables are a fundamental part of any programming language, however narrow its scope may be. Fix is no exception, and allows multiple ways to define and use variables.

To keep track of what variables are available at given point of the program, and what expressions they represent, the Fix interpreter uses environments. An environment is simply a stack of dictionaries (see above). When looking up a variable, Fix will simply search the stack from top to bottom for a dictionary that knows the name of the variable.

One of the ways to define variables is thus to push a dictionary onto the stack, by way of the with <dict>; <expr> builtin.

with {
    a = 3;
    b = "Hello";
}; # The next expression will know about the variables 'a' and 'b'
[a b a]

This is the most general (and sometimes cumbersome) way to define variables. A perhaps more readable way to write the above is by using the lighter def syntax :

def a = 3;
def b = "Hello";
[a b a]

A variant of the def syntax (with slight differences) is the defs ...; and syntax :

defs a = 3;
and  b = "Hello";
[a b a]

All the expressions above will produce the exact same value : [3 "Hello" 3]. Which one you choose to use is mostly up to your personal preference, except for very specific cases.

The specific cases

Internally, def a = <A>; <B> is just syntactic sugar for the equivalent expression with { a = <A>; }; <B>.

TODO keep documenting specific cases

Recursion : Here be dragons

Recursion is the ability to define something that in part depends on itself. In Fix, the only structure that can be defined recursively is a dictionary, by prefixing it with the rec keyword.

A recursive dictionary is just like a regular dictionary, except that it pushes itself onto the environment before evaluating its contents. For example :

def a = 3;
{ a = 4; b = a; } # Without 'rec', this evaluates to { a = 4; b = 3; }

def a = 3;
rec { a = 4; b = a; } # With 'rec', it evaluates to { a = 4; b = 4; }

Recursion is a very powerful language capability, that allows for arbitrarily complex programs to be designed. That being said, if you feel yourself needing a lot of complexity for your build process, maybe you should consider simplifying it before reaching for the hammer that is recursion.

Just because you can doesn't mean you should. Fix is meant as a scripting language, and if you find yourself programming in it, something went terribly wrong. You have been warned.