Evaluation
Hosts sit inside two environments, prod and dev. Each host inherits region and logLevel
from its environment unless it sets its own, and motd is computed per host from the other two.
let gen = (builtins.getFlake "github:sini/gen").lib.mkGenLibs { }; scope = gen.scope;
tree = scope.buildRoots { kinds = scope.mkKinds [ (scope.mkKind { } "env") (scope.mkKind { } "host") ]; types = { prod = "env"; dev = "env"; web-1 = "host"; db-1 = "host"; web-2 = "host"; }; parentGraph = scope.overlay (scope.star "prod" [ "web-1" "db-1" ]) (scope.star "dev" [ "web-2" ]); decls = { prod = { region = "us-east"; logLevel = "warn"; }; dev = { region = "us-west"; logLevel = "debug"; }; web-1 = { role = "webserver"; }; db-1 = { role = "database"; region = "us-west"; }; web-2 = { role = "webserver"; }; }; };
result = scope.eval { } { children = _self: _id: { }; region = scope.inherit' { } (node: node.decls.region or null); logLevel = scope.inherit' { } (node: node.decls.logLevel or null); motd = self: id: "${id} (${self.get id "region"}, log ${self.get id "logLevel"})"; } tree;in{ hosts = builtins.attrNames (result.nodesOfType "host"); web-1 = result.get "web-1" "motd"; db-1 = result.get "db-1" "motd"; web-2 = result.get "web-2" "motd";}$ nix eval --impure --json --file platform-attributes.nix{ "db-1": "db-1 (us-west, log warn)", "hosts": ["db-1", "web-1", "web-2"], "web-1": "web-1 (us-east, log warn)", "web-2": "web-2 (us-west, log debug)"}db-1 sets its own region and inherits its log level. An attribute is computed only when something
reads it, and once per node.
What you can do
Section titled “What you can do”| Feature | Functions | Purpose |
|---|---|---|
| Declare kinds | mkKind, mkKinds | Name the node types a tree may contain |
| Build a tree | buildRoots { kinds; types; parentGraph; decls; } | Turn declarations and a containment graph into a node set |
| Shape the tree | vertices, star, overlay, connect, edge | Combine containment graphs from small pieces |
| Evaluate | eval { } attributes tree → get, nodesOfType, … | Compute attributes on demand, memoised per node |
| Inherit down the tree | inherit' { } select | Take a value from the nearest ancestor that declares one |
| Collect | collect, collectByType, collectByLabel, collectImports | Gather values from related nodes |
| Grow the tree | spawns.<kind> and nta on a kind | Let computed values create new nodes |
| Fixpoints | circular, ascend | Attributes that depend on themselves, iterated until they stop changing |
| Warm evaluation | evalWarm | Re-evaluate after an edit, reusing unchanged attributes |
| Assemble node sets | assemble.assemble, union, mkId, parseId, idsOf | Build one node set from many layered contributions |
| Check a schedule | view.boundedWellDefinedSchedule | Check that every attribute’s dependencies can be ordered before running anything |
Details
Section titled “Details”Attributes
Section titled “Attributes”The second argument to eval is a set of attribute definitions. Each is a function of self (the
evaluator, for reading other attributes) and the node id. self.get id "name" reads an attribute
of any node, so an attribute can depend on its parent, its children or a node it references.
Attributes come in two directions:
- Inherited attributes flow down: a host reads its environment’s
region.inherit'walks up the containment graph to the nearest node that answers. - Synthesized attributes flow up or stay local:
motdcombines values already on the node.
Nix evaluates lazily, so an attribute nobody reads is never computed, and a computed one is shared
by every reader. Each node carries its own cache of computed attributes, so get on a node is a
constant-time lookup. allNodes and nodesOfType build whole lists and cost time in
proportion to the tree.
Kinds and the tree
Section titled “Kinds and the tree”buildRoots needs a kind for every node (types), and a containment graph (parentGraph) built
from star, vertices and overlay. decls is each node’s own data, readable as node.decls
inside inherit'. nodesOfType "host" lists the nodes of one kind.
Growing the tree
Section titled “Growing the tree”children selects among nodes the tree already has. It cannot add nodes: a record under an unknown
key fails with an error naming the node and the keys. New nodes are declared on the kind they grow
from:
spawns.<kind>creates nodes of a kind listed in this kind’sbelow. Every such expansion moves down the kind order, so growth terminates.ntagrows nodes of the node’s own kind from the node’s computed values.
Grown nodes are evaluated like declared ones. nodesOfType lists them with the rest.
Assembly
Section titled “Assembly”assemble builds one node set from many layered contributions and hands it to the evaluator. It
does not evaluate anything itself. substratePreconditions lists the properties an assembly depends
on. An assembly over libraries that lack one of them fails with an error naming the property and its stable record key,
instead of producing a wrong node set.
gen-assemble, gen-inspect and gen-memo never evaluate configuration: they prepare an evaluator’s input, read its output, or decide what it must recompute.
Fixpoints
Section titled “Fixpoints”A circular attribute starts from a bottom value and is recomputed until it stops changing. Use it
for values that depend on themselves through the graph, such as a reachability set on a cyclic
graph.
For the theory, see Values Computed on Demand. The evaluator is a
higher-order attribute grammar, where attributes can
create new tree nodes, and circular follows
circular reference attribute grammars.
Provided by gen-scope (trees, attributes, fixpoints), gen-assemble (node-set assembly) and gen-view (schedule checking).
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).