From 69d29d51ac73828a27152904f009352faf6ae920 Mon Sep 17 00:00:00 2001 From: neu-rah Date: Wed, 30 Sep 2026 02:52:07 +0000 Subject: [PATCH 1/4] hapi/slots.h: slots, state composed along a chain and addressed by tag Slot adds one value of type S to the state the components after it built; slot(state) reaches it by its tag. One object of static size, no heap, an empty slot adds nothing. The same tag twice (also across nested Chain and APIOf) and an absent tag are compile errors with their own messages. What the state means, how it evolves or travels is not decided here: the Contract parameter is how a user adds members to the state's type. hapi.h includes it. Tests: tests/slots_tests.cpp (tags, each order, the Contract, nested Chain and APIOf), tests/negative/slots_*.cpp, tests/slots/run.sh (same flashed AVR program as a hand-indexed array), tests/single_header/run.sh builds slots_tests both ways. Docs: README "Composing state: slots", docs/REFERENCE.md. CHANGELOG: 0.8.0 heading (the items that came after the v0.7.0 tag, single/hapi.h, Distinct and static_net, move under it with the slots); library.json 0.8.0; single/hapi.h regenerated. From R&D/HAPI/typedState @ 41e4086. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 23 ++++-- README.md | 39 ++++++++++ docs/REFERENCE.md | 25 +++++++ include/hapi/hapi.h | 1 + include/hapi/slots.h | 66 +++++++++++++++++ library.json | 2 +- single/hapi.h | 71 +++++++++++++++++- tests/negative/slots_dup_tag.cpp | 8 ++ tests/negative/slots_dup_tag_nested.cpp | 9 +++ tests/negative/slots_missing_tag.cpp | 8 ++ tests/single_header/run.sh | 7 ++ tests/slots/core_avr.cpp | 27 +++++++ tests/slots/run.sh | 14 ++++ tests/slots_tests.cpp | 97 +++++++++++++++++++++++++ 14 files changed, 388 insertions(+), 9 deletions(-) create mode 100644 include/hapi/slots.h create mode 100644 tests/negative/slots_dup_tag.cpp create mode 100644 tests/negative/slots_dup_tag_nested.cpp create mode 100644 tests/negative/slots_missing_tag.cpp create mode 100644 tests/slots/core_avr.cpp create mode 100755 tests/slots/run.sh create mode 100644 tests/slots_tests.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index d4190f8..e235798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,15 @@ # Changelog -## 0.7.0 (not yet tagged) +## 0.8.0 (not yet tagged) ### Added -- **`Expand` / `Expansion` / `IsContainer`**: one extension point that says what a - container holds and which walks may open it. `Traverse`, `FindFirst`, `BuildRules` and `NoCollision` all read it, so a new - container is taught once instead of once per walk. `Chain` (all four bits) and `APIOf` (`validates` only) are built in. See the README - section "Teaching HAPI your own container" and `docs/REFERENCE.md`. -- `Chain::Drop`: the chain without its first `n` elements. +- **`hapi/slots.h`: slots, state composed along a chain and addressed by tag.** `Slot` is a component that adds one value of type S to the + state the components after it built; the state is one object of static size, no heap, and an empty slot adds nothing. `slot(state)` reaches a slot by + its tag, `HasSlot` asks, `each(visitor)` walks the slots in chain order, `SlotApi`/`SlotRoot` end the chain. The same tag twice, also across nested `Chain` + and `APIOf`, and a tag that is not in the state are compile errors with their own messages. Nothing about what the state means, how it evolves or how it is sent + is decided: the third parameter, a `Contract`, is how a user adds members to the state's type. `hapi.h` includes it; it costs nothing unless used. Measured: the same AVR + program as a hand-indexed array (`tests/slots/run.sh`); a chain of 256 slots takes 1.5 s to compile with g++ (`-fsyntax-only`), against 0.9 s for 256 plain Parts; with the default template depth a chain stops at about 450 Parts, slots or not. See the README section + "Composing state: slots" and `docs/REFERENCE.md`. Tests: `tests/slots_tests.cpp`, `tests/negative/slots_*.cpp`, `tests/slots/`. - `single/hapi.h`: the library as one header, generated by `scripts/amalgamate.py` (every `include/hapi/*.h` inlined once, the AVR shim kept under its `#if`, MIT header and version on top; `--check` reports a stale copy). `tests/single_header/run.sh` builds the host examples (`rules`, `std`, `crtp`, `free`, `virt`; g++ and clang++) and static_net's AVR programs against it and against `include/` @@ -17,6 +19,15 @@ compared by `is_base_of`. Covered in `tests/compile_tests.cpp`. Used by the `.RnD/openDerivation` translator in every composed struct. - Example `examples/static_net`: static networks (a typelist of parts wired by index, id or query, no runtime data), tinyML on an 8-bit AVR as the running example, with checks, timing (simavr, confirmed on a real chip), and a comparison against a table loop and against emlearn. Uses `Expand` and `Drop`; GCC/Clang only, not built by CI. + +## 0.7.0 + +### Added +- **`Expand` / `Expansion` / `IsContainer`**: one extension point that says what a + container holds and which walks may open it. `Traverse`, `FindFirst`, `BuildRules` and `NoCollision` all read it, so a new + container is taught once instead of once per walk. `Chain` (all four bits) and `APIOf` (`validates` only) are built in. See the README + section "Teaching HAPI your own container" and `docs/REFERENCE.md`. +- `Chain::Drop`: the chain without its first `n` elements. - Tests: `tests/expand_tests.cpp`, `tests/expand_walks.cpp` (one container per policy bit), `tests/descent_characterization.cpp` (pins how every walk treats every kind of container), and `tests/negative/` (expect-to-fail cases: `tests/negative/run.sh`, `CXX=clang++` to switch compiler; not part of CI's `tests/*.cpp` glob). diff --git a/README.md b/README.md index 6ab5333..76222e9 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,45 @@ Detecting a leaf costs one class template instantiation per element visited (mea --- +## Composing state: slots + +A chain composes data as well as behaviour. `hapi::Slot` is a component that adds one value of type `S` to the state the components after it built. The state is +one object of static size, with no heap, and a slot is reached by its **tag**, not by a name or a position, so two slots may have fields of the same name. + +```cpp +#include +using namespace hapi; + +struct Pos {}; struct Speed {}; // tags: any distinct types +struct PosSlot { int16_t x, y; }; +struct SpeedSlot { int16_t x; }; // a field named like PosSlot's: no clash + +using State = APIOf, Slot>::Res; + +State s{}; +slot(s).x = 1; slot(s).x = 2; +``` + +* **Compile errors, with their own messages:** a tag that is not in the state (`hapi::slot: no slot with that tag in this state`), and the same tag twice, also across a nested + `Chain` or `APIOf` (`hapi::Slot: two Parts claim the same tag`). A nested composition is the same state, the same size and the same walk order as the flat one. +* **Nothing about what the slots mean is decided.** `Slot` takes a third parameter, a template `Contract` whose base carries the state below and the + state's own type `R` (`R::me()` is this slot). It is how a user adds members to the state's type, for instance a `step()`: + +```cpp +template struct Counted : Below { unsigned steps = 0; void step() { ++steps; } }; +using Counting = APIOf>::Res; +``` + +* **`each(visitor)`** walks the slots in chain order (the last-listed first): `visitor.layer(Tag::name())`, then `S::each(slot, visitor)`, which the slot's type defines. `Tag::name()` and + `S::each` are only looked at when `each` is used, and what `name()` returns is the visitor's business. +* **Cost:** the same AVR program as a hand-indexed array (`tests/slots/run.sh` compares the flashed bytes); an empty slot and a nested composition add no size with GCC and Clang (MSVC lays + out several empty bases differently unless `__declspec(empty_bases)` is used; `tests/slots_tests.cpp` prints the sizes there). A chain of 256 slots takes 1.5 s to compile with g++ (`-fsyntax-only`), against + 0.9 s for 256 plain Parts. + +The reference is in [`docs/REFERENCE.md`](docs/REFERENCE.md). + +--- + ## Runtime resolution HAPI also provides `find(object)`. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 4b9c61e..e681556 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -69,6 +69,31 @@ template struct Expand> : Expansion, /*queried*/true, /*selected*/true> {}; ``` +### Slots: `hapi/slots.h` + +State composed along a chain, addressed by tag. `hapi.h` includes it; nothing is instantiated unless used. + +| name | what | +|---|---| +| `Slot` | a component that adds one slot of type `S`; `APIOf, Slot>::Res` is the state | +| `slot(state)` | the slot of `Tag`, `const` or not; a tag that is not in the state is a compile error (`hapi::slot: no slot with that tag ...`) | +| `HasSlot` | whether the state type `R` has a slot for `Tag` | +| `SlotApi`, `SlotRoot` | the end of the chain; `SlotApi::Res` is `SlotRoot`. A contract brings its own terminal API whose `Res` derives from `SlotRoot` | +| `SlotOf` | the slot as a base class of the state; `slot_(...)` converts to it | +| `SlotBase` | the default `Contract`: derives from `Below`, adds nothing | +| `state.each(v)` | `v.layer(Tag::name())`, then `S::each(slot, v)`, for every slot in chain order (the last-listed first); `Tag::name()` and `S::each` are only required when `each` is used | +| `state.me()` (inside a Contract, as `R::me()`) | the slot of this Part | +| `HAPI_SLOT_EACH_INLINE` | macro, empty by default: an attribute (e.g. `[[gnu::always_inline]]`) for `each`, a size policy | + +The same tag twice in one state is a compile error (`hapi::Slot: two Parts claim the same tag`), also when the second is inside a nested `Chain` or `APIOf`; a nested composition gives the +same state, size and walk order as the flat one. A Part sees its own slot and the slots of the Parts after it (`Below`), like every Part in a chain. + +A `Contract` is a class template: the state derives from `Contract` (first) and `SlotOf`, so a contract adds members (a `step`, a serializer, a name policy) without adding an +inheritance level: the state stays one aggregate, whatever the contract (a state of one slot: `State{{}, {slot}}`). What a slot means, how it is named and how the state evolves or is sent are the contract's, not HAPI's. + +Measured: typed slot access is the same flashed AVR program as a hand-indexed array (`tests/slots/run.sh`); a chain of 256 slots takes 1.5 s to compile with g++ (`-fsyntax-only`, 0.9 s for 256 plain Parts); +with the default template depth (g++ 900, clang 1024) a chain of about 450 Parts stops, slots or not. + ## Predicates Predicates are plain types with three members, so `Traverse` can plug them in generically: diff --git a/include/hapi/hapi.h b/include/hapi/hapi.h index c0dd3ce..421ef55 100644 --- a/include/hapi/hapi.h +++ b/include/hapi/hapi.h @@ -7,6 +7,7 @@ #pragma once #include "hapi/rules.h" #include "hapi/meta.h" +#include "hapi/slots.h" namespace hapi { // ====================== APIOf ======================-- diff --git a/include/hapi/slots.h b/include/hapi/slots.h new file mode 100644 index 0000000..4dbad35 --- /dev/null +++ b/include/hapi/slots.h @@ -0,0 +1,66 @@ +/** + * @file slots.h + * @brief Slots: state composed along a chain and addressed by tag. + * + * hapi::Slot is a component that adds one slot, a value of type S, to the state the components after it built. + * The state is one object of static size with no heap; a slot whose S is empty adds nothing. + * + * using State = hapi::APIOf, hapi::Slot>::Res; + * hapi::slot(state).x = 1; // by tag; a tag that is not in the state is a compile error + * state.each(visitor); // visitor.layer(Tag::name()), then S::each(slot, visitor), for every slot, the last-listed first + * + * The same tag twice in one state is a compile error, also across nested chains and nested APIOf. + * + * What the slots mean, what their fields are called, how the state evolves or travels is not decided here. Slot's third parameter, + * a Contract, is how a user adds members to the state's type; R is the state's type and R::me() is the slot of this Part: + * + * template struct Counted : Below { unsigned steps = 0; void step() { ++steps; } }; + * hapi::Slot + * + * each() calls Tag::name() and S::each() only when it is used, and does not look at what Tag::name() returns. + */ +#pragma once +#include "hapi/base.h" + +#ifndef HAPI_SLOT_EACH_INLINE + #define HAPI_SLOT_EACH_INLINE // e.g. [[gnu::always_inline]]: a size policy for walks over the state +#endif + +namespace hapi { + /// @brief the slot as a base class, addressed by its tag + template struct SlotOf : S {}; + template constexpr S& slot_(SlotOf& f) { return f; } + template constexpr const S& slot_(const SlotOf& f) { return f; } + + template T& slot_decl(); + template struct HasSlot : std::false_type {}; + template struct HasSlot(slot_decl()))>> : std::true_type {}; + + /// @brief the slot of Tag in a state + template constexpr decltype(auto) slot(R& r) { + static_assert(HasSlot::value, "hapi::slot: no slot with that tag in this state (a Part sees its own slot and the slots of the Parts after it)"); + return slot_(r); + } + + /// @brief the end of the chain: no slot, nothing to walk. SlotApi is the terminal API of a state that needs nothing more. + struct SlotRoot { template constexpr void each(V&) const {} }; + struct SlotApi { using Res = SlotRoot; }; + + /// @brief what a state adds when no Contract asks for more: nothing + template struct SlotBase : Below {}; + + /// @brief a component that adds a slot. Contract is the base the state derives from, carrying the state below (Below) and the state's + /// own type (R): a Contract adds members to the state without adding an inheritance level, so the state keeps the same aggregate shape. + template class Contract = SlotBase> + struct Slot { + template struct Part : O { + static_assert(!HasSlot::value, "hapi::Slot: two Parts claim the same tag"); + struct Res : Contract, SlotOf { + constexpr S& me() { return slot_(*this); } + constexpr const S& me() const { return slot_(*this); } + template HAPI_SLOT_EACH_INLINE constexpr void each(V& v) { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); } + template HAPI_SLOT_EACH_INLINE constexpr void each(V& v) const { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); } + }; + }; + }; +} diff --git a/library.json b/library.json index e864126..470cecd 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "HAPI", - "version": "0.7.0", + "version": "0.8.0", "description": "The Happy API - compositional API tools", "keywords": "API, static, compositional, zero-cost abstractions, modern C++", "headers": "hapi.h", diff --git a/single/hapi.h b/single/hapi.h index 6fc8c1d..d55409e 100644 --- a/single/hapi.h +++ b/single/hapi.h @@ -21,9 +21,9 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -// HAPI 0.7.0, single header: include/hapi/*.h inlined by scripts/amalgamate.py. Do not edit; +// HAPI 0.8.0, single header: include/hapi/*.h inlined by scripts/amalgamate.py. Do not edit; // regenerate with `python3 scripts/amalgamate.py` (tests/single_header/run.sh checks it is current). -// Headers, in order: hapi/hapi.h, hapi/rules.h, hapi/chain.h, hapi/meta.h, hapi/base.h, hapi/platform/avr/avr_std.h +// Headers, in order: hapi/hapi.h, hapi/rules.h, hapi/chain.h, hapi/meta.h, hapi/base.h, hapi/platform/avr/avr_std.h, hapi/slots.h #pragma once // ---- begin hapi/hapi.h ---- @@ -1039,6 +1039,73 @@ namespace hapi { }; // ---- end hapi/rules.h ---- // #include "hapi/meta.h" -- inlined above +// ---- begin hapi/slots.h ---- +/** + * @file slots.h + * @brief Slots: state composed along a chain and addressed by tag. + * + * hapi::Slot is a component that adds one slot, a value of type S, to the state the components after it built. + * The state is one object of static size with no heap; a slot whose S is empty adds nothing. + * + * using State = hapi::APIOf, hapi::Slot>::Res; + * hapi::slot(state).x = 1; // by tag; a tag that is not in the state is a compile error + * state.each(visitor); // visitor.layer(Tag::name()), then S::each(slot, visitor), for every slot, the last-listed first + * + * The same tag twice in one state is a compile error, also across nested chains and nested APIOf. + * + * What the slots mean, what their fields are called, how the state evolves or travels is not decided here. Slot's third parameter, + * a Contract, is how a user adds members to the state's type; R is the state's type and R::me() is the slot of this Part: + * + * template struct Counted : Below { unsigned steps = 0; void step() { ++steps; } }; + * hapi::Slot + * + * each() calls Tag::name() and S::each() only when it is used, and does not look at what Tag::name() returns. + */ +// #include "hapi/base.h" -- inlined above + +#ifndef HAPI_SLOT_EACH_INLINE + #define HAPI_SLOT_EACH_INLINE // e.g. [[gnu::always_inline]]: a size policy for walks over the state +#endif + +namespace hapi { + /// @brief the slot as a base class, addressed by its tag + template struct SlotOf : S {}; + template constexpr S& slot_(SlotOf& f) { return f; } + template constexpr const S& slot_(const SlotOf& f) { return f; } + + template T& slot_decl(); + template struct HasSlot : std::false_type {}; + template struct HasSlot(slot_decl()))>> : std::true_type {}; + + /// @brief the slot of Tag in a state + template constexpr decltype(auto) slot(R& r) { + static_assert(HasSlot::value, "hapi::slot: no slot with that tag in this state (a Part sees its own slot and the slots of the Parts after it)"); + return slot_(r); + } + + /// @brief the end of the chain: no slot, nothing to walk. SlotApi is the terminal API of a state that needs nothing more. + struct SlotRoot { template constexpr void each(V&) const {} }; + struct SlotApi { using Res = SlotRoot; }; + + /// @brief what a state adds when no Contract asks for more: nothing + template struct SlotBase : Below {}; + + /// @brief a component that adds a slot. Contract is the base the state derives from, carrying the state below (Below) and the state's + /// own type (R): a Contract adds members to the state without adding an inheritance level, so the state keeps the same aggregate shape. + template class Contract = SlotBase> + struct Slot { + template struct Part : O { + static_assert(!HasSlot::value, "hapi::Slot: two Parts claim the same tag"); + struct Res : Contract, SlotOf { + constexpr S& me() { return slot_(*this); } + constexpr const S& me() const { return slot_(*this); } + template HAPI_SLOT_EACH_INLINE constexpr void each(V& v) { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); } + template HAPI_SLOT_EACH_INLINE constexpr void each(V& v) const { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); } + }; + }; + }; +} +// ---- end hapi/slots.h ---- namespace hapi { // ====================== APIOf ======================-- diff --git a/tests/negative/slots_dup_tag.cpp b/tests/negative/slots_dup_tag.cpp new file mode 100644 index 0000000..c7e9c09 --- /dev/null +++ b/tests/negative/slots_dup_tag.cpp @@ -0,0 +1,8 @@ +// EXPECT-ERROR: hapi::Slot: two Parts claim the same tag +#include "../../include/hapi/hapi.h" +using namespace hapi; +struct A { static constexpr int name() { return 1; } }; +struct S1 { int x; template static constexpr void each(Self& s, V& v) { v(s.x); } }; +struct S2 { int y; template static constexpr void each(Self& s, V& v) { v(s.y); } }; +using Bad = APIOf, Slot>::Res; +Bad b; diff --git a/tests/negative/slots_dup_tag_nested.cpp b/tests/negative/slots_dup_tag_nested.cpp new file mode 100644 index 0000000..0f54ed5 --- /dev/null +++ b/tests/negative/slots_dup_tag_nested.cpp @@ -0,0 +1,9 @@ +// EXPECT-ERROR: hapi::Slot: two Parts claim the same tag +// the tag is repeated across a nested APIOf +#include "../../include/hapi/hapi.h" +using namespace hapi; +struct A { static constexpr int name() { return 1; } }; +struct D { static constexpr int name() { return 4; } }; +struct S1 { int x; template static constexpr void each(Self& s, V& v) { v(s.x); } }; +using Bad = APIOf, APIOf>, Slot>::Res; +Bad b; diff --git a/tests/negative/slots_missing_tag.cpp b/tests/negative/slots_missing_tag.cpp new file mode 100644 index 0000000..b57b7d0 --- /dev/null +++ b/tests/negative/slots_missing_tag.cpp @@ -0,0 +1,8 @@ +// EXPECT-ERROR: hapi::slot: no slot with that tag +#include "../../include/hapi/hapi.h" +using namespace hapi; +struct A { static constexpr int name() { return 1; } }; +struct Z { static constexpr int name() { return 26; } }; +struct S1 { int x; template static constexpr void each(Self& s, V& v) { v(s.x); } }; +using State = APIOf>::Res; +int f(State& s) { return slot(s).x; } diff --git a/tests/single_header/run.sh b/tests/single_header/run.sh index dfde7b4..6a7325a 100755 --- a/tests/single_header/run.sh +++ b/tests/single_header/run.sh @@ -29,6 +29,13 @@ both() { local n=$1 dis=$2; shift 2 if cmp -s "$W/include.dis" "$W/single.dis"; then ok "$n: identical disassembly ($(wc -l < "$W/single.dis") lines)"; return 0 else bad "$n" "disassembly differs"; return 1; fi; } +echo "== host: tests/slots_tests.cpp (hapi/slots.h reaches the single header through hapi.h)" +for cxx in g++ clang++; do have $cxx || continue + if both "slots_tests [$cxx]" objdump $cxx -std=c++17 -O2 -I\"\$HAPI_INC\" tests/slots_tests.cpp; then + [ "$("$W/include.elf" 2>&1)" = "$("$W/single.elf" 2>&1)" ] && ok "slots_tests [$cxx]: same output" || bad "slots_tests [$cxx]" "outputs differ" + fi +done + echo "== host examples" for cxx in g++ clang++; do have $cxx || continue for ex in rules std crtp free virt; do diff --git a/tests/slots/core_avr.cpp b/tests/slots/core_avr.cpp new file mode 100644 index 0000000..5cd25b4 --- /dev/null +++ b/tests/slots/core_avr.cpp @@ -0,0 +1,27 @@ +// hapi/slots.h on AVR: typed slots (default) against the same state as a hand-indexed byte array (-DFLAT), one harness. +// tests/slots/run.sh builds both and requires the same program. +#include +#include +struct P { static constexpr int name() { return 1; } }; +struct Q { static constexpr int name() { return 2; } }; +struct SlotP { int16_t x; template static constexpr void each(Self& s, V& v) { v(s.x); } }; +struct SlotQ { int16_t x; uint8_t y; template static constexpr void each(Self& s, V& v) { v(s.x); v(s.y); } }; +using State = hapi::APIOf, hapi::Slot>::Res; // Q is listed last: x at 0, y at 2; P's x at 3 +volatile int16_t in_a; volatile uint8_t in_b; volatile int16_t out_a, out_c; volatile uint8_t out_b; +#ifdef FLAT +struct Flat { uint8_t v[5]; }; +typedef int16_t __attribute__((may_alias)) i16a; +static inline int16_t rd(const uint8_t* p) { return *reinterpret_cast(p); } +static inline void wr(uint8_t* p, int16_t x) { *reinterpret_cast(p) = x; } +extern "C" __attribute__((noinline)) void run(Flat* s) { + wr(s->v + 0, int16_t(rd(s->v + 0) + rd(s->v + 3))); s->v[2] = uint8_t(s->v[2] ^ uint8_t(rd(s->v + 0))); wr(s->v + 3, int16_t(rd(s->v + 3) + 1)); } +int main() { Flat s; wr(s.v + 3, in_a); wr(s.v + 0, in_a); s.v[2] = in_b; + for (;;) { run(&s); out_a = rd(s.v + 3); out_c = rd(s.v + 0); out_b = s.v[2]; } } +#else +extern "C" __attribute__((noinline)) void run(State* s) { + hapi::slot(*s).x = int16_t(hapi::slot(*s).x + hapi::slot

(*s).x); + hapi::slot(*s).y = uint8_t(hapi::slot(*s).y ^ uint8_t(hapi::slot(*s).x)); + hapi::slot

