A Clojure-inspired Lisp that compiles to Nix.
godrick DOCS.md
34 kB · 856 lines
Markdown
at main

Godrick #

Godrick is a Lisp that evaluates to Nix.

This guide covers how to use Godrick. A working knowledge of Nix is expected.


Contents #

  1. Getting started
  2. A first file
  3. The basics
  4. Functions
  5. Bodies: def and do
  6. Attrsets
  7. Patterns
  8. Logic
  9. Working with lists
  10. Loading other files
  11. Macros
  12. NixOS configuration
  13. Packages
  14. Shell scripts
  15. Errors and warnings
  16. The godrick command
  17. Building ahead of time
  18. Things that may surprise you
  19. Quick reference
  20. Working on Godrick itself

Getting started #

Godrick files end in .rick.

The command-line tool #

The flake's package is the godrick command:

godrick compile hello.rick   # show the Nix that hello.rick turns into
godrick check hello.rick     # look for errors and warnings
godrick fmt hello.rick       # tidy the indentation
godrick repl                 # try Godrick out interactively

To try it without installing, use nix run git+https://tangled.org/poacher.dev/godrick -- compile hello.rick. To install it, use nix profile add git+https://tangled.org/poacher.dev/godrick (nix profile install on older versions of Nix). On NixOS, add godrick.packages.${pkgs.stdenv.hostPlatform.system}.default to environment.systemPackages, or apply godrick.overlays.default and use pkgs.godrick. Without flakes, the package is pkgs.callPackage "${builtins.fetchGit "https://tangled.org/poacher.dev/godrick"}/package.nix" {}.

From a copy of this repository you can also run the script directly, as bin/godrick compile hello.rick. The command uses the nix already on your PATH, and it works even when the nix-command experimental feature is turned off.

Loading Godrick from Nix #

The flake gives you these functions under lib:

Function What it does
importGodrick ./file.rick Works like import, but for a Godrick file
importGodrickModule ./file.rick Turns a Godrick file into a NixOS module, for imports
checkBuilt ./dir Checks that godrick build is up to date, for flake checks (see Building ahead of time)
compile, check, format The compiler's own functions, used by the CLI

In a flake:

{
  inputs.godrick.url = "git+https://tangled.org/poacher.dev/godrick";
  outputs = { self, godrick, ... }: {
    value = godrick.lib.importGodrick ./thing.rick;
  };
}

Without flakes, use (import (builtins.fetchGit "https://tangled.org/poacher.dev/godrick")).importGodrick ./thing.rick.

Your flake.nix must stay in Nix because Nix reads a flake's inputs before it evaluates anything. Every other file can be Godrick.


A first file #

Save this as hello.rick:

; Lines starting with ; are comments.
(defn :greet :name "Hello, ${name}!")
(def :people ["Ada" "Grace"])

{~greet
 :messages (builtins.map greet people)}

godrick compile hello.rick shows the Nix it turns into:

{ __dir, importGodrick, importGodrickModule }:
let
  greet = name: "Hello, ${name}!";
  people = [ "Ada" "Grace" ];
in
{ inherit greet; messages = builtins.map greet people; }

The first line is a wrapper that every compiled file gets. When the file is loaded, it is given its own directory, for relative paths, and the loaders, for importing other .rick files. The other examples in this guide leave the wrapper out.

A file is a list of definitions followed by one final value. When you load the file, you get that final value back, just like a Nix file.


The basics #

Defining and using names #

Every name is either being defined or being used, and the syntax tells you which:

Write Meaning
:name Defines name here: a variable, a function parameter, or an attribute
name Uses a name defined somewhere else
"text" A string, including the name of an attribute you are only looking up
~name Inside { }, both: an attribute and a variable with the same name

So (def :x 1) defines x, and x on its own uses it. The colon defines a name, which is why it appears in def, fn, loop, attrset keys and patterns alike. ~name is a shorthand for an attribute and variable that share a name, like Nix's inherit. It only appears in attrsets and attrset patterns.

Calling things #

Code is written as lists in parentheses. The first item is the function and the rest are its arguments:

(add 1 2)                 ; add 1 2
(builtins.map f xs)       ; builtins.map f xs

Values #

