modulep! resolves all module flags to nil in noninteractive sessions (doctor/CLI) since ce615084 — regression of #8843, still unfixed on master
- Dominant language
- Emacs Lisp
- Stars
- 22.7k
- Forks
- 3.1k
- Avg merge
- 10h 46m
- Merged PRs (30d)
- 4
Description
### Describe your issue
Since commit `ce615084` ("fix: stale module hot cache poisoning doom-modules", Fix: #8843), `modulep!` returns `nil` for **all** module flag lookups in noninteractive sessions (`doom doctor`, `doom sync`, and other CLI subcommands). This causes conditional module `+flag.el` files to never load, which produces `void-function` errors for macros/aliases they define (e.g. `define-key!`), plus false "Missing emacs package" reports in `doom doctor`. I verified this is **still present on master** today.
**Expected behavior:** `modulep!` should reflect the user's actual module configuration regardless of interactive vs. noninteractive session, so `doom doctor` can resolve module state accurately.
**Observed behavior:** in any CLI/noninteractive Doom session:
- `(modulep! :doom compat +keybinds)` → `nil`
- Consequenty `modules/doom/compat/+keybinds.el` (which `(defalias 'define-key! #'general-def)`) is never loaded
- Any module calling `define-key!`/`map!` (e.g. `completion/vertico/config.el`) can crash with `Symbol's function definition is void: define-key!`
- Module `packages.el` files branching on `(modulep! :editor evil)` choose the wrong branch, so `doom doctor` reports false "Missing emacs package: goggles / drag-stuff"
**Root cause.** `ce615084` wrapped the module-cache population in `lisp/doom-profiles.el` inside `(static-unless noninteractive ...)`:
```elisp
(static-unless noninteractive
;; Cache module state and flags in symbol plists for quick lookup by
;; `modulep!' later.
,@(cl-loop
for (category . modules) in (seq-group-by #'car config-modules-list)
collect
`(setplist ',category
(quote ,(cl-loop for (_ . module) in modules
nconc `(,module ,(doom-module->context (cons category module))))))))
```
This means in noninteractive sessions the symbol plists are **never populated**, but both relevant code paths read the symbol plist with no fallback:
- `doom-module` (`lisp/doom-modules.el:635`): `(when-let* ((context (get group name)))` — reads the symbol plist only.
- `modulep!` with-group branch (`lisp/doom-modules.el:716`): `(let ((ctxtform \`(get (backquote ,group) (backquote ,module))))` — expands to `(get group name)`.
The hash table `doom-modules` (which *does* carry the correct modules/flags) is restored unconditionally, but neither `doom-module` nor the `modulep!` with-group branch falls back to it. So in noninteractive, `modulep!` always returns `nil`.
**Suggested fix.** Make the plist hot-cache path fall back to the `doom-modules` hash table when the plist lookup is nil — analogous to what the `doom-module` docstring already recommends ("Use `doom-module-get` if correctness is more important (e.g. in non-interactive sessions)"). This is the fix I applied locally (all three doctor warnings now pass with zero errors):
```elisp
;; 1) doom-module: fall back to the hash table when the plist is empty
(defun doom-module (group name &optional property)
(when-let* ((context (or (get group name)
(when (and (boundp 'doom-modules)
(hash-table-p doom-modules)
(gethash (cons group name) doom-modules))
(doom-module->context (cons group name))))))
...))
;; 2) modulep! with-group branch: delegate to doom-module (which has the fallback)
(let ((ctxtform `(doom-module (backquote ,group) (backquote ,module))))
...)
```
Happy to open a PR if that direction is acceptable.
**Note (not every machine crashes):** `doom doctor` will only crash if some enabled module's `+flag.el`/`config.el` calls `define-key!`/`map!` at load time while inside the module loader / `with-doom-module`. If none do, the bug surfaces only as the silent `modulep! => nil` + false "Missing emacs package" warnings — easy to dismiss. The batch check in "Steps to reproduce" is the deterministic proof.
### Steps to reproduce
These steps were verified end-to-end on a second machine. The core directory is `~/.config/emacs` (the repo that contains `lisp/`, `bin/`, etc. — *not* `$DOOMDIR`). The one-off batch check below is the deterministic repro: it does not depend on any module file crashing, so it reproduces on any config.
```bash
cd ~/.config/emacs # the Doom CORE dir (contains lisp/, bin/, etc.)
# 1. do a sync so the generated profile init reflects the bug
./bin/doom sync
# 2. In a noninteractive (batch) session, compare the two sources of truth:
emacs --batch -Q -L ~/.config/emacs/lisp \
--eval '(require (quote doom-core))' \
--eval "(princ (format \"modulep! => %S\n\" (modulep! :doom compat +keybinds)))" \
--eval "(princ (format \"hash table has :doom compat => %S\n\" (gethash (cons :doom (quote compat)) doom-modules)))" 2>&1
```
Expected (buggy) output — the two sources disagree:
```
modulep! => nil
hash table has :doom compat => t ; or the context, confirming the flag exists in doom-modules
```
3. `./bin/doom doctor` — depending on your module set this either crashes on `define-key!` or (more commonly) just reports the false "Missing emacs package: goggles / drag-stuff" warnings. **The batch check in step 2 is the reliable, deterministic repro** — it does not depend on a particular module file crashing.
4. If you want to see the discrepancy at the source, inspect the *generated* profile file `~/.local/etc/@/init.d/80-doom-modules.init.el` (paths under `~/.local/etc/`). It contains the `(static-unless noninteractive ...)`/`(setplist ...)` form from `doom-profiles.el`, whereas the `doom-modules` hash table is restored unconditionally there — that is where the plist vs hash-table divergence becomes visible.
### System information
[doom-info.txt](https://github.com/user-attachments/files/31678482/doom-info.txt)
### Disclosures
- [x] This issue was written with/by AI.
Contributor guide
Research direction
Start in lisp/doom-profiles.el, then inspect doom-module and the modulep! with-group branch in lisp/doom-modules.el. Run the provided emacs --batch reproduction after ./bin/doom sync and compare the plist lookup with doom-modules. Done means modulep! resolves configured flags in noninteractive sessions and the relevant doom doctor warnings or crashes no longer occur.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- emacs-lisp
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100