(*s).x = int16_t(hapi::slot

(*s).x + 1); } +int main() { State s; hapi::slot

(s).x = in_a; hapi::slot(s).x = in_a; hapi::slot(s).y = in_b; + for (;;) { run(&s); out_a = hapi::slot

(s).x; out_c = hapi::slot(s).x; out_b = hapi::slot(s).y; } } +#endif diff --git a/tests/slots/run.sh b/tests/slots/run.sh new file mode 100755 index 0000000..dd68367 --- /dev/null +++ b/tests/slots/run.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# hapi/slots.h costs nothing on AVR: a typed slot access is the same flashed program as the hand-indexed array (avr-g++ -Os, ATmega328p). +# Compares the flashed bytes (.text + .data) of tests/slots/core_avr.cpp with and without -DFLAT. Skipped without avr-g++. +# Usage: tests/slots/run.sh (exit status 1 on a difference; not part of CI's tests/*.cpp glob) +set -u +cd "$(dirname "$0")/../.." +if ! command -v avr-g++ >/dev/null 2>&1 || ! command -v avr-objcopy >/dev/null 2>&1; then echo " note avr-g++ not found: skipped"; exit 0; fi +W=$(mktemp -d); trap 'rm -rf "$W"' EXIT +for v in typed flat; do + fl=""; [ $v = flat ] && fl="-DFLAT" + avr-g++ -std=c++17 -Os -mmcu=atmega328p $fl -Iinclude tests/slots/core_avr.cpp -o "$W/$v.elf" || { echo " FAIL $v does not compile"; exit 1; } + avr-objcopy -O binary -j .text -j .data "$W/$v.elf" "$W/$v.bin" +done +if cmp -s "$W/typed.bin" "$W/flat.bin"; then echo " ok typed slots == hand-indexed array: identical flashed image ($(stat -c %s "$W/typed.bin") bytes)"; else echo " FAIL the flashed images differ"; exit 1; fi diff --git a/tests/slots_tests.cpp b/tests/slots_tests.cpp new file mode 100644 index 0000000..20d2d29 --- /dev/null +++ b/tests/slots_tests.cpp @@ -0,0 +1,97 @@ +/** + * @file slots_tests.cpp + * @brief hapi/slots.h: tag-addressed slots along a chain, nested compositions, the Contract parameter. + * + * Compiled and run; a failing check makes the exit status non-zero. + */ +#include +#include +#include +using namespace hapi; + +namespace slots_test { + // tags: the core does not know what a name is, here an int + struct A { static constexpr int name() { return 11; } }; + struct B { static constexpr int name() { return 22; } }; + struct C { static constexpr int name() { return 33; } }; + struct D { static constexpr int name() { return 44; } }; + struct Empty { static constexpr int name() { return 55; } }; + + struct SlotA { int16_t x; uint8_t y; template static constexpr void each(Self& s, V& v) { v(s.x); v(s.y); } }; + struct SlotB { int16_t x; template static constexpr void each(Self& s, V& v) { v(s.x); } }; // a field named like SlotA's: no clash + struct SlotC { int16_t x; template static constexpr void each(Self& s, V& v) { v(s.x); } }; + struct SlotD { int16_t x; template static constexpr void each(Self& s, V& v) { v(s.x); } }; + struct SlotNone { template static constexpr void each(Self&, V&) {} }; + + using State = APIOf, Slot, Slot>::Res; + static_assert( HasSlot::value && HasSlot::value && HasSlot::value, "a slot is found by its tag"); + static_assert(!HasSlot::value, "a tag that is not in the state is not found"); + + // a Contract adds members to the state's type (this is the example of hapi/slots.h) + template struct Counted : Below { unsigned steps = 0; void step() { ++steps; } }; + using Counting = APIOf>::Res; + + // nested compositions: a nested APIOf (once and twice) and a nested Chain, as one component of an outer chain + using Inner = APIOf, Slot>; + using Pair = Chain, Slot>; + using Flat = APIOf, Slot, Slot, Slot>::Res; + using WithApiOf = APIOf, Inner, Slot>::Res; + using WithChain = APIOf, Pair, Slot>::Res; + using Twice = APIOf, APIOf>, Slot>, Slot>::Res; + + struct Collect { + int layers[8]; int nl = 0; int vals[16]; int nv = 0; + void layer(int n) { layers[nl++] = n; } + template void operator()(const T& x) { vals[nv++] = int(x); } + }; + int failures = 0; + void check(const char* name, bool ok) { printf("CHECK %s: %s\n", name, ok ? "ok" : "FAIL"); if (!ok) failures++; } + + template bool reachable(R& r) { + slot(r).x = 1; slot(r).x = 2; slot(r).x = 3; slot(r).x = 4; + return slot(r).x == 1 && slot(r).x == 2 && slot(r).x == 3 && slot(r).x == 4; + } + template bool same_walk(R& r) { + Flat f{}; reachable(r); reachable(f); + Collect a, b; r.each(a); f.each(b); + bool same = a.nl == b.nl && a.nv == b.nv; + for (int i = 0; same && i < a.nl; i++) same = a.layers[i] == b.layers[i]; + for (int i = 0; same && i < a.nv; i++) same = a.vals[i] == b.vals[i]; + return same; + } +} + +int main() { + using namespace slots_test; + State s{}; slot(s).x = 1; slot(s).x = 2; slot(s).y = 3; + check("tags", slot(s).x == 1 && slot(s).x == 2 && slot(s).y == 3); + + { using One = APIOf>::Res; const One c = One{{}, {SlotA{5, 6}}}; + check("brace-init", slot(c).x == 5 && slot(c).y == 6); } // a state of one slot is { {}, {slot} } + + { Collect v; s.each(v); + check("each-order", v.nl == 3 && v.layers[0] == 55 && v.layers[1] == 22 && v.layers[2] == 11 && v.nv == 3 && v.vals[0] == 2 && v.vals[1] == 1 && v.vals[2] == 3); } // last-listed first + { const State c = s; Collect v; c.each(v); check("each-const", v.nv == 3); } + + { Counting c{}; slot(c).x = 21; c.step(); c.step(); + check("contract", c.steps == 2 && slot(c).x == 21); } + + { WithApiOf a{}; WithChain b{}; Twice t{}; + check("nested-apiof", reachable(a)); + check("nested-chain", reachable(b)); + check("nested-twice", reachable(t)); + check("nested-walk-as-flat", same_walk(a) && same_walk(b) && same_walk(t)); } + + // sizes: an empty slot and nesting cost nothing where the compiler removes empty bases (GCC, Clang); MSVC needs __declspec(empty_bases) for that + { + using Without = APIOf, Slot>::Res; +#ifdef _MSC_VER + printf("note: MSVC sizeof: with an empty slot %zu, without %zu; nested %zu, flat %zu\n", sizeof(State), sizeof(Without), sizeof(WithApiOf), sizeof(Flat)); +#else + check("empty-costs-nothing", sizeof(State) == sizeof(Without)); + check("nesting-costs-nothing", sizeof(WithApiOf) == sizeof(Flat) && sizeof(WithChain) == sizeof(Flat) && sizeof(Twice) == sizeof(Flat)); +#endif + } + printf("%d failed\n", failures); + return failures; +} From 8fcc81ea67ef0cba1deeecac3bb227b0d265fad2 Mon Sep 17 00:00:00 2001 From: neu-rah Date: Wed, 30 Sep 2026 02:54:03 +0000 Subject: [PATCH 2/4] slots: an empty slot adds nothing on GCC and Clang; on MSVC it cost 2 bytes (measured in CI) The first CI run on MSVC printed the sizes that slots_tests.cpp guards: a state of 6 bytes became 8 with an empty slot; a nested composition is still the same size as the flat one (10 = 10). The README, the header comment, the CHANGELOG and the test comment now say that instead of "adds nothing"; __declspec(empty_bases) was not tried. single/hapi.h regenerated (the header comment). Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- README.md | 4 ++-- include/hapi/slots.h | 3 ++- single/hapi.h | 3 ++- tests/slots_tests.cpp | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e235798..9580d04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added - **`hapi/slots.h`: slots, state composed along a chain and addressed by tag.** `Slot` is a component that adds one value of type S to the - state the components after it built; the state is one object of static size, no heap, and an empty slot adds nothing. `slot(state)` reaches a slot by + state the components after it built; the state is one object of static size, no heap, and an empty slot adds nothing on GCC and Clang (2 bytes on MSVC, measured in CI). `slot(state)` reaches a slot by its tag, `HasSlot` asks, `each(visitor)` walks the slots in chain order, `SlotApi`/`SlotRoot` end the chain. The same tag twice, also across nested `Chain` and `APIOf`, and a tag that is not in the state are compile errors with their own messages. Nothing about what the state means, how it evolves or how it is sent is decided: the third parameter, a `Contract`, is how a user adds members to the state's type. `hapi.h` includes it; it costs nothing unless used. Measured: the same AVR diff --git a/README.md b/README.md index 76222e9..c27cc89 100644 --- a/README.md +++ b/README.md @@ -367,8 +367,8 @@ using Counting = APIOf>::Res; * **`each(visitor)`** walks the slots in chain order (the last-listed first): `visitor.layer(Tag::name())`, then `S::each(slot, visitor)`, which the slot's type defines. `Tag::name()` and `S::each` are only looked at when `each` is used, and what `name()` returns is the visitor's business. -* **Cost:** the same AVR program as a hand-indexed array (`tests/slots/run.sh` compares the flashed bytes); an empty slot and a nested composition add no size with GCC and Clang (MSVC lays - out several empty bases differently unless `__declspec(empty_bases)` is used; `tests/slots_tests.cpp` prints the sizes there). A chain of 256 slots takes 1.5 s to compile with g++ (`-fsyntax-only`), against +* **Cost:** the same AVR program as a hand-indexed array (`tests/slots/run.sh` compares the flashed bytes); a nested composition adds no size on GCC, Clang or MSVC; an empty slot adds none on GCC and Clang, but on MSVC it cost 2 bytes in CI (a state of 6 bytes became 8: its layout + of several empty bases; `__declspec(empty_bases)` was not tried). A chain of 256 slots takes 1.5 s to compile with g++ (`-fsyntax-only`), against 0.9 s for 256 plain Parts. The reference is in [`docs/REFERENCE.md`](docs/REFERENCE.md). diff --git a/include/hapi/slots.h b/include/hapi/slots.h index 4dbad35..cb981a1 100644 --- a/include/hapi/slots.h +++ b/include/hapi/slots.h @@ -3,7 +3,8 @@ * @brief Slots: state composed along a chain and addressed by tag. * * hapi::Slot is a component that adds one slot, a value of type S, to the state the components after it built. - * The state is one object of static size with no heap; a slot whose S is empty adds nothing. + * The state is one object of static size with no heap; a slot whose S is empty adds nothing on GCC and Clang (on MSVC it cost 2 bytes in CI: + * its layout of several empty bases differs; `__declspec(empty_bases)` was not tried). * * using State = hapi::APIOf, hapi::Slot>::Res; * hapi::slot(state).x = 1; // by tag; a tag that is not in the state is a compile error diff --git a/single/hapi.h b/single/hapi.h index d55409e..f798dfc 100644 --- a/single/hapi.h +++ b/single/hapi.h @@ -1045,7 +1045,8 @@ namespace hapi { * @brief Slots: state composed along a chain and addressed by tag. * * hapi::Slot is a component that adds one slot, a value of type S, to the state the components after it built. - * The state is one object of static size with no heap; a slot whose S is empty adds nothing. + * The state is one object of static size with no heap; a slot whose S is empty adds nothing on GCC and Clang (on MSVC it cost 2 bytes in CI: + * its layout of several empty bases differs; `__declspec(empty_bases)` was not tried). * * using State = hapi::APIOf, hapi::Slot>::Res; * hapi::slot(state).x = 1; // by tag; a tag that is not in the state is a compile error diff --git a/tests/slots_tests.cpp b/tests/slots_tests.cpp index 20d2d29..46f2024 100644 --- a/tests/slots_tests.cpp +++ b/tests/slots_tests.cpp @@ -82,7 +82,7 @@ int main() { check("nested-twice", reachable(t)); check("nested-walk-as-flat", same_walk(a) && same_walk(b) && same_walk(t)); } - // sizes: an empty slot and nesting cost nothing where the compiler removes empty bases (GCC, Clang); MSVC needs __declspec(empty_bases) for that + // sizes: an empty slot and nesting cost nothing on GCC and Clang; on MSVC nesting still costs nothing but an empty slot costs 2 bytes (measured in CI), so only the sizes are printed there { using Without = APIOf, Slot>::Res; #ifdef _MSC_VER From 9148ec81e7da4a212f551a13485c70725fd3a8ea Mon Sep 17 00:00:00 2001 From: neu-rah Date: Wed, 30 Sep 2026 03:02:13 +0000 Subject: [PATCH 3/4] slots: HAPI_EMPTY_BASES (__declspec(empty_bases) on MSVC) on the state class MSVC applies the empty base optimisation to one base only, so an empty slot cost 2 bytes there (CI, first run). The macro is empty on every other compiler and on clang-cl. Whether it brings MSVC back to zero is what this pull request's CI is for; the corrected wording stays until it says so. single/hapi.h regenerated. Co-Authored-By: Claude Sonnet 5.5 --- include/hapi/slots.h | 9 ++++++++- single/hapi.h | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/include/hapi/slots.h b/include/hapi/slots.h index cb981a1..c1e58cc 100644 --- a/include/hapi/slots.h +++ b/include/hapi/slots.h @@ -23,6 +23,13 @@ #pragma once #include "hapi/base.h" +#ifndef HAPI_EMPTY_BASES + #if defined(_MSC_VER) && !defined(__clang__) + #define HAPI_EMPTY_BASES __declspec(empty_bases) // MSVC removes an empty base from one base only, unless asked + #else + #define HAPI_EMPTY_BASES + #endif +#endif #ifndef HAPI_SLOT_EACH_INLINE #define HAPI_SLOT_EACH_INLINE // e.g. [[gnu::always_inline]]: a size policy for walks over the state #endif @@ -56,7 +63,7 @@ namespace hapi { struct Slot { template struct Part : O { static_assert(!HasSlot::value, "hapi::Slot: two Parts claim the same tag"); - struct Res : Contract, SlotOf { + struct HAPI_EMPTY_BASES Res : Contract, SlotOf { constexpr S& me() { return slot_(*this); } constexpr const S& me() const { return slot_(*this); } template HAPI_SLOT_EACH_INLINE constexpr void each(V& v) { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); } diff --git a/single/hapi.h b/single/hapi.h index f798dfc..67269f4 100644 --- a/single/hapi.h +++ b/single/hapi.h @@ -1064,6 +1064,13 @@ namespace hapi { */ // #include "hapi/base.h" -- inlined above +#ifndef HAPI_EMPTY_BASES + #if defined(_MSC_VER) && !defined(__clang__) + #define HAPI_EMPTY_BASES __declspec(empty_bases) // MSVC removes an empty base from one base only, unless asked + #else + #define HAPI_EMPTY_BASES + #endif +#endif #ifndef HAPI_SLOT_EACH_INLINE #define HAPI_SLOT_EACH_INLINE // e.g. [[gnu::always_inline]]: a size policy for walks over the state #endif @@ -1097,7 +1104,7 @@ namespace hapi { struct Slot { template struct Part : O { static_assert(!HasSlot::value, "hapi::Slot: two Parts claim the same tag"); - struct Res : Contract, SlotOf { + struct HAPI_EMPTY_BASES Res : Contract, SlotOf { constexpr S& me() { return slot_(*this); } constexpr const S& me() const { return slot_(*this); } template HAPI_SLOT_EACH_INLINE constexpr void each(V& v) { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); } From ea906c6e0f32cefcd1b2ad4a76217a1612f6e3ba Mon Sep 17 00:00:00 2001 From: neu-rah Date: Wed, 30 Sep 2026 03:03:26 +0000 Subject: [PATCH 4/4] slots: an empty slot adds nothing on GCC, Clang and MSVC; the size checks are unconditional again CI on MSVC with HAPI_EMPTY_BASES (__declspec(empty_bases)): a state with an empty slot is 6 bytes, the same as without; nested and flat are both 10. The two size checks of slots_tests.cpp no longer skip MSVC, so the claim is enforced there by CI; README, CHANGELOG, REFERENCE and the header comment say it without the footnote. single/hapi.h regenerated. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- README.md | 4 ++-- docs/REFERENCE.md | 1 + include/hapi/slots.h | 4 ++-- single/hapi.h | 4 ++-- tests/slots_tests.cpp | 6 +----- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9580d04..790ac39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added - **`hapi/slots.h`: slots, state composed along a chain and addressed by tag.** `Slot` is a component that adds one value of type S to the - state the components after it built; the state is one object of static size, no heap, and an empty slot adds nothing on GCC and Clang (2 bytes on MSVC, measured in CI). `slot(state)` reaches a slot by + state the components after it built; the state is one object of static size, no heap, and an empty slot adds nothing (on MSVC through `__declspec(empty_bases)`, applied by the header as `HAPI_EMPTY_BASES`). `slot(state)` reaches a slot by its tag, `HasSlot` asks, `each(visitor)` walks the slots in chain order, `SlotApi`/`SlotRoot` end the chain. The same tag twice, also across nested `Chain` and `APIOf`, and a tag that is not in the state are compile errors with their own messages. Nothing about what the state means, how it evolves or how it is sent is decided: the third parameter, a `Contract`, is how a user adds members to the state's type. `hapi.h` includes it; it costs nothing unless used. Measured: the same AVR diff --git a/README.md b/README.md index c27cc89..f2e0114 100644 --- a/README.md +++ b/README.md @@ -367,8 +367,8 @@ using Counting = APIOf>::Res; * **`each(visitor)`** walks the slots in chain order (the last-listed first): `visitor.layer(Tag::name())`, then `S::each(slot, visitor)`, which the slot's type defines. `Tag::name()` and `S::each` are only looked at when `each` is used, and what `name()` returns is the visitor's business. -* **Cost:** the same AVR program as a hand-indexed array (`tests/slots/run.sh` compares the flashed bytes); a nested composition adds no size on GCC, Clang or MSVC; an empty slot adds none on GCC and Clang, but on MSVC it cost 2 bytes in CI (a state of 6 bytes became 8: its layout - of several empty bases; `__declspec(empty_bases)` was not tried). A chain of 256 slots takes 1.5 s to compile with g++ (`-fsyntax-only`), against +* **Cost:** the same AVR program as a hand-indexed array (`tests/slots/run.sh` compares the flashed bytes); an empty slot and a nested composition add no size (checked on GCC, Clang and MSVC; MSVC removes an empty base from one base only unless `__declspec(empty_bases)` is + applied, which the header does through `HAPI_EMPTY_BASES`). A chain of 256 slots takes 1.5 s to compile with g++ (`-fsyntax-only`), against 0.9 s for 256 plain Parts. The reference is in [`docs/REFERENCE.md`](docs/REFERENCE.md). diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index e681556..be88a54 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -83,6 +83,7 @@ State composed along a chain, addressed by tag. `hapi.h` includes it; nothing is | `SlotBase` | the default `Contract`: derives from `Below`, adds nothing | | `state.each(v)` | `v.layer(Tag::name())`, then `S::each(slot, v)`, for every slot in chain order (the last-listed first); `Tag::name()` and `S::each` are only required when `each` is used | | `state.me()` (inside a Contract, as `R::me()`) | the slot of this Part | +| `HAPI_EMPTY_BASES` | macro: `__declspec(empty_bases)` on MSVC (not clang-cl), empty elsewhere; it keeps an empty slot at zero size on MSVC, which removes an empty base from one base only | | `HAPI_SLOT_EACH_INLINE` | macro, empty by default: an attribute (e.g. `[[gnu::always_inline]]`) for `each`, a size policy | The same tag twice in one state is a compile error (`hapi::Slot: two Parts claim the same tag`), also when the second is inside a nested `Chain` or `APIOf`; a nested composition gives the diff --git a/include/hapi/slots.h b/include/hapi/slots.h index c1e58cc..79fa5e0 100644 --- a/include/hapi/slots.h +++ b/include/hapi/slots.h @@ -3,8 +3,8 @@ * @brief Slots: state composed along a chain and addressed by tag. * * hapi::Slot is a component that adds one slot, a value of type S, to the state the components after it built. - * The state is one object of static size with no heap; a slot whose S is empty adds nothing on GCC and Clang (on MSVC it cost 2 bytes in CI: - * its layout of several empty bases differs; `__declspec(empty_bases)` was not tried). + * The state is one object of static size with no heap; a slot whose S is empty adds nothing (on MSVC through __declspec(empty_bases), which the + * header applies; HAPI_EMPTY_BASES is the macro). * * using State = hapi::APIOf, hapi::Slot>::Res; * hapi::slot(state).x = 1; // by tag; a tag that is not in the state is a compile error diff --git a/single/hapi.h b/single/hapi.h index 67269f4..10aedda 100644 --- a/single/hapi.h +++ b/single/hapi.h @@ -1045,8 +1045,8 @@ namespace hapi { * @brief Slots: state composed along a chain and addressed by tag. * * hapi::Slot is a component that adds one slot, a value of type S, to the state the components after it built. - * The state is one object of static size with no heap; a slot whose S is empty adds nothing on GCC and Clang (on MSVC it cost 2 bytes in CI: - * its layout of several empty bases differs; `__declspec(empty_bases)` was not tried). + * The state is one object of static size with no heap; a slot whose S is empty adds nothing (on MSVC through __declspec(empty_bases), which the + * header applies; HAPI_EMPTY_BASES is the macro). * * using State = hapi::APIOf, hapi::Slot>::Res; * hapi::slot(state).x = 1; // by tag; a tag that is not in the state is a compile error diff --git a/tests/slots_tests.cpp b/tests/slots_tests.cpp index 46f2024..f044787 100644 --- a/tests/slots_tests.cpp +++ b/tests/slots_tests.cpp @@ -82,15 +82,11 @@ int main() { check("nested-twice", reachable(t)); check("nested-walk-as-flat", same_walk(a) && same_walk(b) && same_walk(t)); } - // sizes: an empty slot and nesting cost nothing on GCC and Clang; on MSVC nesting still costs nothing but an empty slot costs 2 bytes (measured in CI), so only the sizes are printed there + // sizes: an empty slot and nesting cost nothing (MSVC needs __declspec(empty_bases), which slots.h applies) { using Without = APIOf, Slot>::Res; -#ifdef _MSC_VER - printf("note: MSVC sizeof: with an empty slot %zu, without %zu; nested %zu, flat %zu\n", sizeof(State), sizeof(Without), sizeof(WithApiOf), sizeof(Flat)); -#else check("empty-costs-nothing", sizeof(State) == sizeof(Without)); check("nesting-costs-nothing", sizeof(WithApiOf) == sizeof(Flat) && sizeof(WithChain) == sizeof(Flat) && sizeof(Twice) == sizeof(Flat)); -#endif } printf("%d failed\n", failures); return failures;