394 views
owned this note
IFD Improvement Discussion
---
STATUS: Migrating to https://wiki.lix.systems/books/lix-contributors/page/improving-ifd
## What is IFD?
IFD is a little tricky to describe, but the gist of it is: IFD is a NixLang construct where Nix expression depends on some other expression that needs evaluation and building. It's described in good detail in CppNix 2.24 manual:
https://nix.dev/manual/nix/2.24/language/import-from-derivation#import-from-derivation
## What's the problem with IFD?
The very practical problem with IFD is performance. IFD blocks evaluation (which is single-threaded and sequential) until the nested expression is built. The problem is best illustrated with a diagram from CppNix 2.24 manual:
https://nix.dev/manual/nix/2.24/language/import-from-derivation#illustration
There is also a conceptual problem that IFD heavily breaks the eval/build boundary. It's not great, but opinions on how bad this is differ. Here's a summary of opinions.
**raito:** this is an acceptable tradeoff, because dynamic builds are very valuable; there's not much of a difference between `builtins.runWASM` and IFD
**niko:** `builtins.runWASM` doesn't break the eval/build boundary, so it's conceptually "cleaner", but I dislike the idea of running arbitrary code just to get to eval stage
**Qyriad:** without IFD we have very clean "eval steps -> build steps"; feels bad losing it, but living without IFD is worse
**niko:** dyndrv feels cleaner, because it decouples eval from build
**niko:** IFD is platform-specific due to eval, dyndrv is not
**K900:** dyndrv is also platform-specific
**K900:** the fundamental difference between IFD and dyndrv is that IFD produces NixLang code and dyndrv produces .drv files
**KFears:** is that true? Some of the examples on dyndrv with rec-nix use NixLang code to generate .drv files, so pretty much exactly IFD without nested eval
**KFears:** the boundary is broken because we nest eval+build inside of another eval; if we move that nested eval+build outside of the outer eval (e.g. make it a derivation dependency instead), the entire problem goes away
## Solutions
### Async-y Eval
**K900:** we can keep IFD thunked
**raito:** Snix EvalIO can suspend execution and go evaluate further stuff while waiting on IO
**K900:** there's an evil hack that will allow Hydra to use IFD today: completely ignore IFD thunks. That allows Hydra to get all attribute paths without doing IFD, and then we just do the eval again on the builder
**raito:** feels like a weird eval mode, but whatever :P
**KFears:** in relation to IFD, we already have "default" and "disabled" modes; adding a "hydra" mode is probably fine and not much work?
**K900:** we'd like to have something that is easy to implement on CppNix side
**raito:** agreed
**KFears:** Snix async thunks should be pretty easy to implement in CppNix, because parallel-eval [already uses them](https://determinate.systems/posts/parallel-nix-eval/#:~:text=There%20are%20three%20new%20value%20types)
**KFears:** well actually, parallel-eval is a [Dix](https://chaos.social/@fpletz/114114877909534943) feature; it will probably never land in CppNix, but independently pushing refactors that will enable async thunks should hopefully not encounter any resistance from CppNix team
### Bytecode
**K900:** we can serialize thunks to bytecode
**K900:** that allows us to evaluate up to IFD, do a build, resume eval
**KFears:** that would be great in so many ways, but it will take years to do
**Qyriad:** yeah, it will take a lot of work
**niko:** it would be great to have high level bytecode-ish spec as part of Nix, but it's a ton of work
### RPC Improvements
**KFears:** instead of implicitly doing nested eval+build, we should communicate to the builder to do eval+build
**K900:** so the outer eval process just sits there and waits for the build to be done?
**KFears:** exactly
**K900:** it's the same thing as what we currently have, the only difference is what queues the builds (in this suggestion it's daemon, in current IFD it's the build)
**KFears:** yes, but this solves the conceptual problem, because now there's no nested eval+build - we depend on a different build that is on the same horizontal level
**KFears:** if we get RPC between the build and the builder, we can also later do things much more similar to dyndrv+rec-nix: write a package that does RPC calls to eval and build more packages
**KFears:** wild idea for IFD: what if when we encounter IFD in a derivation, we patch the derivation to use IFD as a built input?
**KFears:** example impl: eval drv -> encounter IFD -> abort eval, return the IFD itself and a special error code -> daemon sees special error code -> queues IFD eval -> patches initial drv to depend on IFD output -> evals patched drv
**KFears:** this example impl is problematic with nested IFD or multiple IFD, but the core idea here is to treat IFD as a special, fundamentally different operation from regular eval
**KFears:** this makes me think of C23's #embed: instead of forcing IFD into the otherwise clean eval/build process, let's treat it as a fundamentally different operation
### Expand Build Process
**Qyriad:** I wonder if the general purpose solution is worth it
**Qyriad:** we can formally expand "eval build plan -> execute build plan" pipeline into "eval fetch plan -> execute fetch plan -> evaluate build plan -> execute build plan", that would solve 90% of IFD usecases
**Qyriad:** I'm not convinced we should actually do that, but it's worth considering
**K900:** I think we should roll a proper, Snix-like solution
**KFears:** I also think Snix-like solution is good, though I also support RPC
**K900:** IFD is a good general solution, because it's "just generating code", which allos you to express basically anything
**KFears:** (thinking out loud) yay metaprogramming!