Skip to content

fix(scanner, manifest, build): one answer per question, and no key that reaches no decision (2026.9.9.1) - #594

Merged
Sunrisepeak merged 8 commits into
mainfrom
fix/two-answers-and-two-silences
Sep 9, 2026
Merged

fix(scanner, manifest, build): one answer per question, and no key that reaches no decision (2026.9.9.1)#594
Sunrisepeak merged 8 commits into
mainfrom
fix/two-answers-and-two-silences

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Four defects reported by a consumer project pinned at 2026.8.17.1, and two
shapes behind them: a question answered twice whose answers are read by
different sites, and a key that is parsed and then reaches no decision.

Design record: .agents/docs/2026-09-09-two-answers-and-two-silences.md.

What was measured

Same machine, gcc 16.1.0 and clang 22.1.8.

Construct 2026.9.8.1 this PR
module : private;, clang scanner error, exit 2 builds, runs, exit 0
module :private;, clang scanner error, exit 2 builds, runs, exit 0
UTF-8 BOM on a .cppm module 'x' not found, at the consumer builds, runs, exit 0
BOM + private fragment graph gains pcm.cache/-.pcm builds, runs; no such edge
implementation unit in a .cppm, clang missing 'export' specifier builds, runs, exit 0
the same, gcc builds builds
[target.linux.runtime] alone dropped applied
unknown key in [runtime] silent reported
BOM on mcpp.toml 1:1: expected key builds

The four

module : private; was read as an implementation partition. The name
tokeniser admits : so M:part scans as one token, and the partition test is
"the name contains a colon" — but this production's colon comes first and
belongs to no name. A valid file was refused with a sentence that is false about
it. A regression from #433, first released in v2026.8.18.1. The scanner also no
longer records an identity no source could have declared.

A UTF-8 byte-order mark is consumed, in the scanner and in every TOML
document mcpp reads. MSVC writes one by default. The failure it caused surfaced
at the consumer rather than at the file that was wrong. UTF-16/32 is refused by
name.

module_lang and module_output are now per-edge, bound from what the scan
found rather than from the file's extension. A module-extension file need not
provide a module; where the classifier and the scanner disagreed, the edge
carried an interface's flags with no BMI. Clang accepted -fmodule-output= with
no value and wrote no BMI at all; GCC's interface spelling is the plain
language, so the identical project built there and not on clang.

ConditionalConfig::empty() replaces two hand-written gates that omitted
libraries and linkLibraryDirs. It composes BuildInputs::empty() and the
existing XlingsConfig::empty(), which closed a fourth instance in the xpkg
gate (privateIncludeDirs). This is the third time this struct has been read by
a list that fell behind it; its own comments record #258 and #359.

Plus: [runtime] and [target.<pred>.runtime] now report an unsupported key —
the rule [build] and [target.<triple>] have followed since #418 and #249.

Deliberately not done

No detector for GNU-spelled ldflags reaching MSVC. ldflags is a documented
raw pass-through and [runtime] libraries is the supported neutral form;
inspecting the former would be a second answer to a question the author already
answered. compat.glfw and friends can already write
windows = { runtime = { libraries = { "gdi32" } } } today — the
mcpp.<platform> channel supports it and is target-axis-typed — so that change
belongs to mcpp-index on its own schedule.

No runtime key added to xpkg's target_cfg: the platform channel covers the
demonstrated need, so it would be a change with nothing behind it.

Evidence the criteria discriminate

Twelve new unit tests and four new e2e tests. Run against the implementation
reverted, ten of the twelve unit tests fail and all four e2e tests fail against
the released 2026.9.8.1 binary. The two that stay green are negative controls
(WellFormedNamesSurviveTheIdentityGuard,
CorrectlySpelledRuntimeKeysAreSilent), which exist to fail if the new refusals
are too broad.

The e2e criteria are assertions on the emitted graph, not on build outcome,
because the default Linux shards have gcc — where three of the four defects are
invisible. # requires: llvm would have skipped silently on both shards.

Locally: unit suite green, all nine workspace members green, all 12 examples
build, index-wide check of 228 mcpp-index descriptors parses identically
before and after.

