{{ message }}
Tags: mcpp-community/mcpp-plugins
Tags
0.4.0: every payload a compile reads is in the build graph (#12) * 0.4.0: the surface becomes pure, and generation becomes an action Every payload a compile reads is now in the build graph. Two mechanisms carry that, and which one applies is decided by WHEN the thing is known. DISCOVERED WHILE THE COMPILER RUNS: a shader's `#include` -------------------------------------------------------- `a.input()` is fixed when `build.mcpp` runs, before the compiler has read a line, so a shader or kernel that includes another file had no edge to it: editing that file rebuilt nothing and the build stayed green over a stale artifact. `grep depfile rules/` returned nothing across all six rules. 每一种拼法都用真实输出验过,不是照着文档写的: glslangValidator --depfile -> out.spv: scale.comp ./common.glsl glslc -MD -MF -> out2.spv: scale.comp common.glsl slangc -depfile -> out.spv: <entry>.slang <included>.slang nvcc -MMD -MF -> n.o : k.cu \ common.cuh clang++ -MMD -MF -> c.o: k.cpp common.cuh bisheng -MMD -MF -> k2.o: k.asc inc.h ascendc 是唯一没有 CI 消费者的 lane,「bisheng 是 clang 血统所以应该支持」这种理由 会把未验证的旗标发进唯一没人检查的规则里。工具包在本机,于是问了它。`-MMD` 而不是 `-MD`:后者在 BiSheng 上实测拉进五十个 `/usr/include` 宿主头,让目标文件依赖共享构建 目录不该携带的绝对路径。 DISCOVERED BY NOBODY: `.incbin` ------------------------------- 汇编器在汇编期打开被内嵌的文件,而生成的 `.S` 自己的文本并不随载荷改变,于是目标文件 只被汇编一次。在已发布的 0.3.0 上、在沙箱里、对着索引解析出来的包实测:改一个 shader, 程序打印的还是上一版载荷的字节数(1480 -> 1480),而 header 存储是 1480 -> 1776。 问汇编器不行,这是实测的:编译器驱动的 `-MD` 是预处理器通道看不见 `.incbin`;GNU as 的 `--MD` 报得出来;clang 的集成汇编器根本没有依赖输出(三种拼法全被拒)。采信它会让这条 依赖在 GCC 上被跟踪、在 Clang 上静默缺失 —— 比两边都缺失更坏。 **修法不是给引擎加一条通道,而是不要在错误的时刻写那个文件。** 一条 `mcpp::action` 声明它的输入;把生成变成 action、载荷作它的声明输入,这条边就是普通的边,用的是引擎 已有的唯一图原语。原型先证后写:`BYTES=64 -> 192`,零引擎改动。 机制逐文件追踪过:载荷 mtime 前移、`.S` 不动(内容没变)、`.o` 前移 —— ninja 因 action 的声明输入变化而重跑它,无 `restat` 于是其 output 视为新的,汇编边随之重跑。去掉载荷 输入,`.o` 不动、字节停住。两个方向都量过。 这要求三件事,而每一件本身都是对的 -------------------------------- 1. **`mcpp.plugins.surface` 只 import `std`。** 它原先读 `mcpp::target_os()`、 `mcpp::compiler()`、`mcpp::package_name()`;现在这些是参数。一个取输入而不读环境的 生成器,在构建程序里和在普通程序里是同一份代码 —— 而 action 的命令必须是程序。 实测:此前这个包**不能**普通构建,`mcpp: failed to read compiled module`。 2. **`mcpp-embed` 由 mcpp 从本包源码造**,走 `tools = ["mcpp-embed"]`,与规则同一条 依赖边。不单独发包:`docs/05` §2.14 写明代价 —— 「工具的版本**就是**依赖的版本, 所以 protoc 与它的 runtime 不匹配这件事不可表达」。生成器与它写下的声明是一个决定。 3. **`src/declare.cppm` 是第二个单元**,承载构建程序侧(`mcpp::action` / `dep_bin` / 按存储分派)。它需要 mcpp 2026.9.8.1:此前一个包的 host module 按**路径**排序, `rules/` 在 `src/` 之前,import 它会失败。 两条 action 而不是一条,这是关于输入的事实而非限制:接口是条目表的函数,实现体还是 载荷的函数。合成一条会让改一个载荷去重写接口、重建每个 import 它的 BMI。 (`mcpp::action::provides` 也是 action 级的,一条 action 三个 output 一个 provides 会让扫描器报 "already provided by" —— 实测。) 只有 `object` 需要这一切。`header` 的字节经由载荷编译器已经写出的数据头(本就是 action 的 output)到达产物,`sidecar` 根本不进编译 —— 都检查过而不是假定。所以默认路径不造 任何工具,不选 object 存储的消费者一个字都不用多写。 其它 ---- `options::store` 改变了形状,所以这是破坏性发布:0.3.x -> 0.4.0。 `storage::object` 要求 `item::payload_path`,现在它拒绝 —— 这条约束一直写在注释里而 没有任何东西执行它。今天只有 spirv 能走到这条路且只有它设这个字段,那是巧合不是保证。 `rules/spirv.cppm` 里 `options::module_name` 的注释说默认值取自「包目录」,而它调用的 函数开头就写着「THE PACKAGE'S NAME. NOT ITS DIRECTORY'S.」。 CI 四条步骤,判据都落在产物的字节上,不落退出码也不落日志行;第四条断言「没要工具就 用 object 存储」被拒绝且点名了要加的键 —— 消息是契约。 * CI: the all-rules denominator counts members, not internal features 这一步比较包里每一个 `[features.<x>]` 与夹具激活的那一列,好让第七个成员不能被悄悄漏出 「every rule module compiles for this host」。`[features.surface]` 让它红了,而且红得对: 那是一个夹具没点名的新 feature。 但它不是成员。它承载 surface 的构建程序那一半,每个成员都 implies 它,消费者永远不写它。 把它加进夹具是错的修法 —— 夹具那一列的含义是「消费者能激活的成员」,为了让检查变绿而往里 填东西,只会让检查的含义变少。 所以分母减去「被别的 feature implies 的」。这条规则从 manifest 推出来而不是列在这里,正是 这个检查原本就有的性质:第七个**成员**仍会被抓到,因为没有东西 implies 它。 两条腿都验过:修正后两列一致;临时加一个新成员,它出现在分母里并会让这一步失败。 * CI: the denominator fix belonged in both jobs, not the first one found 「every rule module compiles for this host」这一步在 `consumers` 与 `rules-cross-platform` 里各有一份。上一次只改了第一份 —— 而那个 job 本来就是绿的; 红的是另一个。 **做这次替换的脚本在锚点「存在」时就放行了。存在不等于唯一,而这个差别就是缺陷本身:** 它验证了自己将要改的东西在,没验证它是唯一的一处。现在按**出现次数**断言,并在写回前 再数一次两个 job 是否都拿到了修好的形状。 顺带把 `<(...)` 换成临时文件:这一步也在 windows-2022 的 Git Bash 上跑,进程替换在那里 是模拟的、不可依赖 —— 而「一个在三台宿主上行为不同的检查」正是这个 job 存在的理由所要 抓的东西,它自己不能引入一个。 --------- Co-authored-by: sunrisepeak <speakshen@163.com>
0.3.0: one surface for every embedded payload, a rule for Slang, and … …a generated island boundary (#11) * 0.3.0: one surface for every embedded payload, and a rule for Slang A consumer of `mcpp.rules.spirv` or `mcpp.tools.embed` had to include a generated header by name. Both now hand their payloads to one generator, `mcpp::plugins::surface`, and a consumer writes `import myapp.shaders;` instead. `mcpp.rules.slang` joins them, producing the same declarations from a different language. WHAT A CONSUMER NAMES The module name and the namespace are one identifier path, and a payload's directory below the globbed tree adds a segment: `shaders/post/tone.comp` becomes `myapp::shaders::post::tone_comp()`. The name is derived from the package unless the project sets `options::module_name`. The interface is a function rather than a variable, because `constexpr` and `extern` are mutually exclusive and a variable could not keep one shape across the ways bytes can be stored. It names no standard-library type: measured with GCC 16.1 on a 1 MB payload, an interface returning `std::span` produced a 1 313 968-byte BMI against 1 808 bytes for the std-free equivalent, and that cost is fixed rather than proportional to the payload -- it is `<span>`'s templates, present whether the payload is 16 KB or 16 MB. The accessors have C language linkage so an ordinary translation unit can define them. Declaring them with module linkage would have needed a module implementation unit, and that unit includes the data headers, which would put the arrays back into the interface's own compilation. THE DEFAULT FOLLOWS THE PROJECT, AND AN OLDER ENGINE KEEPS ITS BEHAVIOUR `[language] modules` decides the surface, reported as `MCPP_LANGUAGE_MODULES`. An engine that does not report it leaves the header surface in place, which is what every consumer of this package had before, so the upgrade needs no project to declare anything. TWO DEFECTS THE NEW FIXTURES FOUND Two shaders with one stem in two directories produced one array, silently. Their generated headers were byte-identical, and GCC's `#pragma once` treats two files with the same size and content as the same file, so the second include did nothing and both accessors returned the first array. The program printed the right magic number twice. The array's name now carries the directory, and the fixture asserts the two pointers differ rather than only that both look like SPIR-V. With no subdirectory the name is unchanged. `tools.embed`'s `<id>_size` counts elements while the surface reports bytes, and `null_terminate` appends a byte that `_size` does not count -- so neither `_size` nor `sizeof` is the answer on its own. Items now carry the size expression their producer knows to be right. ONE PACKAGE, ONE VERSION `mcpp::plugins::version` said `0.1.1` while the package said `0.2.6`, which nothing noticed because nothing read it. Every rule now states it with `mcpp::fact`, and CI compares the two spellings. SLANG A different language, not a second driver for GLSL: its own module system, generics, and targets beyond SPIR-V, for which the Vulkan axis `rules.spirv` reads has no answer. `slangc`'s embedded output names `uint32_t` and `size_t` and includes nothing, which is the defect 0.2.6 fixed for glslang, so this rule writes the public header around it from the start. The size comes from the `_sizeInBytes` constant the compiler emits rather than from `sizeof`. A Slang module package needs no new concept: it is an ordinary mcpp package whose `include_dirs` names its `.slang` directory. `xim:slang` is pinned exactly rather than floored. Slang's version is coupled to no driver, so the rule this package follows would make it a `>=` -- but a `>=` cannot reach xlings on Windows, where cmd.exe reads the `>` as a redirection, and a floor holding on two platforms of three would resolve different payloads for one project depending on the machine that built it. `rules.slang` needs an engine whose device-source table lists `.slang`. A constrained glob's `accel` key does not make a file a device source; the engine's `kDeviceExtensions` does, and before it listed `.slang` the file fell through to the ordinary source scan and was refused. * Pin the engine that escapes for cmd.exe, and restore the floors it makes expressible `MCPP_VERSION` moves to 2026.9.7.1, the release carrying three things this package needs: `.slang` in the device-source extension table, without which `rules.slang` cannot route a file at all; `MCPP_LANGUAGE_MODULES`, which the surface's default reads; and `mcpp.platform.shell`, which escapes a provisioning argument for both cmd.exe and the child's MSVCRT parser. The third restores a constraint. `xim:shaderc` was an exact version on macOS and Windows for one release because the `>` in `>=2026.3` was read by cmd.exe as a redirection and arrived as a packaging failure; the comment in mcpp.toml said both would revert once a released engine escaped the argument. They do here. `xim:slang` becomes a floor for the same reason, and its comment records why the obvious middle ground was rejected: `>=` on the two platforms that tolerated it and an exact version on the third would resolve different payloads for one project depending on the machine that built it, which is how a defect comes to exist on one operating system only. * Declare device_extensions and rule_module per rule feature A consumer now writes one dependency edge and no build program. The two keys are what the engine reads: which device sources a rule compiles, and which module a build program imports to reach it. The engine holds neither this package's name nor any feature spelling, so a third-party rule for the same extension works the same way. tests/spirv-zero-config is the whole path: an edge naming a feature, no host-module, no build.mcpp, and a shader reached through a generated module. * Three storages behind one surface, each with a fixture that runs `storage` decides where a payload's bytes live; the surface decides how a consumer names them. The declarations are identical under all three, so a project changes the first and no consumer changes -- which is what the three new fixtures assert by having the same `src/main.cpp` shape. header a C array in generated source, compiled in (default) object a section, through `.incbin` in a generated .S sidecar a file beside the artifact, read at run time WHICH ONE IS A MEASUREMENT. With GCC 16.1 on 100 payloads of 16 KB each -- an ordinary compute shader's size -- the header route takes 1.10s against the object route's 2.17s, because at that size neither has a measurable marginal cost and the total is decided by how many processes start. The crossover is the TOTAL embedded byte count rather than the payload count: below about 1 MB the header route wins, above about 4 MB it loses by an order of magnitude (2.31s against 0.116s). So `header` stays the default and `object` is what a project reaches for when it has more payload than that. Object storage removes the one place the two shader compilers differed. Under it neither `-mfmt=c` nor `-x --vn` is passed, so glslang and glslc write the same thing: a bare `.spv`. The wrapper header exists only for the storage that needs a translation unit. The assembly is per object format, and the differences are not cosmetic: ELF takes `.section .rodata`, Mach-O `.section __TEXT,__const` with an underscore PREFIXED to every symbol, COFF `.section .rdata,"dr"`. Mach-O's underscore is the one that fails in the quiet direction -- a label without it defines a symbol the C++ side never resolves, and the link error names the accessor rather than the missing prefix. `.balign 4` is written because `VkShaderModuleCreateInfo:: pCode` requires it and a section directive alone does not promise it. MSVC has no GAS and mcpp refuses `.S` under it, so object storage degrades to header storage there with one warning. The surface is unchanged, so a consumer compiled either way is the same source. Sidecar states its cost instead of hiding it: the accessor opens a path relative to the working directory. `tests/spirv-sidecar` asserts both halves -- found from the package root, and reported missing when run from `/tmp`, which is the leg that would otherwise pass against an implementation that had embedded the payload after all. * README: the file-name to call mapping, and the generator's reuse boundary Every name a consumer writes is derived, and the derivation was documented nowhere. The table states it end to end, with the three invariants and the defect each one exists to prevent -- including the byte-identical generated headers that GCC's `#pragma once` collapsed into one. The generator is not shader-specific and three members already share it. A rule package outside this collection reaches it by depending on `mcpp:plugins` and activating no feature; whether it should become a package of its own is left open rather than answered by silence. * mcpp.plugins.island: an island's boundary is generated, not written twice `surface` generates the whole interface for a data payload, because an address and a size are all there is to decide. `island` generates what is mechanical about a code island, and only that: the `extern "C"` header its compiler includes -- guards and `__cplusplus` dance included -- and the module the C++ side imports. THE DECLARATION EXISTS ONCE. Without this a project writes it twice, in a header and again wherever the C++ side reaches it, and C language linkage does not mangle: two copies that disagree are one symbol, the link is clean, and each side reads the arguments by its own ABI with no compile error and no link error. That is the copy this removes, and it is the worst one available at this boundary. `export using ::name;` IS WHY THERE IS NO C PARSER HERE. The module re-exports identifiers rather than restating signatures, so the generator needs only the name before the `(` and the header stays the only place a signature is written. Measured on both implementations this package supports before anything was built on it: a consumer that imports the module and never includes the header calls the entry point and links against an implementation compiled by a different driver, under GCC 16.1 and clang 22.1.8. The C++ interface is still the project's. A seam that turns raw pointers into something worth calling is a design decision no generator makes well; this removes the boilerplate around the boundary, not the boundary's design. `emit_module = false` emits the header alone for a project that keeps a seam that includes rather than imports. `tests/island-interface` uses a `.c` island rather than a `.cu`, because the generator does not know what compiler produced the object and a fixture needing a vendor toolkit could only run where that toolkit is published. It has two entry points, since a generator that re-exported only the first would satisfy a one-function fixture, and the CI step refuses a module that restated a signature even though such a module would compile. The README states why both generators live in this package rather than in mcpp: the engine's module carries the protocol, and a generator is a library on top of it. Code inside the engine changes only with an engine release, which is the coupling this version's `device_extensions` work exists to remove. * island: the entry points are marked where they are defined, and the marker names the mechanism `island::scan` reads the marked declarations out of the island's own source, so the signature sits beside the definition and exists once. `island::emit` still takes a list directly, for entry points a scan cannot see, and a project that wants neither writes its own header and module wrapper. Each layer overrides the one above; the default is the one that keeps the signature in one place. THE MARKER SELECTS, AND THAT IS THE POINT. An island has internal functions, and a generator that exported whatever the file contained would make the boundary an accident of the file's contents. The fixture has a static helper and an unmarked external function, and CI refuses either reaching the generated files -- a check that would otherwise pass, because both files would merely be larger. `MCPP_EXPORT_C` names the mechanism rather than the domain: what is marked is exported across a generated boundary with C linkage, and both halves are in the name. `MCPP_ISLAND_EXPORT` was precise inside docs/20's vocabulary and narrow outside it -- the generator works for any `extern "C"` boundary, and the fixture proves it with a plain `.c`. Anything ending `_API` was rejected outright: that suffix conventionally expands to a visibility attribute and this expands to nothing, so borrowing it would promise what it does not do and collide with a project that later wants the real thing. THE SCAN IS NOT A C PARSER. From the marker it copies verbatim to the parenthesis that closes the parameter list, matching nesting. The fixture's second entry point wraps across three lines for that reason, and CI asserts the flattened declaration, because a line-oriented scan would pass every other check here. A GCC 16 defect was found and worked around while writing it: `for (char c : s)` over a `std::string` inside an exported inline function makes the consumer's build program fail with `inlining failed in call to 'always_inline' ... function body not available` in `<bits/stl_iterator.h>`, naming neither this file nor the loop. Indexing touches no iterator type. The comment at the site records the message so the next person recognises it. The island still includes the generated header, and the README says why that line is not new: a project writes the same include today and writes the header as well. It also earns its place twice, defining the marker and declaring the entry points, so a definition whose signature drifted fails where it was written rather than at the link. Removing it with `-include` is left as an option, since a file whose marker comes from a flag is no longer self-contained. * island: the island writes no include either, and the check that line did survives `force_include_flags` returns what makes a compiler read the generated header before the island's first line -- `-include <path>` for gcc and clang, `/FI<path>` for MSVC. A project using this generator now has no header in its source tree and no line naming one; the fixture's island is a `.c` with zero includes. The line was defended in the previous commit on the grounds that it kept the file self-contained. It did not. The header is generated, so an `#include` of it names a file that does not exist until mcpp writes it, and that translation unit could not be compiled outside mcpp with or without the line. What the line actually bought was a name its author never opens. THE CHECK IT ALSO DID IS STILL THERE, and this was verified rather than assumed. The compiler still sees the declarations, so a definition whose signature drifted fails where it was written: src/kernels/saxpy.c:22:5: error: conflicting types for 'scale_device' That is only reachable on the `emit` path, where a hand-written list and the definition are separate things. Under `scan` the header is generated FROM the definition and the two cannot disagree, so the check is vacuous there -- worth saying, because a criterion that cannot fail is one this project has paid for before. The first attempt at that control reported success for the wrong reason: the fixture was copied to a scratch directory, its `path = "../.."` dependency stopped resolving, and the build failed with `has no mcpp.toml` while the check read "refused". Re-run in place, it fails on the conflicting type. * tools: the island generator is a member, not part of the lib root The lib root carries what members share. `mcpp::plugins::surface` is there because three rules produce the same declarations for a consumer and a fourth copy would drift. Nothing in this collection uses the island generator: a project calls it from its own `build.mcpp`, exactly as it calls `mcpp.tools.embed`. Leaving it in the lib root handed it to every consumer whether or not they asked for it, while `tools-embed` beside it required a feature. It moves to `tools/island.cppm` behind `tools-island`, exporting `mcpp.tools.island`. `all-rules-compile` activates the feature and calls `entry_name`, so the parser is compiled and exercised on every host. * surface: the module root is the package's name, not its directory's `module_root_from_package` derived the module a consumer imports from the leaf of MCPP_MANIFEST_DIR, because that was the only thing a build program was told about itself. It is a directory name, and the two answers differ whenever a project lays a package out under a generic folder: mcpp's own `examples/09-heterogeneous/vulkan/app/` declares `name = "vulkan-saxpy"` and got `app.shaders`, which every `<something>/app/` in a workspace would also claim. mcpp 2026.9.7.1 answers the right question, so this reads `mcpp::package_name()`. The directory leaf stays only for a value that cannot occur -- a manifest with no `[package] name` does not load -- so an empty answer would mean the contract moved. The fixture could not see the difference. Every project under tests/ had a package name equal to its directory name, so both derivations produced the same string. `spirv-module-consumer` now declares `name = "shader-app"` while keeping its directory, and its consumer imports `shader_app.shaders`; reverting this function makes that import unresolvable, which is the measurement the fixture was missing. * tools-island: a seam has two halves, and scan is where they are compared The generator served one island. A seam has two implementations of one `extern "C"` boundary -- a device island and a host fallback -- and exactly one of them is in any link, which is the arrangement every example under `examples/09-heterogeneous` has. Handed only the device half, the generator produced an empty boundary in a `--no-accel` build and the C++ side failed on an unresolved name. `scan` now merges entries by name, so a build program hands it both files unconditionally: both exist on disk in either build, and which one is compiled is the manifest's decision rather than a condition the build program repeats. Two definitions of one name that declare it differently are refused there, naming both files and both signatures. Nothing else catches that. The two halves are never in one translation unit and never in one link, and C language linkage does not mangle, so a build with disagreeing halves is clean and the artifact reads its arguments by whichever signature it was compiled with. `scan` is the only point at which both texts exist at once. The fixture gains the host half under `cfg(not(accelerator = "vulkan"))`, so both legs are built and run, and a CI step perturbs one signature and asserts the refusal names both files. The README paragraph claiming the two could not disagree under `scan` was true only while `scan` took one file. * tools-island: the fixture reaches the boundary through a seam module Every example under `examples/09-heterogeneous` puts a module of the project between the consumer and the island: `app.cppm` imports the boundary and turns pointers and a count back into spans, and it is the one place a backend can be exchanged. The fixture reached the generated module from `src/main.cpp`, which is not a module unit -- so its import said nothing about the case the examples are in, where one module interface written by the project imports a module interface written into the build directory during the same build. `src/app.cppm` is that seam, and `src/main.cpp` now names no boundary symbol at all. Both properties are asserted: a consumer that named `saxpy_device` would mean the seam is not hiding which island is underneath. Both legs build and run with it. * rules: cuda, hip and sycl gain the flag pass-through ascendc already had `mcpp.tools.island::force_include_flags` returns the flags that make a compiler read the generated boundary header before the island's first line, and three of the four device rules had no way to deliver them. Measured on `examples/09-heterogeneous/cuda`: the island is compiled by nvcc, or by clang in CUDA mode, on a command line the rule assembles -- a driver mcpp did not invoke, which inherits nothing from `mcpp::cflag` or `mcpp::cxxflag`. The project-wide channels are not merely too wide for the job, they are wrong for it. `cxxflag` forces the header into every C++ translation unit, including the seam `src/app.cppm`, and a module interface unit must begin with `export module` -- declarations ahead of that line are ill-formed. `mcpp.rules.ascendc` already had `options::flags`, appended last. The other three now carry the same field with the same position and the same pass-through-unexamined contract, because one rule with it and three without is the shape where a project's build program works for one backend and not the next -- which is what a seam exists to prevent. Verified end to end on this host: the cuda example's island compiles with `-include .../app.kernels.h` on its own command line, and the artifact runs on an RTX 4080. * README: two claims left over from the design that was abandoned The `rules-slang` row said its engine need is `.slang` in the device-source table. That was the first fix and it inverts the dependency; what shipped is the feature declaring `device_extensions` and `rule_module` itself, with `.slang` REMOVED from the engine's table. Its absence from the list of per-member floors is now stated rather than left as a gap, because that absence is the criterion for the whole arrangement. The shared floor moves to 2026.9.7.1, and the two usage snippets pin 0.3.0. * surface: a generated name may not be a C++ keyword Six sites across four files ran the same character filter -- letters, digits and `_` survive, everything else becomes `_`, a leading digit gets a `_` -- and none of them asked whether the result is reserved. `default`, `template`, `operator`, `private` and `union` are valid identifiers to that filter, and `shaders/default/` is an ordinary name for a shader directory. Measured: renaming the fixture's `shaders/a` to `shaders/default` makes the generator write namespace default { and the build fails at `expected identifier before 'default'`, inside a generated file, on a line the author of that directory has never opened. `surface::identifier` does all three transformations in one place, and a reserved result gets a TRAILING underscore -- a leading one is itself reserved at namespace scope, so prefixing would trade one reserved name for another. Namespace segments in `rules-spirv` and `rules-slang` go through it, as does `tools-embed`'s accessor name. The accessor is sanitised where it is EMITTED rather than in each producer. A rule that builds `item::identifier` from a file stem cannot know it has produced `my-shader_comp` or `default` until it reaches the one line that writes the function's name. `accessor_base` and the `_spv` symbols are left alone: both carry fixed affixes and cannot come out reserved, and routing them would rename symbols already published. The fixture could not see any of this: `a` and `b` are ordinary identifiers, so both filters produce the same file. It gains `shaders/default/`, and CI asserts the generated interface contains `namespace default_ {`. * surface: a module name whose segment is not an identifier is refused The module name is the one part of this surface a project writes itself, and each of its segments becomes a namespace. `app.my-shaders` produced `namespace my-shaders {` in a generated file, so the error named a line nobody wrote and a file the author had never opened. Refused where it was written, naming the offending segment and the spelling that would work: mcpp.plugins.surface: `app.my-shaders` is not a usable module name: the segment `my-shaders` is not a C++ identifier. Each segment becomes a namespace, so it has to be one; `my_shaders` would work. The predicate is `identifier()` applied to the segment and compared with it, so the refusal and the derivation cannot disagree about what an identifier is -- including the keyword case. * ci: the denominator reads feature names off the section headers `all-rules-compile` asserts EVERY feature is compiled for this host, and "every" is a list the fixture carries -- so the list is compared against the package's own features before the build. That extractor was written for [features] rules-cuda = { sources = ["rules/cuda.cppm"] } and 0.3.0 spells them as sub-tables, because a feature now carries `device_extensions` and `rule_module` as well and TOML 1.0 forbids a multi-line inline table. `grep -oE '^[a-z-]+ +='` then matched the keys INSIDE those tables: package: sources sources sources sources fixture: rules-ascendc rules-cuda rules-hip rules-slang ... which reads as "the fixture is wrong" while the fixture is complete and the check is what broke. A denominator that cannot state the set it measures against is worse than none: it fails loudly for the wrong reason, and the next person edits the fixture. The names come off `[features.<name>]` headers now, and the empty-result guard says the manifest's spelling changed rather than that the package has no features. Both copies of the check are replaced; fixing one would leave the other failing on the next host. * ci: the duplicate-stem step asserts what replaced the refusal Two shaders differing only by directory used to produce one output -- the name was the stem and the stage, so `a/x.vert` and `b/x.vert` both gave `x_vert.h` declaring `x_vert_spv` -- and the rule refused. The shader's directory is now part of the generated file's path and of the array's name, so there is nothing to refuse and the step failed with FAIL: two shaders mapping to one output were accepted which is the correct new behaviour reported as a defect. That change was not made for ergonomics: two byte-identical generated headers collapse under GCC's `#pragma once`, which treats two files of the same size and content as the same file, so the second include silently did nothing and both accessors returned one array. The step now asserts the property that made the refusal unnecessary -- `scale_comp_spv` and `dup_scale_comp_spv` are both declared -- and the criterion is the generated names rather than the run, because a build that collapsed them would also produce a working binary. The refusal stays in the rule. Its condition is now two shaders with one name in one directory, which a single glob cannot construct and an explicit `options::base_dir` can. Verified locally against the released 2026.9.7.1 binary. * ci: the consumer check asks about code, not about text The step asserts that `tests/island-interface/src/main.cpp` names no boundary symbol, because the seam exists to hide which island is underneath. It was a plain grep, and that file EXPLAINS the property in a comment -- "Nothing here names \`saxpy_device\`" -- so the criterion reported the file for saying what it does. Comment lines are stripped first. The difference is between asking whether the file mentions the name and whether it uses it, and only the second is the property. Measured both ways: `src/main.cpp` has zero uses after stripping and `src/app.cppm`, which is the seam and is supposed to call them, has two. * ci: two more criteria that read text where they meant code `spirv-zero-config` explains in its own header that `host-module = true` is not written and not needed, and the step asserting that greps the manifest -- so the fixture was reported for saying what it does. Comment lines are stripped first. The island step's `grep 'include' build.mcpp` had the mirror-image problem: it matched that program's comments and its `mcpp::include_dir` line, so it would have passed with the forced include removed. A criterion that cannot fail is not one. It names `force_include_flags` now, which is the mechanism it is there to protect. Both were found by reading every CI assertion that greps a source file rather than by waiting for the next round trip; the other sixteen are either over program output, which has no comments, or over patterns no comment in those files carries.
fix(rules-sycl): name the C library, which the rule never did (0.2.1) (… …#4) * fix(rules-sycl): name the C library, which the rule never did (0.2.1) The rule names the C++ standard library because, left alone, dpcpp's clang reads the host's. The C library is the same question one layer down and had no answer. Measured on dpcpp 7.1.0 with the flags the rule passed: …/xim-x-gcc/15.1.0/…/include/c++/15.1.0 <- ecosystem, correct …/xim-x-dpcpp/7.1.0/lib/clang/22/include /usr/local/include <- the HOST /usr/include/x86_64-linux-gnu /usr/include No ecosystem glibc path anywhere on the list. So `<cstdio>` in a `.sycl` unit reached libstdc++ from the payload and `<stdio.h>` from the host. FORWARDING THE SYSROOT DOES NOT ANSWER IT HERE, which is why the documented fix for a second compiler does not apply. `toolchain_sysroot()` is EMPTY under an llvm toolchain -- measured -- and a SYCL project pins llvm because mcpp's own clang has no SYCL front end. The LLVM payload's clang does not need it, being configured with the ecosystem glibc; dpcpp's clang is a different clang and is not. So the rule names the C library the way it already names the C++ one, and requires `xim:glibc` and `xim:linux-headers` as it already requires `xim:gcc`. THE OLD CRITERION COULD NOT SEE THIS, and its reasoning says why. It asserts "no host C++ standard library" rather than "no /usr", because the engine's own compiles leave `/usr/include` last as a fallback for C headers -- true, and it holds only when the ecosystem's C library is AHEAD of it. For dpcpp's clang `/usr/include` was not a fallback, it was the only source. The new assertion is therefore about ORDER, not absence: every device compile must either name the ecosystem glibc or forward a sysroot. Measured across all three fixtures; the SYCL one satisfies it only after this change, and the ecosystem glibc moves from absent to third on the list. Found by enumerating the rules after the same defect class broke llama.cpp-m's CI, not by a failure. It ships in 0.2.0, so this is 0.2.1. * test(plugins): the C-library criterion reads the compiler's search list The first form grepped the build log, and the build log carries the ENGINE's compile lines -- which do name the ecosystem glibc. So it passed for a rule whose device compile named nothing, and removing the fix it was written for left it green. Verified, which is the only reason it is not still that shape. Two things it now gets right: * THE OBJECT. An implicit include search never appears on a command line, and the log mixes compiles from different compilers. `tools/check_device_c_library.sh` extracts the device compile's own C-library flags from build.ninja, asks that compiler to print its `#include <...>` search list, and reads the order. * THE PATTERN. "Any ecosystem path" matched dpcpp's own `include/`, because a compiler's resource directory lives in the store too. It now matches what actually provides `features.h`: the glibc payload, or a sysroot's `usr/include` beneath the registry. Two admissible answers, both the same property from either side: the rule names the C library, or the compiler it drives already knows it. Refused is a device compile that reaches a host path with no ecosystem C library above it. Measured across all three fixtures -- cuda at position 4, hip at 4, sycl at 2, first host path at 8, 8 and 6 -- and verified to FAIL for sycl with the fix removed. * fix(rules-sycl): the C library declaration is unpinned, because the project does not choose its version The first form asked for `"xim:glibc" = "2.44"`, and CI refused the fixture that declared exactly that: the runner's runtime binding had chosen glibc 2.44.2. `xpkg_dir` with a pin answers for that version or for nothing, so the rule told a project to declare something it had already declared. The C library version is the RUNTIME BINDING's choice. `""` -- present, any version -- is the only thing a project can truthfully say about a library it does not select, and it resolves to whatever the binding installed. Measured: `"xim:glibc" = ""` answers `…/xim-x-glibc/2.44.2` on the machine that has 2.44.2 in its store. This is the same shape as the defect the round found in mcpp itself: a value that belongs to one layer being spelled by another. * test(plugins): the check builds what it inspects, and selects by content It read whatever `build.ninja` sorted first under a fixture's target, which is a guess about what a previous step left. The fixture steps in CI end with `build --no-accel`, whose graph has no device action at all, so the check reported "declares no action to inspect" for three fixtures that were correct. Two changes, both removing a guess: * it runs the accel build itself, because that build is what it is about; * it selects the graph that CONTAINS `rule mcpp_action_`, rather than the one that sorts first. It also prints which build.ninja it read, so a future disagreement names its object instead of being inferred. Fourth time in this round that a check chose its own object and chose wrong, and the shape is identical every time: a path or a file picked by convention rather than by the property being measured. Verified against the CI shape locally -- accel build followed by `--no-accel`, then the check -- and still FAILS for sycl with the fix removed. * test(plugins): the probe asks only about the C library, in plain C++ Carrying the rule's language machinery into the probe was a mistake with a silent shape: the CUDA and HIP commands begin with `-x cuda`, the probe added its own `-x c++`, and a compiler given two of them prints no search list at all. The check reported that as a defect in three fixtures, two of which were correct. Where `features.h` comes from does not depend on the language being compiled, so the probe no longer names one. It keeps `--sysroot=`, `--gcc-install-dir=` and `-isystem` -- the flags that move the C library -- and asks in plain C++. It also reads the compiler's stderr, because `-v` writes the search list there, and prints what the compiler said when there is still nothing to read. The previous form could only say "printed no search list", which is a description of the check rather than of the problem. Measured: cuda 4/7, hip 4/7, sycl 2/8 (ecosystem C library position, first host path), and still FAILS for sycl with the rule's fix removed.
PreviousNext
