This guide provides essential context for working with HNix - a Haskell implementation of the Nix expression language using advanced functional programming techniques including recursion schemes and abstract definitional interpreters.
# Enter development environment
nix-shell
# Build the project
cabal v2-configure
cabal v2-build
# Run a single test
cabal v2-test --test-options="--pattern '/Parser/basic literals/'"
# Interactive REPL for exploration
cabal v2-repl
> :load Nix.Eval
> :type evalExprLoc
# Quick evaluation test
cabal v2-run hnix -- --eval --expr '1 + 1'# Standard test suite
cabal v2-test
# All tests including Nixpkgs parsing (slow)
env ALL_TESTS=yes cabal v2-test
# Only Nixpkgs compatibility tests
env NIXPKGS_TESTS=yes cabal v2-test
# Pretty-printer round-trip tests
env PRETTY_TESTS=yes cabal v2-test
# Test with coverage
cabal v2-configure --enable-coverage
cabal v2-test --enable-coverage# Memory profiling (upload .prof to speedscope.app)
cabal v2-run --enable-profiling --flags=profiling \
hnix -- --eval --expr 'builtins.length [1 2 3]' +RTS -hy -l
# Stack trace on error
cabal v2-run hnix -- --trace --eval --expr 'throw "error"' +RTS -xc
# Heap profiling for thunk leaks
cabal v2-run hnix -- --eval --expr 'import <nixpkgs> {}' \
+RTS -h -i0.1 -RTS && hp2ps -e8in -c hnix.hp
# Reduce complex expressions for minimal repro
hnix --reduce bug.nix --eval --expr 'import ./bug.nix'-- The functor (non-recursive structure)
data NExprF r -- 18 constructors: NConstant, NStr, NSym, NList, etc.
-- Fixed point gives recursion
type NExpr = Fix NExprF
-- Location annotations via composition
type NExprLoc = Fix (AnnF SrcSpan NExprF)The adi function (src/Nix/Utils.hs:345) enables behavior injection:
-- Example: Add tracing to evaluation
tracingEval :: NExprLoc -> m (NValue t f m)
tracingEval = adi addTrace baseEval
where
addTrace :: Transform NExprLocF (m (NValue t f m))
addTrace f e = do
traceM $ "Evaluating: " ++ show (void e)
result <- f e
traceM $ "Result: " ++ show result
pure resultCommon ADI use cases:
- Error context:
evalWithMetaInfo = adi addMetaInfo evalContent - Profiling: Inject timing measurements at each recursion
- Memoization: Cache results of sub-expressions
- Debugging: Track evaluation path
type NValue t f m = Free (NValue' t f m) t
-- Pure t = thunk (unevaluated)
-- Free v = evaluated valueMemory implications:
- Thunks accumulate until forced
- Use
forceexplicitly to prevent buildup - Monitor with
+RTS -sfor thunk statistics
class MonadEval v m where
evalExprLoc :: NExprLoc -> m v -- Evaluate expression
evalError :: Doc v -> m a -- Report error
class MonadThunk t m a | t -> m a where
thunk :: m a -> m t -- Create thunk
force :: t -> m a -- Force evaluation
class (MonadEval v m, MonadThunk t m v) => MonadNix e t f m-- Define capability
class Monad m => MonadMyEffect m where
myOperation :: String -> m Int
-- Add to evaluation monad
newtype MyNix m a = MyNix (ReaderT MyEnv m a)
deriving (Functor, Applicative, Monad)
instance MonadMyEffect (MyNix m) where
myOperation s = MyNix $ asks (lookupThing s . myEnvData)- Add to
src/Nix/Builtins.hs:
builtinsList :: [(Text, BuiltinType)]
builtinsList =
[ ("myBuiltin", arity2 myBuiltinImpl)
-- ...
]
myBuiltinImpl :: MonadNix e t f m => NValue t f m -> NValue t f m -> m (NValue t f m)
myBuiltinImpl arg1 arg2 = do
-- Force evaluation if needed
str <- fromStringNoContext =<< fromValue arg1
num <- fromValue arg2
-- Perform operation
pure $ nvStr $ makeNixString (str <> show num)- Test in
tests/EvalTests.hs - Document behavior matching Nix semantics
-- Hook into evaluation via MonadEval instance
instance MonadEval (NValue t f m) MyCustomNix where
evalExprLoc expr = do
-- Pre-evaluation hook
logExpression expr
-- Delegate to standard evaluation
result <- standardEvalExprLoc expr
-- Post-evaluation hook
recordMetrics expr result
pure resultProblem: Thunk accumulation causing memory exhaustion
-- BAD: Builds huge thunk chain
foldl' (\acc x -> thunk (acc + x)) 0 [1..1000000]
-- GOOD: Forces evaluation incrementally
foldl' (\acc x -> force acc >>= \a -> pure (a + x)) 0 [1..1000000]Problem: Lazy fields in strict data
-- BAD: ~ makes field lazy despite ! on data
data MyData = MyData { ~myField :: !Int }
-- GOOD: Strict field in strict data
data MyData = MyData { myField :: !Int }- Enable tracing:
--traceflag - Use
--reduceto minimize test case - Add ADI transform to track recursion depth:
depthCheck :: Transform NExprLocF (ReaderT Int m (NValue t f m))
depthCheck f e = do
depth <- ask
when (depth > 1000) $ error "Recursion limit"
local (+1) (f e)Profile first:
# Generate flamegraph
cabal v2-run hnix -- --eval --expr 'import <nixpkgs> {}' \
+RTS -p -RTSCommon optimizations:
- Add strictness annotations to accumulators
- Use
HashMapinstead of association lists - Cache frequently computed values
- Specialize polymorphic functions with
{-# SPECIALIZE #-}
┌─────────────────┐
│ Builtins │ (100+ built-in functions)
├─────────────────┤
│ Effects │ (MonadNix, MonadEval constraints)
├─────────────────┤
│ Exec │ (High-level evaluation)
├─────────────────┤
│ Eval │ (Core evaluation with ADI)
├─────────────────┤
│ Value/Thunk │ (Free monad values, lazy evaluation)
├─────────────────┤
│ Expr │ (NExprF functor, parser, pretty-printer)
└─────────────────┘
- Adding language features: Start with
src/Nix/Parser.hs, add toNExprFinsrc/Nix/Expr/Types.hs - Modifying evaluation:
src/Nix/Eval.hsfor core,src/Nix/Exec.hsfor high-level - Debugging issues:
src/Nix/Reduce.hsfor test reduction,src/Nix/Cited.hsfor error context - Performance work:
src/Nix/Thunk/Basic.hsfor thunk implementation - Built-in functions:
src/Nix/Builtins.hs- match Nix semantics exactly
- Language tests (
tests/NixLanguageTests.hs): Official Nix test suite - Evaluation tests (
tests/EvalTests.hs): HNix-specific behavior - Parser tests (
tests/ParserTests.hs): Round-trip properties - Pretty tests (
tests/PrettyTests.hs): Pretty-printer correctness
-- Property-based test for parser round-trip
prop_parse_pretty :: NExpr -> Property
prop_parse_pretty expr =
parseNixText (prettyNix expr) === Right expr
-- Golden test for evaluation
goldenEval :: String -> NExpr -> TestTree
goldenEval name expr = goldenVsString name path $ do
result <- runLazyM defaultOptions $ evalExprLoc expr
pure $ encodeUtf8 $ prettyNValue resultUses relude with project utilities in Nix.Utils. Key differences:
panicinstead oferrorfor impossible casespassfor noop in do-blocks- Strict
Textby default
Nix strings carry derivation context - critical for store paths:
-- Context propagates through operations
makeNixString :: Text -> NixString -- No context
makeNixStringWithContext :: Text -> Context -> NixStringWarning: derivationStrict creates real /nix/store entries. Use --dry-run for testing.
Custom NSourcePos for performance - strict fields prevent memory leaks during parsing.
Primary Goal: Evaluate all of Nixpkgs
hnix --eval --expr "import <nixpkgs> {}" --findWorking: Parser, lazy evaluation, most built-ins, REPL, type inference
In Progress: Full Nixpkgs evaluation, performance optimization
Known Issues: Tests disabled by default (doCheck = false) due to store interaction
- Win for Recursion Schemes - Essential architectural context
- Design of HNix
- Gitter Chat