fake.modules transposition for aspect-oriented Dendritic Nix. with cross-aspect dependencies. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ dendrix.oeiuwq.com/Dendritic.html
dendritic nix aspect oriented

Compare changes

Choose any two refs to compare.

+175 -253
+14 -11
nix/aspects.nix
··· 1 + # Transpose aspects.<aspect>.<class> to modules.<class>.<aspect> 2 + # Resolves aspect dependencies and applies transformations during transposition 3 + 1 4 lib: aspects: 2 5 let 6 + # Import transpose utility with custom emit function for aspect resolution 3 7 transpose = import ./. { inherit lib emit; }; 4 - emit = 5 - transposed: 6 - let 7 - aspect = aspects.${transposed.child}; 8 - in 9 - [ 10 - { 11 - inherit (transposed) parent child; 12 - value = aspect.resolve { class = transposed.parent; }; 13 - } 14 - ]; 8 + 9 + # Emit function: resolves each aspect for its target class 10 + # Returns: [{ parent = class, child = aspect, value = resolved-module }] 11 + emit = transposed: [ 12 + { 13 + inherit (transposed) parent child; 14 + value = aspects.${transposed.child}.resolve { class = transposed.parent; }; 15 + } 16 + ]; 15 17 in 16 18 { 19 + # Exports: transposed.<class>.<aspect> = resolved-module 17 20 transposed = transpose aspects; 18 21 }
+18
nix/default.nix
··· 1 + # Generic 2-level attribute set transposition 2 + # Swaps parent/child levels: { a.b = 1; } โ†’ { b.a = 1; } 3 + # Parameterized via emit function for custom value handling 4 + 1 5 { 2 6 lib, 7 + # emit: Customization function for each item during transpose 8 + # Signature: { child, parent, value } โ†’ [{ parent, child, value }] 9 + # Default: lib.singleton (identity transformation) 3 10 emit ? lib.singleton, 4 11 }: 5 12 let 13 + # Create transposition metadata by calling emit 6 14 transposeItem = 7 15 child: parent: value: 8 16 emit { inherit child parent value; }; 17 + 18 + # Fold accumulator: rebuilds transposed structure 9 19 accTransposed = 10 20 acc: item: 11 21 acc ··· 14 24 ${item.child} = item.value; 15 25 }; 16 26 }; 27 + 28 + # Process all children of a parent 17 29 transposeItems = parent: lib.mapAttrsToList (transposeItem parent); 30 + 31 + # Flatten input into transposition items 18 32 deconstruct = lib.mapAttrsToList transposeItems; 33 + 34 + # Fold items back into swapped structure 19 35 reconstruct = lib.foldl accTransposed { }; 36 + 37 + # Main transpose: deconstruct โ†’ flatten โ†’ reconstruct 20 38 transpose = 21 39 attrs: 22 40 lib.pipe attrs [
+7
nix/flakeModule.nix
··· 1 + # Flake-parts integration for aspect-oriented configuration 2 + # Provides flake.aspects (input) and flake.modules (output) 3 + 1 4 { 2 5 lib, 3 6 config, 4 7 ... 5 8 }: 9 + # Invoke new() factory to create flake.aspects and flake.modules 6 10 import ./new.nix lib (option: transposed: { 11 + # User-facing aspects input 7 12 options.flake.aspects = option; 13 + 14 + # Computed modules output organized by class 8 15 config.flake.modules = transposed; 9 16 }) config.flake.aspects
+11
nix/lib.nix
··· 1 + # Public API entry point for flake-aspects library 2 + # Exports: types, transpose, aspects, new, new-scope 1 3 lib: 2 4 let 5 + # Type system: aspectsType, aspectSubmodule, providerType 3 6 types = import ./types.nix lib; 7 + 8 + # Generic transposition utility: parameterized by emit function 4 9 transpose = 5 10 { 6 11 emit ? lib.singleton, 7 12 }: 8 13 import ./default.nix { inherit lib emit; }; 14 + 15 + # Aspect transposition with resolution 9 16 aspects = import ./aspects.nix lib; 17 + 18 + # Low-level scope factory: parameterized by callback 10 19 new = import ./new.nix lib; 20 + 21 + # High-level named scope factory 11 22 new-scope = import ./new-scope.nix new; 12 23 in 13 24 {
+6 -14
nix/new-scope.nix
··· 1 - # usage: 2 - # 3 - # { inputs, ... }: { 4 - # imports = [ (new-scope "foo") ]; 5 - # foo.aspects.<aspect> = ...; 6 - # # and use foo.modules.<class>.<aspect> 7 - # } 8 - # 9 - # returns a nix module that defines the ${name} option having: 10 - # 11 - # options.${name}.aspects # for user 12 - # options.${name}.modules # read-only resolved modules. 13 - # 14 - # for lower-level usage like using other option names, see new.nix. 1 + # Creates named aspect scopes: ${name}.aspects and ${name}.modules 2 + # Enables multiple independent aspect namespaces 15 3 new: name: 16 4 { config, lib, ... }: 5 + # Invoke new() to create ${name}.aspects and ${name}.modules 17 6 new (option: transposed: { 18 7 options.${name} = { 8 + # User-facing aspects input 19 9 aspects = option; 10 + 11 + # Computed modules output (read-only) 20 12 modules = lib.mkOption { 21 13 readOnly = true; 22 14 default = transposed;
+9 -7
nix/new.nix
··· 1 - # creates a new aspects option. 2 - # See flakeModule for usage. 1 + # Low-level aspect scope factory 2 + # Creates aspect integration via callback pattern for maximum flexibility 3 3 lib: cb: cfg: 4 4 let 5 + # Import aspects transposer: validates and transposes aspect config 5 6 aspects = import ./aspects.nix lib cfg; 7 + 8 + # Import type system for aspect validation 6 9 types = import ./types.nix lib; 10 + 11 + # Create aspects input option 7 12 option = lib.mkOption { 8 13 default = { }; 9 - description = '' 10 - Attribute set of `<aspect>.<class>` modules. 11 - 12 - Convenience transposition of `flake.modules.<class>.<aspect>`. 13 - ''; 14 + description = "Aspect definitions organized as <aspect>.<class>"; 14 15 type = types.aspectsType; 15 16 }; 16 17 in 18 + # Invoke callback with option and transposed results 17 19 cb option aspects.transposed
+5 -1
nix/resolve.nix
··· 1 + # Core aspect resolution algorithm 2 + # Resolves aspect definitions into nixpkgs modules with dependency resolution 3 + 1 4 lib: 2 5 let 3 - 6 + # Process a single provider: invoke with context and resolve 4 7 include = 5 8 class: aspect-chain: provider: 6 9 let ··· 8 11 in 9 12 resolve class aspect-chain provided; 10 13 14 + # Main resolution: extract class config and recursively resolve includes 11 15 resolve = class: aspect-chain: provided: { 12 16 imports = lib.flatten [ 13 17 (provided.${class} or { })
+105 -220
nix/types.nix
··· 1 + # Core type system for aspect-oriented configuration 2 + 1 3 lib: 2 4 let 3 - # Import the resolve function which handles aspect resolution and dependency injection 4 5 resolve = import ./resolve.nix lib; 5 6 6 - # Top-level aspects container type 7 - # This is the entry point for defining all aspects in a flake 8 - # Structure: aspects.<aspectName> = { ... } 9 - # Makes the entire aspects config available as 'aspects' in module args 10 - # allowing cross-referencing between aspects 11 - aspectsType = lib.types.submodule ( 12 - { config, ... }: 13 - { 14 - # Allow arbitrary aspect definitions as attributes 15 - # Each aspect can be either: 16 - # - An aspect submodule (aspectSubmoduleAttrs) 17 - # - A provider function (providerType) 18 - freeformType = lib.types.lazyAttrsOf (lib.types.either aspectSubmoduleAttrs providerType); 19 - # Inject the aspects config into _module.args for cross-referencing 20 - config._module.args.aspects = config; 21 - } 22 - ); 23 - 24 - # Type checker for provider functions with specific argument patterns 25 - # Valid provider function signatures: 26 - # 1. { class } => aspect-object 27 - # 2. { aspect-chain } => aspect-object 28 - # 3. { class, aspect-chain } => aspect-object 29 - # 4. { class, ... } => aspect-object (with other ignored args) 30 - # 5. { aspect-chain, ... } => aspect-object (with other ignored args) 31 - # 32 - # This ensures that provider functions receive the proper context when invoked 33 - functionToAspect = lib.types.addCheck (lib.types.functionTo aspectSubmodule) ( 34 - f: 35 - let 36 - args = lib.functionArgs f; 37 - arity = lib.length (lib.attrNames args); 38 - hasClass = args ? class; 39 - hasChain = args ? aspect-chain; 40 - classOnly = hasClass && arity == 1; 41 - chainOnly = hasChain && arity == 1; 42 - both = hasClass && hasChain && arity == 2; 43 - in 44 - classOnly || chainOnly || both 45 - ); 46 - 47 - # Provider functions can be: 48 - # 1. A function taking { class, aspect-chain } and returning an aspect (functionToAspect) 49 - # 2. A function taking parameters and returning another provider (curried) 50 - # This allows for parametric aspects and lazy evaluation 51 - functionProviderType = lib.types.either functionToAspect (lib.types.functionTo providerType); 52 - 53 - # Provider type allows three forms: 54 - # 1. A function provider (functionProviderType) 55 - # 2. An aspect configuration (aspectSubmodule) 56 - # This enables both immediate aspect definitions and deferred/parametric ones 57 - providerType = lib.types.either functionProviderType aspectSubmodule; 58 - 59 - # Additional validation for aspect submodules to ensure they're not mistyped functions 60 - # An aspectSubmoduleAttrs is either: 61 - # - Not a function at all (plain attribute set) 62 - # - A function with submodule-style arguments (lib, config, options, aspect) 63 - # This prevents accidentally treating provider functions as aspect configs 64 - aspectSubmoduleAttrs = lib.types.addCheck aspectSubmodule ( 65 - m: (!builtins.isFunction m) || (isAspectSubmoduleFn m) 66 - ); 67 - 68 - # Helper to identify if a function is a submodule-style function 69 - # Submodule functions take args like { lib, config, options, aspect, ... } 70 - # Returns true if the function accepts at least one of these special args 71 - isAspectSubmoduleFn = 72 - m: 73 - lib.pipe m [ 74 - lib.functionArgs 75 - lib.attrNames 76 - (lib.intersectLists [ 77 - "lib" 78 - "config" 79 - "options" 80 - "aspect" 81 - ]) 82 - (x: lib.length x > 0) 83 - ]; 84 - 85 - # Special type that accepts any value but always merges to null 86 - # Used for internal computed values that shouldn't be serialized 87 - # This prevents type errors when values don't have proper types 7 + # Type for computed values that only exist during evaluation 88 8 ignoredType = lib.types.mkOptionType { 89 9 name = "ignored type"; 90 10 description = "ignored values"; ··· 92 12 check = _: true; 93 13 }; 94 14 95 - # Core aspect definition type 96 - # Each aspect represents a reusable configuration module that can: 97 - # - Define configuration for multiple "classes" (e.g., nixos, home-manager, darwin) 98 - # - Include other aspects as dependencies 99 - # - Provide sub-aspects for selective composition 100 - # - Be parametrized via __functor 101 - aspectSubmodule = lib.types.submodule ( 102 - { 103 - name, 104 - config, 105 - ... 106 - }: 107 - { 108 - # Allow arbitrary class configurations (e.g., nixos, home-manager, etc.) 109 - # Each class maps to a deferred module that will be resolved later 110 - freeformType = lib.types.attrsOf lib.types.deferredModule; 111 - 112 - # Make the aspect config available as 'aspect' in module args 113 - # This allows modules within the aspect to reference their own aspect 114 - config._module.args.aspect = config; 15 + # Create internal read-only option with custom apply function 16 + mkInternal = 17 + desc: type: fn: 18 + lib.mkOption { 19 + internal = true; 20 + visible = false; 21 + readOnly = true; 22 + description = desc; 23 + inherit type; 24 + apply = fn; 25 + }; 26 + 27 + # Check if function has submodule-style arguments 28 + isSubmoduleFn = 29 + m: 30 + lib.length ( 31 + lib.intersectLists [ "lib" "config" "options" "aspect" ] (lib.attrNames (lib.functionArgs m)) 32 + ) > 0; 115 33 116 - # Create "_" as a shorthand alias for "provides" 117 - # Allows writing: aspect._.foo instead of aspect.provides.foo 118 - # This improves ergonomics for the common case of defining sub-aspects 119 - imports = [ (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) ]; 34 + # Check if function accepts { class } and/or { aspect-chain } 35 + isProviderFn = 36 + f: 37 + let 38 + args = lib.functionArgs f; 39 + n = lib.length (lib.attrNames args); 40 + in 41 + (args ? class && n == 1) 42 + || (args ? aspect-chain && n == 1) 43 + || (args ? class && args ? aspect-chain && n == 2); 120 44 121 - # Human-readable aspect name, defaults to the attribute name 122 - # Used in aspect-chain tracking and for display purposes 123 - options.name = lib.mkOption { 124 - description = "Aspect name"; 125 - default = name; 126 - type = lib.types.str; 127 - }; 45 + # Direct provider function: ({ class, aspect-chain }) โ†’ aspect 46 + directProviderFn = lib.types.addCheck (lib.types.functionTo aspectSubmodule) isProviderFn; 128 47 129 - # Optional description for documentation purposes 130 - # Defaults to a generic description using the aspect name 131 - options.description = lib.mkOption { 132 - description = "Aspect description"; 133 - default = "Aspect ${name}"; 134 - type = lib.types.str; 135 - }; 48 + # Curried provider function: (params) โ†’ provider (enables parametrization) 49 + curriedProviderFn = lib.types.functionTo providerType; 136 50 137 - # Dependencies: list of other providers this aspect includes 138 - # During resolution, included aspects are merged with this aspect 139 - # Includes can be: 140 - # - Direct aspect references: aspects.otherAspect 141 - # - Parametrized providers: aspects.other.provides.foo "param" 142 - # - Functorized aspects: aspects.otherAspect { param = value; } 143 - # The resolution order matters for module merging semantics 144 - options.includes = lib.mkOption { 145 - description = "Providers to ask aspects from"; 146 - type = lib.types.listOf providerType; 147 - default = [ ]; 148 - }; 51 + # Any provider function: direct or curried 52 + providerFn = lib.types.either directProviderFn curriedProviderFn; 149 53 150 - # Sub-aspects that can be selectively included by other aspects 151 - # This allows aspects to expose multiple named variants or components 152 - # Creates a fixpoint where provides can reference the aspects in their scope 153 - # The provides scope gets its own 'aspects' arg for internal cross-referencing 154 - options.provides = lib.mkOption { 155 - description = "Providers of aspect for other aspects"; 156 - default = { }; 157 - type = lib.types.submodule ( 158 - { config, ... }: 159 - { 160 - # Allow arbitrary sub-aspect definitions 161 - freeformType = lib.types.attrsOf providerType; 162 - # Make the provides scope available as 'aspects' for fixpoint references 163 - # This enables provides.foo to reference provides.bar via aspects.bar 164 - config._module.args.aspects = config; 165 - } 166 - ); 167 - }; 54 + # Provider type: function or aspect that can provide configurations 55 + providerType = lib.types.either providerFn aspectSubmodule; 168 56 169 - # Functor enables aspects to be callable like functions 170 - # When defined, calling aspect { param = value; } invokes the functor 171 - # The functor receives: 172 - # 1. The aspect config itself 173 - # 2. The parameters passed by the caller (which must include class and aspect-chain) 174 - # This allows aspects to be parametrized and context-aware 175 - # 176 - # The default functor: 177 - # - Takes the aspect config 178 - # - Takes { class, aspect-chain } parameters 179 - # - Returns the aspect unchanged (identity function with parameter access) 180 - # - The weird `if true || (class aspect-chain) then` is to silence nixf-diagnose 181 - # about unused variables while ensuring they're in scope 182 - options.__functor = lib.mkOption { 183 - internal = true; 184 - visible = false; 185 - description = "Functor to default provider"; 186 - type = lib.types.functionTo providerType; 187 - default = 188 - aspect: 189 - { class, aspect-chain }: 190 - # silence nixf-diagnose about unused variables 191 - if true || (class aspect-chain) then aspect else aspect; 192 - }; 57 + # Core aspect submodule with all aspect properties 58 + aspectSubmodule = lib.types.submodule ( 59 + { name, config, ... }: 60 + { 61 + freeformType = lib.types.lazyAttrsOf lib.types.deferredModule; 62 + config._module.args.aspect = config; 63 + imports = [ (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) ]; 193 64 194 - # Convenience accessor: aspect.modules.<class> automatically resolves 195 - # This is equivalent to calling aspect.resolve { class = "<class>"; } 196 - # Returns a map of all classes with their resolved modules 197 - # 198 - # For example: aspect.modules.nixos == aspect.resolve { class = "nixos"; } 199 - # 200 - # This is computed lazily and uses ignoredType to avoid serialization issues 201 - options.modules = lib.mkOption { 202 - internal = true; 203 - visible = false; 204 - readOnly = true; 205 - description = "resolved modules from this aspect"; 206 - type = ignoredType; 207 - # For each class in the aspect, resolve it with empty aspect-chain 208 - apply = _: lib.mapAttrs (class: _: config.resolve { inherit class; }) config; 209 - }; 65 + options = { 66 + name = lib.mkOption { 67 + description = "Aspect name"; 68 + default = name; 69 + type = lib.types.str; 70 + }; 71 + 72 + description = lib.mkOption { 73 + description = "Aspect description"; 74 + default = "Aspect ${name}"; 75 + type = lib.types.str; 76 + }; 77 + 78 + includes = lib.mkOption { 79 + description = "Providers to ask aspects from"; 80 + type = lib.types.listOf providerType; 81 + default = [ ]; 82 + }; 83 + 84 + provides = lib.mkOption { 85 + description = "Providers of aspect for other aspects"; 86 + default = { }; 87 + type = lib.types.submodule ( 88 + { config, ... }: 89 + { 90 + freeformType = lib.types.lazyAttrsOf providerType; 91 + config._module.args.aspects = config; 92 + } 93 + ); 94 + }; 95 + 96 + __functor = lib.mkOption { 97 + internal = true; 98 + visible = false; 99 + description = "Functor to default provider"; 100 + type = lib.types.functionTo providerType; 101 + default = aspect: { class, aspect-chain }: if true || (class aspect-chain) then aspect else aspect; 102 + }; 103 + 104 + modules = mkInternal "resolved modules from this aspect" ignoredType ( 105 + _: lib.mapAttrs (class: _: config.resolve { inherit class; }) config 106 + ); 210 107 211 - # Main resolution function that converts an aspect into a nixpkgs module 212 - # Takes { class, aspect-chain } and returns a resolved module 213 - # - class: The target configuration class (e.g., "nixos", "home-manager") 214 - # - aspect-chain: List of aspects traversed so far (for tracking dependencies) 215 - # 216 - # The resolution process: 217 - # 1. Invokes the aspect config with class and aspect-chain parameters 218 - # This triggers the __functor if defined, allowing parametrization 219 - # 2. Calls resolve.nix to recursively resolve includes 220 - # 3. Returns a module with imports from the aspect and its dependencies 221 - # 222 - # The aspect-chain parameter allows aspects to introspect their dependency tree 223 - # This is useful for debugging and for aspects that need to know their context 224 - options.resolve = lib.mkOption { 225 - internal = true; 226 - visible = false; 227 - readOnly = true; 228 - description = "function to resolve a module from this aspect"; 229 - type = ignoredType; 230 - apply = 108 + resolve = mkInternal "function to resolve a module from this aspect" ignoredType ( 231 109 _: 232 110 { 233 111 class, 234 112 aspect-chain ? [ ], 235 113 }: 236 - # Invoke config (the aspect) with class and aspect-chain parameters 237 - # This works because config is wrapped with __functor via the submodule system 238 - # Then pass the result to resolve for dependency resolution 239 114 resolve class aspect-chain (config { 240 115 inherit class aspect-chain; 241 - }); 116 + }) 117 + ); 242 118 }; 243 119 } 244 120 ); 245 121 246 122 in 247 123 { 248 - inherit 249 - aspectsType # Main entry point for flake.aspects 250 - aspectSubmodule # Individual aspect definition type 251 - providerType # Type for provider expressions 252 - ; 124 + # Top-level aspects container with fixpoint semantics 125 + aspectsType = lib.types.submodule ( 126 + { config, ... }: 127 + { 128 + freeformType = lib.types.lazyAttrsOf ( 129 + lib.types.either (lib.types.addCheck aspectSubmodule ( 130 + m: (!builtins.isFunction m) || isSubmoduleFn m 131 + )) providerType 132 + ); 133 + config._module.args.aspects = config; 134 + } 135 + ); 136 + 137 + inherit aspectSubmodule providerType; 253 138 }