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.