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..03881f18 --- /dev/null +++ b/.agents/docs/2026-09-08-island-boundary-names-implementation-plan.md @@ -0,0 +1,215 @@ +--- +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.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. + +## 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 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 + +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..8544338c --- /dev/null +++ b/.agents/docs/2026-09-08-island-boundary-names.md @@ -0,0 +1,471 @@ +--- +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 | +| 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 | +| a CPU implementation keeps its place under `backends/` | the additive fixture's three backends land in three namespaces, none of them moved | + +## 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. 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. + +**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 + +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..4ef7aa60 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,118 @@ 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 +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"; +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. + +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 +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..584e5f72 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,96 @@ 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 +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"; +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..9d53ba72 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.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/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..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.4.0", features = ["tools-island"], host-module = true } +plugins = { version = "0.5.2", 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..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.4.0", 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 68e5b0cc..37ff5b30 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.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/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..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.4.0", 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/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..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.4.0", 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 bd61e58d..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.4.0", 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 42cd5ae1..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.4.0", 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/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..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.4.0", 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/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..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.4.0", 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 ed9500e5..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.4.0", 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 f3ce5431..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.4.0", 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.