diff --git a/doc/todo.md b/doc/todo.md index 7f5eee4..6f615a5 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -104,6 +104,55 @@ version-bump. --- +## Structure + +- [ ] **Helpers must not stay in the environment (do first)** — internal + functions (`_` prefix, `helpers/`) are not part of the user interface, yet 16 + of them are `export -f`, so they leak into the environment of every child + process. By design they should only exist where they are needed: + - stop exporting helpers by default; unset the ones only needed at load time + (as `profile.sh` already does for `parse_conf`, `load_alias`, ...); + - a helper stays exported only when an exported public function calls it and + must work in a child bash (`bash -c`, `find -exec`, `xargs`), which has to + be checked and documented one by one (otherwise that public function is no + longer exported either, or waits for the stub mechanism below); + - add a check (shellcheck-like script or `bats` test) listing exported + functions starting with `_`, so none comes back unnoticed. + This is the first step: it does not depend on the lazy loading below, which + builds on it. **[medium]** +- [ ] **Load code on demand (stubs, lazy loading, optional unloading)** — + today every module is sourced at startup (about 63 ms, 109 functions) and 80 + functions are exported (about 250 KB of environment inherited by every + external command, which makes each one about 25 % slower and eats `ARG_MAX`). + Replace this by: + - *Stubs*: each lazy command is a tiny generated function + (`rain() { _lazy rain "$@"; }`), the only thing exported. The stubs come + from a command-to-module index, built at upgrade time from a + `# provides: ...` header in each module (so `help`, `type` and completion + still see every command). + - *Lazy loading* (`_try_load`): the first call sources the module and its + `helpers/.sh` (and the modules it depends on, declared in the + header), then runs the real function (`_exec`). The code stays loaded + afterwards. Helpers and implementations are no longer exported, which also + removes them from the environment of child shells. + - *Optional unloading* (`_free`, e.g. `PROFILE_UNLOAD=1`): after the call, + put the stub back instead of unsetting the function. Must be idempotent + (Ctrl-C or `return` skip the cleanup), must not free code still referenced + by a trap (`_rain_stop`, WINCH handler) or used by a running caller + (reference count), and costs about 4 ms per call, so it is not the default. + - *Always resident*: `disp`, `conf`, `debug`, `prompt` (and the `DEBUG` timer + hook), `lang` shortcuts and `help`. With unloading, state kept across calls + (`pwd` pool, current theme, `settrace`) must live in variables that are not + freed. Lazy candidates: `compress`, + `filefct`, `fun`, `git`, `info`, `net`, `packages`, `processes`, `pwd`, + `rain`, `ssh`, `updates`. + - *To solve*: `load_conf`/`CONF_*` must stay available at call time; child + shells need `MYPATH` and the loader exported to resolve stubs; tracebacks + show the loader frames. Do it after the test suite exists, as regressions + would be silent. **[hard]** + +--- + ## Infrastructure - [ ] **Test suite** — add a `test/` directory with `bats` (Bash Automated