dtdoctor: reverse-engineer four more error classes, report disabled ancestors - #501
Draft
kartben wants to merge 9 commits into
Draft
dtdoctor: reverse-engineer four more error classes, report disabled ancestors#501kartben wants to merge 9 commits into
kartben wants to merge 9 commits into
Conversation
kartben
force-pushed
the
claude/error-macro-reverse-engineer-t29qdc
branch
from
August 25, 2026 05:34
e291676 to
cd198de
Compare
kartben
force-pushed
the
dtdoctor-tests
branch
5 times, most recently
from
August 25, 2026 16:08
753245f to
dc21102
Compare
kartben
force-pushed
the
claude/error-macro-reverse-engineer-t29qdc
branch
from
August 25, 2026 16:20
cd198de to
96ce7da
Compare
kartben
force-pushed
the
dtdoctor-tests
branch
from
August 25, 2026 16:57
dc21102 to
c1c9e92
Compare
kartben
force-pushed
the
claude/error-macro-reverse-engineer-t29qdc
branch
from
August 25, 2026 17:00
96ce7da to
23c302d
Compare
kartben
force-pushed
the
dtdoctor-tests
branch
from
August 25, 2026 17:03
c1c9e92 to
c9c8b2e
Compare
kartben
force-pushed
the
claude/error-macro-reverse-engineer-t29qdc
branch
from
August 25, 2026 17:04
23c302d to
5670a60
Compare
kartben
force-pushed
the
dtdoctor-tests
branch
from
August 25, 2026 19:31
c9c8b2e to
7ffd85e
Compare
DT Doctor could only explain one kind of build error:
__device_dts_ord_<N>, i.e. DEVICE_DT_GET() on a node identifier that
resolved but has no driver.
The errors users hit more often are the ones where the node identifier
itself never resolves, so the unexpanded macro leaks into the compiler
output:
error: '__device_dts_ord_DT_N_NODELABEL_my_serial_ORD' undeclared
error: 'DT_N_S_soc_S_uart_40002000_P_currentspeed' undeclared
error: '__device_dts_ord_DT_N_INST_0_vnd_foo_ORD' undeclared
Everything needed to explain those is already in edt.pickle. Map every
node identifier gen_defines.py generates back to its node, then match
a failing macro against that map: the longest part that does name a
node tells us what exists, and the leftover tells us what does not.
That covers three new families of error:
- a node identifier naming no node (DT_NODELABEL(), DT_ALIAS(),
DT_CHOSEN() and DT_PATH()), reported with fuzzy suggestions drawn
from the devicetree and the DTS-versus-C spelling rules
- a property a node does not have, distinguishing a typo, a property
the binding declares but the node leaves unset, and an index or cell
the property does not define
- DT_INST()/DT_DRV_INST() past the end of a compatible, or on a
compatible nothing declares
A node identifier that does resolve now falls through to the existing
enabled/disabled diagnosis, so DEVICE_DT_GET(DT_NODELABEL(x)) gets the
same answer the ordinal form already got.
Recovering a name from a macro that has no generated counterpart needs
care. Dropping trailing '_' components until something fuzzy-matches
over-trims, and would report DT_N_ALIAS_no_such_alias_ORD as an alias
named 'no_such': a wrong name next to a suggestion, which is worse
than no suggestion. There is no need to guess. What <devicetree.h>
appends always starts with an upper case component ('_ORD', '_P_',
'_REG_IDX_0'), and gen_defines.py lower-cases every name it builds an
identifier from, so the first upper case component is exactly where
the name ends.
The name mangling itself is taken from gen_defines.py rather than
reimplemented, since the two drifting apart would silently resolve
macros to the wrong node.
Assisted-by: Claude:opus-5
Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
The wrapper only captured symbols matching __device_dts_ord_<digits>, so none of the errors where the node identifier never expanded were even handed to the analyzer. Widen the captured symbol to any device symbol or bare devicetree macro, keeping the five toolchain message patterns as they are. The GNU ld pattern becomes lazy so it captures the whole device symbol rather than settling for the DT_N_ macro pasted inside it. Verified against real gcc, clang and GNU ld output. Assisted-by: Claude:opus-5 Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
Add a fixture devicetree carrying everything the new diagnoses need something real to compare against: node labels, an alias, a /chosen entry, two instances of one compatible, and properties that are set, declared but unset, and of a type with no plain value macro. Then a test module per family. Pin the name recovery rule in particular: an appended '_ORD', '_EXISTS' or '_P_<prop>' must not be read as part of a node label, and a label containing underscores must survive intact. Extend the wrapper's toolchain table with the new symbol shapes across the gcc, GNU ld, clang and lld message formats, taken from real toolchain output, and check that unrelated undeclared symbols are still ignored. Assisted-by: Claude:opus-5 Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
Give the fixture devicetree a property that is set and one the binding only declares, and note that its two nodes are also the compatible's two DT_INST() instances. Exercise each new diagnosis family against the build's real edt.pickle, and run the unexpanded macro shapes through the wrapper with the toolchain the application was built with, both at compile time and, for one of them, at link time. Toolchain message drift is what these shapes are most exposed to, and it is not something the unit suite can catch. Assisted-by: Claude:opus-5 Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
Cells were the weakest thing DT Doctor printed, and they reached two different generic fallbacks depending on where they came from. DT_GPIO_PIN() landed on the property message, "usually an index past the end of the property, or a name or cell it does not define", which mentions cells but names none. DT_IRQ() did not even get that far, falling to "the devicetree API asked for 'IRQ_IDX_0_VAL_priority' on this node". Both are high traffic: DT_GPIO_PIN(), DT_GPIO_FLAGS(), DT_PWMS_CHANNEL(), DT_IRQ() and DT_PHA_BY_NAME() all bottom out in cell macros. And the answer the user needs, which cells exist, lives in the *controller's* binding, not in the node the macro was written on. That indirection is precisely what cannot be chased from the error text, and edt.pickle already holds it. phandle-array, 'interrupts' and 'reg' all reduce to a list of entries picked by index or by name, each defining a set of cells, so one parser and one handler explain all three. Nothing keys off 'gpios' in particular: any phandle-array property gets this, and Zephyr's in-tree bindings declare several hundred distinct ones. Only 'interrupts' and 'reg' are a fixed pair, since those hang off the node rather than off a property. The parser leans on the same upper/lower case split the node identifier code already relies on, which is what keeps '_VAL_' from being read as part of a name. A bad cell now names the controller, lists the cells its binding defines with the values this node gives them, and suggests the closest. A bad index or name lists the entries that do exist. Entry names are looked up by specifier space rather than by property name, so 'cs-gpios' entries are correctly named through 'gpio-names' and not 'cs-gpio-names'. Anything the parser does not recognise keeps the existing message: this narrows the generic fallback, it does not replace it. Assisted-by: Claude:opus-5 Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
Add a fixture devicetree with all three specifier spaces on one node: a phandle-array whose entries are named and whose cells come from a controller's binding, an interrupt whose entries are deliberately not named, and a register. A second phandle-array, 'pwms', uses a different specifier space and cell count so the diagnoses stay pinned as working on any phandle-array rather than just on 'gpios'. Cover a bad cell, a bad index and a bad name in each space, plus the two cases that have to keep the old message: a valid entry with no cell named, and a non-specifier suffix. Pin every accessor shape against the parser directly, since that is the part most exposed to <devicetree.h> growing new accessors. Assisted-by: Claude:opus-5 Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
Give the fixture devicetree a GPIO controller carrying real 'pin'/'flags' cells and a consumer that uses them, then check a bad cell, index and name against the build's real edt.pickle, and run the bad-cell shape through the wrapper with the toolchain the application was built with. The consumer's property is 'dtdoctor-gpios' while its names come from 'gpio-names', so the fixture also pins the specifier-space naming rule rather than just asserting it in a unit test. Assisted-by: Claude:opus-5 Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
edtlib and gen_defines.py only look at a node's own 'status', so a node below a disabled parent still counts as okay: its driver builds and its instance macros exist, and DT Doctor's diagnoses never mentioned the parent. Whether the node was reported as disabled or as lacking a driver, following the advice just moved the failure to the parent device on the next build. Walk the ancestor chain in both the enabled-node and disabled-node diagnoses and name every disabled ancestor with the file and line where its status is set, so the whole chain gets fixed in one go. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
Unit-test the ancestor walk in both diagnoses: an enabled node under a disabled parent (with the Kconfig analysis still running), a disabled node under a disabled parent, a disabled grandparent behind an okay parent reported as an "ancestor" without dragging the okay parent in, and no note at all when the whole chain is okay. End to end, give the fixture devicetree an enabled device below a disabled parent, under its own compatible so the DT_INST() instance counts the other tests rely on stay untouched. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
kartben
force-pushed
the
claude/error-macro-reverse-engineer-t29qdc
branch
from
August 25, 2026 19:48
5670a60 to
52b23fd
Compare
kartben
pushed a commit
that referenced
this pull request
Aug 25, 2026
Merges dtdoctor-tests, PR #501 (four new error-macro families + disabled-ancestor reporting), and sca-launcher-ccache, then documents a real build of tests/drivers/coredump/coredump_api for qemu_riscv32/qemu_virt_riscv32/aia-direct (and its /smp variant) — matching the DT_N_S_coredump_device0_P_memory_regions_IDX_0 failure from the weekly-CI twister report — showing dtdoctor's raw diagnosis of the missing node label and missing property, and the underlying devicetree overlay-selection bug it points to. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbyFdGk3NBCRUsEhcQWxFN
kartben
pushed a commit
that referenced
this pull request
Aug 25, 2026
Extends the single-case demo with a full sweep of the remaining devicetree-related weekly-CI build failures: 19 additional (board, test) pairs built for real against the Zephyr SDK v1.0.1 and real vendor HALs (Infineon, TI, Renesas, Espressif, Nordic, ST, Raspberry Pi, Atmel, Sifli, NXP, WCH), each first reproducing the exact leaked-macro/undefined-reference signature from the CI report, then rebuilding with -DZEPHYR_SCA_VARIANT=dtdoctor and capturing the raw diagnosis. 18/19 matched (dtdoctor correctly diagnosed the real failure); 1 (heltec_t114_v2) was confirmed to be a plain C bug unrelated to devicetree, not a dtdoctor case. 0 blocked. Report also assesses which of PR #501's diagnosis families got exercised by real CI failures and which didn't. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbyFdGk3NBCRUsEhcQWxFN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on zephyrproject-rtos#117239 (
dtdoctor-tests).DT Doctor currently understands exactly one symbol shape:
__device_dts_ord_<N>, i.e.DEVICE_DT_GET()on a node identifier that does resolve but has nostruct device. That is only the first of the failure modesdoc/build/dts/troubleshooting.rstdocuments.The errors users hit more often are the ones where the node identifier itself never resolves, so the unexpanded macro leaks straight into the compiler output:
Today the wrapper's regexes require
__device_dts_ord_\d+, so none of these are even detected. The user gets the raw macro soup and nothing else. Everything needed to explain them is already inedt.pickle.What's added
DT_N_NODELABEL_*,DT_N_ALIAS_*,DT_CHOSEN_*, unknownDT_N_S_*<node-id>_P_<prop>DT_PROP_OR()/DT_NODE_HAS_PROP())DT_INST()/DT_DRV_COMPATDT_N_INST_<i>_<compat>DT_DRV_COMPATlowercase-and-underscores rule<node-id>_P_<pha>_IDX_<i>_VAL_<cell>,_IRQ_*,_REG_*__device_dts_ord_<N>(resolved node)How
One reverse map: every node identifier macro
gen_defines.pyemits, mapped back to itsedtlib.Node. Match a failing macro against it. The longest part that does name a node tells us what exists, and the leftover tells us what does not.The name mangling is taken from
gen_defines.node_z_path_id()/str2ident()rather than reimplemented, since the two drifting apart would silently resolve macros to the wrong node.A node identifier that resolves now falls through to the existing enabled/disabled diagnosis, so
DEVICE_DT_GET(DT_NODELABEL(x))gets the same answer the ordinal form already got.Example output
and for a cell, where the answer lives in a binding the user never mentioned:
Subtleties worth a look during review
Path identifiers are prefixes of each other and of the alternate-ID namespaces. A naive longest-prefix match resolved
DT_N_NODELABEL_foo_ORDto the root node andDT_N_S_soc_S_uart_9999to/soc. Matching is scoped to the namespace the macro is written in, plus a leftover-_S_check so a partially matched path blames the node the full path names rather than the ancestor that happened to match.Recovering a name from a macro that has no generated counterpart. Trimming trailing
_-separated components until something fuzzy-matched over-trims, and would reportDT_N_ALIAS_no_such_alias_ORDas an alias namedno_such: a wrong name next to a suggestion, which is worse than no suggestion. There is no need to guess. What<devicetree.h>appends always starts with an upper case component (_ORD,_P_,_REG_IDX_0), andgen_defines.pylower-cases every name it builds identifiers from, so the first upper case component is exactly where the name ends. The specifier parser leans on the same invariant to keep_VAL_from being read as part of a name.Nothing keys off
gpiosin particular. The specifier handler runs on anyphandle-arrayproperty, and Zephyr's in-tree bindings declare several hundred distinct ones (reset-gpios,int-gpios,clocks,pwms,dmas). Onlyinterruptsandregare a fixed pair, since those hang off the node rather than off a property. The fixture carries a second phandle-array with a different specifier space so this stays pinned.Specifier entry names are keyed by the specifier space, not the property name.
cs-gpiosentries are named throughgpio-names, notcs-gpio-names. The integration fixture uses adtdoctor-gpiosproperty withgpio-namesso this is pinned end to end rather than only in a unit test.A node below a disabled parent still counts as okay.
edtlibandgen_defines.pyonly look at a node's ownstatus, so its driver builds, its instance macros exist, and the failure surfaces on the parent's ordinal. Reporting disabled ancestors in both node diagnoses is what keeps the user from fixing one node per rebuild all the way up the chain.Testing
Every commit in the series is green on its own, with the suites growing 37 → 83 → 105 → 110 unit and 7 → 20 → 26 → 27 integration.
tests/misc/dtdoctor/unit/fixture devicetrees carry node labels, an alias, a/chosenentry, two instances of one compatible, properties that are set / declared-but-unset / of a type with no plain value macro, all three specifier spaces (a named phandle-array, a second one in a different space, a deliberately unnamed interrupt, a register), and enabled/disabled nodes below disabled parents and grandparents. One test module per family. 110 tests pass.tests/misc/dtdoctor/unit/test_dtdoctor_wrapper.pycovers the new symbol shapes across the gcc, GNU ld, clang and lld message formats, transcribed from real toolchain output, plus a check that unrelated undeclared symbols are still ignored.tests/misc/dtdoctor/e2e/exercises each family against the build's realedt.pickle, and compiles real macro uses (DEVICE_DT_GET(DT_NODELABEL(dtdoctor_no_such_label)),DT_PROP(…, no_such_prop),DT_PHA_BY_IDX(…)) with the application's own compile commands replayed fromcompile_commands.json, in C and C++, so the leaked-macro shapes come from the real<devicetree.h>expansion rather than transcription. Toolchain message drift is what these shapes are most exposed to, and it is not something the unit suite can catch. 27 tests collected.ruff checkandruff formatclean on all touched files;check_compliance.py(Gitlint, Ruff, YAMLLint, KeepSorted, Identity) clean over the whole range.One caveat: the unit suite runs everywhere, but the integration suite was validated against a hand-built
edt.pickleusing host toolchains. The real twister/ctest run against the Zephyr SDK onqemu_cortex_m3still needs CI.