Aspect-oriented composition types for Nix module systems.
A pure type library: no resolve, no pipeline, no framework. Provides the structural types for defining aspects — composable configuration units with identity, includes, and class-separated content. Consumers (like den) bring their own evaluation pipeline.
let
aspects = import gen-aspects { inherit lib; };
eval = lib.evalModules {
modules = [{
options.aspects = lib.mkOption {
type = aspects.aspectsType {
classes = { nixos = {}; homeManager = {}; };
};
default = {};
};
config.aspects.networking = {
nixos.networking.hostName = "myhost";
nixos.networking.firewall.enable = true;
};
config.aspects.desktop = {
includes = [ eval.config.aspects.fonts ];
homeManager.programs.alacritty.enable = true;
};
config.aspects.fonts = {
nixos.fonts.packages = [ pkgs.noto-fonts ];
};
}];
};
in
eval.config.aspects.networking.nixos
# => { imports = [{ networking.hostName = "myhost"; ... }]; }
# Clean deferredModule — no structural keys (name, includes, meta, etc.)Aspects are submodules with structural identity (name, key, meta, includes) and freeform content. Every non-structural, non-class key becomes a nested aspect with its own identity.
Classes are registered content buckets (nixos, homeManager, darwin). When registered via cnf.classes, class keys become explicit deferredModule options — clean content with no structural keys injected. This is the module system's own option/freeform separation, not a custom dispatch mechanism.
Guard functions like { host, ... }: { nixos = ...; } are context-dependent aspects that should not be evaluated eagerly. They're detected via canTake (all required args must be known module args) and wrapped via functionTo for pipeline resolution later.
Module functions like { config, ... }: { ... } or { aspect, ... }: { ... } are evaluated immediately by the submodule — they have access to _module.args.aspect (self-reference) and standard module args.
aspects = import gen-aspects { inherit lib; };-
aspectsType cnf— top-level container. Submodule withfreeformType = lazyAttrsOf (aspectType cnf)and fixpoint (_module.args.aspects = config). -
aspectSubmodule cnf— aspect entry. Submodule with structural options (name,description,key,meta,includes), explicitdeferredModuleoptions per registered class, and freeform for nested aspects. -
aspectType cnf— Palmer flat dispatch. One type, dispatch in merge. Attrsets and module functions →aspectSubmodule. Guard functions →functionTowrapper. Primitives → passthrough. -
aspectOrFn cnf—either aspectType aspectSubmodule. Recursion-safe binding forincludesand nested aspect positions.
aspectsType {
# Registered class names → explicit deferredModule options (clean content)
classes = { nixos = {}; homeManager = {}; };
# Known module args for module/guard function detection
# Default: { lib, config, options, pkgs, modulesPath, aspect }
moduleArgs = { lib = true; config = true; /* ... */ };
# Additional NixOS modules imported into every aspect entry
# Use for pipeline-specific options (excludes, policies, etc.)
aspectModules = [
({ config, ... }: {
options.excludes = lib.mkOption { default = []; type = lib.types.listOf lib.types.str; };
})
];
}canTake— function arg introspection.canTake.upTo params fnchecks if all required args offnare satisfiable byparams.mkIsModuleFn cnf—canTake.upTo (cnf.moduleArgs or defaults). Returns a predicate that classifies functions as module fns or guard fns.key,aspectPath,pathKey,isMeaningfulName— identity computation frommeta+name.
Based on three papers:
Palmer et al. (2024) "Intensional Functions" — One type dispatches by value shape in merge (§3). Guard functions are defunctionalized as callable first-order data with inspectable args (§5.1). Identity keys enable diamond dedup in fold-based collect (§3, Theorem 5.12).
Lorenzen et al. (2025) "First-Order Laziness" — Class content as deferredModule is a lazy constructor: inspectable before forcing, evaluated only when the consuming NixOS evaluation imports it (§2.4).
Reynolds (1972) "Definitional Interpreters" — Guard functions wrapped via functionTo are Reynolds defunctionalization: closures become tagged data (__isWrappedFn, __functionArgs) with explicit dispatch (__functor).
nix shell nixpkgs#nix-unit -c nix-unit \
--override-input target . \
--flake './templates/ci#.tests'40 tests covering: class content cleanliness, nested aspect identity, includes/fixpoint, module vs guard function dispatch, multi-def merging, primitive passthrough, deep nesting, extensions, and canTake introspection.