…at reaches no decision (2026.9.9.1)

Four defects with two shapes behind them: a question answered twice whose
answers are read by different sites, and a key that is parsed and then reaches
no decision. Design record:
.agents/docs/2026-09-09-two-answers-and-two-silences.md

`module : private;` was read as an implementation partition. The name tokeniser
admits `:` so that `M:part` scans as one token, and the partition test is "the
name contains a colon" -- but the colon in the private module fragment comes
first and belongs to no name. A valid file was refused at scan time with a
sentence that is false about it. A regression from #433, first released in
v2026.8.18.1: on clang the same source built and ran under 2026.8.17.1.
The scanner also no longer records an identity no source could have declared;
a BOM combined with a fragment previously produced a BMI named after a colon.

A UTF-8 byte-order mark is consumed where bytes become lines, and by every TOML
document mcpp reads. MSVC writes one by default, so this is ordinary input.
The failure it caused appeared at the consumer (`module 'x' not found`) rather
than at the file that was wrong. UTF-16/32 is refused by name.

`module_lang` and `module_output` become per-edge, bound from what the scan
found rather than from the file's extension. A module-extension file need not
provide a module, and where the classifier and the scanner disagreed the edge
carried an interface's flags with no BMI: clang accepted `-fmodule-output=`
with no value and wrote no BMI at all, and compiled implementation units as
interfaces. GCC's interface spelling is the plain language, so the identical
project built there and not on clang.

`ConditionalConfig::empty()` replaces two hand-written gates that omitted
`libraries` and `linkLibraryDirs`, so a predicate carrying only a
`[target.<pred>.runtime]` table was parsed and discarded. It composes
`BuildInputs::empty()` and the existing `XlingsConfig::empty()`, closing a
fourth instance in the xpkg gate. All 228 descriptors in mcpp-index parse
identically before and after.

`[runtime]` and `[target.<pred>.runtime]` now report an unsupported key, the
rule `[build]` and `[target.<triple>]` have followed since #418 and #249.

Deliberately not done: no detector for GNU-spelled `ldflags` reaching MSVC.
`ldflags` is a documented raw pass-through and `[runtime] libraries` is the
supported neutral form; inspecting the former would be a second answer to a
question the author already answered.
`[[runtime.artifacts]]` and `[[runtime.requirements]]` are arrays rather than
tables, so they reach the unknown-key sweep instead of being skipped with the
`[runtime.<capability>]` provider channel. They are what `mcpp pack` writes into
every packed library, so a false positive there would warn on all of them.
Measured as zero before this was added; the assertion is what keeps it zero.
… recorded

The tokeniser stops at the first character it does not accept, so a declaration
it cannot read at all yields an EMPTY name -- and `'' is not a module name`
names nothing the author can find. Measured on a non-ASCII name, where
`is_module_name_char` tests bytes with std::isalnum and every byte of a UTF-8
sequence is false: the message read `''` where the source said `模块`.

GCC 16.1 refuses that declaration too (`unrecognized 'MODULE-EXPORT ...'`), so
this is a clearer sentence for a refusal that already existed rather than a new
restriction. The message now also states what a name is.
… gate

`mcpp xpkg parse` does not print conditional configs, so it could not have shown
a difference there. The statement with a denominator is that no descriptor in
the index uses `target_cfg` at all (0 of 228), so the xpkg half of the change
cannot alter a published package today.
…checks

The message spelled its two keys as prose while the check compared two separate
literals. The `[build]` sweep a few hundred lines above carries the note saying
why that matters: its message was once a third hand-written copy that had
drifted from both others, so the only spelling that turned the feature on was
the one being reported as unsupported.
…ling is

Windows CI reported `test_modgraph.cpp` failing to compile with

  optional:262: error: no matching constructor for initialization of
  '_SMF_control<_Optional_construct_base<basic_string<char,...>>, ...>'

against `types.cppm`'s `Profile`, a struct this change never touched. The chain
is `Manifest` -> `std::map<std::string, Profile>` -> `Profile`'s
`std::optional<std::string>`, and that member's own comment already records the
same breakage under clang with the MSVC standard library on an earlier occasion.