Godrick Nix Notes
42, -1, 3.5 42, -1, 3.5 Numbers
true, false, null the same
"hi ${name}" "hi ${name}" Strings, with the same escapes as Nix
""" … """ '' … '' Multi-line strings, with the same indentation rules as Nix
./src, ../lib, <nixpkgs> paths Relative paths still work after compiling
[1 2 3] [ 1 2 3 ] Lists
{:a 1 :b 2} { a = 1; b = 2; } Attrsets (see Attrsets)

Operators #

These turn into Nix's own operators:

Godrick Nix
(+ a b c) a + b + c
(- a b), (* a b), (/ a b) a - b, a * b, a / b
(== a b), (!= a b), (< a b), (<= a b), (> a b), (>= a b) the same
(++ xs ys) xs ++ ys (join lists)
(// a b) a // b (merge attrsets)
(and a b), (or a b), (not a) a && b, a || b, !a

An operator can also be passed as a value. For example, (builtins.foldl' + 0 xs) adds up a list.

Names #

A name must be a valid Nix name: letters, digits, _, ' and -, and it can't start with a digit. This means some names common in other Lisps aren't allowed:

  • empty?, set! and *x* are errors. Use camelCase, as nixpkgs does: isEmpty.
  • Nix keywords such as if, let, in, with, rec, inherit and or can't be used as names.
  • Names containing __ are kept for the compiler.
  • builtins, true, false and null can't be redefined.

An attribute whose name isn't a valid Nix name is reached with a string: (. x "empty?"), and defined with (key "empty?").

Reaching into attrsets #

cfg.services.nginx.enable       ; cfg.services.nginx.enable
(. cfg "services" "nginx")      ; cfg.services.nginx (the long form of the above)
(get cfg "port" 80)             ; cfg.port or 80
(? cfg "services" "nginx")      ; cfg ? services.nginx
(. x "empty?" "b")              ; x."empty?".b (any name works in a string)
(. x "a.b")                     ; x."a.b" (a string is always one name)
(. x key)                       ; x.${key} (any expression works too)

Functions #

Nix functions take exactly one argument, so each parameter is one function. Every form but the last is a parameter, and the last is the body:

(fn :x body)                            ; x: body
(fn :a :b body)                         ; a: b: body
(fn {~pkgs ~lib} body)                  ; { pkgs, lib, ... }: body
(fn {~pname ~version ? "1.0"} body)     ; { pname, version ? "1.0", ... }: body
(fn (@ :args {~pkgs}) body)             ; args@{ pkgs, ... }: body
(fn _ body)                             ; _: body

An attrset parameter always accepts attributes it doesn't name, so it always ends in .... ? default gives a default for a missing attribute, as in Nix.

The body is a single form. Use do when it needs definitions (see Bodies).

defn defines a named function:

(defn :add :a :b (+ a b))           ; add = a: b: a + b;
(add 1 2)                           ; 3

Bodies: def and do #

A body is any number of defs, then one final value. A file is a body.

(def :version "1.2")
(def :name (+ "foo-" version))
name
let version = "1.2"; name = "foo-" + version; in name

To use a body in the middle of an expression, wrap it in do:

(do
  (def :x 1)
  (def :y 2)
  (+ x y))

The defs in one body can refer to each other in any order, because they all become one Nix let.

Pulling names out of an attrset #

(def {~zlib ~openssl} pkgs)          ; inherit (pkgs) zlib openssl;
(def {~port ? 80} cfg)               ; port = cfg.port or 80;
(def {:port :p} cfg)                 ; p = cfg.port;

After this, you can write zlib instead of pkgs.zlib. ~name keeps the attribute's name, and :name :other binds it as other instead (see Patterns).


Attrsets #

Inside { }, each entry starts with :name, and the next item is its value:

{:pname "foo" :version "1.0"}      ; { pname = "foo"; version = "1.0"; }

~name takes the variable of the same name, like Nix's inherit:

{~pname ~version}                  ; { inherit pname version; }
{~pname :src ./src}                ; { inherit pname; src = ./src; }

To take an attribute of another attrset, use (inherit src :name ...):

{(inherit pkgs :zlib :openssl)}    ; { inherit (pkgs) zlib openssl; }

To nest settings, nest the attrsets:

{:services {:openssh {:enable true}}}   ; { services = { openssh = { enable = true; }; }; }

To compute a key, or to use a name that isn't a valid Nix name, use (key ...):

{(key name) "value"}               ; { ${name} = "value"; }
{(key "resolv.conf") "..."}        ; { "resolv.conf" = "..."; }

Patterns #

Patterns take values apart. They are used by fn parameters, def, match and for, and they look like the value they match:

Pattern Matches
:name Anything, and gives it that name
_ Anything, and ignores it
name A value equal to the variable name (only in match)
1, "text", true, null That exact value (only in match)
[:a :b] A list of exactly two items
[:a & :rest] A list of at least one item, with the remaining items in rest
{~a ~b} An attrset with a and b, which become variables
{:a pattern} An attrset whose a matches pattern
{:a :x} An attrset with a, which becomes the variable x
{~a ? 1}, {:a :x ? 1} An attrset, with a set to 1 if it is missing
(@ :all pattern) What pattern matches, with the whole value also called all

Attrset patterns always allow other attributes. fn parameters can't contain nested patterns or list patterns yet, because Nix functions can't express them; take the value whole and use match.

Renaming an attribute #

~name binds attribute name to a variable called name. To use a different variable name, follow the key with a binder, as :name :a:

(match pair
  [{:name :a} {:name :b}] "${a} and ${b}")

Renaming is needed when two attributes would otherwise bind the same name, as in the example. It works in def, match and for, but not in fn parameters, since Nix functions can't rename an attribute; write ~name there, and (def :a name) in the body.


Logic #

(if (> n 0) "positive" "not positive")     ; both branches are required

(when (> n 0) "yes")                        ; if n > 0 then "yes" else null
(unless (> n 0) "no")                       ; if n > 0 then null else "no"

(cond
  (< n 0) "negative"
  (== n 0) "zero"
  "positive")

A lone value at the end of cond is the default. If there is no default and no test matches, evaluation stops with an error that points at the cond.

match #

match compares a value against a series of patterns and uses the first one that fits:

(match v
  [] "empty list"
  [:x] "one item"
  [:x & :rest] "starts with ${(toString x)}"
  {:type "file" ~path} "file at ${path}"
  _ "something else")

A plain name compares with the variable of that name, so (match v limit "at the limit" _ "not yet") checks whether v equals limit. A value on its own at the end works as the default. If there is no default and nothing matches, match stops with an error.

Threading #

-> and ->> let you write a chain of steps from top to bottom instead of nesting calls. A string step looks up that attribute, and a bare function name is called.

-> passes each result in as the first argument of the next step:

; Frankly, -> isn't very useful and this is contrived
cfg.services.nginx
(-> cfg "services" "nginx")

->> passes it in as the last:

; Nested, read from the inside out:
(builtins.foldl' + 0 (builtins.map builtins.stringLength (builtins.filter isDjango names)))

; Threaded, read from top to bottom:
(->> names
     (builtins.filter isDjango)
     (builtins.map builtins.stringLength)
     (builtins.foldl' + 0))

Nix primarily has data as the last argument, so you'll be using ->> most of the time.


Working with lists #

Form Result
(for :x [1 2] :y [10 20] (+ x y)) [11 21 12 22], every combination in order
(for [:a :b] [[1 2] [3 4]] (* a b)) [2 12]; any pattern can take items apart
(reduce + 0 [1 2 3]) 6, using builtins.foldl'
(builtins.map f xs), (builtins.filter f xs) Any Nix builtin works

loop and recur #

Nix has no loops, and deep recursion can hit its depth limit. loop gives you a safe, fast way to repeat something:

(loop :i 0 :total 0
  (if (== i 10)
    total
    (recur (+ i 1) (+ total i))))       ; 45

loop names some values and gives them starting values, then takes one body. recur starts the next round with new values, in the same order as the names. It must be the last thing the loop does, and it must give a new value for every name.

Behind the scenes:

  • A simple counting loop like the one above becomes the builtins.foldl' you would write by hand.
  • Any other loop becomes recursion that is split into chunks, so it never hits Nix's depth limit. A small loop body can be copied into the output up to 32 times. That is normal.

In both cases, loops run within about 10% of the speed of hand-written Nix.

Tips for fast code:

  • Use builtins.map, builtins.filter and reduce when they fit. They run in native code.
  • Avoid stepping through a list with head/tail. Nix copies the list each time, so the loop gets slow on long lists. Use an index with builtins.elemAt instead.
  • Recursion that isn't the last step, such as walking a tree, still uses up Nix's depth limit.

Loading other files #

(import ./other.rick)     ; loads a Godrick file
(import ./other.nix)      ; loads a Nix file, as usual

Godrick picks the right one from the file extension. If the path is computed, call importGodrick yourself:

(importGodrick (+ ./modules "/${name}.rick"))

Relative paths like ./src always point next to the .rick file, even though the compiled Nix lives in the store.


Macros #

A macro is a function that runs while your code is being compiled. It takes code as input and returns new code, which goes in its place. Most of Godrick's own features, such as defn, cond, match and ->, are macros.

Writing a macro #

Use a backquote to write a code template, , to insert a value, and ,@ to insert a list of items:

(defmacro :unless [:c & :body]
  `(if ,c null (do ,@body)))

The parameters are a list pattern over the macro's arguments, so & :body collects any remaining arguments into a list. Godrick checks the number of arguments every time a macro is used. A macro's body is one form.

A macro can also return plain data. Strings, numbers, booleans, null and lists all work:

(defmacro :answer [] 42)

Choosing safe names inside a macro #

A macro's code is pasted into your code, so a name it defines could clash with one of yours. To avoid this, end the name with #, and Godrick will replace it with a unique one. :t# defines it and t# uses it:

(defmacro :orElse [:e :fallback]
  `(do (def :t# ,e)
       (if (== t# null) ,fallback t#)))

(fn :t (orElse t 0))
; t: let t__1 = t; in if t__1 == null then 0 else t__1

If a macro defines a plain name without #, Godrick gives you an error. If you really do want to create a name for the user, write it as ,(binder "name"); (sym "name") makes the matching use of it, and (pun "name") makes a ~name.

Store values you use more than once. If a macro inserts an argument twice, Nix computes it twice. Define it once with def and a # name, as orElse does.

definline: functions that are pasted in #

definline defines a small helper that behaves exactly like a function, but its body is pasted where it is used, so the call costs nothing:

(definline :isEmpty :xs (== (builtins.length xs) 0))

(isEmpty ys)               ; builtins.length ys == 0
(builtins.map isEmpty xss) ; builtins.map (xs: builtins.length xs == 0) xss

An inline function can only use its own parameters, builtins and other inline functions. It can't call itself.

Sharing macros between files #

(import-macros ./lib/my-macros.rick :orElse :isEmpty)

This loads the named defmacros and definlines from another file, defining them in this one. You have to name each one you want. defmacro, definline and import-macros must appear at the top level of a file.

Notes #

  • A name you define yourself, with def, defn or a function parameter, takes priority over a macro with the same name. So (def :match builtins.match) makes match a normal function again.
  • A macro can only use builtins, sym, binder, pun, and macros defined earlier in the file. Your other definitions don't exist yet when the macro runs.
  • Errors in code a macro produces point at the place where you used the macro.
  • '(1 2 3) is quoted code for macros, not a list. Use [1 2 3] for a list.

NixOS configuration #

A configuration can be written much like Nix, and is about the same length:

(fn {~config ~lib ~pkgs}
  {:imports [./hardware.nix ./greeter.rick]

   :networking {:hostName "box"}
   :services {:openssh {:enable true}}
   :users {:users {:alice {:isNormalUser true
                           :extraGroups ["wheel"]}}}
   :environment {:systemPackages (from pkgs ["git" "vim" "htop"])}
   :system {:stateVersion "26.05"}})

Load it from Nix with importGodrickModule:

nixosSystem {
  modules = [ (godrick.lib.importGodrickModule ./configuration.rick) ];
}

Any .rick file listed in :imports is loaded the same way, so error messages, disabledModules and duplicate handling behave exactly as they do for .nix files.

from #

(from pkgs ["git" "vim"]) turns into [ pkgs.git pkgs.vim ]. It is nearly as short as with pkgs; [ git vim ], without hiding where the names come from. Each string is one attribute name, so names that aren't valid Nix names work ("0ad"). For nested packages, give the nested attrset as the source and join the lists:

(++ (from pkgs ["git" "vim"])
    (from pkgs.python3Packages ["requests"]))

defmodule #

defmodule writes the usual module boilerplate for you:

(defmodule ["services" "greeter"]
  ; options
  {:enable (lib.mkEnableOption "the greeter")
   :name (option lib.types.str "world" "Who to greet.")
   :package (option lib.types.package pkgs.hello "The package that greets.")}
  ; config
  {:environment {:systemPackages [cfg.package]
                 :etc {:greeting {:text "hello ${cfg.name}\n"}}}})

It does the following for you:

  • Takes { config, lib, pkgs, ... } as the module's arguments
  • Sets cfg to config.services.greeter
  • Puts your options under options.services.greeter
  • Wraps your config in lib.mkIf cfg.enable if there is an enable option

You can pass a fourth attrset to add other top-level attributes, such as {:imports [...]}. For modules that need something else, write the module with fn instead.

option #

(option type default description) turns into lib.mkOption { type = ...; default = ...; description = ...; }. Everything after the type is optional, and you can add extra keys at the end:

(option lib.types.str "world" "Who to greet." :example "godrick")

lib.mkIf and friends #

lib.mkIf, lib.mkDefault, lib.mkForce and lib.mkMerge are normal functions. Call them directly. Don't use when in place of lib.mkIf, because they do different things.


Packages #

A package is a function that takes its dependencies, just like in nixpkgs:

(fn {~lib ~stdenv ~fetchurl}
  (stdenv.mkDerivation (fn :finalAttrs
    {:pname "hello"
     :version "2.12.3"
     :src (fetchurl {:url "mirror://gnu/hello/hello-${finalAttrs.version}.tar.gz"
                     :hash "sha256-..."})
     :meta {:license lib.licenses.gpl3Plus
            :mainProgram "hello"}})))
pkgs.callPackage (godrick.lib.importGodrick ./hello.rick) { }

callPackage sees the same argument names as it would for a Nix file. The examples/ folder contains hello, figlet and cowsay written in Godrick. Each one builds exactly the same derivation as nixpkgs.

Shell code such as postInstall can stay a string, or be written with bash (see the next section).


Shell scripts #

bash writes a shell script as forms. It turns into an ordinary Nix string, so it works anywhere a script does: a build phase, writeShellScript, a systemd script.

:postInstall (bash
  (wrapProgram $out/bin/cowsay --suffix COWPATH ":" $out/share/cowsay/cows)
  (cp -ar "${finalAttrs.contributed}/*" $out/share/figlet/)
  (for :f [$out/share/*.txt] (chmod 644 $f)))
postInstall = ''
  wrapProgram $out/bin/cowsay --suffix COWPATH : $out/share/cowsay/cows
  cp -ar "${finalAttrs.contributed}"/* $out/share/figlet/
  for f in $out/share/*.txt; do
    chmod 644 $f
  done
'';

Each argument is a command, and each command is printed on its own line. The script is bash, as every build phase is, so stdenv's functions (wrapProgram, substituteInPlace, installManPage, …) are ordinary commands.

Words #

Inside bash, a name is a shell word, not a Godrick name. cp, -ar, $out/bin and && mean exactly what they say. Pipes, redirects, && and || are words too: (make && make install), (sort a | uniq > $out/list).

Write Shell
cp, -ar, $out/bin, ./configure, 2>&1 The same text
"text" The text as it is. Use a string for anything a name can't hold, such as ":", ";" or "'two words'".
"${x}/*" "${x}"/*: an interpolation is double-quoted, and the rest of the string isn't
,x "${x}", a Nix value as one word
,@xs Each item of the Nix list xs as its own double-quoted word
(date +%s) "$(date +%s)"
(<< "text") A heredoc (see below)

Double-quoting Nix values means a value with spaces stays one word. Don't add quotes around an interpolation yourself.

Shell variables are plain words: $out, $f. Write ${...} inside a string, escaped as in Nix: "\${!outputBin}/bin".

Paths are words too, so ./configure runs the script in the build directory. To use a file from your source tree, unquote it: ,./fix.patch.

[[ ... ]] is not supported, because [ starts a Godrick list. Use test, or write the test in a string: (if "[[ -d $out/doc ]]" ...).

Control flow #

Form Shell
(if test then), (if test then else) if ...; then ... else ... fi. An if in the else branch becomes elif.
(for :f [words...] commands...) for f in words; do ... done
(while test commands...) while ...; do ... done
(case word pattern command ...) case word in pattern) ... ;; esac
(defn :name commands...) name() { ... }
(do commands...) { ...; }
(sub commands...) ( ... ), a subshell

A test is one command, such as (test -f x) or a string. An if or case branch is one command, and (do ...) groups several there without braces. A group can be redirected by putting it first in a command: ((do (make) (make check)) > log 2>&1).

The names in for and defn define shell names, not Godrick ones, so write $f to use the loop variable. They must be valid shell names: letters, digits and _.

Heredocs #

(<< text) is a heredoc. The shell never expands its text, so $ needs no escaping. Nix interpolations are inserted as they are, and (placeholder "out") stands for $out:

(cat > $out/bin/cowthink (<< """
  #!/usr/bin/env perl
  $0 = "cowthink";
  do "${(placeholder "out")}/bin/.cowsay-wrapped";
"""))

Raw shell, and helpers #

A string or a ,x on its own, in place of a command, is copied in as lines of shell. This lets you move a script to bash one line at a time, and write anything bash doesn't cover:

(bash
  (make)
  """
    install -Dm755 tool $out/bin/tool
  """)

To reuse shell code, write a function that returns a script and splice it in with ,:

(defn :installDocs :dir (bash (mkdir -p ,dir) (cp -r docs/* ,dir)))

(bash (make) ,(installDocs "$out/share/doc"))

A spliced script is followed by a newline, so a script that already ends in one leaves a blank line. That makes no difference to the shell.

; comments are dropped, as everywhere in Godrick.


Errors and warnings #

Godrick errors name the file, line and column:

app.rick:3:1: 'c' is not defined
app.rick:2:1: duplicate def 'a'
app.rick:2:4: 'f' requires attribute 'b'
app.rick:1:6: binder ':isEmpty?' must be a Nix identifier; write (key "isEmpty?") for other attribute names
app.rick:1:6: expected a binder; write :x to bind x
app.rick:1:50: recur is only allowed in tail position inside a loop

You see these errors when the file is loaded or when you run godrick check.

Warnings only appear from godrick check, so they never clutter a normal build:

app.rick:2:6: warning: unused def 'unused'
app.rick:3:4: warning: 'f' does not use attribute 'c'

To keep an unused name without a warning, start it with _. The second warning comes from calling a function in the same file with an attribute its pattern doesn't name; the function accepts it, but it is often a typo.

Errors that happen while Nix runs the code point at the generated Nix. The output is close to what you wrote, so it is usually easy to find the matching line. To see it, run godrick compile.


The godrick command #

Command What it does
godrick compile FILE Prints the Nix that a file turns into. This is for reading, not for deploying.
godrick check FILE... Prints errors and warnings. Exits with a failure if any file has an error.
godrick fmt FILE... Fixes indentation in place.
godrick fmt --check FILE... Lists files that need formatting, without changing them.
godrick build [PATH...] Compiles files ahead of time into .godrick directories. See Building ahead of time.
godrick build --check [PATH...] Lists files whose build is missing or out of date, without changing anything.
godrick repl Opens nix repl with the compiler loaded.

The formatter only changes indentation. It keeps your line breaks and comments, and it never touches the inside of strings.

In the REPL, rick runs a piece of Godrick code:

nix-repl> rick "(for :x [1 2] (* x 10))"
[ 10 20 ]

Editor setup #

Godrick looks enough like Clojure for most editors. Tell your editor to treat *.rick as Clojure for syntax highlighting. Paren-editing tools such as paredit, parinfer and smartparens work as they are. Clojure formatters such as cljfmt and zprint don't work, so use godrick fmt instead.


Building ahead of time #

Normally Godrick compiles each file every time Nix evaluates it. A large configuration can spend a noticeable part of its evaluation doing that. godrick build compiles your files once, ahead of time:

godrick build             # every .rick file under the current directory
godrick build modules/    # or under the paths you give

Each dir/name.rick is compiled to dir/.godrick/name.nix. Files whose names start with a dot are skipped, as are .git and .godrick directories. When importGodrick (or import, or importGodrickModule) loads a file, it uses the built version if it is up to date, and skips compiling.

The first line of a built file records hashes of its source, of the compiler that built it, and of any files it loads macros from. The built file is up to date while all of these still match. If one has changed and the file wasn't rebuilt, Godrick compiles the file as usual and prints a warning:

trace: godrick: warning: /…/.godrick/app.nix is out of date, so /…/app.rick was compiled instead; run `godrick build`

Things that may surprise you #

  • There is no with or rec as these are poor practices.
  • {:a :b} is not two names. In a pattern it binds attribute a as the variable b, and in an attrset it is an error, since :b is not a value. Write {~a ~b} for both.
  • Godrick needs to write to the Nix store while evaluating through builtins.toFile. However, Godrick doesn't use import-from-derivation.

Quick reference #

Core forms #

Form Becomes in Nix
(fn param... body) x: ..., { a, b ? d, ... }: ..., x@{ ... }: ...
(def :name value) A binding in the surrounding let
(def {~a ~b} src) inherit (src) a b;
(do defs... value) let ... in value
(if c a b) if c then a else b
(. x "a" "b") or x.a.b x.a.b
(get x "a" default) x.a or default
(? x "a") x ? a
(assert c) assert c; ...
(loop :n init ... body) / (recur ...) A foldl' or safe recursion
(defmacro :name [:arg ...] body) Nothing. It runs while compiling.
(definline :name :arg ... body) Pasted where it is used
(import-macros ./f.rick :name...) Nothing. It loads macros.

Built-in macros #

Group Macros
Definitions defn
Choices when, unless, cond, match
Chains ->, ->>
Lists for, reduce
NixOS defmodule, option, from
Shell bash

Syntax #

Write Meaning
( ) Call
[ ] List, or a list pattern
{ } Attrset, or an attrset pattern
; Comment until end of line
:name Defines a name: a variable, parameter or attribute
~name Takes the variable name into an attrset, or binds attribute name in a pattern
a.b.c Attribute access
./path, <nixpkgs> Paths
"...", """...""" Strings, with ${...} interpolation
_, &, ?, @ Pattern parts: ignore, rest, default, whole value
'x, `x, ,x, ,@x Quote, template, insert, insert-all (for macros)
:x#, x# A unique name inside a macro template

Working on Godrick itself #

File What it is
compiler.rick The compiler, written in Godrick
.godrick/compiler.nix The same compiler, built by itself (generated, but committed). It is an ordinary built file: its header records its own hash, so it is up to date exactly when it reproduces itself.
loader.nix Loading files from Nix (importGodrick) and ahead-of-time builds, in plain Nix. It isn't part of the compiler, so changing it needs no bootstrap and leaves built files up to date.
bin/godrick, package.nix The command-line tool, and its package. The package installs the script with the compiler and loader beside it, under share/godrick. If the script starts loading another file, add it to the package's file set.
bootstrap.nix Rebuilds .godrick/compiler.nix until it settles, and checks that it compiles itself to the same output
tests/ The test suite. Run it with tests/run.sh; nix flake check runs the parts that need neither <nixpkgs> nor error-message matching. tests/cache/fresh is built ahead of time, so rebuild it with bin/godrick build tests/cache/fresh after any change to the compiler.
examples/ Packages and a NixOS system that must match their Nix versions exactly. nix build -f examples/build.nix also builds the *-bash.rick packages and checks that their outputs match nixpkgs.

After you change compiler.rick:

nix eval --raw -f bootstrap.nix built > .godrick/compiler.nix.new && mv .godrick/compiler.nix.new .godrick/compiler.nix
nix eval -f bootstrap.nix check        # must be true
bin/godrick build tests/cache/fresh     # the new compiler makes this fixture out of date
tests/run.sh

Write to a new file first. .godrick/compiler.nix is used to build the new version, so writing straight into it would wipe it before it is read.

bin/godrick build compiler.rick also works, but each run is only one step. A change can take two or three runs to settle (until bin/godrick build --check compiler.rick passes), which is what bootstrap.nix does in one go.

Never run a formatter such as alejandra on .godrick/compiler.nix, the golden files or .godrick directories.

If compiler.rick needs a new compiler feature, add the feature first, rebuild .godrick/compiler.nix, and only then start using it.