diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc80def..e45489f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ env: # `em++`'s C compiler is `emcc` and not `em` (a C translation unit in the # conformance suite does not compile without that), and the wasm target rows # resolve the emsdk payload. - MCPP_VERSION: 2026.9.11.4 + MCPP_VERSION: 2026.9.12.2 XLINGS_VERSION: v2026.8.17.2 XLINGS_NON_INTERACTIVE: '1' @@ -167,8 +167,9 @@ jobs: # The set is named rather than `full`: clause 6.1 says an implementation # provides an interface in whole or not at all, and a group this platform # does not have is an absence rather than a deviation. `task` is not in - # the set -- see the task-gate step below for why it needs a link this - # suite cannot currently be given. + # the set: the suite's consumer manifest is generated by the + # specification's runner, which does not state the ABI switch threads + # need. The task-gate step below builds and runs a task program with it. - name: Every interface this platform has, examined run: | OPENKAL_CONFORMANCE_RUNNER="$NODE_BIN" \ @@ -288,20 +289,24 @@ jobs: done test "$fail" -eq 0 - # THE TASK GATE, IN BOTH DIRECTIONS. + # THE TASK GATE, IN THREE DIRECTIONS. # # `openkal.task` needs `-pthread`, which selects a different C library - # build and a different memory model -- a property of the whole link. So - # the interface is carried by a feature: without it the translation unit - # is empty and the eight symbols do not exist, which is the same - # treatment the three absent interfaces get and for the same reason. + # build and a different memory model -- a property of the whole link, and + # therefore of the consumer's root manifest (`[target..abi] + # threads = true`, mcpp 2026.9.12.2). The `threads` feature states that it + # needs the switch. Asserted: # - # Both directions are asserted, because a gate is only a gate if the - # other side of it differs. The conformance suite cannot yet be built - # with this switch -- it would have to reach the specification package's - # modules too, and mcpp has no whole-graph flag channel -- so what is - # asserted here is the gating, and the README records the rest. - - name: The task interface exists in a -pthread link and in no other + # 1. without the feature and the switch, no task symbol exists; + # 2. the feature without the switch is refused before anything compiles, + # naming the feature; + # 3. with both, the eight symbols exist and a program that starts a task + # and joins it exits 0 under node. + # + # The consumer names `openkal` by path, as the implementation's manifest + # now does (the step above): mcpp refuses one package reached both by path + # and by version. + - name: The task interface needs the ABI switch, is refused without it, and runs with it run: | set -euo pipefail rm -rf target @@ -311,14 +316,55 @@ jobs: test "$n" -eq 0 || { echo "::error::$n kal_task symbols without the feature"; exit 1; } echo " ok no task symbol without the feature" - rm -rf target - mcpp build --target wasm32-emscripten --features threads - objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' | tr '\n' ' ') + work="$RUNNER_TEMP/tasks" + impl="$(pwd)" + rm -rf "$work"; mkdir -p "$work/src" + cat > "$work/src/main.cpp" << 'CPP' + import openkal.task; + static void entry(void* arg) { *static_cast(arg) = 1; } + int main() { + // KAL_TASK_PROP_PARALLEL is bit 1 (openkal/task.h); a macro does not + // cross a module. + if ((kal_task_props() & 2u) == 0) return 2; + int ran = 0; + kal_task t{}; + if (kal_task_start(&entry, &ran, &t) != 0) return 3; + if (kal_task_join(t) != 0) return 4; + return ran == 1 ? 0 : 5; + } + CPP + manifest() { + cat > "$work/mcpp.toml" << TOML + [package] + name = "tasks" + version = "0.1.0" + + [dependencies] + openkal = { path = "$impl/.spec" } + openkal-emscripten = { path = "$impl", features = ["threads"] } + $1 + TOML + } + + manifest "" + if ( cd "$work" && mcpp build --target wasm32-emscripten ) > refused.log 2>&1; then + cat refused.log; echo "::error::the threads feature built without the ABI switch"; exit 1 + fi + grep -q "requires the artefact's ABI to have threads (feature \`threads\`)" refused.log \ + || { cat refused.log; echo "::error::the refusal does not name the feature"; exit 1; } + echo " ok the feature without the switch is refused, naming the feature" + + manifest "$(printf '\n[target.%s.abi]\nthreads = true\n' "'cfg(os = \"emscripten\")'")" + ( cd "$work" && mcpp build --target wasm32-emscripten ) + objs=$(find "$work/target" -path '*/obj/*' -name '*.o' ! -name '*.m.o' | tr '\n' ' ') n=$("$NM" --defined-only $objs 2>/dev/null \ | awk '$2=="T"||$2=="W"{print $3}' | sed 's/^_//' \ | grep -c '^kal_task' || true) - test "$n" -eq 8 || { echo "::error::expected 8 kal_task symbols with the feature, found $n"; exit 1; } - echo " ok eight task symbols with the feature" + test "$n" -eq 8 || { echo "::error::expected 8 kal_task symbols with the switch, found $n"; exit 1; } + echo " ok eight task symbols with the switch" + ( cd "$work" && timeout 300 mcpp run --target wasm32-emscripten ) \ + || { echo "::error::the task program did not exit 0 under node"; exit 1; } + echo " ok a task starts, runs and joins under node" - name: Restore the manifest if: always() diff --git a/README.md b/README.md index ebcdd66..6c53b75 100644 --- a/README.md +++ b/README.md @@ -61,30 +61,48 @@ So the interface is carried by a feature: ```toml [dependencies] -openkal-emscripten = { version = "0.1.0", features = ["threads"] } +openkal-emscripten = { version = "0.1.1", features = ["threads"] } ``` Without it, `src/threads/task.cpp` compiles to nothing, the eight `kal_task_*` symbols do not exist, and `kal_interfaces()` does not claim the interface -- the same treatment the three absent interfaces get, for the same reason. -**This feature is not yet usable end to end, and the limitation is the build -tool's rather than this package's.** `-pthread` changes the module -configuration of *every* translation unit in the link, including the -specification package's, and mcpp has no channel for a flag that applies to a -whole dependency graph: a feature contributes sources, defines and per-glob -compile flags, and `[build] ldflags` reaches the root package only. Measured -2026-09-11 with the feature active and `-pthread` on the consumer: +**The switch belongs to the artefact, and the root manifest states it.** +`-pthread` changes the module configuration of *every* translation unit in the +link, including the specification package's, so it cannot be a flag of this +package's own units. With mcpp 2026.9.12.2 or later, the consumer writes + +```toml +[target.'cfg(os = "emscripten")'.abi] +threads = true +``` + +and the switch reaches the standard library module, every translation unit of +every package, and the link. The `threads` feature states that it needs the +switch (`requires_abi = { threads = true }`), so a consumer that activates the +feature without the table is refused before anything compiles, naming the +feature and the table. Measured 2026-09-12 with emsdk 6.0.9 and a development +build of mcpp 2026.9.12.2: with the table, a program that starts a task and +joins it exits 0 under node, and without it the build stops at + +``` +error: `openkal-emscripten` requires the artefact's ABI to have threads (feature `threads`), and this build does not state it. +``` + +An earlier version of this section, measured 2026-09-11, concluded that mcpp had +no channel for a flag that applies to a whole dependency graph. What had been +measured was `-pthread` in the consumer's per-package `cxxflags`: ``` error: POSIX thread support was disabled in precompiled file '.../pcm.cache/openkal.types.pcm' but is currently enabled ``` -which is the specification package's module, compiled without the switch. The -interface word is already correct for a link that manages it; closing the gap -is an engine change of the same shape as the existing whole-graph runtime -flags, and it is recorded rather than worked around. +That channel does not reach the specification package's module. A graph-wide +channel did exist (`[build] dialect_cxxflags`); what was missing was a way to +scope it to one target and to let this feature state its requirement, which the +typed table provides. ## Conformance @@ -128,7 +146,7 @@ implementation in its source: openkal = "0.12.0" [target.'cfg(os = "emscripten")'.dependencies] -openkal-emscripten = "0.1.0" +openkal-emscripten = "0.1.1" ``` ## Licence diff --git a/mcpp.toml b/mcpp.toml index c953524..4b2bcb7 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-emscripten" -version = "0.1.0" +version = "0.1.1" description = "An implementation of openkal for Emscripten, written ABOVE a C library rather than beneath one, because on this platform there is no kernel to issue calls to." license = "Apache-2.0" @@ -59,29 +59,24 @@ default = [] # capability is not a property of the machine this runs on, and it is not # discoverable at run time by anything this package can ask. # -# Declaring it as a feature puts the decision where the rest of the link is -# decided, and `kal_task_props` reports the consequence: with this feature the -# word carries KAL_TASK_PROP_PARALLEL, and without it the interface is still -# provided and `kal_task_start` reports kal_err_not_supported -- which is -# clause 6.2's third time, a capability word read before use, rather than the -# shape the specification forbids. +# THE SWITCH IS THE ARTEFACT'S, SO THE ROOT MANIFEST STATES IT. `-pthread` +# changes the module configuration of every translation unit in the link, the +# specification package's included, so it cannot be a flag of this package's +# own units. mcpp 2026.9.12.2 carries it as a typed member of the root's target +# table, # -# `-pthread` ON THIS PACKAGE'S OWN UNITS, AND NOT A DEFINE OF ITS OWN. Passing -# the switch is what makes Emscripten define `__EMSCRIPTEN_PTHREADS__`, so the -# code keys on that rather than on a name this manifest invented: one -# mechanism, and it is the compiler's own answer to "was this compiled with -# threads" instead of a second word that could disagree with it. +# [target.'cfg(os = "emscripten")'.abi] +# threads = true # -# THE LINK HALF IS THE CONSUMER'S, AND THAT IS A LIMITATION WORTH NAMING. A -# feature contributes build INPUTS -- sources, defines, flags -- and mcpp's -# feature axis has no `ldflags`, so this cannot put `-pthread` on the link -# line. A project that activates this feature must also write +# which reaches the standard library module, every translation unit and the +# link. This feature states that it needs the switch: a build that activates it +# without the table is refused before anything compiles, naming the feature and +# the table. # -# [build] -# ldflags = ["-pthread"] -# -# and a project that forgets fails at LINK naming the pthread symbols, which -# is a loud failure rather than a silent single-threaded program. -threads = { flags = [ - { glob = "src/**", cxxflags = ["-pthread"] }, -] } +# `kal_task_props` reports the consequence: with the switch the word carries +# KAL_TASK_PROP_PARALLEL, and without it `kal_task_start` reports +# kal_err_not_supported -- clause 6.2's third time, a capability word read +# before use. The code keys on `__EMSCRIPTEN_PTHREADS__`, which the compiler +# defines when `-pthread` is passed: one mechanism, and it is the compiler's own +# answer to "was this compiled with threads". +threads = { requires_abi = { threads = true } }