doomemacs / doomemacs/core

modulep! resolves all module flags to nil in noninteractive sessions (doctor/CLI) since ce615084 — regression of #8843, still unfixed on master

Open
#8,884 1 comment 0 reactions 0 assignees View on GitHub
AI core:modules
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.