Skip to content
Oeiuwq Faith Blog OpenSource Porfolio

sini/gen-aspects

gen-aspects: aspect-oriented composition types for Nix module systems

sini/gen-aspects.json
{
"createdAt": "2026-05-21T01:08:18Z",
"defaultBranch": "main",
"description": "gen-aspects: aspect-oriented composition types for Nix module systems",
"fullName": "sini/gen-aspects",
"homepage": null,
"language": "Nix",
"name": "gen-aspects",
"pushedAt": "2026-05-31T22:05:08Z",
"stargazersCount": 3,
"topics": [],
"updatedAt": "2026-05-31T22:03:11Z",
"url": "https://github.com/sini/gen-aspects"
}

CI License: MIT Sponsor

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.

  • [Terminology]!(#terminology)
  • [Gen Ecosystem]!(#gen-ecosystem)
  • [Usage]!(#usage)
  • [Core Concepts]!(#core-concepts)
  • [API]!(#api)
    • [Types]!(#types)
    • [Configuration]!(#configuration-cnf)
    • [Utilities]!(#utilities)
  • [Schema Integration]!(#schema-integration)
  • [Flat Registry]!(#flat-registry)
  • [Demo]!(#demo)
  • [Testing]!(#testing)
  • [Theoretical Foundations]!(#theoretical-foundations)
TermDefinition
TraitsThe aspect type — one type, dispatch in merge (Palmer 2024)
ClassesOutput targets (NixOS, darwin, homeManager module systems)
CollectionsNamed data aggregation (aspect keys matching registered collection names)
EdgesComposition relationships: includes (forward I), neededBy (reverse I)
ConstraintsPruning rules: meta.guard, meta.drop, meta.substitute
LibraryRole
gen-algebraPure primitives (search, record, identity)
gen-schemaTyped registries (kinds, instances, collections, refs)
gen-aspectsAspect types (traits, classification, dispatch, schema integration)
gen-graphGraph queries (combinators, traversals, fixpoint)
gen-scopeScope graphs (construction, evaluation, resolution)
gen-selectSelector algebra (pattern matching over graph positions)
gen-bindModule binding (inject args into NixOS modules)
gen-deriveRule dispatch (stratified phases, fixpoint, conflict resolution)
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 with freeformType = lazyAttrsOf (aspectType cnf) and fixpoint (_module.args.aspects = config).

  • aspectSubmodule cnf — aspect entry. Submodule with structural options (name, description, key, meta, includes), explicit deferredModule options 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 → functionTo wrapper. Primitives → passthrough.

  • aspectOrFn cnfeither aspectType aspectSubmodule. Recursion-safe binding for includes and 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; };
})
];
# List of NixOS modules imported into each aspect's `meta` submodule.
# Allows consumers to declare typed meta options (e.g., `meta.guard`,
# `meta.priority`) alongside the freeform attrs.
metaModules = [ ];
}
  • canTake — function arg introspection. canTake.upTo params fn checks if all required args of fn are satisfiable by params.
  • mkIsModuleFn cnfcanTake.upTo (cnf.moduleArgs or defaults). Returns a predicate that classifies functions as module fns or guard fns.
  • key, aspectPath, pathKey, isMeaningfulName — identity computation from meta + name. key handles both static aspects (via meta.aspect-chain) and wrapped guard functions (via meta.loc).

gen-aspects depends on gen-schema and provides mkAspectSchema to bridge aspect types with gen-schema’s kind-level infrastructure (collections, introspection, schema extensions).

aspects = import gen-aspects { inherit lib; };
schema = aspects.mkAspectSchema cnf;

mkAspectSchema cnf returns:

FieldDescription
schemaOptiongen-schema option wrapping aspectType as the custom entry type
mkAspectOption { providerPrefix? }Declares options.aspects with lazyAttrsOf aspectType
mkAspectModule { providerPrefix? }NixOS module declaring both options.aspects and options.schema, lazily threading schema-declared options into every aspect instance
mkNamespaceType { }Submodule type for namespace composition — includes schema, classes, and freeform aspect content
aspectTypeRe-exported aspect type
identityBundled identity functions (aspectPath, pathKey, key, isMeaningfulName)
canTakeRe-exported function arg introspection
mkIsModuleFnRe-exported module function predicate

Schema-declared options propagate to aspect instances via mkAspectModule. When a schema kind entry declares options (e.g., priority, tier), those options become available on every aspect:

{ config, ... }:
{
imports = [ (schema.mkAspectModule { }) ];
# Collections and extensions declared on the schema kind
schema.aspect = {
settings = { }; # collection
tags = { }; # collection
# options.priority = lib.mkOption { ... }; # schema extension
};
# Every aspect now has access to schema-declared options
aspects.networking.priority = 10;
}

mkAspectModule lazily injects config.schema.aspect.__defsModule into each aspect’s aspectModules, so schema extensions are available without manual wiring.

The flatten function walks the recursive aspect tree and produces a flat attrset keyed by path identity:

aspects = import gen-aspects { inherit lib; };
flat = aspects.flatten eval.config.aspects;
# => { "networking" = ...; "networking/firewall" = ...; }

Entries are the aspect values unchanged — flatten does not inject any fields. Parent relationships are implicit in the path key: "networking/firewall" → parent is "networking". Guard functions (__isWrappedFn) are included as entries but not recursed into.

Detection is structural rather than relying on a hardcoded key list:

  • Nested aspects are attrsets with a name field (from aspectSubmodule)
  • Class content (deferredModule) lacks name and is skipped
  • Primitives (strings, lists) are skipped

The flat registry enables gen-graph and gen-select queries over the aspect tree. Parent accessors derive from the key:

parentOf = id:
let parts = lib.splitString "/" id;
in if builtins.length parts <= 1 then null
else lib.concatStringsSep "/" (lib.init parts);

The examples/demo/ directory exercises all 8 gen libraries together: gen-algebra, gen-schema, gen-aspects, gen-graph, gen-scope, gen-select, gen-bind, and gen-derive. It demonstrates entities, aspects, namespaces, policies, queries, bindings, composition, and settings in a single integrated flake.

Terminal window
nix shell nixpkgs#nix-unit -c nix-unit \
--override-input target . \
--flake './ci#.tests'

67 tests covering: class content cleanliness, nested aspect identity, includes/fixpoint, module vs guard function dispatch, multi-def merging, primitive passthrough, deep nesting, extensions, canTake introspection, schema integration, and flat registry.

PaperRelationshipMechanism
Palmer et al. (2024) “Intensional Functions”ImplementsFlat dispatch via one type in merge §2, identity §2.2; identity keys enable consumer-side dedup
Lorenzen et al. (2025) “First-Order Laziness”Informed bydeferredModule inspectable before forcing (via Nix native laziness, not Lorenzen’s mechanism) §1-2.3
Reynolds (1972) “Definitional Interpreters”ImplementsGuard function defunctionalization — closures become tagged data

Palmer et al. (2024) “Intensional Functions” — One type dispatches by value shape in merge (§2). Guard functions are defunctionalized as callable first-order data with inspectable args (§5.1). Identity keys enable consumer-side diamond dedup (Lemma 5.12 + Theorem 1, closure consistency); gen-aspects supplies the keys, the dedup lives in the consumer.

Lorenzen et al. (2025) “First-Order Laziness” (informed by) — Class content as deferredModule is inspectable before forcing, evaluated only when the consuming NixOS evaluation imports it (§1-2.3). This property comes from Nix native laziness plus nixpkgs deferredModule, NOT from Lorenzen’s mechanism (first-order named constructors, defunctionalized deferred operations, in-place memoization). The citation is provenance for the laziness idea, not an implementation of the paper.

Reynolds (1972) “Definitional Interpreters” — Guard functions wrapped via functionTo are Reynolds defunctionalization: closures become tagged data (__isWrappedFn, __functionArgs) with explicit dispatch (__functor).