The cause was making the new emptiness predicate an inline MEMBER of two structs
this module exports: that changes what importers materialise from its BMI, and
what it materialised here was a copy constructor that does not compile. `append`,
the operation this one is the sibling of, has been a free function since it was
written. `is_empty` now matches it, and BuildInputs and ConditionalConfig have
exactly the member sets they had before.

Also records where the four new e2e tests run. All declare `# requires: gcc`,
which `run_all.sh` grants only on Linux, so the end-to-end leg is Linux-only and
the cross-dialect coverage -- including the MSVC `/ifcOutput` spelling that no
Linux runner can reach -- is in the unit tests, which run everywhere.
Windows CI could not compile `tests/unit/test_modgraph.cpp`:

  optional:262: error: no matching constructor for initialization of
  '_SMF_control<_Optional_construct_base<basic_string<char,...>>, ...>'

reached through `Manifest` -> `std::map<std::string, Profile>` ->
`Profile::dependencyLinkage`, an `std::optional<std::string>` DATA MEMBER of an
exported struct. `TargetEntry::sysroot`'s comment in the same file already
forbids that shape and prescribes the remedy -- two plain members -- after the
same error on an earlier occasion. `Profile` was the last member in the module
still shaped that way.

Semantics are unchanged: not declared still means "whatever [build] said", which
is what the optional carried and what `dependencyLinkageDeclared` carries now.

WHICH EDIT IN THIS BRANCH TIPS IT IS NOT ESTABLISHED. Two diagnoses were
refuted: that the new emptiness predicate's being an inline member was the cause
(made it a free function; CI failed identically), and that the runner image had
changed (re-ran main's own Windows job unchanged on today's image; it passed).
`test_modgraph.cpp` copies a Manifest by value on main too, so the landmine was
already armed; this removes the type that cannot be copied rather than the
perturbation that reached it.
@Sunrisepeak
Sunrisepeak merged commit 7a4b839 into main Sep 9, 2026
37 checks passed
Sunrisepeak added a commit to Sunrisepeak/xrgui that referenced this pull request Sep 9, 2026
…tires

mcpp 2026.9.9.1 (mcpp-community/mcpp#594) fixes four defects this port had
been working around or was about to be broken by. Taking the version lets three
things go away, and each is deleted here rather than left as dead weight.

A FLOOR, not a preference. src/font/font.ixx uses the private module fragment
`module : private;`, and from 2026.8.18.1 through 2026.9.8.1 mcpp read that as
an implementation partition named ":" -- "file already provides module
mo_yanxi.font\; cannot also provide ':'". A regression from mcpp#433. This
job is green today only because it was pinned below the break, so the whole
range is unusable and the pin has to move past it, not merely forward.

The BOM strip is gone. mcpp now consumes a byte-order mark where bytes become
lines, so the submodules keep theirs and this job no longer rewrites checked-out
sources before building them. MSVC writes a BOM by default; the workaround was
against ordinary input.

The submodule patch is gone, replaced by a `[scan_overrides]` entry in
mcpp/mo_yanxi_vulkan_wrapper/mcpp.toml. That is the escape hatch mcpp documents
for this exact shape (its own example is fmt guarding `import std;` behind
`#ifdef FMT_IMPORT_STD`), and mcpp audits the declaration against the compiler's
P1689 scan every build, so it cannot drift from the source it describes. The
declaration is true on MSVC, where __cpp_lib_stacktrace is defined; this
manifest builds a Windows/MSVC target and does not claim more.

The Windows system libraries move from build.mcpp to
`[target.windows.runtime] libraries`. They were only ever expressed as a C++
program because a conditional table carrying just a `[target.<pred>.runtime]`
was parsed and then discarded -- silently, so the natural spelling looked like
it worked. mcpp#594 applies it and adds unknown-key reporting to `[runtime]`
too. What stays in build.mcpp is vulkan-1, whose SEARCH PATH genuinely depends
on what is installed on the machine and so cannot be written down in advance.

Verified before pushing: xrgui configures clean on Linux for the first time --
1062 compile commands, zero scanner errors. Previously the scan always ended on
either font.ixx or stack_trace.cpp.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants