Nix's lazy evaluation means attribute paths are lazily-generated trees where only accessed nodes are evaluated. This post exploits that property to encode NES button presses as attribute paths — each press becomes its own Nix derivation, with the previous press's savestate as input. The result is a Nix flake (nes-nix) that plays Super Mario Bros. 3 where the Nix store acts as a savestate history. Branching mid-run reuses cached derivations, and the dependency graph encodes the full input sequence. The post also explores practical limits: ~2,400 presses before stack overflow (raisable to 20,000 with max-call-depth), and a Linux kernel cap of 21,845 presses per argv argument, with a file-based escape hatch. Build cost is linear at ~1.27s per press with substituters, ~0.28s without.

Questions this post answers
What is the default max-call-depth in Nix and how many attribute path steps does it allow before stack overflow?
Nix's max-call-depth defaults to 10,000, and evaluating each attribute path step costs roughly four nested calls, which limits you to about 2,400 steps before hitting a stack overflow. Raising max-call-depth to 10,000,000 (combined with ulimit -s unlimited) allows up to 20,000 steps, evaluated in roughly 14 seconds at ~0.7ms per step. Developers pushing Nix evaluation limits track workarounds like these on daily.dev.
What is the Linux kernel limit on the number of characters in a single command-line argument passed to nix eval?
Linux caps individual argument size at 131,072 bytes (MAX_ARG_STRLEN). Since each attribute path segment like 'right.' is six bytes, this limits a single nix eval argument to 21,845 such segments. The workaround is to pass the sequence via a file using builtins.getFlake and a sequenceFile attribute, which produces byte-identical derivations and still shares store paths. Engineers hitting OS-level Nix constraints find practical workarounds discussed on daily.dev.
How does Nix store caching affect build time when each derivation depends on the previous one serially?
When derivations form a serial chain (each depending on the previous), build cost is linear in the number of steps. With substituters enabled the cost is roughly 1.27 seconds per step; with substituters disabled it drops to ~0.28 seconds per step. The round-trip cost of checking the remote cache dominates over the actual computation time. Setting preferLocalBuild or allowSubstitutes avoids this overhead. Teams optimizing Nix build pipelines with substituter trade-offs share findings on daily.dev.