Skip to content

0.5.0: an island's entry points arrive in the module's own namespace - #13

Merged
Sunrisepeak merged 1 commit into
mainfrom
feat/island-boundary-names
Sep 8, 2026
Merged

0.5.0: an island's entry points arrive in the module's own namespace#13
Sunrisepeak merged 1 commit into
mainfrom
feat/island-boundary-names

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Implements .agents/docs/2026-09-08-island-boundary-names.md in this repository. The mcpp pull request that updates the documentation and the examples follows this one, because examples/09-heterogeneous/boundary resolves this package through the index.

What was wrong

docs/42 states one rule for both lanes: the module name and the namespace are one identifier path. The shader lane follows it -- myapp.shaders gives myapp::shaders::post::tonemap_frag(). This generator did not: every entry point was emitted at global scope, so import app.kernels bought a file name and nothing else, and a project with device code in several directories got no help from the names it reached that code through.

What changed

A root's directories extend the namespace exactly as a payload tree's do:

src/backends/cuda/image/blur.cu   myapp_blur   ->  myapp::kernels::image::myapp_blur
src/backends/cuda/saxpy.cu        myapp_saxpy  ->  myapp::kernels::myapp_saxpy

One import, one flat header. The header keeps no namespace: it is read by a C or a device compiler, and neither has one to read.

The check is what makes the namespace honest. A namespace over a flat symbol is a lookup alias. Measured with clang++ (DPC++ 7.1.0), -std=c++23: two modules re-exporting one extern "C" name into two namespaces produce two spellings of one entity, &a::f == &b::f. The shader lane escapes this because it composes its own symbols; an island's symbol is written by its author and this generator only reads it. So one name declared twice in one root is refused, naming both files. A name then exists in exactly one namespace.

Roots, not a file list. options::roots names the directories implementations live under; options::layout_root names the one whose structure decides where entry points live and defaults to the first. A file list's common ancestor moves when a file is added, and a consumer's qualified name would move with it. Roots must also not come from mcpp::device_sources(), which accel narrows to nothing under --no-accel.

Every other root only has to define the names, so a fallback tree may be one flat file and may be reorganised without renaming anything a consumer wrote. This is a naming role and not a rank: every root compiles, links and is equally a backend.

options::strip_prefix emits a short spelling beside the authored name, which stays canonical. A constexpr function pointer costs the artifact nothing; the pair is one symbol, asserted with nm.

One sanitiser. common_base_dir and namespace_of had been written in rules/spirv.cppm and again in rules/slang.cppm. They move to mcpp::plugins::names with the identifier sanitiser.

Criteria

The fixture's shape is itself a criterion: src/kernels is the layout root and holds a subdirectory, and src/cpu implements the same two entry points in one flat file. Nine steps, each with a denominator:

criterion how it fails without the change
the module name and the namespace are one path the entry points are at global scope
a directory below the layout root extends it image/scale.c lands in ::kernels
the header is flat a namespace in a header no C compiler can read
the short name is a spelling, not a function a second symbol in every artifact
the CPU leg reaches the same qualified names the .cppm differs between the two legs
reorganising a non-layout root renames nothing the discarded deepest-directory rule passes everything else and fails this
a new island file reaches the boundary declared inputs are hashed contents, so a new file changes none of them
two files in one root, one name accepted, and the linker reports it far from the cause
a short name colliding with an authored one the generated module declares one name twice
roots holding no marker a module exporting nothing
two implementations that disagree unchanged from 0.4.0, and still the only place both texts exist at once

All nine were run locally against the released mcpp 2026.9.8.1 before this was opened, together with every rule module compiles for this host, rules-spirv through the module surface, rules-slang through a consumer and tools-embed through the module surface for the shared-derivation move.

Compatibility

Breaking, deliberately and without a flag. A consumer writes app::kernels::saxpy_device(...) where it wrote saxpy_device(...). tools-island shipped 2026.9.7.1 and the call sites are the three heterogeneous examples in mcpp and this fixture. A flag that kept the global spelling alive would leave the namespace decorative, which is the outcome the design exists to avoid.

The floor does not move: this changes what the package generates, not what it asks the engine for.

`docs/42` states one rule for both lanes -- the module name and the namespace
are one identifier path -- and the shader lane follows it while this generator
did not. Every entry point was emitted at global scope, so `import app.kernels`
bought a file name and nothing else, and a project with device code in several
directories got no help from the names it reached that code through.

The names now follow the rule. A root's directories extend the namespace
exactly as a payload tree's do, and the C++ side reaches
`app::kernels::image::blur` through one import and one flat header.

A namespace over a flat symbol is a lookup alias. Measured with clang++
(DPC++ 7.1.0), -std=c++23: two modules re-exporting one `extern "C"` name into
two namespaces produce two spellings of one entity, `&a::f == &b::f`. The
shader lane does not have this problem because it composes its own symbols; an
island's symbol is written by its author and this generator only reads it. So
the namespaces are admissible only together with a refusal: one name declared
twice in one root is a collision, named with both files. A name then exists in
exactly one namespace and cannot lie about what a call resolves to.

The API says roots rather than files. `options::roots` names the directories
implementations live under and `options::layout_root` names the one whose
structure decides where entry points live. A file list's common ancestor moves
when a file is added, and a consumer's qualified name would move with it; a
root taken from `mcpp::device_sources()` would vanish under `--no-accel` and
the namespace would come from the fallback tree instead. Every other root only
has to define the names, so a fallback may be one flat file and may be
reorganised without renaming anything a consumer wrote.

Two drafts were discarded and are recorded in the design: requiring every
implementation to sit at the same relative directory, which refused an ordinary
flat fallback; and taking the deepest directory among them, which let a
refactor of a tree nobody consumes rename what every consumer writes.

`options::strip_prefix` emits a short spelling beside the authored name, which
stays canonical: an island's symbol is global to the whole program, so an entry
point carries a prefix the namespace then repeats. A `constexpr` function
pointer costs the artifact nothing -- the pair is one symbol, asserted with nm.

`common_base_dir` and `namespace_of` had been written in `rules/spirv.cppm` and
again in `rules/slang.cppm`. They move to `mcpp::plugins::names` with the
identifier sanitiser, so a directory named `default` or `2d` gets one answer
rather than two that agree by inspection.

Also: a scan that finds no marked entry point is an error naming the roots
rather than a module exporting nothing; the walk is ordered, so the generated
files are a function of the tree and not of the filesystem's enumeration; and
each root is registered as a glob, so adding a file re-runs the program.

The floor does not move. This changes what the package generates, not what it
asks the engine for.
@Sunrisepeak
Sunrisepeak merged commit 992a1b4 into main Sep 8, 2026
3 checks passed
@Sunrisepeak
Sunrisepeak deleted the feat/island-boundary-names branch September 8, 2026 17:58
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.

1 participant