diff --git a/CHANGELOG.md b/CHANGELOG.md index d4190f8..790ac39 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 (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 + 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..f2e0114 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 (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). + +--- + ## Runtime resolution HAPI also provides `find(object)`. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 4b9c61e..be88a54 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -69,6 +69,32 @@ 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_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 +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..79fa5e0 --- /dev/null +++ b/include/hapi/slots.h @@ -0,0 +1,74 @@ +/** + * @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 (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 + * 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_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 + +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 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); } + 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..10aedda 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,81 @@ 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 (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 + * 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_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 + +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 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); } + 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..f044787 --- /dev/null +++ b/tests/slots_tests.cpp @@ -0,0 +1,93 @@ +/** + * @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 (MSVC needs __declspec(empty_bases), which slots.h applies) + { + using Without = APIOf, Slot>::Res; + check("empty-costs-nothing", sizeof(State) == sizeof(Without)); + check("nesting-costs-nothing", sizeof(WithApiOf) == sizeof(Flat) && sizeof(WithChain) == sizeof(Flat) && sizeof(Twice) == sizeof(Flat)); + } + printf("%d failed\n", failures); + return failures; +}