Skip to content

Overview

yen is a query language for Nix configurations. You declare data and behavior in plain Nix files. yen answers queries on them: the systems to build, the errors, and the graph of who gives what to whom.

yen is one small Nix file. It uses Nix builtins only: no nixpkgs, no module system, no flakes. It never calls throw. An error is data that you can query.

Kinds

Typed data. A host, a user, an environment: you declare what exists and which fields it has.

Scopes

Where things live. spawns places each instance in a tree: a user under a host, a host under an environment.

Aspects

Behavior. An aspect is active where its at predicate holds, and gives elements by key.

Sinks

Where elements go. An element goes up to the nearest scope that collects it, and the sink reduces them.

Everything in yen is one of four words. Read them once, in order, before any code:

  1. Kind. A type of thing you configure, and the fields each one has. You name your kinds: host, user, cluster, printer, editor. An instance is one thing of a kind: yen.host.web.
  2. Scope. One instance at one place in a tree. A kind’s spawns says which instances live under it: a user under each host it logs in to, a host under its environment. The kind that nothing spawns is the root.
  3. Aspect. One feature: “ssh on”, “nginx with these sites”. Its at is a predicate that says at which scopes it is active. Its other keys each give one element: a value, or a function of the data.
  4. Sink. Where elements go. A kind declares sinks. An element goes up the tree to the nearest scope with a sink of the same name, and the sink reduces all its elements to one value: a list, a set, a merged attrset, one value.

So: kinds say what exists, scopes say where it lives, aspects say what to do, and sinks say who collects the result. Sinks also build the outputs: the sink of a host can call nixosSystem on the modules it collected.

This example has no NixOS. An office has two machines. Each machine collects a set of package names.

Kinds and scopes. office is the root kind, because nothing spawns it. It spawns every machine. A machine has an owner field and a packages sink.

modules/kinds.nix
# Kinds: typed data. `office` is the root: nothing spawns it.
{ yen, lib, ... }:
{
yen.kinds.office = {
spawns.machine = _: yen.machine; # scopes: every machine lives in the office
sinks.out.reduce = yen.lib.reduce.merge;
};
yen.kinds.machine = {
fields.owner = lib.types.str;
sinks.packages.reduce = yen.lib.reduce.set; # sinks: where elements go
sinks.packages.out =
{ machine, packages, ... }:
{
inventory.${machine.name} = packages;
};
};
yen.office.main = { };
yen.machine.reception.owner = "ana";
yen.machine.studio.owner = "bo";
yen.flake = { out, ... }: out;
}

Aspects. basics is active at every machine. design is active at studio only. versioned-assets is active wherever design is active: an aspect can be a predicate.

modules/aspects.nix
# Aspects: behavior, active where `at` holds. Each key gives one element to the sink of that name.
{ yen, ... }:
{
yen.aspects.basics = {
at = yen.kinds.machine; # every machine
packages = "git";
};
yen.aspects.design = {
at = yen.machine.studio; # one machine
packages = "gimp";
};
yen.aspects.versioned-assets = {
at = yen.aspects.design; # wherever `design` is active
packages = "git"; # `set` keeps one copy
};
}

Sinks. Each aspect key gives one element to the sink of that name. The packages sink of each machine reduces its elements with set, so git arrives twice at studio and stays once:

result
{
"office:main/machine:reception" = [
"git"
];
"office:main/machine:studio" = [
"git"
"gimp"
];
}

The scope tree and the routed elements, drawn from r.graph:

flowchart TD
  n5b6391cead["office main"]
  nc8151a7fe5["machine reception"]
  nb9517ee5c2["machine studio"]
  n5b6391cead --> nc8151a7fe5
  n5b6391cead --> nb9517ee5c2

There is no evalModules fixpoint and no merge step. Each read of a declaration is a query on the files that answer it. A value is computed only when something reads it. yen follows Cycle.js: a component reads sources and writes sinks, and Nix laziness closes the loop.

Contribute Community Sponsor