From 1b2c26d473b85a0c1f3e5ab6d71e0bf32c4efee4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:22:30 +0800 Subject: [PATCH 1/4] docs, examples: an island's entry points arrive in the module's own namespace `docs/42` states one rule for both lanes -- the module name and the namespace are one identifier path -- and the shader lane followed it while `mcpp.tools.island` did not: every entry point was emitted at global scope, so `import app.kernels` bought a file name and nothing else. `mcpp:plugins` 0.5.0 makes the island lane follow it, and this updates the three examples that use the generator and the chapters that describe it. The rule is now stated once, for both lanes, with the leaf identifier as the only difference: the data lane derives it from a file name because a payload has no name of its own, and the island lane takes the entry point's name because the author wrote one. That is also why a file name reaches nothing on the island lane, and why moving a function between two files in one directory renames nothing a consumer wrote. The generator's section moves from 31 to 42. 31's reader is a rule author, and no rule package calls the generator -- `tools/island.cppm` says a project does, from its own build program. What stays in 31 is the one field a rule owes it, `options::flags`, and a link. `docs/README.md`'s concept row moves with it, and `MCPP_EXPORT_C` is now explained in the chapter that row points at rather than named in a table cell. `examples/09-heterogeneous/boundary` gains a second island one directory deeper, because the namespace mirroring the tree is the thing the example exists to show, and it takes `strip_prefix` -- it is the one example whose consumer writes a boundary name directly. `cuda` and `sycl` name two roots and say which supplies the shape; their seams call `kernels::saxpy_device` from inside `namespace app`. Every example pins 0.5.0, so one version of the package is resolved across the tree. Verified against the released mcpp 2026.9.8.1 before the pin was restored: `boundary` prints `6 12 18 24` and `nm` shows one `boundary_saxpy` and no `saxpy`; the CPU legs of `cuda` and `sycl` build and run with the namespaced boundary. `check_docs_structure.sh`, `check_docs_style.sh`, `check_version_pins.sh` and e2e 616 pass. --- ...land-boundary-names-implementation-plan.md | 212 +++++++++ .../docs/2026-09-08-island-boundary-names.md | 437 ++++++++++++++++++ .agents/docs/README.md | 9 +- docs/31-authoring-a-rule-package.md | 51 +- docs/42-heterogeneous-builds.md | 110 ++++- docs/README.md | 4 +- docs/zh/31-authoring-a-rule-package.md | 38 +- docs/zh/42-heterogeneous-builds.md | 91 +++- docs/zh/README.md | 4 +- examples/09-heterogeneous/README.md | 15 +- examples/09-heterogeneous/boundary/README.md | 11 +- examples/09-heterogeneous/boundary/build.mcpp | 28 +- examples/09-heterogeneous/boundary/mcpp.toml | 4 +- .../boundary/src/kernels/saxpy.c | 13 +- .../boundary/src/kernels/vec/scale.c | 15 + .../09-heterogeneous/boundary/src/main.cpp | 20 +- examples/09-heterogeneous/cann/app/mcpp.toml | 2 +- examples/09-heterogeneous/cuda/README.md | 23 +- examples/09-heterogeneous/cuda/app/build.mcpp | 39 +- examples/09-heterogeneous/cuda/app/mcpp.toml | 2 +- .../09-heterogeneous/cuda/app/src/app.cppm | 10 +- examples/09-heterogeneous/hip/app/mcpp.toml | 2 +- .../09-heterogeneous/multi-backend/README.md | 2 +- .../09-heterogeneous/multi-backend/mcpp.toml | 2 +- examples/09-heterogeneous/sycl/app/build.mcpp | 26 +- examples/09-heterogeneous/sycl/app/mcpp.toml | 2 +- .../09-heterogeneous/sycl/app/src/app.cppm | 9 +- .../09-heterogeneous/vulkan/app/mcpp.toml | 2 +- examples/10-graphics/README.md | 2 +- examples/10-graphics/offscreen/mcpp.toml | 2 +- 30 files changed, 1039 insertions(+), 148 deletions(-) create mode 100644 .agents/docs/2026-09-08-island-boundary-names-implementation-plan.md create mode 100644 .agents/docs/2026-09-08-island-boundary-names.md create mode 100644 examples/09-heterogeneous/boundary/src/kernels/vec/scale.c diff --git a/.agents/docs/2026-09-08-island-boundary-names-implementation-plan.md b/.agents/docs/2026-09-08-island-boundary-names-implementation-plan.md new file mode 100644 index 00000000..2db15a1c --- /dev/null +++ b/.agents/docs/2026-09-08-island-boundary-names-implementation-plan.md @@ -0,0 +1,212 @@ +--- +subject: heterogeneous +status: active +--- + +# Implementation plan: the island boundary's names + +Executes [2026-09-08-island-boundary-names.md](2026-09-08-island-boundary-names.md) +across three repositories. One pull request per repository, ordered by what each +one's CI needs to be green. + +## Global constraints + +* `mcpp:plugins` 0.5.0. The floor stays **mcpp 2026.9.8.1** -- nothing here uses + an engine API newer than 0.4.0 already required, so `MCPP_VERSION` in the + plugins CI does not move and neither does any index floor. +* The generated header stays flat, `extern "C"`, free of namespaces, and is what + a device compiler reads. Only the `.cppm` gains namespaces. +* No compatibility flag. The four call sites are updated in the same batch. +* Prose is declarative and free of decoration; no emoji in documentation, in + code comments, or in commit messages. +* Every generated name passes through one sanitiser, shared by both lanes. + +## The order, and why it is not free + +``` + T1..T5 mcpp-plugins PR ──> merge ──> tag v0.5.0 ──> GitHub release + │ + ├─> gtc: CN asset + │ + T6 mcpp-index PR (0.5.0 + latest) + │ + T7..T10 mcpp PR (docs + examples) + │ + T11 sandbox verification +``` + +`examples/09-heterogeneous/boundary` is built by mcpp's own CI +(`.github/tools/build_examples.sh`) and resolves `mcpp:plugins` through the +index, so the mcpp pull request cannot be green until 0.5.0 is published and +indexed. The plugins fixtures use `path = "../.."` and are therefore independent. + +--- + +## T1 -- one sanitiser, shared by both lanes + +**Files:** `src/plugins.cppm` (add `mcpp::plugins::names`), +`rules/spirv.cppm` (call it instead of defining it). + +`identifier`, `split_module_name`, `common_base_dir` and `namespace_of` become +`mcpp::plugins::names::*`. `surface` and the island generator both call them, so +a directory named `2d` or `default` gets one answer rather than two that agree +today by inspection. + +Exported inline functions in this package must not range-for over a +`std::string`: GCC 16 then instantiates `std::string::iterator` in the BMI and +every consumer's build program fails to compile in ``. +Index instead. `tools/island.cppm` records the measurement. + +*Criterion:* `rules/spirv.cppm` contains no definition of `common_base_dir` or +`namespace_of`, and `tests/spirv-module-consumer` still emits +`namespace default_ {`. + +## T2 -- roots, layout root, and namespaced emission + +**Files:** `tools/island.cppm`. + +```cpp +struct options { + std::string module_name; // the module root + std::string out_dir; + std::string produced_by; + bool emit_module = true; + std::string marker = "MCPP_EXPORT_C"; + std::vector roots; // directories, or single files + std::string layout_root; // default: roots.front() + std::vector extensions; // default: device table + C/C++ + std::string strip_prefix; // empty: no short name +}; + +struct entry { + std::string decl; // verbatim, as scanned + std::string name; // the identifier before `(` + std::vector name_space; // segments below the layout root + std::string origin; // the file it was first seen in +}; + +std::optional> scan(const options&); +std::optional emit(std::span, const options&); +entry declared(std::string decl, std::vector ns = {}); // rung L2 +``` + +`scan` walks each root in declaration order, sorted by path, reading files whose +extension is in the set. For each marked declaration it records the entry, its +name and the namespace segments of its directory relative to its own root. An +entry's namespace is the one it has in the layout root; an entry the layout root +does not declare keeps the namespace of the first root that does. + +`emit` writes one flat header and one module. The module groups entries by +namespace path and opens each block once. + +*Criteria:* the four fixtures below. + +## T3 -- the two checks + +**Files:** `tools/island.cppm`. + +* A name declared twice within one root is refused, naming both files and saying + that two implementations of one entry point belong in two roots. +* A name declared in several roots whose declarations differ is refused, naming + both files and both declarations. This is the check 0.4.0 performs; what + changes is that it no longer doubles as the collision check. +* `scan` that finds no marked entry point in any root is an error naming the + roots. An empty module is a misspelled path or a marker that never arrived. + +## T4 -- the short name + +**Files:** `tools/island.cppm`. + +`strip_prefix` non-empty emits `inline constexpr auto = ;` beside +`using ::;`. The short name passes through `names::identifier`. Two entries +stripping to one short name are refused naming both. An entry not carrying the +prefix gets no short name. + +Measured 2026-09-08: `app::kernels::image::blur == &app::kernels::image::opkit_blur` +under clang++ (DPC++ 7.1.0) `-std=c++23`, and the same declarations compile under +GCC 13. + +## T5 -- fixtures and CI + +**Files:** `tests/island-interface/**`, `.github/workflows/ci.yml`, +`README.md`, `mcpp.toml` (0.5.0), `src/plugins.cppm` (the version constant). + +The fixture gains a subdirectory in the layout root and a flat fallback file, so +the namespace, the flat-fallback allowance and the layout root's authority are +all exercised by the shape of the tree rather than by an assertion about it: + +``` +src/kernels/saxpy.c island_saxpy_device -> ::kernels +src/kernels/image/scale.c island_scale_device -> ::kernels::image +src/cpu/ops.c both, flat +``` + +CI steps, each with a denominator: + +1. the module carries `export namespace island_interface::kernels {` and + `export namespace island_interface::kernels::image {`; +2. the header carries neither `namespace` nor any of the short names; +3. the flat fallback is accepted and `image` survives -- moving `src/cpu/ops.c` + to `src/cpu/deep/ops.c` leaves the generated `.cppm` byte-identical; +4. two files in ONE root declaring one name are refused naming both; +5. two entries stripping to one short name are refused naming both; +6. roots that hold no marker are refused naming the roots; +7. the short name and the long name are one entity, and the artifact holds one + symbol for the pair; +8. the 0.4.0 disagreement check still fires, unchanged; +9. the CPU leg (`--no-accel`) reaches the same qualified names. + +## T6 -- mcpp-index + +**Files:** `pkgs/m/mcpp.plugins.lua`. + +A `0.5.0` entry in each of the three platform tables with the release tarball's +sha256, and `["latest"] = { ref = "0.5.0" }` in each. The header comment gains +the paragraph describing what 0.5.0 changes. Existing consumers pin exact +versions, so moving `latest` breaks none of them. + +*Criterion:* `mcpp add mcpp:plugins` in a sandbox resolves 0.5.0, and the +descriptor holds exactly one `["latest"]` per platform table. + +## T7 -- examples + +**Files:** `examples/09-heterogeneous/boundary/**`, +`examples/09-heterogeneous/cuda/app/**`, `examples/09-heterogeneous/sycl/app/**`. + +Each pins `plugins = { version = "0.5.0", ... }`, passes `roots` and a +`layout_root`, and reaches the boundary through the qualified name. `boundary` +additionally shows `strip_prefix`, because it is the example whose whole subject +is the generated interface. + +## T8 -- documentation + +**Files:** `docs/42-heterogeneous-builds.md`, `docs/zh/42-...`, +`docs/31-authoring-a-rule-package.md`, `docs/zh/31-...`, `docs/README.md`, +`docs/zh/README.md`, `examples/09-heterogeneous/boundary/README.md`, +`examples/09-heterogeneous/README.md`. + +The island generator's section moves from 31 to 42, because 31's reader is a +rule author and no shipped rule calls the generator. 31 keeps one sentence and a +link. The name table in 42 gains the island rows. Both languages, with the +section numbering and the cross-references checked. + +## T9 -- the check that keeps the two languages equal + +`.github/tools/` already holds the documentation parity check that CI runs. The +new sections are added to both languages in the same commit, and the check is +run locally before the pull request. + +## T10 -- release + +Tag `v0.5.0`, GitHub release from the tag, `gtc` upload of the same bytes to +GitCode, then T6. + +## T11 -- ecosystem verification + +In an xlings sandbox, with the CN mirror configured for both mcpp and xlings: +resolve `mcpp:plugins@0.5.0` from the index, build and run the `boundary` +example against the published package rather than a path override, and confirm +the qualified names in the generated module. + +*Criterion:* the sandbox is the only thing that verifies the published artifact; +a path override in a working tree verifies the working tree. diff --git a/.agents/docs/2026-09-08-island-boundary-names.md b/.agents/docs/2026-09-08-island-boundary-names.md new file mode 100644 index 00000000..f3906df7 --- /dev/null +++ b/.agents/docs/2026-09-08-island-boundary-names.md @@ -0,0 +1,437 @@ +--- +subject: heterogeneous +status: active +--- + +# The island boundary's names: one rule for both lanes, and the check that makes it true + +A project that puts device code in several directories and several files gets no +help from the names it reaches that code through. `mcpp.tools.island` writes a +module and puts every entry point at global scope, while the shader lane writes +a module whose namespaces mirror the payload tree. This record says why the two +differ today, what the single rule should be, and which check has to exist +before that rule is honest. + +Measured 2026-09-08 against `mcpp` `origin/main` at `9a71c9a6` (engine +2026.9.8.1) and `mcpp:plugins` `origin/main` (0.4.0). + +--- + +## 1. What was measured + +### 1.1 The rule the documentation already states + +`docs/42-heterogeneous-builds.md:225` states it for both lanes at once: + +> The module and the namespace are one identifier path, derived from names the +> project already wrote. + +The data lane obeys it. `rules/spirv.cppm` derives a base directory from the +payload set (`common_base_dir`, :457), takes the path from that base to each +payload as namespace segments (`namespace_of`, :477), and emits **one** module +whose namespaces nest inside it. `shaders/post/tonemap.frag` under +`myapp.shaders` is reached as `myapp::shaders::post::tonemap_frag()`. + +The island lane does not. `tools/island.cppm` emits `export using ::name;` at +global scope, so a consumer that writes `import app.kernels;` then calls +`saxpy_device(...)` unqualified. The module name buys nothing but a file name. + +A second point the same measurement settles: **the data lane does not put +directories in the module name either.** There is one module per group. A design +that gave islands `import pkg.dir1.dir2` would not be aligning with the shader +lane, it would be inventing a third convention. + +### 1.2 Why the island lane has no namespaces today + +Not the C ABI, which both lanes have. The difference is who owns the symbol. + +The data lane's accessors are `extern "C"` as well, but their linker names are +composed by the generator -- `accessor_base` (`src/plugins.cppm:309`) builds +`mcpp_embed___` -- and the C++ +namespace holds an inline forwarder onto that name. The path is IN the symbol, +so two payloads sharing a stem are two symbols. + +An island's symbol is written by the author, in the island's own source, and the +generator only reads it (`entry_name` takes the identifier before the `(`). The +path cannot enter the symbol without rewriting the island's definition, which +needs a C parser and a second compiler's cooperation. + +### 1.3 Three measurements on what a namespace over a flat symbol does + +Run with `clang++` (DPC++ 7.1.0), `-std=c++23`, on 2026-09-08. + +**A namespaced re-export of a C-linkage entity is well-formed.** +`export namespace app::dir1 { using ::compute; }` compiles, exports and links. + +**It provides no isolation.** Two modules, `app.dir1` and `app.dir2`, each +re-exporting `::compute` into its own namespace, linked against one definition: + +``` +dir1=6 dir2=6 same=1 +``` + +where `same` is `&app::dir1::compute == &app::dir2::compute`. Two namespaces, +one entity. + +**A declaration split across two groups is not caught anywhere.** A module whose +header declared `int compute(float, unsigned)` against a definition of +`int compute(double, unsigned)`, exactly one of them in the link: + +``` +LINK: clean +0 (expected 6) +``` + +No compile error, no link error, no diagnostic. This is the defect +`mcpp.tools.island` exists to prevent, and it returns the moment the entry +points of one package are scanned in more than one call. + +--- + +## 2. The rule + +> The module name is the root. Each directory below the group's base directory +> extends the **namespace**. The leaf identifier is decided by the lane: the data +> lane derives it from the file name, because a payload has no name of its own; +> the island lane takes the entry point's name, because the author wrote one. + +| | written | reached as | +|---|---|---| +| data lane | `myapp.shaders` + `shaders/post/tonemap.frag` | `myapp::shaders::post::tonemap_frag()` | +| island lane | `app.kernels` + `img_blur` in `src/kernels/image/blur.cu` | `app::kernels::image::img_blur(...)` | + +One `import` per group on both lanes. One module, one header, namespaces nested +inside. + +This is not a new rule. It is `docs/42:225` applied to the lane that does not +follow it, and the change to the island lane is one emission site. + +## 3. What a developer sees + +``` +roots: src/backends/cuda, src/backends/cpu + +src/backends/cuda/image/blur.cu img_blur, img_sharpen, img_unsharp +src/backends/cuda/image/resize.cu img_resize, img_crop +src/backends/cuda/audio/fft.cu aud_fft, aud_ifft +src/backends/cuda/saxpy.cu saxpy +src/backends/cpu/image/ops.cpp the CPU implementations of the img_* entries +src/backends/cpu/audio/ops.cpp the CPU implementations of the aud_* entries +src/backends/cpu/saxpy.cpp the CPU implementation of saxpy +``` + +```cpp +import app.kernels; + +app::kernels::image::img_blur(...); // blur.cu, and blur's CPU implementation +app::kernels::image::img_resize(...); // resize.cu, same directory, same namespace +app::kernels::audio::aud_fft(...); +app::kernels::saxpy(...); // directly under a root, no segment +``` + +The file name contributes nothing, on either lane and for two independent +reasons. On the data lane it is already spent: it becomes the identifier. On the +island lane a file holds zero, one or many marked entry points and each carries +its own name, so there is nothing for a file name to name. A directory is also +the coarser unit: moving a function between two files in one directory is an +edit developers make freely, and it must not rename anything a consumer wrote. +`ops.cpp` above holds five entry points and names none of them. + +What each implementation of one entry point shares is its **directory** within +its root -- `image/` under `cuda/` and `image/` under `cpu/` -- and not its file +name. + +## 4. The check that makes the namespace true + +A namespace over a flat symbol is a lookup alias (§1.3). It becomes honest only +if a name cannot appear in two of them. So the rule above is admissible only +together with: + +**Uniqueness within a root.** Two entry points with the same name anywhere in +one root are refused, naming both files. With that check a name exists in +exactly one namespace, and the namespace never lies about what a call resolves +to. (§5 says what the several-roots case means and why it is not this one.) + +**Agreement across roots, kept.** The same name in another root is the same +entry point implemented elsewhere. The two declarations must match verbatim, or +the build is refused naming both files -- the check `scan` performs today. + +These two are conflated today. `scan` merges by name and treats textually +identical declarations as agreement, so "the seam's two halves" and "two +different functions that collided" produce the same reading. Separating them is +the substantive repair in this design; the namespaces are what makes the repair +visible. + +## 5. The model: parallel roots, and no implementation is second class + +A first draft of this design named one tree "primary" and called every other +implementation a "variant" with no location of its own. That is wrong on the +face of it: a CPU implementation is a backend, not an annex to one, and the +model forced it out of a `backends/` directory to satisfy the generator. The +model is roots instead. + +**A project declares one or more roots. A name that appears in several roots is +one entry point implemented several times, and an entry point's namespace is the +path it occupies in the root that supplies the shape (below).** + +Two layouts follow, and both are ordinary: + +*Substitution* -- one entry point, several implementations, exactly one in any +link. Each implementation tree is a root, and one entry point's namespace is the +path it occupies inside each of them: + +``` +roots: src/backends/cuda, src/backends/cpu + src/backends/cuda/image/blur.cu img_blur -> app::kernels::image::img_blur + src/backends/cpu/ops.cpp img_blur -> the same entry point +``` + +The CPU tree is flat here and the CUDA tree is organised by subject, which is +allowed and changes nothing: the shape comes from one of them, and the other +only has to define the names. + +*Additive* -- several backends with distinct names, all in one link, chosen at +run time as `examples/09-heterogeneous/multi-backend` does. One root holds them +and the backend directory becomes the namespace: + +``` +root: src/backends + src/backends/cuda/saxpy.cu opkit_cuda_saxpy -> app::kernels::cuda::opkit_cuda_saxpy + src/backends/vulkan/host.cpp opkit_vulkan_saxpy -> app::kernels::vulkan::opkit_vulkan_saxpy + src/backends/cpu/saxpy.cpp opkit_cpu_saxpy -> app::kernels::cpu::opkit_cpu_saxpy +``` + +The CPU implementation stays under `backends/` in both. Which layout a project +has is decided by the names its entry points carry -- one name means +substitution, distinct names mean addition -- and the roots it declares say +which of the two it means. Nothing in the model ranks one implementation above +another. + +One root also supplies the **shape**, and this is a naming role rather than a +rank: every root compiles, links and is equally a backend, and exactly one of +them additionally answers where entry points live. Every other root provides implementations and its directory +structure is never read for naming. `opt.layout_root` names it and defaults to +the first root. + +```cpp +opt.roots = { "src/backends/cuda", "src/backends/cpu" }; +opt.layout_root = "src/backends/cuda"; // the default is roots[0] +``` + +`cuda/image/blur.cu` puts `img_blur` in `app::kernels::image`, and it stays +there however `cpu/` is organised -- one flat file, six directories, or +reorganised next week. An entry point the layout root does not declare takes its +path from the first root that does, which is what a backend-only kernel needs. + +A rejected alternative is worth recording, because it is the obvious one. Let +every root contribute, take the DEEPEST relative directory among an entry +point's implementations, and require the others to be prefixes of it. It accepts +the same layouts as the rule above, and it has a failure this one does not: +moving `cpu/ops.cpp` to `cpu/image/detail/ops.cpp` deepens the winner and +renames `app::kernels::image::img_blur` to `app::kernels::image::detail::img_blur`. +A refactor of a tree nobody consumes would rename what everybody consumes. The +shape has to come from one stated place. + +Two rules then make the namespaces true, and neither constrains a layout: + +* **A name appears at most once per root.** Two files in one root declaring one + name are one symbol, and the build is refused naming both. The message says + what to do: if they are two implementations of one entry point, they belong in + two roots. +* **Declarations of one name across roots must match verbatim**, which is the + check `scan` performs today and the one nothing else in the toolchain can + perform (§1.3). + +## 6. The API + +```cpp +island::options opt; +opt.module_name = "app.kernels"; // the module root; directories extend it + +// one root, additive backends: it is the layout root by default +opt.roots = { root + "/src/backends" }; + +// or several roots, one entry point implemented in each +opt.roots = { root + "/src/backends/cuda", root + "/src/backends/cpu" }; +opt.layout_root = root + "/src/backends/cuda"; // default: roots[0] + +const auto all = island::scan(opt); // reads every root +const auto out = island::emit(*all, opt); // one .cppm, one flat header +``` + +* `scan` takes roots, not a file list. A root is where a namespace path starts, + so it is declared rather than inferred, and the call says what the developer + means: the device code is under here. +* A root may also be a single file, for a project that keeps one implementation + beside another in one directory. The file is then its own root and contributes + no namespace segment. +* It registers `mcpp::rerun_if_changed` for every file it reads and + `mcpp::rerun_if_changed_glob` for the tree, so **adding** a file re-runs the + build program. A file list cannot express that, and the current examples + register the two paths they name and nothing else. +* Files are selected by extension: the device table plus `.c`, `.cc`, `.cpp`, + `.cxx`, overridable through `opt.extensions`. Headers are excluded, and not + only for cost: a project that declares its entry points in a `.cuh` and + defines them in a `.cu` would otherwise hand the uniqueness check two files + for one name and be refused for a layout that is correct. +* The walk is ordered -- files sorted by path, entries within a file in source + order -- so the generated header and module are a function of the tree and not + of the filesystem's enumeration. Without it `write_if_different` sees a + different file on a run that changed nothing, the force-included header's + timestamp moves, and every island translation unit rebuilds. +* The header stays one file, flat, with no namespaces. It is read by a C or a + device compiler, and those have no namespaces to read. The module is a view + onto it. +* Overlapping roots are refused. A file reachable from two roots would have two + namespace paths, and which one it got would depend on the order of the list. +* A `scan` that finds no marked entry point is an error, not an empty + module. A tree named explicitly and yielding nothing is a misspelled path or a + marker that never arrived, and both of those fail later and less clearly -- + the consumer's import resolves to a module that exports nothing. +* A project that wants one module per subtree calls `emit` several times against + one `scan_tree` result, filtered by subtree, with an explicit module name for + each. One scan, so §4 still holds; extra `.cppm` files, no extra headers. Not + the default, because the data lane's default is one module per group. + +### 6.1 The short name, beside the authored one + +An island's symbol is global to the whole program, so an entry point carries a +package prefix whether or not it sits in a namespace -- and the namespace then +repeats what the prefix already said: +`opkit::kernels::image::opkit_blur(...)`. + +`opt.strip_prefix` emits a second spelling beside the first: + +```cpp +export namespace opkit::kernels::image { +using ::opkit_blur; // the authored name; this is the symbol +inline constexpr auto blur = opkit_blur; // the short name, for the call site +} +``` + +Measured 2026-09-08 with `clang++` (DPC++ 7.1.0), `-std=c++23`: both spellings +call one entity (`app::kernels::image::blur == &app::kernels::image::opkit_blur` +is true), and the same declarations compile under GCC 13 outside a module. + +* It needs only the identifier, which is why it fits a generator that does not + parse C. A function-pointer constant is callable, and `constexpr` makes the + indirection disappear. +* Empty by default. A project that wants one spelling gets one. +* The authored name is always emitted and stays canonical. It is what `nm`, a + link error, a profiler, a backtrace and `dlsym` show, and a reader who greps + for the symbol finds the definition. The short name is a convenience at the + call site and nowhere else -- which is the difference between this and the + symbol decoration §10 refuses. +* Two entries stripping to one short name are refused, naming both. The short + name goes through the same `identifier()` sanitiser as every other generated + name (§8), so a prefix that leaves `default` or `2d` behind is handled once. +* An entry that does not carry the prefix gets no short name and no message. It + is visible at the first call site that tries the short spelling. + +## 7. The rule that keeps a name stable across builds + +**The roots must not depend on the accelerator.** `mcpp::device_sources()` is +narrowed by `accel`: under `--no-accel` it is empty. Roots taken from it would +lose the device tree in a CPU-only build, so an entry point would be found only +in the CPU root and a consumer's qualified name would differ between +`mcpp build` and `mcpp build --no-accel`. + +`opt.roots` names directories on disk and is therefore accel-independent by +construction. `accel` continues to decide which files are compiled and linked, +which is a different question and stays where it is (the manifest). + +## 8. Shared derivation, so the lanes cannot drift + +`common_base_dir`, `namespace_of` and the identifier sanitiser (`identifier`, +`src/plugins.cppm`, which handles non-identifier characters, a leading digit and +C++ keywords) move to the `mcpp.plugins` lib root, and both `rules/spirv.cppm` +and `tools/island.cppm` call them. The alignment is then structural: a directory +named `2d` or `default` gets the same answer on both lanes because it is the +same function, not because two documents agree. + +## 9. What changes, and where + +**`mcpp:plugins` 0.5.0.** `tools/island.cppm`: `options::roots`, +`options::layout_root`, `options::extensions`, `options::strip_prefix`, namespaced emission, the +per-root uniqueness check, the prefix-directory check. `src/plugins.cppm`: the three derivations move +in. `rules/spirv.cppm`: calls them instead of defining them. + +**Breaking.** A consumer writes `app::kernels::saxpy_device(...)` where it wrote +`saxpy_device(...)`. Four call sites exist, all in repositories we control: +`examples/09-heterogeneous/{boundary,cuda,sycl}` and +`mcpp-plugins/tests/island-interface`. `tools-island` shipped 2026.9.7.1, so +adoption outside is effectively zero. No compatibility flag: a flag that keeps +the global spelling alive makes the namespace decorative, which is the outcome +this design exists to avoid. + +**`mcpp` documentation.** The island row joins the table in `docs/42` §"The name +a payload arrives under", and the rule in §2 above replaces the two separate +statements. The `mcpp.tools.island` section moves from +`docs/31-authoring-a-rule-package.md` (whose reader is a rule author; no shipped +rule calls the generator, and `tools/island.cppm:7-8` says a project calls it) to +`docs/42`, and `docs/README.md`'s index row moves with it. Both languages. + +## 10. Considered and not done + +**Symbol decoration.** Making `MCPP_EXPORT_C` rewrite the linker name -- through +a `#define` or a GCC/Clang `__asm__` label -- is the only way to make the +namespace true at the symbol level rather than by a check. It is refused: MSVC +has no asm labels, a renamed symbol breaks `dlsym`, a hand-written header and +any non-mcpp C consumer of the same boundary, and the name in the object stops +being the name in the source. §4 buys the same guarantee for the cost of a +comparison. + +**Directories in the module name.** `import pkg.dir1.dir2` is not what the data +lane does (§1.1), multiplies modules with the tree, and -- if it also split the +scan -- would restore the defect measured in §1.3. + +**File names as namespace segments.** A function moved between two files in one +directory would change a consumer's spelling with no ABI change. + +**Generating the boundary from inside a rule.** `mcpp.rules.cuda` could take an +`island_module` option and call the generator itself, which would give the +common case a project with no `scan` call of its own. It is deferred rather +than refused: the module name is a project decision and the rule is the wrong +owner for it, and the shape above has to be used before it is wrapped. + +## 11. Criteria + +Each has a denominator or a construction that distinguishes it from its +negation. + +| criterion | measured | +|---|---| +| the namespace mirrors the tree | fixtures with 0, 1 and 2 directory levels; the emitted `.cppm` carries 0, 1 and 2 namespace segments | +| one module, one header, whatever the tree | `ls /island/*.h \| wc -l` is 1 for the 2-level fixture | +| a name cannot appear in two namespaces | negative fixture: two files in ONE root declare one name; the build fails and the message names both files | +| the layout root alone decides a namespace | move the fallback under a deeper directory; the generated `.cppm` is byte-identical | +| the qualified name does not depend on the build | `examples/09-heterogeneous/cuda` built with and without `--accel`; the generated `.cppm` is byte-identical | +| adding a file re-runs the generator | append a `.cu` to the tree, touch nothing else, rebuild; the new entry point is in the module | +| the two lanes share one derivation | `grep -c "common_base_dir\|namespace_of" rules/spirv.cppm` counts calls, not definitions | +| the short name and the authored name are one entity | `blur == &opkit_blur` in a fixture, and the artifact holds one symbol (`nm \| grep -c blur` is 1) | +| a prefix that collides is refused | negative fixture: two entries strip to one name; the message names both | +| a flatter fallback tree is accepted | fixture: `cuda/image/blur.cu` with `cpu/ops.cpp`; the namespace is `image` | +| a backend-only entry point still has a place | fixture: a name only the second root declares; its namespace is that root's path | +| the disagreement check still fires | the existing `island-interface` negative fixture, unchanged in behaviour | +| the output is a function of the tree, not of the walk | the same tree scanned twice produces byte-identical files, and a no-op rebuild touches neither | +| an empty root set is an error | a fixture whose roots hold sources but no marker; the build fails naming the root | +| two files in ONE root, one name | refused, and the message says the two belong in two roots | +| a CPU implementation keeps its place under `backends/` | the additive fixture's three backends land in three namespaces, none of them moved | + +## 12. Decided in review + +1. **No compatibility flag** (§9). The four call sites are ours and are updated + with the release; a flag that kept the global spelling alive would leave the + namespace decorative, which is the outcome this design exists to avoid. +2. **`variants` is gone.** The primary/variant model ranked a CPU implementation + below a device one and forced it out of a `backends/` directory. Roots + replace it (§5), and a CPU backend is a backend. +3. **The short name ships with this design, not after it** (§6.1). It is purely + additive, so shipping it late would have been safe -- but the stutter it + removes is visible in the first example anyone reads, and the design is being + adopted by its examples in the same batch. +4. **The shape comes from one stated root, and no rule constrains a layout** + (§5). Two drafts were discarded: requiring every implementation to sit at the + same relative directory, which refused an ordinary flat fallback file; and + taking the deepest directory among them, which let a refactor of any tree + rename what every consumer writes. diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 33623f14..03047ff8 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -270 records. +272 records. ## By subject @@ -30,11 +30,18 @@ Records that declare one. Everything else is listed by date below. - [A curriculum for the examples, a reference for the documentation, and a check with a denominator](2026-09-08-examples-curriculum-and-documentation-plan.md) — superseded by [2026-09-08-documentation-architecture-three-trees.md](2026-09-08-documentation-architecture-three-trees.md) - [Three documentation trees, three audiences, and the rule for citing between them](2026-09-08-documentation-architecture-three-trees.md) — active +### heterogeneous + +- [The island boundary's names: one rule for both lanes, and the check that makes it true](2026-09-08-island-boundary-names.md) — active +- [Implementation plan: the island boundary's names](2026-09-08-island-boundary-names-implementation-plan.md) — active + ## By date ### 2026-09 - [The documentation as a book: a chapter-by-chapter design](2026-09-08-the-documentation-as-a-book.md) — active +- [The island boundary's names: one rule for both lanes, and the check that makes it true](2026-09-08-island-boundary-names.md) — active +- [Implementation plan: the island boundary's names](2026-09-08-island-boundary-names-implementation-plan.md) — active - [A curriculum for the examples, a reference for the documentation, and a check with a denominator](2026-09-08-examples-curriculum-and-documentation-plan.md) — superseded by [2026-09-08-documentation-architecture-three-trees.md](2026-09-08-documentation-architecture-three-trees.md) - [Three documentation trees, three audiences, and the rule for citing between them](2026-09-08-documentation-architecture-three-trees.md) — active - [第六轮生态复核:工具平面的目标轴](2026-09-07-round6-ecosystem-review.md) diff --git a/docs/31-authoring-a-rule-package.md b/docs/31-authoring-a-rule-package.md index 6e68733e..dd770f42 100644 --- a/docs/31-authoring-a-rule-package.md +++ b/docs/31-authoring-a-rule-package.md @@ -249,40 +249,23 @@ A bare version is a **choice** a project may override; `>=` is a **requirement** a project may not go below. See *One package, one version* in [23 — The Project Environment](23-the-project-environment.md). -## Generating an island's boundary - -A device translation unit cannot import a module, so the boundary between it and -the C++ side is an `extern "C"` header. `mcpp.tools.island` from `mcpp:plugins` -reads the entry points marked `MCPP_EXPORT_C` out of both implementations and -writes that header and a module over it, so each signature exists once. - -```cpp -mcpp::tools::island::options opt; -opt.module_name = "myapp.kernels"; -opt.out_dir = std::string(mcpp::out_dir()) + "/island"; - -const auto entries = mcpp::tools::island::scan(halves, opt); -const auto out = mcpp::tools::island::emit(*entries, opt); -mcpp::generated(out->interface_file.c_str()); -``` - -Four rungs are available, and each overrides the one above: - -| rung | written by hand | the consumer writes | -|---|---|---| -| L0 | nothing but the marked entry points | `import myapp.kernels` — the island's own C-shaped interface | -| L1 | a seam module over the generated one | `import myapp.saxpy` — the interface the project designed | -| L2 | a seam, plus the entry list passed to `emit` directly | the same, for entry points a scan cannot see | -| L3 | the header and the module | the same, with the signature written twice | - -The generated header reaches the island through -`mcpp::tools::island::force_include_flags`, whose flags go to the **rule** that -drives the device compiler rather than through `mcpp::cxxflag` — forcing a -header into every C++ translation unit puts declarations ahead of a module -interface's `export module` line, which is ill-formed. - -[`examples/09-heterogeneous/boundary`](../examples/09-heterogeneous/boundary/) -is L0 and states what each rung costs. +## An island's boundary is generated, and not by a rule + +`mcpp.tools.island` writes the `extern "C"` header a device translation unit +includes and the module the C++ side imports. It is **not** a rule package and +nothing in `mcpp:plugins` calls it: a project calls it from its own +`build.mcpp`, because the module name and the shape of the namespace are the +project's decisions rather than the rule's. + +What a rule owes it is one field. The generated header reaches the island +through `mcpp::tools::island::force_include_flags`, and those flags go on the +device compiler's own command line -- `options::flags` on `cuda`, `hip`, `sycl` +and `ascendc` -- because that driver inherits nothing from `mcpp::cxxflag`, and +because forcing a header into every C++ translation unit would put declarations +ahead of a module interface's `export module` line, which is ill-formed. + +See [42 — Heterogeneous Builds](42-heterogeneous-builds.md) under *Generating +the boundary* for the generator itself. ## Reporting what the build should know diff --git a/docs/42-heterogeneous-builds.md b/docs/42-heterogeneous-builds.md index ba30b6a1..07d54c5a 100644 --- a/docs/42-heterogeneous-builds.md +++ b/docs/42-heterogeneous-builds.md @@ -223,14 +223,20 @@ the header is an intermediate that no consumer names. #### The name a payload arrives under The module and the namespace are one identifier path, derived from names the -project already wrote. +project already wrote. One rule, both lanes: + +> The module name is the root. Each directory below the group's base extends the +> **namespace**. The leaf identifier is decided by the lane: the data lane +> derives it from the file name, because a payload has no name of its own; the +> island lane takes the entry point's name, because the author wrote one. | Written | Reached as | |---|---| | `[package] name = "myapp"` | module root `myapp` | | `shaders/scale.comp` | `myapp::shaders::scale_comp()` | | `shaders/a/scale.comp` | `myapp::shaders::a::scale_comp()` | -| a `MCPP_EXPORT_C` entry point | `export using ::the_name;` in the boundary module | +| `myapp_saxpy` in `kernels/saxpy.cu` | `myapp::kernels::myapp_saxpy(...)` | +| `myapp_blur` in `kernels/image/blur.cu` | `myapp::kernels::image::myapp_blur(...)` | The root is the **package's** name with non-identifier characters replaced, not its directory's -- the two differ whenever a package sits under a generic folder, @@ -241,10 +247,110 @@ what makes two shaders sharing a stem two things rather than a collision. A project that wants another name passes one; `examples/09-heterogeneous/cuda` does, so its boundary is `app.kernels` beside its seam `app.saxpy`. +**The file name reaches nothing on the island lane, and the reason is in the +table.** A payload has no name, so the data lane has to derive one and needs the +directory to keep two files sharing a stem apart. An entry point already carries +a name its author wrote, and two entry points sharing one are one symbol +whatever directory each sits in -- so a file name would name nothing, and moving +a function between two files in one directory renames nothing a consumer wrote. + The accessor answers with the address and the byte count together. `sizeof` is not merely awkward at this boundary, it is unanswerable: the words may be in an object rather than in an array, and there is then nothing to take the size of. +### Generating the boundary + +The `extern "C"` header and the module over it are mechanical, and +`mcpp.tools.island` from `mcpp:plugins` writes both. A project calls it from its +own `build.mcpp`; it is not a device rule and no rule package uses it. + +**What a project writes is one marker, where the entry point is defined.** + +```c +// src/kernels/saxpy.cu -- no include: the generated header arrives through the +// compiler's forced-include flag, which is also what defines the marker as +// nothing. + +MCPP_EXPORT_C +int app_saxpy(float a, const float* x, const float* y, float* out, unsigned n) +{ ... } +``` + +`MCPP_EXPORT_C` names the mechanism rather than the domain: what is marked is +exported across a generated boundary, with C linkage. It expands to nothing -- +the generated header defines it -- so it deliberately does not end in `_API`, a +suffix that conventionally expands to a visibility attribute. It is +configurable through `options::marker`. + +The marker selects. An island has internal functions, and a generator that +exported whatever a file contained would make the boundary an accident of that +file's contents. + +```cpp +mcpp::tools::island::options opt; +opt.module_name = "app.kernels"; +opt.out_dir = std::string(mcpp::out_dir()) + "/island"; +opt.roots = { root + "/src/kernels", root + "/src/cpu" }; +opt.layout_root = root + "/src/kernels"; // the default is roots.front() +opt.strip_prefix = "app_"; // optional, see below + +const auto entries = mcpp::tools::island::scan(opt); +const auto out = mcpp::tools::island::emit(*entries, opt); +mcpp::generated(out->interface_file.c_str()); +``` + +**A root is a tree, and one of them supplies the shape.** Each root holds one +implementation of the boundary, and several roots mean one entry point +implemented several times -- which is the ordinary shape of a seam, where +exactly one implementation is in any link. The layout root's directories are +what extend the namespace; every other root only has to define the same names, +so a fallback tree may be one flat file and may be reorganised without renaming +anything a consumer wrote. It is a naming role and not a rank: every root +compiles, links, and is equally an implementation. + +Roots are directories on disk and must stay independent of the accelerator. A +root taken from `mcpp::device_sources()` would be narrowed to nothing under +`--no-accel`, and an entry point's namespace would then come from the fallback +tree -- so a consumer's qualified name would differ between two builds of one +project. + +**Two refusals, and they answer different questions.** One name declared twice +in ONE root is a collision: C language linkage does not mangle, so those are one +symbol, and a namespace that appeared to separate them would promise an +isolation the linker does not provide. One name in SEVERAL roots is one entry +point implemented several times, and the declarations must then agree verbatim. +The second is the check nothing else in the toolchain can perform; the first is +what makes the namespace honest. + +**`strip_prefix` is a spelling, not a second entity.** An island's symbol is +global to the whole program, so an entry point carries a package prefix whether +or not it sits in a namespace, and the namespace then repeats it. The option +emits `inline constexpr auto blur = app_blur;` beside `using ::app_blur;`. The +authored name stays canonical -- it is the symbol, and it is what `nm`, a link +error, a profiler and `dlsym` show. + +**Four rungs, and each overrides the one above:** + +| rung | written by hand | the consumer writes | +|---|---|---| +| L0 | nothing but the marked entry points | `import app.kernels` — the island's own C-shaped interface | +| L1 | a seam module over the generated one | `import app.saxpy` — the interface the project designed | +| L2 | a seam, plus entries built with `island::declared` | the same, for entry points a scan cannot see | +| L3 | the header and the module | the same, with the signature written twice | + +The generated header reaches the island through +`mcpp::tools::island::force_include_flags`, whose flags go to the **rule** that +drives the device compiler rather than through `mcpp::cxxflag` — forcing a +header into every C++ translation unit puts declarations ahead of a module +interface's `export module` line, which is ill-formed. A host implementation +compiled by mcpp itself has no such command line and writes one ordinary +`#include` of the generated header. + +[`examples/09-heterogeneous/boundary`](../examples/09-heterogeneous/boundary/) +is L0 and states what each rung costs; `.../cuda` and `.../sycl` are L1; +`.../hip`, `.../vulkan` and `.../cann` keep the header by hand, so the two can +be read side by side. + ## Compiling an island The command that invokes a device compiler is not built into mcpp. It is diff --git a/docs/README.md b/docs/README.md index 70206d3c..09eaffcf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -140,9 +140,9 @@ token in front of a reader to the chapter that owns it. | | chapter | | chapter | |---|---|---|---| | what mcpp is, and the guarantee | [00](00-what-mcpp-is.md) | `import std`, module interfaces, BMIs | [20](20-toolchains.md), [30](30-build-mcpp.md) | -| `mcpp::action`, a build program | [30](30-build-mcpp.md) | a rule package, `MCPP_EXPORT_C` | [31](31-authoring-a-rule-package.md) | +| `mcpp::action`, a build program | [30](30-build-mcpp.md) | a rule package | [31](31-authoring-a-rule-package.md) | | a target triple, the support matrix | [21](21-the-target-triple.md) | a runner, a named runner | [41](41-devices.md) | -| an island, a seam, `accel` | [42](42-heterogeneous-builds.md) | a descriptor, an index | [11](11-publishing-a-library.md) | +| an island, a seam, `accel`, `MCPP_EXPORT_C` | [42](42-heterogeneous-builds.md) | a descriptor, an index | [11](11-publishing-a-library.md) | | an ABI tag, a prebuilt artifact | [12](12-binary-distribution.md) | exit codes, JSON output | [50](50-machine-output.md) | | an `xim:` payload, `[xlings.workspace]` | [32](32-authoring-a-payload.md), [23](23-the-project-environment.md) | a `compat:` adapter, `runtime.library_dirs` | [33](33-authoring-an-adapter.md) | diff --git a/docs/zh/31-authoring-a-rule-package.md b/docs/zh/31-authoring-a-rule-package.md index a291575e..fabeaa33 100644 --- a/docs/zh/31-authoring-a-rule-package.md +++ b/docs/zh/31-authoring-a-rule-package.md @@ -224,37 +224,19 @@ device link 在引擎侧的全部内容:N 个 `artifact` action,其输出不进 裸版本是项目可以覆盖的**选择**;`>=` 是项目不得低于的**要求**。见 [23 —— 项目环境](23-the-project-environment.md) 的*一个包一个版本*。 -## 生成岛的边界 +## 岛的边界是生成的,而且不由规则生成 -设备翻译单元不能 import 模块,所以它与 C++ 侧之间的边界是一个 `extern "C"` 头。 -`mcpp:plugins` 的 `mcpp.tools.island` 从两个实现里读出标了 `MCPP_EXPORT_C` 的 -入口点,写出那个头以及它之上的模块,于是每个签名只存在一份。 +`mcpp.tools.island` 写出设备翻译单元包含的 `extern "C"` 头,以及 C++ 侧 import 的 +模块。它**不是**规则包,`mcpp:plugins` 里也没有任何东西调用它:调用它的是工程自己的 +`build.mcpp`,因为模块名与命名空间的形状是工程的决定而不是规则的。 -```cpp -mcpp::tools::island::options opt; -opt.module_name = "myapp.kernels"; -opt.out_dir = std::string(mcpp::out_dir()) + "/island"; - -const auto entries = mcpp::tools::island::scan(halves, opt); -const auto out = mcpp::tools::island::emit(*entries, opt); -mcpp::generated(out->interface_file.c_str()); -``` - -可用的有四级,每一级覆盖上一级: - -| 级 | 手写的部分 | 消费者写 | -|---|---|---| -| L0 | 只有被标记的入口点 | `import myapp.kernels` —— 岛自己的 C 形状接口 | -| L1 | 生成模块之上的一个接缝模块 | `import myapp.saxpy` —— 项目设计的接口 | -| L2 | 接缝,外加直接传给 `emit` 的入口点列表 | 同上,用于 scan 看不见的入口点 | -| L3 | 头文件与模块都手写 | 同上,签名写了两遍 | - -生成的头经由 `mcpp::tools::island::force_include_flags` 到达岛,而这些旗标交给 -驱动设备编译器的那条**规则**,不走 `mcpp::cxxflag` —— 把一个头强制灌进每个 C++ -翻译单元,会让声明出现在模块接口的 `export module` 之前,那是非良构的。 +规则欠它的只有一个字段。生成的头经由 `mcpp::tools::island::force_include_flags` +到达岛,而这些旗标要落在设备编译器自己的命令行上 —— `cuda`、`hip`、`sycl` 与 +`ascendc` 的 `options::flags` —— 因为那个驱动不继承 `mcpp::cxxflag` 的任何东西, +也因为把一个头强制灌进每个 C++ 翻译单元会让声明出现在模块接口的 `export module` +之前,那是非良构的。 -[`examples/09-heterogeneous/boundary`](../../examples/09-heterogeneous/boundary/) -是 L0,并写明了每一级的代价。 +生成器本身见 [42 —— 异构硬件构建](42-heterogeneous-builds.md) 的*生成这个边界*。 ## 报告构建应当知道的事 diff --git a/docs/zh/42-heterogeneous-builds.md b/docs/zh/42-heterogeneous-builds.md index 6ca6757c..4984fa95 100644 --- a/docs/zh/42-heterogeneous-builds.md +++ b/docs/zh/42-heterogeneous-builds.md @@ -182,14 +182,20 @@ mcpp 2026.9.7.1 之前,那条 lane 是唯一的例外:生成的头文件**就是 #### 载荷到达时的名字 -模块名与命名空间是同一条标识符路径,由工程已经写下的名字推导。 +模块名与命名空间是同一条标识符路径,由工程已经写下的名字推导。一条规则,两条 lane +共用: + +> 模块名是根。组基准目录以下的每层目录延长**命名空间**。末端标识符由 lane 决定: +> 数据 lane 从文件名推导,因为载荷没有自己的名字;岛 lane 取入口点的名字,因为 +> 作者写了一个。 | 写下的 | 到达时的样子 | |---|---| | `[package] name = "myapp"` | 模块根 `myapp` | | `shaders/scale.comp` | `myapp::shaders::scale_comp()` | | `shaders/a/scale.comp` | `myapp::shaders::a::scale_comp()` | -| 一个 `MCPP_EXPORT_C` 入口点 | 边界模块里的 `export using ::the_name;` | +| `kernels/saxpy.cu` 里的 `myapp_saxpy` | `myapp::kernels::myapp_saxpy(...)` | +| `kernels/image/blur.cu` 里的 `myapp_blur` | `myapp::kernels::image::myapp_blur(...)` | 根取自**包名**(非标识符字符替换掉),不是目录名 —— 包放在一个通用目录下时两者不同, 而 mcpp 从 2026.9.7.1 起把包名报给构建程序,正是为了这一条。文件名的 stem 与 stage @@ -198,9 +204,90 @@ mcpp 2026.9.7.1 之前,那条 lane 是唯一的例外:生成的头文件**就是 `examples/09-heterogeneous/cuda` 就传了,于是它的边界是 `app.kernels`,与接缝 `app.saxpy` 同根。 +**岛 lane 上文件名什么都不变成,理由就在这张表里。** 载荷没有名字,所以数据 lane +必须推导一个,并且需要目录把两个同 stem 的文件分开。入口点已经带着作者写下的名字, +而两个同名入口点无论各自在哪个目录都是同一个符号 —— 于是文件名不命名任何东西,把一个 +函数在同一目录的两个文件之间搬动也不会改掉消费者写下的名字。 + 访问器同时回答地址与字节数。在这个边界上 `sizeof` 不只是别扭,而是**答不出来**: 那些字可能放在一个对象里而不是一个数组里,那时根本没有数组可以取 size。 +### 生成这个边界 + +`extern "C"` 头以及它之上的模块都是机械的,`mcpp:plugins` 的 `mcpp.tools.island` +把两者都写出来。它由工程在自己的 `build.mcpp` 里调用;它不是设备规则,`mcpp:plugins` +里也没有任何规则用它。 + +**工程要写的只有一个标记,写在入口点被定义的地方。** + +```c +// src/kernels/saxpy.cu —— 没有 include:生成的头经由编译器的强制包含旗标到达, +// 而那个头同时把这个标记定义成空。 + +MCPP_EXPORT_C +int app_saxpy(float a, const float* x, const float* y, float* out, unsigned n) +{ ... } +``` + +`MCPP_EXPORT_C` 命名的是机制而不是领域:被标记的东西以 C 链接跨越一个生成出来的 +边界被导出。它展开为空 —— 由生成的头定义 —— 所以它刻意不以 `_API` 结尾,那个后缀 +按惯例展开为一个可见性属性。标记名可以由 `options::marker` 配置。 + +标记做的是选择。岛有自己的内部函数,而一个把文件里所有东西都导出的生成器,会让边界 +变成那个文件恰好包含了什么的意外结果。 + +```cpp +mcpp::tools::island::options opt; +opt.module_name = "app.kernels"; +opt.out_dir = std::string(mcpp::out_dir()) + "/island"; +opt.roots = { root + "/src/kernels", root + "/src/cpu" }; +opt.layout_root = root + "/src/kernels"; // 默认取 roots.front() +opt.strip_prefix = "app_"; // 可选,见下 + +const auto entries = mcpp::tools::island::scan(opt); +const auto out = mcpp::tools::island::emit(*entries, opt); +mcpp::generated(out->interface_file.c_str()); +``` + +**根是一棵树,其中一个根提供形状。** 每个根持有这个边界的一份实现,多个根意味着 +同一个入口点被实现了多次 —— 这正是接缝的常见形态,任何一次链接里只有一份实现在场。 +layout root 的目录才延长命名空间;其余的根只需要定义同样的名字,所以回退树可以是 +一个平铺文件,也可以被重新组织而不改掉消费者写下的任何名字。这是一个命名上的角色, +不是等级:每个根都参与编译与链接,都同样是一份实现。 + +根是磁盘上的目录,并且必须与加速器无关。取自 `mcpp::device_sources()` 的根在 +`--no-accel` 下会被收窄成空,入口点的命名空间就会改从回退树来 —— 同一个工程的两次 +构建会给出不同的限定名。 + +**两条拒绝,回答的是不同的问题。** 同一个名字在**一个**根里出现两次是冲突:C 语言 +链接不做名字修饰,所以那是同一个符号,而看起来把它们分开的命名空间会承诺一个链接器 +并不提供的隔离。同一个名字出现在**多个**根里是同一个入口点的多份实现,这时它们的 +声明必须逐字一致。后者是工具链里没有别的东西能做的检查;前者是让命名空间不说谎的 +那一条。 + +**`strip_prefix` 是一种拼法,不是第二个实体。** 岛的符号对整个程序是全局的,所以 +入口点无论是否落在命名空间里都带着包前缀,而命名空间随后又把它重复一遍。这个选项在 +`using ::app_blur;` 旁边写出 `inline constexpr auto blur = app_blur;`。作者写下的 +名字仍然是规范名 —— 它是符号,也是 `nm`、链接错误、profiler 与 `dlsym` 显示的东西。 + +**四级,每一级覆盖上一级:** + +| 级 | 手写的部分 | 消费者写 | +|---|---|---| +| L0 | 只有被标记的入口点 | `import app.kernels` —— 岛自己的 C 形状接口 | +| L1 | 生成模块之上的一个接缝模块 | `import app.saxpy` —— 项目设计的接口 | +| L2 | 接缝,外加用 `island::declared` 构造的条目 | 同上,用于 scan 看不见的入口点 | +| L3 | 头文件与模块都手写 | 同上,签名写了两遍 | + +生成的头经由 `mcpp::tools::island::force_include_flags` 到达岛,而这些旗标交给驱动 +设备编译器的那条**规则**,不走 `mcpp::cxxflag` —— 把一个头强制灌进每个 C++ 翻译单元, +会让声明出现在模块接口的 `export module` 之前,那是非良构的。由 mcpp 自己编译的宿主 +实现没有这样一条命令行,它写一行普通的 `#include`。 + +[`examples/09-heterogeneous/boundary`](../../examples/09-heterogeneous/boundary/) +是 L0 并写明了每一级的代价;`.../cuda` 与 `.../sycl` 是 L1;`.../hip`、`.../vulkan` +与 `.../cann` 保留手写的头,于是两者可以对照着读。 + ## 编译一个岛 调用设备编译器的那条命令不内置在 mcpp 里,而是由**构建规则包**提供 —— diff --git a/docs/zh/README.md b/docs/zh/README.md index e5a8257e..f3b7dc06 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -136,9 +136,9 @@ | | 章节 | | 章节 | |---|---|---|---| | mcpp 是什么,以及那条保证 | [00](00-what-mcpp-is.md) | `import std`、模块接口、BMI | [20](20-toolchains.md)、[30](30-build-mcpp.md) | -| `mcpp::action`、构建程序 | [30](30-build-mcpp.md) | 规则包、`MCPP_EXPORT_C` | [31](31-authoring-a-rule-package.md) | +| `mcpp::action`、构建程序 | [30](30-build-mcpp.md) | 规则包 | [31](31-authoring-a-rule-package.md) | | 目标三元组、支持矩阵 | [21](21-the-target-triple.md) | runner、具名 runner | [41](41-devices.md) | -| 岛、接缝、`accel` | [42](42-heterogeneous-builds.md) | 描述符、索引 | [11](11-publishing-a-library.md) | +| 岛、接缝、`accel`、`MCPP_EXPORT_C` | [42](42-heterogeneous-builds.md) | 描述符、索引 | [11](11-publishing-a-library.md) | | ABI tag、预建产物 | [12](12-binary-distribution.md) | 退出码、JSON 输出 | [50](50-machine-output.md) | | `xim:` 载荷、`[xlings.workspace]` | [32](32-authoring-a-payload.md)、[23](23-the-project-environment.md) | `compat:` 适配包、`runtime.library_dirs` | [33](33-authoring-an-adapter.md) | diff --git a/examples/09-heterogeneous/README.md b/examples/09-heterogeneous/README.md index 93eda6d1..29e31ab2 100644 --- a/examples/09-heterogeneous/README.md +++ b/examples/09-heterogeneous/README.md @@ -80,12 +80,17 @@ them does not rebuild from scratch. seam are each entry point's signature stated a second time, at the one place a disagreement is invisible: C language linkage does not mangle, and an island and its host fallback are never in one link. `mcpp.tools.island` reads the marked -declarations out of both implementations and writes that header and a module -over it, so the signatures exist once. `boundary/`, `cuda/` and `sycl/` take -that route; `hip/`, `vulkan/` and `cann/` keep the header written by hand, so -the two can be read side by side. [`boundary/`](boundary/) states what each +declarations out of every implementation tree and writes that header and a +module over it, so the signatures exist once. `boundary/`, `cuda/` and `sycl/` +take that route; `hip/`, `vulkan/` and `cann/` keep the header written by hand, +so the two can be read side by side. [`boundary/`](boundary/) states what each rung costs. +The generated names arrive in the boundary module's own namespace, extended by +the directories of the tree that supplies the shape -- +`app::kernels::image::app_blur` — which is the rule the shader lane already +followed. `docs/42` states it once for both lanes. + **A rule package, which brings its own environment.** Every vendor spelling — `--cuda-gpu-arch`, `-gencode`, `--target-env`, `-fsycl-targets`, `-fsycl-link`, `--cce-aicore-arch` — lives in `mcpp:plugins`, a package the project depends on @@ -99,7 +104,7 @@ selects it and the accelerator it serves, so a project writes one edge and no ```toml [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-cuda", "tools-island"], host-module = true } ``` `multi-backend/` is the one example here that also pins a version, and it does diff --git a/examples/09-heterogeneous/boundary/README.md b/examples/09-heterogeneous/boundary/README.md index d8079da6..0f6ff7d6 100644 --- a/examples/09-heterogeneous/boundary/README.md +++ b/examples/09-heterogeneous/boundary/README.md @@ -5,7 +5,7 @@ contains no seam, no header, and no hand-written `.cppm`. ``` cd examples/09-heterogeneous/boundary -mcpp run # 12 24 36 48 +mcpp run # 6 12 18 24 ``` Read this before [`../cuda`](../cuda/). It isolates the boundary between an @@ -57,8 +57,9 @@ and nothing about the boundary changes when that happens. | L2 | a seam, and the entry list passed to `emit` directly | `import app.saxpy` | the same, for entry points a scan cannot see | | L3 — `../hip`, `../vulkan`, `../cann` | the header and the module | `import app.saxpy` | the same, with the signature written twice | -**What L0 does not have.** The interface is C-shaped: `saxpy_device(2.0f, x, y, -out, 4)` rather than a span. And there is no place for a +**What L0 does not have.** The interface is C-shaped: +`boundary::kernels::saxpy(2.0f, x, y, out, 4)` rather than a span. And there is +no place for a `cfg(accelerator = ...)` section to apply, because a seam is the single point at which one implementation is exchanged for another. A project with one island and one backend can stop here; a project that will swap backends needs L1. @@ -75,7 +76,9 @@ the module wrapper. |---|---| | the consumer imports the generated module and names no header | `grep -rn '#include' src/` is empty | | the project has no hand-written `.cppm` | the file list above | -| the result is the family's | `12 24 36 48` | +| a directory below the root extends the namespace | `boundary::kernels::vec::scale` resolves, and `vec` appears in no source of this project | +| the short name and the authored one are one entity | `nm` on the artifact shows `boundary_saxpy` once and no `saxpy` | +| the result is the family's, halved by the second island | `6 12 18 24` | The first two are why this example exists. `mcpp:plugins` records that a consumer importing the generated module links against an implementation diff --git a/examples/09-heterogeneous/boundary/build.mcpp b/examples/09-heterogeneous/boundary/build.mcpp index bddbf356..8b42edcf 100644 --- a/examples/09-heterogeneous/boundary/build.mcpp +++ b/examples/09-heterogeneous/boundary/build.mcpp @@ -5,24 +5,28 @@ import mcpp.tools.island; // The lowest rung: the generated boundary module IS the interface, and this // project contains no seam and no header. // -// Both halves are handed to `scan` unconditionally. They are the only place the -// entry points' signatures are written, and the generator is the only point at -// which both texts exist at once -- C language linkage does not mangle, so two -// halves that declared one name differently would link cleanly and read the -// arguments by whichever signature each was compiled with. +// ONE ROOT, AND IT IS A TREE. `scan` reads every marked entry point under it +// and the directories below it extend the namespace, so `src/kernels/vec` is +// reached as `boundary::kernels::vec`. A root is also what makes ADDING a file +// re-run this program: declared inputs are hashed contents, and a new file +// changes none of them, so the root is registered as a glob as well. +// +// A single root is also the layout root, so nothing here has to say which tree +// decides where entry points live. `../cuda` has two and says so. int main() { - const std::string root = mcpp::manifest_dir(); - const std::vector halves{ - root + "/src/kernels/saxpy.c", - }; - for (auto const& f : halves) mcpp::rerun_if_changed(f.c_str()); - mcpp::tools::island::options opt; opt.module_name = "boundary.kernels"; opt.out_dir = std::string(mcpp::out_dir()) + "/island"; opt.produced_by = "the boundary example"; + opt.roots = { std::string(mcpp::manifest_dir()) + "/src/kernels" }; + + // An island's symbol is global to the whole program, so an entry point + // carries a prefix whether or not it sits in a namespace, and the namespace + // then repeats it. This emits `saxpy` beside `boundary_saxpy`; the authored + // name is the symbol and stays canonical. + opt.strip_prefix = "boundary_"; - const auto entries = mcpp::tools::island::scan(halves, opt); + const auto entries = mcpp::tools::island::scan(opt); if (!entries) return 1; const auto out = mcpp::tools::island::emit(*entries, opt); if (!out) return 1; diff --git a/examples/09-heterogeneous/boundary/mcpp.toml b/examples/09-heterogeneous/boundary/mcpp.toml index 5decbd75..2a8d09c9 100644 --- a/examples/09-heterogeneous/boundary/mcpp.toml +++ b/examples/09-heterogeneous/boundary/mcpp.toml @@ -8,7 +8,7 @@ standard = "c++23" # `tools-island` is not a device rule: it claims no extension and names no rule # module, so the edge states `host-module = true` itself. [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["tools-island"], host-module = true } +plugins = { version = "0.5.0", features = ["tools-island"], host-module = true } [build] -sources = ["src/*.cpp", "src/kernels/*.c"] +sources = ["src/*.cpp", "src/kernels/**/*.c"] diff --git a/examples/09-heterogeneous/boundary/src/kernels/saxpy.c b/examples/09-heterogeneous/boundary/src/kernels/saxpy.c index 491ab608..6fbbb24f 100644 --- a/examples/09-heterogeneous/boundary/src/kernels/saxpy.c +++ b/examples/09-heterogeneous/boundary/src/kernels/saxpy.c @@ -1,17 +1,22 @@ /* The island. An ordinary C file here, so this example runs on any machine; substituting a `.cu` and a device rule is what `../cuda` shows. What makes this an island is the property the boundary exists for: it is compiled - separately, it cannot import a module, and its interface is `extern "C"`. */ + separately, it cannot import a module, and its interface is `extern "C"`. + + The entry points carry a package prefix because the symbol they define is + global to the whole program. The namespace they arrive in does not change + that, which is why `strip_prefix` exists: `boundary::kernels::saxpy` is the + short spelling of `boundary_saxpy`, and both name one entity. */ static const char* g_ran_on = ""; MCPP_EXPORT_C -int saxpy_device(float a, const float* x, const float* y, - float* out, unsigned n) { +int boundary_saxpy(float a, const float* x, const float* y, + float* out, unsigned n) { for (unsigned i = 0; i < n; ++i) out[i] = a * x[i] + y[i]; g_ran_on = "cpu (this example's island is an ordinary C file)"; return 0; } MCPP_EXPORT_C -const char* saxpy_device_name(void) { return g_ran_on; } +const char* boundary_ran_on(void) { return g_ran_on; } diff --git a/examples/09-heterogeneous/boundary/src/kernels/vec/scale.c b/examples/09-heterogeneous/boundary/src/kernels/vec/scale.c new file mode 100644 index 00000000..4d2e2451 --- /dev/null +++ b/examples/09-heterogeneous/boundary/src/kernels/vec/scale.c @@ -0,0 +1,15 @@ +/* A second island, one directory deeper, and the whole reason this directory + exists: the generated namespace mirrors the tree. `vec/` is what puts this + entry point in `boundary::kernels::vec` while the file above it is in + `boundary::kernels`. + + The file name reaches no name. An island holds zero, one or many marked + entry points and each carries its own -- which is the difference from the + shader lane, where a file name becomes the identifier because a payload has + no name of its own. */ + +MCPP_EXPORT_C +int boundary_scale(float a, float* out, unsigned n) { + for (unsigned i = 0; i < n; ++i) out[i] = a * out[i]; + return 0; +} diff --git a/examples/09-heterogeneous/boundary/src/main.cpp b/examples/09-heterogeneous/boundary/src/main.cpp index dd4e03fc..7bbe3339 100644 --- a/examples/09-heterogeneous/boundary/src/main.cpp +++ b/examples/09-heterogeneous/boundary/src/main.cpp @@ -5,8 +5,10 @@ import std; // build directory by `mcpp.tools.island` during this build. // // What arrives is the island's own interface -- pointers and a count, an -// `extern "C"` signature. `../cuda` adds a hand-written seam over exactly this -// module and turns it into spans; the README states what each rung costs. +// `extern "C"` signature -- in the namespace the module name spells, extended +// by the directories under the root. `../cuda` adds a hand-written seam over +// exactly this module and turns it into spans; the README states what each +// rung costs. import boundary.kernels; int main() { @@ -14,11 +16,19 @@ int main() { const float y[]{ 10, 20, 30, 40 }; float out[4]{}; - if (saxpy_device(2.0f, x, y, out, 4) != 0) { - std::println(std::cerr, "saxpy_device failed"); + // `saxpy` is the short spelling of `boundary_saxpy`, which is the symbol. + // Both are exported and both name one entity. + if (boundary::kernels::saxpy(2.0f, x, y, out, 4) != 0) { + std::println(std::cerr, "boundary_saxpy failed"); + return 1; + } + // One directory deeper, so one namespace deeper. Nothing in this call + // names a file. + if (boundary::kernels::vec::scale(0.5f, out, 4) != 0) { + std::println(std::cerr, "boundary_scale failed"); return 1; } for (int i = 0; i < 4; ++i) std::print("{}{}", out[i], i == 3 ? "\n" : " "); - std::println("ran on: {}", saxpy_device_name()); + std::println("ran on: {}", boundary::kernels::ran_on()); return 0; } diff --git a/examples/09-heterogeneous/cann/app/mcpp.toml b/examples/09-heterogeneous/cann/app/mcpp.toml index 27625bc0..10f55a02 100644 --- a/examples/09-heterogeneous/cann/app/mcpp.toml +++ b/examples/09-heterogeneous/cann/app/mcpp.toml @@ -14,7 +14,7 @@ import_std = true # imports it as `mcpp.rules.ascendc`. `[build-dependencies]`, because a rule # package's library must never reach the target while its rule is wanted. [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-ascendc"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-ascendc"], host-module = true } # NO [xlings.workspace]. `mcpp.rules.ascendc` declares `xim:cann-toolkit` # itself, gated on this accelerator and on the feature that selects the rule -- diff --git a/examples/09-heterogeneous/cuda/README.md b/examples/09-heterogeneous/cuda/README.md index 68e5b0cc..0df5c13b 100644 --- a/examples/09-heterogeneous/cuda/README.md +++ b/examples/09-heterogeneous/cuda/README.md @@ -39,13 +39,20 @@ from mcpp's toolchain. **That interface is generated, and the signatures exist once.** Each entry point is marked with `MCPP_EXPORT_C` where it is defined; `mcpp.tools.island` -reads the marked declarations out of both implementations and writes the header -the island's compiler reads and the module the seam imports. A hand-written -header states each signature a second time, at the one boundary where a -disagreement is invisible: C language linkage does not mangle and the two halves -are never in one link, so two that disagreed would build cleanly and the -artifact would read its arguments by whichever signature it was compiled with. -The generator is handed both halves and refuses that there. +reads the marked declarations out of both implementation trees and writes the +header the island's compiler reads and the module the seam imports. A +hand-written header states each signature a second time, at the one boundary +where a disagreement is invisible: C language linkage does not mangle and the +two halves are never in one link, so two that disagreed would build cleanly and +the artifact would read its arguments by whichever signature it was compiled +with. The generator sees both texts at once and refuses there. + +`build.mcpp` names two roots -- `src/kernels` and `src/cpu` -- and says the +first supplies the shape. The entry points then arrive as `app::kernels::…`, +which is the boundary module's own path, and the seam calls them from inside +`namespace app` as `kernels::saxpy_device`. The CPU root only has to define the +same names: it may be organised however it likes, and reorganising it renames +nothing this seam or any consumer wrote. `examples/09-heterogeneous/hip` keeps the hand-written header for the contrast. Everything else about the two examples is the same computation, so the @@ -91,7 +98,7 @@ The rule names it. This project writes one edge and no payload list at all: ```toml [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-cuda", "tools-island"], host-module = true } ``` `mcpp.rules.cuda` declares nvcc, cudart, cuRAND's headers, CCCL and the driver diff --git a/examples/09-heterogeneous/cuda/app/build.mcpp b/examples/09-heterogeneous/cuda/app/build.mcpp index 8ce24a18..985763eb 100644 --- a/examples/09-heterogeneous/cuda/app/build.mcpp +++ b/examples/09-heterogeneous/cuda/app/build.mcpp @@ -3,12 +3,12 @@ import mcpp; import mcpp.tools.island; import mcpp.rules.cuda; -// THE BOUNDARY IS GENERATED, AND BOTH HALVES ARE READ. +// THE BOUNDARY IS GENERATED, AND EVERY ROOT IS READ. // // A seam has two implementations of one `extern "C"` boundary -- the island and -// the host fallback -- and exactly one of them is in any link. Both are handed -// to `scan` unconditionally: both exist on disk in either build, and which one -// is compiled is the manifest's decision, which this program does not repeat. +// the host fallback -- and exactly one of them is in any link. Both trees are +// named unconditionally: both exist on disk in either build, and which one is +// compiled is the manifest's decision, which this program does not repeat. // // What that buys is a check nothing else performs. C language linkage does not // mangle and the two halves never meet at the link, so two that declared one @@ -32,22 +32,39 @@ import mcpp.rules.cuda; // unit, including `src/app.cppm`, and a module interface unit must begin with // `export module`. Declarations ahead of that line are ill-formed. static std::optional generate_boundary() { - const std::vector halves{ - std::string(mcpp::manifest_dir()) + "/src/kernels/saxpy.cu", - std::string(mcpp::manifest_dir()) + "/src/cpu/saxpy.cpp", - }; - for (auto const& f : halves) mcpp::rerun_if_changed(f.c_str()); + const std::string root = std::string(mcpp::manifest_dir()); mcpp::tools::island::options opt; // Named rather than defaulted, so this project's modules share one root: // `app.saxpy` is the seam and `app.kernels` is the boundary under it. The // default derives from the package name, which would give - // `cuda_saxpy.kernels`. + // `cuda_saxpy.kernels`. It is also the NAMESPACE the entry points arrive + // in: `app::kernels::saxpy_device`. opt.module_name = "app.kernels"; opt.out_dir = std::string(mcpp::out_dir()) + "/island"; opt.produced_by = "the cuda example"; - const auto entries = mcpp::tools::island::scan(halves, opt); + // TWO ROOTS, AND THE FIRST OF THEM SUPPLIES THE SHAPE. + // + // Each is a tree holding one implementation of this boundary, and exactly + // one of them is in any link. Both are named unconditionally: both exist on + // disk in either build, and which one is compiled is the manifest's + // decision rather than a condition this program repeats. + // + // `layout_root` is where entry points LIVE -- the directories under it + // extend the namespace -- and the CPU root only has to define the same + // names. That is what lets `src/cpu` be one flat file against a device tree + // organised however it grows, and why reorganising it renames nothing a + // consumer wrote. It is a naming role and not a rank: both roots compile, + // link, and are equally an implementation. + opt.roots = { root + "/src/kernels", root + "/src/cpu" }; + opt.layout_root = root + "/src/kernels"; + + // NO `strip_prefix` HERE, and `../boundary` has one. This project has a + // seam, so no consumer ever writes a boundary name; the shorter spelling + // buys nothing that `app::saxpy` does not already give. + + const auto entries = mcpp::tools::island::scan(opt); if (!entries) return std::nullopt; auto out = mcpp::tools::island::emit(*entries, opt); if (!out) return std::nullopt; diff --git a/examples/09-heterogeneous/cuda/app/mcpp.toml b/examples/09-heterogeneous/cuda/app/mcpp.toml index c9da8914..befa71ee 100644 --- a/examples/09-heterogeneous/cuda/app/mcpp.toml +++ b/examples/09-heterogeneous/cuda/app/mcpp.toml @@ -23,7 +23,7 @@ default = "llvm@22.1.8" # `extern "C"` boundary the island's compiler reads and the module the seam # imports, so neither signature is stated twice. [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-cuda", "tools-island"], host-module = true } # The driver's userspace library, reached through an index package that owns # the one hop mcpp needs: a directory on the artifact's runtime search path. diff --git a/examples/09-heterogeneous/cuda/app/src/app.cppm b/examples/09-heterogeneous/cuda/app/src/app.cppm index 6c5dd997..e539bdf8 100644 --- a/examples/09-heterogeneous/cuda/app/src/app.cppm +++ b/examples/09-heterogeneous/cuda/app/src/app.cppm @@ -11,6 +11,10 @@ import std; // declarations in the two implementations. No global module fragment and // no header: this seam imports the boundary the same way any consumer // would, and each declaration exists once, beside its definition. +// +// The names arrive in `app::kernels`, which is the boundary module's own path. +// This file is already inside `namespace app`, so `kernels::` is what is left +// to write. import app.kernels; export namespace app { @@ -21,14 +25,14 @@ std::optional> saxpy(float a, std::span x, std::span y) { if (x.size() != y.size()) return std::nullopt; std::vector out(x.size()); - if (saxpy_device(a, x.data(), y.data(), out.data(), - static_cast(x.size())) != 0) + if (kernels::saxpy_device(a, x.data(), y.data(), out.data(), + static_cast(x.size())) != 0) return std::nullopt; return out; } // The seam answers this too, because the seam is the only place that knows // which island was linked. -std::string_view device_name() { return saxpy_device_name(); } +std::string_view device_name() { return kernels::saxpy_device_name(); } } // namespace app diff --git a/examples/09-heterogeneous/hip/app/mcpp.toml b/examples/09-heterogeneous/hip/app/mcpp.toml index 7cba9239..9b14faa4 100644 --- a/examples/09-heterogeneous/hip/app/mcpp.toml +++ b/examples/09-heterogeneous/hip/app/mcpp.toml @@ -20,7 +20,7 @@ import_std = true default = "llvm@22.1.8" [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-hip"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-hip"], host-module = true } # The driver's userspace library. HIP reaches the device through the CUDA # runtime here, so this is the same one hop the CUDA consumer needs: mcpp's diff --git a/examples/09-heterogeneous/multi-backend/README.md b/examples/09-heterogeneous/multi-backend/README.md index bd61e58d..1c211566 100644 --- a/examples/09-heterogeneous/multi-backend/README.md +++ b/examples/09-heterogeneous/multi-backend/README.md @@ -93,7 +93,7 @@ saying it after the vocabulary grows. ```toml [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-cuda", "rules-spirv"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-cuda", "rules-spirv"], host-module = true } ``` Two rules, in one build program, which is what an additive-backend package diff --git a/examples/09-heterogeneous/multi-backend/mcpp.toml b/examples/09-heterogeneous/multi-backend/mcpp.toml index 42cd5ae1..3e5164ae 100644 --- a/examples/09-heterogeneous/multi-backend/mcpp.toml +++ b/examples/09-heterogeneous/multi-backend/mcpp.toml @@ -40,7 +40,7 @@ default = "llvm@22.1.8" # The rules are declared unconditionally because `build.mcpp` imports them # unconditionally -- each returns immediately when its own backend is absent. [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-cuda", "rules-spirv"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-cuda", "rules-spirv"], host-module = true } # ── the payloads, gated on the device they are for ────────────────────────── # diff --git a/examples/09-heterogeneous/sycl/app/build.mcpp b/examples/09-heterogeneous/sycl/app/build.mcpp index c4948a1f..d7b1376a 100644 --- a/examples/09-heterogeneous/sycl/app/build.mcpp +++ b/examples/09-heterogeneous/sycl/app/build.mcpp @@ -3,12 +3,12 @@ import mcpp; import mcpp.tools.island; import mcpp.rules.sycl; -// THE BOUNDARY IS GENERATED, AND BOTH HALVES ARE READ. +// THE BOUNDARY IS GENERATED, AND EVERY ROOT IS READ. // // A seam has two implementations of one `extern "C"` boundary -- the island and -// the host fallback -- and exactly one of them is in any link. Both are handed -// to `scan` unconditionally: both exist on disk in either build, and which one -// is compiled is the manifest's decision, which this program does not repeat. +// the host fallback -- and exactly one of them is in any link. Both trees are +// named unconditionally: both exist on disk in either build, and which one is +// compiled is the manifest's decision, which this program does not repeat. // // What that buys is a check nothing else performs. C language linkage does not // mangle and the two halves never meet at the link, so two that declared one @@ -16,23 +16,27 @@ import mcpp.rules.sycl; // arguments by whichever signature it was compiled with. `scan` is the only // point at which both texts exist at once, and it refuses there. static std::optional generate_boundary() { - const std::vector halves{ - std::string(mcpp::manifest_dir()) + "/src/kernels/saxpy.sycl", - std::string(mcpp::manifest_dir()) + "/src/cpu/saxpy.cpp", - }; - for (auto const& f : halves) mcpp::rerun_if_changed(f.c_str()); + const std::string root = std::string(mcpp::manifest_dir()); mcpp::tools::island::options opt; // Named rather than defaulted, so this project's modules share one root: // `app.saxpy` is the seam and `app.kernels` is the boundary under it. The // default derives from the package name, which would give // `sycl_saxpy.kernels` -- what `examples/09-heterogeneous/vulkan` uses for its - // shaders. + // shaders. It is also the NAMESPACE the entry points arrive in: + // `app::kernels::saxpy_device`. opt.module_name = "app.kernels"; opt.out_dir = std::string(mcpp::out_dir()) + "/island"; opt.produced_by = "the sycl example"; - const auto entries = mcpp::tools::island::scan(halves, opt); + // TWO ROOTS, AND THE FIRST OF THEM SUPPLIES THE SHAPE. The directories + // under `layout_root` extend the namespace; the CPU root only has to define + // the same names, so it may be organised however it likes and reorganising + // it renames nothing a consumer wrote. + opt.roots = { root + "/src/kernels", root + "/src/cpu" }; + opt.layout_root = root + "/src/kernels"; + + const auto entries = mcpp::tools::island::scan(opt); if (!entries) return std::nullopt; auto out = mcpp::tools::island::emit(*entries, opt); if (!out) return std::nullopt; diff --git a/examples/09-heterogeneous/sycl/app/mcpp.toml b/examples/09-heterogeneous/sycl/app/mcpp.toml index a997f2ff..a7396f65 100644 --- a/examples/09-heterogeneous/sycl/app/mcpp.toml +++ b/examples/09-heterogeneous/sycl/app/mcpp.toml @@ -20,7 +20,7 @@ import_std = true default = "llvm@22.1.8" [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-sycl", "tools-island"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-sycl", "tools-island"], host-module = true } # The SYCL runtime, on the artifact's runtime search path. mcpp's private # loader does not consult /usr/lib, so `libsycl.so.9` -- which the rule diff --git a/examples/09-heterogeneous/sycl/app/src/app.cppm b/examples/09-heterogeneous/sycl/app/src/app.cppm index 1e8eeab2..f8538467 100644 --- a/examples/09-heterogeneous/sycl/app/src/app.cppm +++ b/examples/09-heterogeneous/sycl/app/src/app.cppm @@ -11,6 +11,9 @@ import std; // declarations in the two implementations. No global module fragment and // no header: this seam imports the boundary the same way any consumer // would, and each declaration exists once, beside its definition. +// The names arrive in `app::kernels`, the boundary module's own path. This +// file is already inside `namespace app`, so `kernels::` is what is left to +// write. import app.kernels; export namespace app { @@ -21,14 +24,14 @@ std::optional> saxpy(float a, std::span x, std::span y) { if (x.size() != y.size()) return std::nullopt; std::vector out(x.size()); - if (saxpy_device(a, x.data(), y.data(), out.data(), - static_cast(x.size())) != 0) + if (kernels::saxpy_device(a, x.data(), y.data(), out.data(), + static_cast(x.size())) != 0) return std::nullopt; return out; } // The seam answers this too, because the seam is the only place that knows // which island was linked. -std::string_view device_name() { return saxpy_device_name(); } +std::string_view device_name() { return kernels::saxpy_device_name(); } } // namespace app diff --git a/examples/09-heterogeneous/vulkan/app/mcpp.toml b/examples/09-heterogeneous/vulkan/app/mcpp.toml index 6fdd6a11..bfca906b 100644 --- a/examples/09-heterogeneous/vulkan/app/mcpp.toml +++ b/examples/09-heterogeneous/vulkan/app/mcpp.toml @@ -13,7 +13,7 @@ import_std = true # The rule that compiles the shaders lives in the official plugin collection, # selected by its feature; `build.mcpp` imports it as `mcpp.rules.spirv`. [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-spirv"], host-module = true } # The Khronos loader, built by the index rather than taken from the host, and # the adapter that makes the host's own ICDs reachable from a binary running diff --git a/examples/10-graphics/README.md b/examples/10-graphics/README.md index ed9500e5..9144b663 100644 --- a/examples/10-graphics/README.md +++ b/examples/10-graphics/README.md @@ -53,7 +53,7 @@ tells the two apart, which is why it is printed. ```toml [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-spirv"], host-module = true } [build] accel = "vulkan1.2" diff --git a/examples/10-graphics/offscreen/mcpp.toml b/examples/10-graphics/offscreen/mcpp.toml index f3ce5431..09b63ae9 100644 --- a/examples/10-graphics/offscreen/mcpp.toml +++ b/examples/10-graphics/offscreen/mcpp.toml @@ -13,7 +13,7 @@ import_std = true # One edge. `mcpp.rules.spirv` declares the shader compiler it drives, so this # project names no payload for it. [build-dependencies.mcpp] -plugins = { version = "0.4.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.5.0", features = ["rules-spirv"], host-module = true } # The Khronos loader. Unconditional, and the reason is a rule of the engine # rather than a preference: A DEPENDENCY CANNOT BE CONDITIONED ON A LAYER. From 57ad8ef25993823fac9813f8d2428fc74caac2a2 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:56:52 +0800 Subject: [PATCH 2/4] examples: pin mcpp:plugins 0.5.1, and record what shipped 0.5.1 adds the refusal of overlapping roots that the design record states and 0.5.0 shipped without. Every example resolves one version of the package. --- .../docs/2026-09-08-island-boundary-names.md | 18 +++++++++++++++++- examples/09-heterogeneous/README.md | 2 +- examples/09-heterogeneous/boundary/mcpp.toml | 2 +- examples/09-heterogeneous/cann/app/mcpp.toml | 2 +- examples/09-heterogeneous/cuda/README.md | 2 +- examples/09-heterogeneous/cuda/app/mcpp.toml | 2 +- examples/09-heterogeneous/hip/app/mcpp.toml | 2 +- .../09-heterogeneous/multi-backend/README.md | 2 +- .../09-heterogeneous/multi-backend/mcpp.toml | 2 +- examples/09-heterogeneous/sycl/app/mcpp.toml | 2 +- examples/09-heterogeneous/vulkan/app/mcpp.toml | 2 +- examples/10-graphics/README.md | 2 +- examples/10-graphics/offscreen/mcpp.toml | 2 +- 13 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.agents/docs/2026-09-08-island-boundary-names.md b/.agents/docs/2026-09-08-island-boundary-names.md index f3906df7..d821b440 100644 --- a/.agents/docs/2026-09-08-island-boundary-names.md +++ b/.agents/docs/2026-09-08-island-boundary-names.md @@ -418,7 +418,23 @@ negation. | two files in ONE root, one name | refused, and the message says the two belong in two roots | | a CPU implementation keeps its place under `backends/` | the additive fixture's three backends land in three namespaces, none of them moved | -## 12. Decided in review +## 12. What shipped + +`mcpp:plugins` **0.5.0** (2026-09-08) carries §2 through §8 and §6.1. **0.5.1** +carries the refusal of overlapping roots that §6 states and 0.5.0 omitted -- +the shape of defect §10 of the plan warns about, where a requirement folded +into a larger change disappears when that change ships. It also stops a +single-file root from registering a re-run glob over the directory that file +happens to sit in. + +Both are indexed, and `mcpp` pins 0.5.1 across the example tree. The published +0.5.1 was verified in an xlings sandbox against the index rather than a working +tree: a project written inside the sandbox resolved +`registry/data/xpkgs/mcpp-x-plugins/0.5.1`, printed `6 12 18 24`, and its +generated module carried `export namespace sandbox::kernels` and +`export namespace sandbox::kernels::vec`. + +## 13. Decided in review 1. **No compatibility flag** (§9). The four call sites are ours and are updated with the release; a flag that kept the global spelling alive would leave the diff --git a/examples/09-heterogeneous/README.md b/examples/09-heterogeneous/README.md index 29e31ab2..13c70125 100644 --- a/examples/09-heterogeneous/README.md +++ b/examples/09-heterogeneous/README.md @@ -104,7 +104,7 @@ selects it and the accelerator it serves, so a project writes one edge and no ```toml [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-cuda", "tools-island"], host-module = true } ``` `multi-backend/` is the one example here that also pins a version, and it does diff --git a/examples/09-heterogeneous/boundary/mcpp.toml b/examples/09-heterogeneous/boundary/mcpp.toml index 2a8d09c9..496a9ce6 100644 --- a/examples/09-heterogeneous/boundary/mcpp.toml +++ b/examples/09-heterogeneous/boundary/mcpp.toml @@ -8,7 +8,7 @@ standard = "c++23" # `tools-island` is not a device rule: it claims no extension and names no rule # module, so the edge states `host-module = true` itself. [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["tools-island"], host-module = true } +plugins = { version = "0.5.1", features = ["tools-island"], host-module = true } [build] sources = ["src/*.cpp", "src/kernels/**/*.c"] diff --git a/examples/09-heterogeneous/cann/app/mcpp.toml b/examples/09-heterogeneous/cann/app/mcpp.toml index 10f55a02..cf8715d0 100644 --- a/examples/09-heterogeneous/cann/app/mcpp.toml +++ b/examples/09-heterogeneous/cann/app/mcpp.toml @@ -14,7 +14,7 @@ import_std = true # imports it as `mcpp.rules.ascendc`. `[build-dependencies]`, because a rule # package's library must never reach the target while its rule is wanted. [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-ascendc"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-ascendc"], host-module = true } # NO [xlings.workspace]. `mcpp.rules.ascendc` declares `xim:cann-toolkit` # itself, gated on this accelerator and on the feature that selects the rule -- diff --git a/examples/09-heterogeneous/cuda/README.md b/examples/09-heterogeneous/cuda/README.md index 0df5c13b..f17f4637 100644 --- a/examples/09-heterogeneous/cuda/README.md +++ b/examples/09-heterogeneous/cuda/README.md @@ -98,7 +98,7 @@ The rule names it. This project writes one edge and no payload list at all: ```toml [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-cuda", "tools-island"], host-module = true } ``` `mcpp.rules.cuda` declares nvcc, cudart, cuRAND's headers, CCCL and the driver diff --git a/examples/09-heterogeneous/cuda/app/mcpp.toml b/examples/09-heterogeneous/cuda/app/mcpp.toml index befa71ee..6a6fe409 100644 --- a/examples/09-heterogeneous/cuda/app/mcpp.toml +++ b/examples/09-heterogeneous/cuda/app/mcpp.toml @@ -23,7 +23,7 @@ default = "llvm@22.1.8" # `extern "C"` boundary the island's compiler reads and the module the seam # imports, so neither signature is stated twice. [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-cuda", "tools-island"], host-module = true } # The driver's userspace library, reached through an index package that owns # the one hop mcpp needs: a directory on the artifact's runtime search path. diff --git a/examples/09-heterogeneous/hip/app/mcpp.toml b/examples/09-heterogeneous/hip/app/mcpp.toml index 9b14faa4..9f5c3f6a 100644 --- a/examples/09-heterogeneous/hip/app/mcpp.toml +++ b/examples/09-heterogeneous/hip/app/mcpp.toml @@ -20,7 +20,7 @@ import_std = true default = "llvm@22.1.8" [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-hip"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-hip"], host-module = true } # The driver's userspace library. HIP reaches the device through the CUDA # runtime here, so this is the same one hop the CUDA consumer needs: mcpp's diff --git a/examples/09-heterogeneous/multi-backend/README.md b/examples/09-heterogeneous/multi-backend/README.md index 1c211566..1143d001 100644 --- a/examples/09-heterogeneous/multi-backend/README.md +++ b/examples/09-heterogeneous/multi-backend/README.md @@ -93,7 +93,7 @@ saying it after the vocabulary grows. ```toml [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-cuda", "rules-spirv"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-cuda", "rules-spirv"], host-module = true } ``` Two rules, in one build program, which is what an additive-backend package diff --git a/examples/09-heterogeneous/multi-backend/mcpp.toml b/examples/09-heterogeneous/multi-backend/mcpp.toml index 3e5164ae..3961e700 100644 --- a/examples/09-heterogeneous/multi-backend/mcpp.toml +++ b/examples/09-heterogeneous/multi-backend/mcpp.toml @@ -40,7 +40,7 @@ default = "llvm@22.1.8" # The rules are declared unconditionally because `build.mcpp` imports them # unconditionally -- each returns immediately when its own backend is absent. [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-cuda", "rules-spirv"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-cuda", "rules-spirv"], host-module = true } # ── the payloads, gated on the device they are for ────────────────────────── # diff --git a/examples/09-heterogeneous/sycl/app/mcpp.toml b/examples/09-heterogeneous/sycl/app/mcpp.toml index a7396f65..e3f0c5a3 100644 --- a/examples/09-heterogeneous/sycl/app/mcpp.toml +++ b/examples/09-heterogeneous/sycl/app/mcpp.toml @@ -20,7 +20,7 @@ import_std = true default = "llvm@22.1.8" [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-sycl", "tools-island"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-sycl", "tools-island"], host-module = true } # The SYCL runtime, on the artifact's runtime search path. mcpp's private # loader does not consult /usr/lib, so `libsycl.so.9` -- which the rule diff --git a/examples/09-heterogeneous/vulkan/app/mcpp.toml b/examples/09-heterogeneous/vulkan/app/mcpp.toml index bfca906b..cf2204de 100644 --- a/examples/09-heterogeneous/vulkan/app/mcpp.toml +++ b/examples/09-heterogeneous/vulkan/app/mcpp.toml @@ -13,7 +13,7 @@ import_std = true # The rule that compiles the shaders lives in the official plugin collection, # selected by its feature; `build.mcpp` imports it as `mcpp.rules.spirv`. [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-spirv"], host-module = true } # The Khronos loader, built by the index rather than taken from the host, and # the adapter that makes the host's own ICDs reachable from a binary running diff --git a/examples/10-graphics/README.md b/examples/10-graphics/README.md index 9144b663..01a9d080 100644 --- a/examples/10-graphics/README.md +++ b/examples/10-graphics/README.md @@ -53,7 +53,7 @@ tells the two apart, which is why it is printed. ```toml [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-spirv"], host-module = true } [build] accel = "vulkan1.2" diff --git a/examples/10-graphics/offscreen/mcpp.toml b/examples/10-graphics/offscreen/mcpp.toml index 09b63ae9..8c73d179 100644 --- a/examples/10-graphics/offscreen/mcpp.toml +++ b/examples/10-graphics/offscreen/mcpp.toml @@ -13,7 +13,7 @@ import_std = true # One edge. `mcpp.rules.spirv` declares the shader compiler it drives, so this # project names no payload for it. [build-dependencies.mcpp] -plugins = { version = "0.5.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.5.1", features = ["rules-spirv"], host-module = true } # The Khronos loader. Unconditional, and the reason is a rule of the engine # rather than a preference: A DEPENDENCY CANNOT BE CONDITIONED ON A LAYER. From 01645c79e66285b7fef8c24c0989b75c9c6e1f9e Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 9 Sep 2026 01:44:03 +0800 Subject: [PATCH 3/4] examples: pin mcpp:plugins 0.5.2 0.5.2 fixes a defect the cross-platform run of the generator's fixture found: `namespace_of` trimmed a base directory off a file's directory as a string, and on Windows a root stated with forward slashes against a directory iterator appending with the preferred separator left `\image` rather than `image` -- so the root directory became a namespace segment and an entry point landed in `app::kernels::_::image`. The shader lane shares that function and had the defect latent: its Windows fixture keeps every payload in one directory, so a segment was never derived there. `examples/09-heterogeneous/vulkan` and `examples/10-graphics/offscreen` therefore take this pin for a reason of their own. Verified against the published package: `boundary` resolves `mcpp-x-plugins/0.5.2` from the index and prints `6 12 18 24`. --- .../docs/2026-09-08-island-boundary-names.md | 34 ++++++++++++++----- docs/42-heterogeneous-builds.md | 8 +++++ docs/zh/42-heterogeneous-builds.md | 6 ++++ examples/09-heterogeneous/README.md | 2 +- examples/09-heterogeneous/boundary/mcpp.toml | 2 +- examples/09-heterogeneous/cann/app/mcpp.toml | 2 +- examples/09-heterogeneous/cuda/README.md | 2 +- examples/09-heterogeneous/cuda/app/mcpp.toml | 2 +- examples/09-heterogeneous/hip/app/mcpp.toml | 2 +- .../09-heterogeneous/multi-backend/README.md | 2 +- .../09-heterogeneous/multi-backend/mcpp.toml | 2 +- examples/09-heterogeneous/sycl/app/mcpp.toml | 2 +- .../09-heterogeneous/vulkan/app/mcpp.toml | 2 +- examples/10-graphics/README.md | 2 +- examples/10-graphics/offscreen/mcpp.toml | 2 +- 15 files changed, 52 insertions(+), 20 deletions(-) diff --git a/.agents/docs/2026-09-08-island-boundary-names.md b/.agents/docs/2026-09-08-island-boundary-names.md index d821b440..8544338c 100644 --- a/.agents/docs/2026-09-08-island-boundary-names.md +++ b/.agents/docs/2026-09-08-island-boundary-names.md @@ -413,6 +413,8 @@ negation. | a flatter fallback tree is accepted | fixture: `cuda/image/blur.cu` with `cpu/ops.cpp`; the namespace is `image` | | a backend-only entry point still has a place | fixture: a name only the second root declares; its namespace is that root's path | | the disagreement check still fires | the existing `island-interface` negative fixture, unchanged in behaviour | +| overlapping roots are refused | negative fixture: `src` named beside `src/kernels`; the message names both roots | +| the derivation runs on every host | the fixture builds and its namespaces are asserted on macOS and Windows, not only Linux | | the output is a function of the tree, not of the walk | the same tree scanned twice produces byte-identical files, and a no-op rebuild touches neither | | an empty root set is an error | a fixture whose roots hold sources but no marker; the build fails naming the root | | two files in ONE root, one name | refused, and the message says the two belong in two roots | @@ -421,17 +423,33 @@ negation. ## 12. What shipped `mcpp:plugins` **0.5.0** (2026-09-08) carries §2 through §8 and §6.1. **0.5.1** -carries the refusal of overlapping roots that §6 states and 0.5.0 omitted -- -the shape of defect §10 of the plan warns about, where a requirement folded -into a larger change disappears when that change ships. It also stops a +carries the refusal of overlapping roots that §6 states and 0.5.0 omitted. That +is the shape of defect where a requirement folded into a larger change +disappears when that change ships: §6 stated it in prose and §11 gave it no +criterion of its own, so nothing was red when it was absent. It has one now. It also stops a single-file root from registering a re-run glob over the directory that file happens to sit in. -Both are indexed, and `mcpp` pins 0.5.1 across the example tree. The published -0.5.1 was verified in an xlings sandbox against the index rather than a working -tree: a project written inside the sandbox resolved -`registry/data/xpkgs/mcpp-x-plugins/0.5.1`, printed `6 12 18 24`, and its -generated module carried `export namespace sandbox::kernels` and +**0.5.2** carries a defect the cross-platform run found. `mcpp.tools.island` +compiled on all three hosts and had been exercised on one; the first Windows +run of the fixture failed to compile with `no member named 'image' in namespace +'island_interface::kernels'`. `namespace_of` trimmed the base directory off a +file's directory as a STRING, and a root stated with forward slashes against a +directory iterator appending with the preferred separator left `\image` rather +than `image` -- so the root directory became a segment, sanitised to `_`. It +compares components now. + +The shader lane shares that function and had the defect latent: its Windows +fixture keeps every payload in one directory, so a segment was never derived +there. The leg that surfaced it is not the lane it lives on, and a step now +asserts the derivation exists once so the island fixture's cross-platform run +keeps protecting both. + +All three are indexed, and `mcpp` pins 0.5.2 across the example tree. The +published package was verified in an xlings sandbox against the index rather +than a working tree: a project written inside the sandbox resolved +`registry/data/xpkgs/mcpp-x-plugins`, printed `6 12 18 24`, and its generated +module carried `export namespace sandbox::kernels` and `export namespace sandbox::kernels::vec`. ## 13. Decided in review diff --git a/docs/42-heterogeneous-builds.md b/docs/42-heterogeneous-builds.md index 07d54c5a..4ef7aa60 100644 --- a/docs/42-heterogeneous-builds.md +++ b/docs/42-heterogeneous-builds.md @@ -287,6 +287,8 @@ exported whatever a file contained would make the boundary an accident of that file's contents. ```cpp +const std::string root = std::string(mcpp::manifest_dir()); + mcpp::tools::island::options opt; opt.module_name = "app.kernels"; opt.out_dir = std::string(mcpp::out_dir()) + "/island"; @@ -322,6 +324,12 @@ point implemented several times, and the declarations must then agree verbatim. The second is the check nothing else in the toolchain can perform; the first is what makes the namespace honest. +Two configuration errors are refused beside them: roots that overlap, because a +file reachable from both has two namespace paths and which one it got would +depend on the order of the list; and roots that hold no marked entry point at +all, because an empty module fails later and less clearly than a misspelled +path does here. + **`strip_prefix` is a spelling, not a second entity.** An island's symbol is global to the whole program, so an entry point carries a package prefix whether or not it sits in a namespace, and the namespace then repeats it. The option diff --git a/docs/zh/42-heterogeneous-builds.md b/docs/zh/42-heterogeneous-builds.md index 4984fa95..584e5f72 100644 --- a/docs/zh/42-heterogeneous-builds.md +++ b/docs/zh/42-heterogeneous-builds.md @@ -237,6 +237,8 @@ int app_saxpy(float a, const float* x, const float* y, float* out, unsigned n) 变成那个文件恰好包含了什么的意外结果。 ```cpp +const std::string root = std::string(mcpp::manifest_dir()); + mcpp::tools::island::options opt; opt.module_name = "app.kernels"; opt.out_dir = std::string(mcpp::out_dir()) + "/island"; @@ -265,6 +267,10 @@ layout root 的目录才延长命名空间;其余的根只需要定义同样的 声明必须逐字一致。后者是工具链里没有别的东西能做的检查;前者是让命名空间不说谎的 那一条。 +此外还有两种配置错误被拒绝:互相重叠的根 —— 同时可以从两个根到达的文件有两条命名 +空间路径,而它拿到哪一条取决于这个列表的顺序;以及根里一个被标记的入口点都没有 —— +一个什么都不导出的模块,比一个写错的路径在这里被指出来要晚得多、也难懂得多。 + **`strip_prefix` 是一种拼法,不是第二个实体。** 岛的符号对整个程序是全局的,所以 入口点无论是否落在命名空间里都带着包前缀,而命名空间随后又把它重复一遍。这个选项在 `using ::app_blur;` 旁边写出 `inline constexpr auto blur = app_blur;`。作者写下的 diff --git a/examples/09-heterogeneous/README.md b/examples/09-heterogeneous/README.md index 13c70125..9d53ba72 100644 --- a/examples/09-heterogeneous/README.md +++ b/examples/09-heterogeneous/README.md @@ -104,7 +104,7 @@ selects it and the accelerator it serves, so a project writes one edge and no ```toml [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-cuda", "tools-island"], host-module = true } ``` `multi-backend/` is the one example here that also pins a version, and it does diff --git a/examples/09-heterogeneous/boundary/mcpp.toml b/examples/09-heterogeneous/boundary/mcpp.toml index 496a9ce6..1d1eebe1 100644 --- a/examples/09-heterogeneous/boundary/mcpp.toml +++ b/examples/09-heterogeneous/boundary/mcpp.toml @@ -8,7 +8,7 @@ standard = "c++23" # `tools-island` is not a device rule: it claims no extension and names no rule # module, so the edge states `host-module = true` itself. [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["tools-island"], host-module = true } +plugins = { version = "0.5.2", features = ["tools-island"], host-module = true } [build] sources = ["src/*.cpp", "src/kernels/**/*.c"] diff --git a/examples/09-heterogeneous/cann/app/mcpp.toml b/examples/09-heterogeneous/cann/app/mcpp.toml index cf8715d0..93505dcc 100644 --- a/examples/09-heterogeneous/cann/app/mcpp.toml +++ b/examples/09-heterogeneous/cann/app/mcpp.toml @@ -14,7 +14,7 @@ import_std = true # imports it as `mcpp.rules.ascendc`. `[build-dependencies]`, because a rule # package's library must never reach the target while its rule is wanted. [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-ascendc"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-ascendc"], host-module = true } # NO [xlings.workspace]. `mcpp.rules.ascendc` declares `xim:cann-toolkit` # itself, gated on this accelerator and on the feature that selects the rule -- diff --git a/examples/09-heterogeneous/cuda/README.md b/examples/09-heterogeneous/cuda/README.md index f17f4637..37ff5b30 100644 --- a/examples/09-heterogeneous/cuda/README.md +++ b/examples/09-heterogeneous/cuda/README.md @@ -98,7 +98,7 @@ The rule names it. This project writes one edge and no payload list at all: ```toml [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-cuda", "tools-island"], host-module = true } ``` `mcpp.rules.cuda` declares nvcc, cudart, cuRAND's headers, CCCL and the driver diff --git a/examples/09-heterogeneous/cuda/app/mcpp.toml b/examples/09-heterogeneous/cuda/app/mcpp.toml index 6a6fe409..e6dbd5b7 100644 --- a/examples/09-heterogeneous/cuda/app/mcpp.toml +++ b/examples/09-heterogeneous/cuda/app/mcpp.toml @@ -23,7 +23,7 @@ default = "llvm@22.1.8" # `extern "C"` boundary the island's compiler reads and the module the seam # imports, so neither signature is stated twice. [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-cuda", "tools-island"], host-module = true } # The driver's userspace library, reached through an index package that owns # the one hop mcpp needs: a directory on the artifact's runtime search path. diff --git a/examples/09-heterogeneous/hip/app/mcpp.toml b/examples/09-heterogeneous/hip/app/mcpp.toml index 9f5c3f6a..9a00017e 100644 --- a/examples/09-heterogeneous/hip/app/mcpp.toml +++ b/examples/09-heterogeneous/hip/app/mcpp.toml @@ -20,7 +20,7 @@ import_std = true default = "llvm@22.1.8" [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-hip"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-hip"], host-module = true } # The driver's userspace library. HIP reaches the device through the CUDA # runtime here, so this is the same one hop the CUDA consumer needs: mcpp's diff --git a/examples/09-heterogeneous/multi-backend/README.md b/examples/09-heterogeneous/multi-backend/README.md index 1143d001..d06094ec 100644 --- a/examples/09-heterogeneous/multi-backend/README.md +++ b/examples/09-heterogeneous/multi-backend/README.md @@ -93,7 +93,7 @@ saying it after the vocabulary grows. ```toml [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-cuda", "rules-spirv"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-cuda", "rules-spirv"], host-module = true } ``` Two rules, in one build program, which is what an additive-backend package diff --git a/examples/09-heterogeneous/multi-backend/mcpp.toml b/examples/09-heterogeneous/multi-backend/mcpp.toml index 3961e700..9c083ea3 100644 --- a/examples/09-heterogeneous/multi-backend/mcpp.toml +++ b/examples/09-heterogeneous/multi-backend/mcpp.toml @@ -40,7 +40,7 @@ default = "llvm@22.1.8" # The rules are declared unconditionally because `build.mcpp` imports them # unconditionally -- each returns immediately when its own backend is absent. [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-cuda", "rules-spirv"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-cuda", "rules-spirv"], host-module = true } # ── the payloads, gated on the device they are for ────────────────────────── # diff --git a/examples/09-heterogeneous/sycl/app/mcpp.toml b/examples/09-heterogeneous/sycl/app/mcpp.toml index e3f0c5a3..3fc75d80 100644 --- a/examples/09-heterogeneous/sycl/app/mcpp.toml +++ b/examples/09-heterogeneous/sycl/app/mcpp.toml @@ -20,7 +20,7 @@ import_std = true default = "llvm@22.1.8" [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-sycl", "tools-island"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-sycl", "tools-island"], host-module = true } # The SYCL runtime, on the artifact's runtime search path. mcpp's private # loader does not consult /usr/lib, so `libsycl.so.9` -- which the rule diff --git a/examples/09-heterogeneous/vulkan/app/mcpp.toml b/examples/09-heterogeneous/vulkan/app/mcpp.toml index cf2204de..fa752cc0 100644 --- a/examples/09-heterogeneous/vulkan/app/mcpp.toml +++ b/examples/09-heterogeneous/vulkan/app/mcpp.toml @@ -13,7 +13,7 @@ import_std = true # The rule that compiles the shaders lives in the official plugin collection, # selected by its feature; `build.mcpp` imports it as `mcpp.rules.spirv`. [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-spirv"], host-module = true } # The Khronos loader, built by the index rather than taken from the host, and # the adapter that makes the host's own ICDs reachable from a binary running diff --git a/examples/10-graphics/README.md b/examples/10-graphics/README.md index 01a9d080..6c80891b 100644 --- a/examples/10-graphics/README.md +++ b/examples/10-graphics/README.md @@ -53,7 +53,7 @@ tells the two apart, which is why it is printed. ```toml [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-spirv"], host-module = true } [build] accel = "vulkan1.2" diff --git a/examples/10-graphics/offscreen/mcpp.toml b/examples/10-graphics/offscreen/mcpp.toml index 8c73d179..e67d0fa3 100644 --- a/examples/10-graphics/offscreen/mcpp.toml +++ b/examples/10-graphics/offscreen/mcpp.toml @@ -13,7 +13,7 @@ import_std = true # One edge. `mcpp.rules.spirv` declares the shader compiler it drives, so this # project names no payload for it. [build-dependencies.mcpp] -plugins = { version = "0.5.1", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.5.2", features = ["rules-spirv"], host-module = true } # The Khronos loader. Unconditional, and the reason is a rule of the engine # rather than a preference: A DEPENDENCY CANNOT BE CONDITIONED ON A LAYER. From 0e4dde93264510c61df948b501133123b8d7db45 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Wed, 9 Sep 2026 01:46:36 +0800 Subject: [PATCH 4/4] docs(plan): the plan records three releases, not one 0.5.0 carried the design, 0.5.1 the refusal of overlapping roots the design states and 0.5.0 omitted, and 0.5.2 the separator defect the first cross-platform run of the generator's fixture found. --- ...26-09-08-island-boundary-names-implementation-plan.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.agents/docs/2026-09-08-island-boundary-names-implementation-plan.md b/.agents/docs/2026-09-08-island-boundary-names-implementation-plan.md index 2db15a1c..03881f18 100644 --- a/.agents/docs/2026-09-08-island-boundary-names-implementation-plan.md +++ b/.agents/docs/2026-09-08-island-boundary-names-implementation-plan.md @@ -173,7 +173,7 @@ descriptor holds exactly one `["latest"]` per platform table. **Files:** `examples/09-heterogeneous/boundary/**`, `examples/09-heterogeneous/cuda/app/**`, `examples/09-heterogeneous/sycl/app/**`. -Each pins `plugins = { version = "0.5.0", ... }`, passes `roots` and a +Each pins `plugins = { version = "0.5.2", ... }`, passes `roots` and a `layout_root`, and reaches the boundary through the qualified name. `boundary` additionally shows `strip_prefix`, because it is the example whose whole subject is the generated interface. @@ -198,8 +198,11 @@ run locally before the pull request. ## T10 -- release -Tag `v0.5.0`, GitHub release from the tag, `gtc` upload of the same bytes to -GitCode, then T6. +Tag the release, publish it from the tag, `gtc` upload of the same bytes to +GitCode, then T6. This ran three times: 0.5.0 for the design, 0.5.1 for the +refusal of overlapping roots that the design states and 0.5.0 omitted, and +0.5.2 for the separator defect the first cross-platform run of the fixture +found. ## T11 -- ecosystem verification