{{ message }}
0.3.0: one surface for every embedded payload, a rule for Slang, and a generated island boundary - #11
Merged
Merged
Conversation
A consumer of `mcpp.rules.spirv` or `mcpp.tools.embed` had to include a generated header by name. Both now hand their payloads to one generator, `mcpp::plugins::surface`, and a consumer writes `import myapp.shaders;` instead. `mcpp.rules.slang` joins them, producing the same declarations from a different language. WHAT A CONSUMER NAMES The module name and the namespace are one identifier path, and a payload's directory below the globbed tree adds a segment: `shaders/post/tone.comp` becomes `myapp::shaders::post::tone_comp()`. The name is derived from the package unless the project sets `options::module_name`. The interface is a function rather than a variable, because `constexpr` and `extern` are mutually exclusive and a variable could not keep one shape across the ways bytes can be stored. It names no standard-library type: measured with GCC 16.1 on a 1 MB payload, an interface returning `std::span` produced a 1 313 968-byte BMI against 1 808 bytes for the std-free equivalent, and that cost is fixed rather than proportional to the payload -- it is `<span>`'s templates, present whether the payload is 16 KB or 16 MB. The accessors have C language linkage so an ordinary translation unit can define them. Declaring them with module linkage would have needed a module implementation unit, and that unit includes the data headers, which would put the arrays back into the interface's own compilation. THE DEFAULT FOLLOWS THE PROJECT, AND AN OLDER ENGINE KEEPS ITS BEHAVIOUR `[language] modules` decides the surface, reported as `MCPP_LANGUAGE_MODULES`. An engine that does not report it leaves the header surface in place, which is what every consumer of this package had before, so the upgrade needs no project to declare anything. TWO DEFECTS THE NEW FIXTURES FOUND Two shaders with one stem in two directories produced one array, silently. Their generated headers were byte-identical, and GCC's `#pragma once` treats two files with the same size and content as the same file, so the second include did nothing and both accessors returned the first array. The program printed the right magic number twice. The array's name now carries the directory, and the fixture asserts the two pointers differ rather than only that both look like SPIR-V. With no subdirectory the name is unchanged. `tools.embed`'s `<id>_size` counts elements while the surface reports bytes, and `null_terminate` appends a byte that `_size` does not count -- so neither `_size` nor `sizeof` is the answer on its own. Items now carry the size expression their producer knows to be right. ONE PACKAGE, ONE VERSION `mcpp::plugins::version` said `0.1.1` while the package said `0.2.6`, which nothing noticed because nothing read it. Every rule now states it with `mcpp::fact`, and CI compares the two spellings. SLANG A different language, not a second driver for GLSL: its own module system, generics, and targets beyond SPIR-V, for which the Vulkan axis `rules.spirv` reads has no answer. `slangc`'s embedded output names `uint32_t` and `size_t` and includes nothing, which is the defect 0.2.6 fixed for glslang, so this rule writes the public header around it from the start. The size comes from the `_sizeInBytes` constant the compiler emits rather than from `sizeof`. A Slang module package needs no new concept: it is an ordinary mcpp package whose `include_dirs` names its `.slang` directory. `xim:slang` is pinned exactly rather than floored. Slang's version is coupled to no driver, so the rule this package follows would make it a `>=` -- but a `>=` cannot reach xlings on Windows, where cmd.exe reads the `>` as a redirection, and a floor holding on two platforms of three would resolve different payloads for one project depending on the machine that built it. `rules.slang` needs an engine whose device-source table lists `.slang`. A constrained glob's `accel` key does not make a file a device source; the engine's `kDeviceExtensions` does, and before it listed `.slang` the file fell through to the ordinary source scan and was refused.
…kes expressible `MCPP_VERSION` moves to 2026.9.7.1, the release carrying three things this package needs: `.slang` in the device-source extension table, without which `rules.slang` cannot route a file at all; `MCPP_LANGUAGE_MODULES`, which the surface's default reads; and `mcpp.platform.shell`, which escapes a provisioning argument for both cmd.exe and the child's MSVCRT parser. The third restores a constraint. `xim:shaderc` was an exact version on macOS and Windows for one release because the `>` in `>=2026.3` was read by cmd.exe as a redirection and arrived as a packaging failure; the comment in mcpp.toml said both would revert once a released engine escaped the argument. They do here. `xim:slang` becomes a floor for the same reason, and its comment records why the obvious middle ground was rejected: `>=` on the two platforms that tolerated it and an exact version on the third would resolve different payloads for one project depending on the machine that built it, which is how a defect comes to exist on one operating system only.
A consumer now writes one dependency edge and no build program. The two keys are what the engine reads: which device sources a rule compiles, and which module a build program imports to reach it. The engine holds neither this package's name nor any feature spelling, so a third-party rule for the same extension works the same way. tests/spirv-zero-config is the whole path: an edge naming a feature, no host-module, no build.mcpp, and a shader reached through a generated module.
`storage` decides where a payload's bytes live; the surface decides how a
consumer names them. The declarations are identical under all three, so a
project changes the first and no consumer changes -- which is what the three new
fixtures assert by having the same `src/main.cpp` shape.
header a C array in generated source, compiled in (default)
object a section, through `.incbin` in a generated .S
sidecar a file beside the artifact, read at run time
WHICH ONE IS A MEASUREMENT. With GCC 16.1 on 100 payloads of 16 KB each -- an
ordinary compute shader's size -- the header route takes 1.10s against the
object route's 2.17s, because at that size neither has a measurable marginal
cost and the total is decided by how many processes start. The crossover is the
TOTAL embedded byte count rather than the payload count: below about 1 MB the
header route wins, above about 4 MB it loses by an order of magnitude (2.31s
against 0.116s). So `header` stays the default and `object` is what a project
reaches for when it has more payload than that.
Object storage removes the one place the two shader compilers differed. Under it
neither `-mfmt=c` nor `-x --vn` is passed, so glslang and glslc write the same
thing: a bare `.spv`. The wrapper header exists only for the storage that needs
a translation unit.
The assembly is per object format, and the differences are not cosmetic: ELF
takes `.section .rodata`, Mach-O `.section __TEXT,__const` with an underscore
PREFIXED to every symbol, COFF `.section .rdata,"dr"`. Mach-O's underscore is
the one that fails in the quiet direction -- a label without it defines a symbol
the C++ side never resolves, and the link error names the accessor rather than
the missing prefix. `.balign 4` is written because `VkShaderModuleCreateInfo::
pCode` requires it and a section directive alone does not promise it.
MSVC has no GAS and mcpp refuses `.S` under it, so object storage degrades to
header storage there with one warning. The surface is unchanged, so a consumer
compiled either way is the same source.
Sidecar states its cost instead of hiding it: the accessor opens a path relative
to the working directory. `tests/spirv-sidecar` asserts both halves -- found
from the package root, and reported missing when run from `/tmp`, which is the
leg that would otherwise pass against an implementation that had embedded the
payload after all.
…dary Every name a consumer writes is derived, and the derivation was documented nowhere. The table states it end to end, with the three invariants and the defect each one exists to prevent -- including the byte-identical generated headers that GCC's `#pragma once` collapsed into one. The generator is not shader-specific and three members already share it. A rule package outside this collection reaches it by depending on `mcpp:plugins` and activating no feature; whether it should become a package of its own is left open rather than answered by silence.
…wice `surface` generates the whole interface for a data payload, because an address and a size are all there is to decide. `island` generates what is mechanical about a code island, and only that: the `extern "C"` header its compiler includes -- guards and `__cplusplus` dance included -- and the module the C++ side imports. THE DECLARATION EXISTS ONCE. Without this a project writes it twice, in a header and again wherever the C++ side reaches it, and C language linkage does not mangle: two copies that disagree are one symbol, the link is clean, and each side reads the arguments by its own ABI with no compile error and no link error. That is the copy this removes, and it is the worst one available at this boundary. `export using ::name;` IS WHY THERE IS NO C PARSER HERE. The module re-exports identifiers rather than restating signatures, so the generator needs only the name before the `(` and the header stays the only place a signature is written. Measured on both implementations this package supports before anything was built on it: a consumer that imports the module and never includes the header calls the entry point and links against an implementation compiled by a different driver, under GCC 16.1 and clang 22.1.8. The C++ interface is still the project's. A seam that turns raw pointers into something worth calling is a design decision no generator makes well; this removes the boilerplate around the boundary, not the boundary's design. `emit_module = false` emits the header alone for a project that keeps a seam that includes rather than imports. `tests/island-interface` uses a `.c` island rather than a `.cu`, because the generator does not know what compiler produced the object and a fixture needing a vendor toolkit could only run where that toolkit is published. It has two entry points, since a generator that re-exported only the first would satisfy a one-function fixture, and the CI step refuses a module that restated a signature even though such a module would compile. The README states why both generators live in this package rather than in mcpp: the engine's module carries the protocol, and a generator is a library on top of it. Code inside the engine changes only with an engine release, which is the coupling this version's `device_extensions` work exists to remove.
…arker names the mechanism `island::scan` reads the marked declarations out of the island's own source, so the signature sits beside the definition and exists once. `island::emit` still takes a list directly, for entry points a scan cannot see, and a project that wants neither writes its own header and module wrapper. Each layer overrides the one above; the default is the one that keeps the signature in one place. THE MARKER SELECTS, AND THAT IS THE POINT. An island has internal functions, and a generator that exported whatever the file contained would make the boundary an accident of the file's contents. The fixture has a static helper and an unmarked external function, and CI refuses either reaching the generated files -- a check that would otherwise pass, because both files would merely be larger. `MCPP_EXPORT_C` names the mechanism rather than the domain: what is marked is exported across a generated boundary with C linkage, and both halves are in the name. `MCPP_ISLAND_EXPORT` was precise inside docs/20's vocabulary and narrow outside it -- the generator works for any `extern "C"` boundary, and the fixture proves it with a plain `.c`. Anything ending `_API` was rejected outright: that suffix conventionally expands to a visibility attribute and this expands to nothing, so borrowing it would promise what it does not do and collide with a project that later wants the real thing. THE SCAN IS NOT A C PARSER. From the marker it copies verbatim to the parenthesis that closes the parameter list, matching nesting. The fixture's second entry point wraps across three lines for that reason, and CI asserts the flattened declaration, because a line-oriented scan would pass every other check here. A GCC 16 defect was found and worked around while writing it: `for (char c : s)` over a `std::string` inside an exported inline function makes the consumer's build program fail with `inlining failed in call to 'always_inline' ... function body not available` in `<bits/stl_iterator.h>`, naming neither this file nor the loop. Indexing touches no iterator type. The comment at the site records the message so the next person recognises it. The island still includes the generated header, and the README says why that line is not new: a project writes the same include today and writes the header as well. It also earns its place twice, defining the marker and declaring the entry points, so a definition whose signature drifted fails where it was written rather than at the link. Removing it with `-include` is left as an option, since a file whose marker comes from a flag is no longer self-contained.
…did survives
`force_include_flags` returns what makes a compiler read the generated header
before the island's first line -- `-include <path>` for gcc and clang,
`/FI<path>` for MSVC. A project using this generator now has no header in its
source tree and no line naming one; the fixture's island is a `.c` with zero
includes.
The line was defended in the previous commit on the grounds that it kept the
file self-contained. It did not. The header is generated, so an `#include` of it
names a file that does not exist until mcpp writes it, and that translation unit
could not be compiled outside mcpp with or without the line. What the line
actually bought was a name its author never opens.
THE CHECK IT ALSO DID IS STILL THERE, and this was verified rather than assumed.
The compiler still sees the declarations, so a definition whose signature
drifted fails where it was written:
src/kernels/saxpy.c:22:5: error: conflicting types for 'scale_device'
That is only reachable on the `emit` path, where a hand-written list and the
definition are separate things. Under `scan` the header is generated FROM the
definition and the two cannot disagree, so the check is vacuous there -- worth
saying, because a criterion that cannot fail is one this project has paid for
before.
The first attempt at that control reported success for the wrong reason: the
fixture was copied to a scratch directory, its `path = "../.."` dependency
stopped resolving, and the build failed with `has no mcpp.toml` while the check
read "refused". Re-run in place, it fails on the conflicting type.
The lib root carries what members share. `mcpp::plugins::surface` is there because three rules produce the same declarations for a consumer and a fourth copy would drift. Nothing in this collection uses the island generator: a project calls it from its own `build.mcpp`, exactly as it calls `mcpp.tools.embed`. Leaving it in the lib root handed it to every consumer whether or not they asked for it, while `tools-embed` beside it required a feature. It moves to `tools/island.cppm` behind `tools-island`, exporting `mcpp.tools.island`. `all-rules-compile` activates the feature and calls `entry_name`, so the parser is compiled and exercised on every host.
`module_root_from_package` derived the module a consumer imports from the leaf of MCPP_MANIFEST_DIR, because that was the only thing a build program was told about itself. It is a directory name, and the two answers differ whenever a project lays a package out under a generic folder: mcpp's own `examples/09-heterogeneous/vulkan/app/` declares `name = "vulkan-saxpy"` and got `app.shaders`, which every `<something>/app/` in a workspace would also claim. mcpp 2026.9.7.1 answers the right question, so this reads `mcpp::package_name()`. The directory leaf stays only for a value that cannot occur -- a manifest with no `[package] name` does not load -- so an empty answer would mean the contract moved. The fixture could not see the difference. Every project under tests/ had a package name equal to its directory name, so both derivations produced the same string. `spirv-module-consumer` now declares `name = "shader-app"` while keeping its directory, and its consumer imports `shader_app.shaders`; reverting this function makes that import unresolvable, which is the measurement the fixture was missing.
The generator served one island. A seam has two implementations of one `extern "C"` boundary -- a device island and a host fallback -- and exactly one of them is in any link, which is the arrangement every example under `examples/09-heterogeneous` has. Handed only the device half, the generator produced an empty boundary in a `--no-accel` build and the C++ side failed on an unresolved name. `scan` now merges entries by name, so a build program hands it both files unconditionally: both exist on disk in either build, and which one is compiled is the manifest's decision rather than a condition the build program repeats. Two definitions of one name that declare it differently are refused there, naming both files and both signatures. Nothing else catches that. The two halves are never in one translation unit and never in one link, and C language linkage does not mangle, so a build with disagreeing halves is clean and the artifact reads its arguments by whichever signature it was compiled with. `scan` is the only point at which both texts exist at once. The fixture gains the host half under `cfg(not(accelerator = "vulkan"))`, so both legs are built and run, and a CI step perturbs one signature and asserts the refusal names both files. The README paragraph claiming the two could not disagree under `scan` was true only while `scan` took one file.
Every example under `examples/09-heterogeneous` puts a module of the project between the consumer and the island: `app.cppm` imports the boundary and turns pointers and a count back into spans, and it is the one place a backend can be exchanged. The fixture reached the generated module from `src/main.cpp`, which is not a module unit -- so its import said nothing about the case the examples are in, where one module interface written by the project imports a module interface written into the build directory during the same build. `src/app.cppm` is that seam, and `src/main.cpp` now names no boundary symbol at all. Both properties are asserted: a consumer that named `saxpy_device` would mean the seam is not hiding which island is underneath. Both legs build and run with it.
`mcpp.tools.island::force_include_flags` returns the flags that make a compiler read the generated boundary header before the island's first line, and three of the four device rules had no way to deliver them. Measured on `examples/09-heterogeneous/cuda`: the island is compiled by nvcc, or by clang in CUDA mode, on a command line the rule assembles -- a driver mcpp did not invoke, which inherits nothing from `mcpp::cflag` or `mcpp::cxxflag`. The project-wide channels are not merely too wide for the job, they are wrong for it. `cxxflag` forces the header into every C++ translation unit, including the seam `src/app.cppm`, and a module interface unit must begin with `export module` -- declarations ahead of that line are ill-formed. `mcpp.rules.ascendc` already had `options::flags`, appended last. The other three now carry the same field with the same position and the same pass-through-unexamined contract, because one rule with it and three without is the shape where a project's build program works for one backend and not the next -- which is what a seam exists to prevent. Verified end to end on this host: the cuda example's island compiles with `-include .../app.kernels.h` on its own command line, and the artifact runs on an RTX 4080.
The `rules-slang` row said its engine need is `.slang` in the device-source table. That was the first fix and it inverts the dependency; what shipped is the feature declaring `device_extensions` and `rule_module` itself, with `.slang` REMOVED from the engine's table. Its absence from the list of per-member floors is now stated rather than left as a gap, because that absence is the criterion for the whole arrangement. The shared floor moves to 2026.9.7.1, and the two usage snippets pin 0.3.0.
Six sites across four files ran the same character filter -- letters, digits
and `_` survive, everything else becomes `_`, a leading digit gets a `_` --
and none of them asked whether the result is reserved. `default`,
`template`, `operator`, `private` and `union` are valid identifiers to that
filter, and `shaders/default/` is an ordinary name for a shader directory.
Measured: renaming the fixture's `shaders/a` to `shaders/default` makes the
generator write
namespace default {
and the build fails at `expected identifier before 'default'`, inside a
generated file, on a line the author of that directory has never opened.
`surface::identifier` does all three transformations in one place, and a
reserved result gets a TRAILING underscore -- a leading one is itself
reserved at namespace scope, so prefixing would trade one reserved name for
another. Namespace segments in `rules-spirv` and `rules-slang` go through
it, as does `tools-embed`'s accessor name.
The accessor is sanitised where it is EMITTED rather than in each producer.
A rule that builds `item::identifier` from a file stem cannot know it has
produced `my-shader_comp` or `default` until it reaches the one line that
writes the function's name. `accessor_base` and the `_spv` symbols are left
alone: both carry fixed affixes and cannot come out reserved, and routing
them would rename symbols already published.
The fixture could not see any of this: `a` and `b` are ordinary identifiers,
so both filters produce the same file. It gains `shaders/default/`, and CI
asserts the generated interface contains `namespace default_ {`.
The module name is the one part of this surface a project writes itself,
and each of its segments becomes a namespace. `app.my-shaders` produced
`namespace my-shaders {` in a generated file, so the error named a line
nobody wrote and a file the author had never opened.
Refused where it was written, naming the offending segment and the spelling
that would work:
mcpp.plugins.surface: `app.my-shaders` is not a usable module name: the
segment `my-shaders` is not a C++ identifier.
Each segment becomes a namespace, so it has to be one; `my_shaders`
would work.
The predicate is `identifier()` applied to the segment and compared with it,
so the refusal and the derivation cannot disagree about what an identifier
is -- including the keyword case.
`all-rules-compile` asserts EVERY feature is compiled for this host, and
"every" is a list the fixture carries -- so the list is compared against
the package's own features before the build. That extractor was written for
[features]
rules-cuda = { sources = ["rules/cuda.cppm"] }
and 0.3.0 spells them as sub-tables, because a feature now carries
`device_extensions` and `rule_module` as well and TOML 1.0 forbids a
multi-line inline table. `grep -oE '^[a-z-]+ +='` then matched the keys
INSIDE those tables:
package: sources sources sources sources
fixture: rules-ascendc rules-cuda rules-hip rules-slang ...
which reads as "the fixture is wrong" while the fixture is complete and the
check is what broke. A denominator that cannot state the set it measures
against is worse than none: it fails loudly for the wrong reason, and the
next person edits the fixture.
The names come off `[features.<name>]` headers now, and the empty-result
guard says the manifest's spelling changed rather than that the package has
no features. Both copies of the check are replaced; fixing one would leave
the other failing on the next host.
Two shaders differing only by directory used to produce one output -- the
name was the stem and the stage, so `a/x.vert` and `b/x.vert` both gave
`x_vert.h` declaring `x_vert_spv` -- and the rule refused. The shader's
directory is now part of the generated file's path and of the array's name,
so there is nothing to refuse and the step failed with
FAIL: two shaders mapping to one output were accepted
which is the correct new behaviour reported as a defect.
That change was not made for ergonomics: two byte-identical generated
headers collapse under GCC's `#pragma once`, which treats two files of the
same size and content as the same file, so the second include silently did
nothing and both accessors returned one array.
The step now asserts the property that made the refusal unnecessary --
`scale_comp_spv` and `dup_scale_comp_spv` are both declared -- and the
criterion is the generated names rather than the run, because a build that
collapsed them would also produce a working binary.
The refusal stays in the rule. Its condition is now two shaders with one
name in one directory, which a single glob cannot construct and an explicit
`options::base_dir` can. Verified locally against the released 2026.9.7.1
binary.
The step asserts that `tests/island-interface/src/main.cpp` names no boundary symbol, because the seam exists to hide which island is underneath. It was a plain grep, and that file EXPLAINS the property in a comment -- "Nothing here names \`saxpy_device\`" -- so the criterion reported the file for saying what it does. Comment lines are stripped first. The difference is between asking whether the file mentions the name and whether it uses it, and only the second is the property. Measured both ways: `src/main.cpp` has zero uses after stripping and `src/app.cppm`, which is the seam and is supposed to call them, has two.
`spirv-zero-config` explains in its own header that `host-module = true` is not written and not needed, and the step asserting that greps the manifest -- so the fixture was reported for saying what it does. Comment lines are stripped first. The island step's `grep 'include' build.mcpp` had the mirror-image problem: it matched that program's comments and its `mcpp::include_dir` line, so it would have passed with the forced include removed. A criterion that cannot fail is not one. It names `force_include_flags` now, which is the mechanism it is there to protect. Both were found by reading every CI assertion that greps a source file rather than by waiting for the next round trip; the other sixteen are either over program output, which has no comments, or over patterns no comment in those files carries.
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.

0.3.0. One surface for every embedded payload, a rule for Slang, a generator for
an island's
extern "C"boundary, and the two declarations that let a rulepackage introduce a device language without an engine release.
Requires mcpp 2026.9.7.1.
The surface: a payload is reached by importing a module
mcpp::plugins::surfaceis in the lib root because three rules produce the samedeclarations for a consumer and a fourth copy would drift. It writes, from one
description, either a C header or a C++ module interface:
The consumer writes
import myapp.shaders;andmyapp::shaders::scale_comp().Nothing in a project names a generated file, so the rule is free to change where
it writes and how it stores the words.
The interface names no standard-library type, and that is measured rather
than stylistic: an interface carrying
std::spanputs that header's templatesinto every consumer's BMI, at 727 times the size of the std-free form on GCC 16.
The accessor answers with the address and the byte count together.
sizeofis not merely awkward at this boundary, it is unanswerable: under object storage
there is no array to take the size of, and under
null_terminateneithersizeofnor the declared_sizeis the payload's length.From a file name to a call
shaders/a/scale.compbecomesmyapp::shaders::a::scale_comp(). The directorybelow the group's base is part of the namespace, which is what makes two shaders
with one stem in two directories two things rather than a collision -- the
fixture asserts they are DISTINCT objects, because a build that collapsed them
would still print two correct lines. GCC's
#pragma oncetreats two files of thesame size and content as the same file, so before the namespace reached the
symbol name the second include silently did nothing and both accessors returned
one array.
Three storages behind it
header,objectandsidecar, each with a fixture that builds and runs. Thecrossover is around 1 MB: below it the header is faster to compile than the
assembler round trip; above it the compiler's own parse dominates.
mcpp.rules.slangSlang is a language rather than a second driver for GLSL, so it has its own rule
and its own extension.
-source-embed-style u32, with the output named<base>_embed.hbecause slangc appends.hto whatever-onames.Each rule declares what it compiles, and who compiles it
mcpp 2026.9.7.1 reads both. The engine holds no package name, no feature
spelling and no module name, and
.slangis not in its built-in table --tests/slang-consumerbuilds and runs anyway, which is the whole criterion.With the two keys declared, a project that names the feature needs no
build.mcppat all:tests/spirv-zero-confighas neither a build program norhost-module, and asserts both absences.mcpp.tools.island: theextern "C"boundary, generatedA member behind
tools-island, liketools-embed: nothing in this collectionuses it, a project does.
Entry points are marked where they are defined:
scanreads the marked declarations,emitwrites the boundary header theisland's compiler reads and the module the C++ side imports. The module
re-exports NAMES (
export using ::saxpy_device;) rather than restatingsignatures, because a second copy of a signature is the thing this exists to
remove.
A seam has two halves, and
scanis where they are compared. A device islandand a host fallback implement one boundary and exactly one is in any link. Both
are handed to
scanunconditionally; entries merge by name, and two definitionsthat declare one name differently are refused there, naming both files and both
signatures. Nothing else catches that: the two never meet at the link and C
language linkage does not mangle, so a disagreement builds cleanly and the
artifact reads its arguments by whichever signature it was compiled with.
The island writes no
#include.force_include_flagsreturns the flags thatmake the compiler read the generated header before the island's first line. Those
flags now have somewhere to go:
cuda,hipandsyclgained theoptions::flagspass-throughascendcalready had, because the island iscompiled by a driver mcpp did not invoke and inherits nothing from
mcpp::cflag.The project-wide channels are not merely too wide for that job, they are wrong
for it:
cxxflagwould force the header into every C++ translation unit,including a seam
.cppm, and a module interface unit must begin withexport module.The module root is the package's name
module_root_from_packagederived it from the leaf ofMCPP_MANIFEST_DIR--a directory name -- because that was the only thing a build program was told.
mcpp 2026.9.7.1 answers the right question, so it reads
mcpp::package_name().Every fixture had a package name equal to its directory name, so neither
derivation was observable.
tests/spirv-module-consumernow declaresname = "shader-app"while keeping its directory, and importsshader_app.shaders; reverting the function makes that import unresolvable.A generated name may not be a C++ keyword
Six sites ran the same character filter -- letters, digits and
_survive,everything else becomes
_, a leading digit gets a_-- and none asked whetherthe result is reserved.
shaders/default/is an ordinary shader directory name,and it produced
namespace default {in a generated file: an error on a line theauthor of that directory has never opened.
surface::identifierowns all three transformations now, with a TRAILINGunderscore for a keyword because a leading one is itself reserved at namespace
scope. The accessor is sanitised where it is EMITTED rather than in each
producer -- a rule that builds the name from a file stem cannot know it has
produced
my-shader_compordefaultuntil it reaches the line that writes thefunction's name.
A
module_namewhose segment is not an identifier is refused where it waswritten, naming the segment and the spelling that would work, rather than in a
generated file.
Four CI criteria that read text where they meant code
Each of these failed and none was about the package.
all-rules-compileDENOMINATOR read[features]inline keys, and 0.3.0spells features as sub-tables, so it printed
package: sources sources sources sourcesand reported the fixture asincomplete while the extractor was what broke.
the directory is now part of the name, so two shaders sharing a stem are two
payloads. It asserts that instead.
a comment it does not use, so each reported the file for saying what it does.
Comment lines are stripped first now.
grep 'include' build.mcppmatched that program's owncomments and its
mcpp::include_dirline, so it would have passed with theforced include removed. It names
force_include_flags.The last one was found by reading all eighteen assertions that grep a source
file at once, rather than by waiting for another round trip.
Fixtures
Ten, all building and running locally against the 2026.9.7.1 engine:
spirv-consumer,spirv-module-consumer,spirv-zero-config,spirv-object-storage,spirv-sidecar,embed-consumer,embed-module-consumer,island-interface,slang-consumer,all-rules-compile.