Skip to content

Commit 9484468

Browse files
authored
Merge pull request #5 from InternetOfPins/slots-core
hapi/slots.h: slots, state composed along a chain and addressed by tag
2 parents 18b8d77 + ea906c6 commit 9484468

14 files changed

Lines changed: 401 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
# Changelog
22

3-
## 0.7.0 (not yet tagged)
3+
## 0.8.0 (not yet tagged)
44

55
### Added
6-
- **`Expand<O>` / `Expansion<Children,Queried,Selected,Validates,Searched>` / `IsContainer<O>`**: one extension point that says what a
7-
container holds and which walks may open it. `Traverse`, `FindFirst`, `BuildRules` and `NoCollision` all read it, so a new
8-
container is taught once instead of once per walk. `Chain` (all four bits) and `APIOf` (`validates` only) are built in. See the README
9-
section "Teaching HAPI your own container" and `docs/REFERENCE.md`.
10-
- `Chain<OO...>::Drop<n>`: the chain without its first `n` elements.
6+
- **`hapi/slots.h`: slots, state composed along a chain and addressed by tag.** `Slot<Tag,S,Contract>` is a component that adds one value of type S to the
7+
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<Tag>(state)` reaches a slot by
8+
its tag, `HasSlot<Tag,R>` asks, `each(visitor)` walks the slots in chain order, `SlotApi`/`SlotRoot` end the chain. The same tag twice, also across nested `Chain`
9+
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
10+
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
11+
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
12+
"Composing state: slots" and `docs/REFERENCE.md`. Tests: `tests/slots_tests.cpp`, `tests/negative/slots_*.cpp`, `tests/slots/`.
1113
- `single/hapi.h`: the library as one header, generated by `scripts/amalgamate.py` (every `include/hapi/*.h` inlined once, the
1214
AVR shim kept under its `#if`, MIT header and version on top; `--check` reports a stale copy). `tests/single_header/run.sh` builds the
1315
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 @@
1719
compared by `is_base_of`. Covered in `tests/compile_tests.cpp`. Used by the `.RnD/openDerivation` translator in every composed struct.
1820
- 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
1921
(simavr, confirmed on a real chip), and a comparison against a table loop and against emlearn. Uses `Expand` and `Drop<n>`; GCC/Clang only, not built by CI.
22+
23+
## 0.7.0
24+
25+
### Added
26+
- **`Expand<O>` / `Expansion<Children,Queried,Selected,Validates,Searched>` / `IsContainer<O>`**: one extension point that says what a
27+
container holds and which walks may open it. `Traverse`, `FindFirst`, `BuildRules` and `NoCollision` all read it, so a new
28+
container is taught once instead of once per walk. `Chain` (all four bits) and `APIOf` (`validates` only) are built in. See the README
29+
section "Teaching HAPI your own container" and `docs/REFERENCE.md`.
30+
- `Chain<OO...>::Drop<n>`: the chain without its first `n` elements.
2031
- Tests: `tests/expand_tests.cpp`, `tests/expand_walks.cpp` (one container per policy bit), `tests/descent_characterization.cpp` (pins how every
2132
walk treats every kind of container), and `tests/negative/` (expect-to-fail cases: `tests/negative/run.sh`, `CXX=clang++` to switch
2233
compiler; not part of CI's `tests/*.cpp` glob).

‎README.md‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,45 @@ Detecting a leaf costs one class template instantiation per element visited (mea
336336
337337
---
338338
339+
## Composing state: slots
340+
341+
A chain composes data as well as behaviour. `hapi::Slot<Tag, S>` is a component that adds one value of type `S` to the state the components after it built. The state is
342+
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.
343+
344+
```cpp
345+
#include <hapi/hapi.h>
346+
using namespace hapi;
347+
348+
struct Pos {}; struct Speed {}; // tags: any distinct types
349+
struct PosSlot { int16_t x, y; };
350+
struct SpeedSlot { int16_t x; }; // a field named like PosSlot's: no clash
351+
352+
using State = APIOf<SlotApi, Slot<Pos, PosSlot>, Slot<Speed, SpeedSlot>>::Res;
353+
354+
State s{};
355+
slot<Pos>(s).x = 1; slot<Speed>(s).x = 2;
356+
```
357+
358+
* **Compile errors, with their own messages:** a tag that is not in the state (`hapi::slot<Tag>: no slot with that tag in this state`), and the same tag twice, also across a nested
359+
`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.
360+
* **Nothing about what the slots mean is decided.** `Slot<Tag, S, Contract>` takes a third parameter, a template `Contract<Below, R>` whose base carries the state below and the
361+
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()`:
362+
363+
```cpp
364+
template<class Below, class R> struct Counted : Below { unsigned steps = 0; void step() { ++steps; } };
365+
using Counting = APIOf<SlotApi, Slot<Pos, PosSlot, Counted>>::Res;
366+
```
367+
368+
* **`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
369+
`S::each` are only looked at when `each` is used, and what `name()` returns is the visitor's business.
370+
* **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
371+
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
372+
0.9 s for 256 plain Parts.
373+
374+
The reference is in [`docs/REFERENCE.md`](docs/REFERENCE.md).
375+
376+
---
377+
339378
## Runtime resolution
340379
341380
HAPI also provides `find<Q>(object)`.

‎docs/REFERENCE.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,32 @@ template<typename... II>
6969
struct Expand<Box<II...>> : Expansion<Chain<II...>, /*queried*/true, /*selected*/true> {};
7070
```
7171
72+
### Slots: `hapi/slots.h`
73+
74+
State composed along a chain, addressed by tag. `hapi.h` includes it; nothing is instantiated unless used.
75+
76+
| name | what |
77+
|---|---|
78+
| `Slot<Tag, S, Contract = SlotBase>` | a component that adds one slot of type `S`; `APIOf<SlotApi, Slot<A,SA>, Slot<B,SB>>::Res` is the state |
79+
| `slot<Tag>(state)` | the slot of `Tag`, `const` or not; a tag that is not in the state is a compile error (`hapi::slot<Tag>: no slot with that tag ...`) |
80+
| `HasSlot<Tag, R>` | whether the state type `R` has a slot for `Tag` |
81+
| `SlotApi`, `SlotRoot` | the end of the chain; `SlotApi::Res` is `SlotRoot`. A contract brings its own terminal API whose `Res` derives from `SlotRoot` |
82+
| `SlotOf<Tag, S>` | the slot as a base class of the state; `slot_<Tag>(...)` converts to it |
83+
| `SlotBase<Below, R>` | the default `Contract`: derives from `Below`, adds nothing |
84+
| `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 |
85+
| `state.me()` (inside a Contract, as `R::me()`) | the slot of this Part |
86+
| `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 |
87+
| `HAPI_SLOT_EACH_INLINE` | macro, empty by default: an attribute (e.g. `[[gnu::always_inline]]`) for `each`, a size policy |
88+
89+
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
90+
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.
91+
92+
A `Contract<Below, R>` is a class template: the state derives from `Contract<Below, R>` (first) and `SlotOf<Tag, S>`, so a contract adds members (a `step`, a serializer, a name policy) without adding an
93+
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.
94+
95+
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);
96+
with the default template depth (g++ 900, clang 1024) a chain of about 450 Parts stops, slots or not.
97+
7298
## Predicates
7399
74100
Predicates are plain types with three members, so `Traverse` can plug them in generically:

‎include/hapi/hapi.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#pragma once
88
#include "hapi/rules.h"
99
#include "hapi/meta.h"
10+
#include "hapi/slots.h"
1011

1112
namespace hapi {
1213
// ====================== APIOf ======================--

‎include/hapi/slots.h‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @file slots.h
3+
* @brief Slots: state composed along a chain and addressed by tag.
4+
*
5+
* hapi::Slot<Tag, S> is a component that adds one slot, a value of type S, to the state the components after it built.
6+
* 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
7+
* header applies; HAPI_EMPTY_BASES is the macro).
8+
*
9+
* using State = hapi::APIOf<hapi::SlotApi, hapi::Slot<A, SlotA>, hapi::Slot<B, SlotB>>::Res;
10+
* hapi::slot<A>(state).x = 1; // by tag; a tag that is not in the state is a compile error
11+
* state.each(visitor); // visitor.layer(Tag::name()), then S::each(slot, visitor), for every slot, the last-listed first
12+
*
13+
* The same tag twice in one state is a compile error, also across nested chains and nested APIOf.
14+
*
15+
* What the slots mean, what their fields are called, how the state evolves or travels is not decided here. Slot's third parameter,
16+
* 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:
17+
*
18+
* template<class Below, class R> struct Counted : Below { unsigned steps = 0; void step() { ++steps; } };
19+
* hapi::Slot<A, SlotA, Counted>
20+
*
21+
* each() calls Tag::name() and S::each() only when it is used, and does not look at what Tag::name() returns.
22+
*/
23+
#pragma once
24+
#include "hapi/base.h"
25+
26+
#ifndef HAPI_EMPTY_BASES
27+
#if defined(_MSC_VER) && !defined(__clang__)
28+
#define HAPI_EMPTY_BASES __declspec(empty_bases) // MSVC removes an empty base from one base only, unless asked
29+
#else
30+
#define HAPI_EMPTY_BASES
31+
#endif
32+
#endif
33+
#ifndef HAPI_SLOT_EACH_INLINE
34+
#define HAPI_SLOT_EACH_INLINE // e.g. [[gnu::always_inline]]: a size policy for walks over the state
35+
#endif
36+
37+
namespace hapi {
38+
/// @brief the slot as a base class, addressed by its tag
39+
template<class Tag, class S> struct SlotOf : S {};
40+
template<class Tag, class S> constexpr S& slot_(SlotOf<Tag,S>& f) { return f; }
41+
template<class Tag, class S> constexpr const S& slot_(const SlotOf<Tag,S>& f) { return f; }
42+
43+
template<class T> T& slot_decl();
44+
template<class Tag, class R, class = void> struct HasSlot : std::false_type {};
45+
template<class Tag, class R> struct HasSlot<Tag, R, std::void_t<decltype(slot_<Tag>(slot_decl<R>()))>> : std::true_type {};
46+
47+
/// @brief the slot of Tag in a state
48+
template<class Tag, class R> constexpr decltype(auto) slot(R& r) {
49+
static_assert(HasSlot<Tag,R>::value, "hapi::slot<Tag>: no slot with that tag in this state (a Part sees its own slot and the slots of the Parts after it)");
50+
return slot_<Tag>(r);
51+
}
52+
53+
/// @brief the end of the chain: no slot, nothing to walk. SlotApi is the terminal API of a state that needs nothing more.
54+
struct SlotRoot { template<class V> constexpr void each(V&) const {} };
55+
struct SlotApi { using Res = SlotRoot; };
56+
57+
/// @brief what a state adds when no Contract asks for more: nothing
58+
template<class Below, class R> struct SlotBase : Below {};
59+
60+
/// @brief a component that adds a slot. Contract<Below, R> is the base the state derives from, carrying the state below (Below) and the state's
61+
/// own type (R): a Contract adds members to the state without adding an inheritance level, so the state keeps the same aggregate shape.
62+
template<class Tag, class S, template<class, class> class Contract = SlotBase>
63+
struct Slot {
64+
template<class O> struct Part : O {
65+
static_assert(!HasSlot<Tag, typename O::Res>::value, "hapi::Slot: two Parts claim the same tag");
66+
struct HAPI_EMPTY_BASES Res : Contract<typename O::Res, Res>, SlotOf<Tag,S> {
67+
constexpr S& me() { return slot_<Tag>(*this); }
68+
constexpr const S& me() const { return slot_<Tag>(*this); }
69+
template<class V> HAPI_SLOT_EACH_INLINE constexpr void each(V& v) { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); }
70+
template<class V> HAPI_SLOT_EACH_INLINE constexpr void each(V& v) const { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); }
71+
};
72+
};
73+
};
74+
}

‎library.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "HAPI",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"description": "The Happy API - compositional API tools",
55
"keywords": "API, static, compositional, zero-cost abstractions, modern C++",
66
"headers": "hapi.h",

‎single/hapi.h‎

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
* SOFTWARE.
2323
*/
24-
// HAPI 0.7.0, single header: include/hapi/*.h inlined by scripts/amalgamate.py. Do not edit;
24+
// HAPI 0.8.0, single header: include/hapi/*.h inlined by scripts/amalgamate.py. Do not edit;
2525
// regenerate with `python3 scripts/amalgamate.py` (tests/single_header/run.sh checks it is current).
26-
// Headers, in order: hapi/hapi.h, hapi/rules.h, hapi/chain.h, hapi/meta.h, hapi/base.h, hapi/platform/avr/avr_std.h
26+
// 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
2727
#pragma once
2828

2929
// ---- begin hapi/hapi.h ----
@@ -1039,6 +1039,81 @@ namespace hapi {
10391039
};
10401040
// ---- end hapi/rules.h ----
10411041
// #include "hapi/meta.h" -- inlined above
1042+
// ---- begin hapi/slots.h ----
1043+
/**
1044+
* @file slots.h
1045+
* @brief Slots: state composed along a chain and addressed by tag.
1046+
*
1047+
* hapi::Slot<Tag, S> is a component that adds one slot, a value of type S, to the state the components after it built.
1048+
* 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
1049+
* header applies; HAPI_EMPTY_BASES is the macro).
1050+
*
1051+
* using State = hapi::APIOf<hapi::SlotApi, hapi::Slot<A, SlotA>, hapi::Slot<B, SlotB>>::Res;
1052+
* hapi::slot<A>(state).x = 1; // by tag; a tag that is not in the state is a compile error
1053+
* state.each(visitor); // visitor.layer(Tag::name()), then S::each(slot, visitor), for every slot, the last-listed first
1054+
*
1055+
* The same tag twice in one state is a compile error, also across nested chains and nested APIOf.
1056+
*
1057+
* What the slots mean, what their fields are called, how the state evolves or travels is not decided here. Slot's third parameter,
1058+
* 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:
1059+
*
1060+
* template<class Below, class R> struct Counted : Below { unsigned steps = 0; void step() { ++steps; } };
1061+
* hapi::Slot<A, SlotA, Counted>
1062+
*
1063+
* each() calls Tag::name() and S::each() only when it is used, and does not look at what Tag::name() returns.
1064+
*/
1065+
// #include "hapi/base.h" -- inlined above
1066+
1067+
#ifndef HAPI_EMPTY_BASES
1068+
#if defined(_MSC_VER) && !defined(__clang__)
1069+
#define HAPI_EMPTY_BASES __declspec(empty_bases) // MSVC removes an empty base from one base only, unless asked
1070+
#else
1071+
#define HAPI_EMPTY_BASES
1072+
#endif
1073+
#endif
1074+
#ifndef HAPI_SLOT_EACH_INLINE
1075+
#define HAPI_SLOT_EACH_INLINE // e.g. [[gnu::always_inline]]: a size policy for walks over the state
1076+
#endif
1077+
1078+
namespace hapi {
1079+
/// @brief the slot as a base class, addressed by its tag
1080+
template<class Tag, class S> struct SlotOf : S {};
1081+
template<class Tag, class S> constexpr S& slot_(SlotOf<Tag,S>& f) { return f; }
1082+
template<class Tag, class S> constexpr const S& slot_(const SlotOf<Tag,S>& f) { return f; }
1083+
1084+
template<class T> T& slot_decl();
1085+
template<class Tag, class R, class = void> struct HasSlot : std::false_type {};
1086+
template<class Tag, class R> struct HasSlot<Tag, R, std::void_t<decltype(slot_<Tag>(slot_decl<R>()))>> : std::true_type {};
1087+
1088+
/// @brief the slot of Tag in a state
1089+
template<class Tag, class R> constexpr decltype(auto) slot(R& r) {
1090+
static_assert(HasSlot<Tag,R>::value, "hapi::slot<Tag>: no slot with that tag in this state (a Part sees its own slot and the slots of the Parts after it)");
1091+
return slot_<Tag>(r);
1092+
}
1093+
1094+
/// @brief the end of the chain: no slot, nothing to walk. SlotApi is the terminal API of a state that needs nothing more.
1095+
struct SlotRoot { template<class V> constexpr void each(V&) const {} };
1096+
struct SlotApi { using Res = SlotRoot; };
1097+
1098+
/// @brief what a state adds when no Contract asks for more: nothing
1099+
template<class Below, class R> struct SlotBase : Below {};
1100+
1101+
/// @brief a component that adds a slot. Contract<Below, R> is the base the state derives from, carrying the state below (Below) and the state's
1102+
/// own type (R): a Contract adds members to the state without adding an inheritance level, so the state keeps the same aggregate shape.
1103+
template<class Tag, class S, template<class, class> class Contract = SlotBase>
1104+
struct Slot {
1105+
template<class O> struct Part : O {
1106+
static_assert(!HasSlot<Tag, typename O::Res>::value, "hapi::Slot: two Parts claim the same tag");
1107+
struct HAPI_EMPTY_BASES Res : Contract<typename O::Res, Res>, SlotOf<Tag,S> {
1108+
constexpr S& me() { return slot_<Tag>(*this); }
1109+
constexpr const S& me() const { return slot_<Tag>(*this); }
1110+
template<class V> HAPI_SLOT_EACH_INLINE constexpr void each(V& v) { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); }
1111+
template<class V> HAPI_SLOT_EACH_INLINE constexpr void each(V& v) const { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); }
1112+
};
1113+
};
1114+
};
1115+
}
1116+
// ---- end hapi/slots.h ----
10421117

10431118
namespace hapi {
10441119
// ====================== APIOf ======================--

‎tests/negative/slots_dup_tag.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// EXPECT-ERROR: hapi::Slot: two Parts claim the same tag
2+
#include "../../include/hapi/hapi.h"
3+
using namespace hapi;
4+
struct A { static constexpr int name() { return 1; } };
5+
struct S1 { int x; template<class Self, class V> static constexpr void each(Self& s, V& v) { v(s.x); } };
6+
struct S2 { int y; template<class Self, class V> static constexpr void each(Self& s, V& v) { v(s.y); } };
7+
using Bad = APIOf<SlotApi, Slot<A,S1>, Slot<A,S2>>::Res;
8+
Bad b;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// EXPECT-ERROR: hapi::Slot: two Parts claim the same tag
2+
// the tag is repeated across a nested APIOf
3+
#include "../../include/hapi/hapi.h"
4+
using namespace hapi;
5+
struct A { static constexpr int name() { return 1; } };
6+
struct D { static constexpr int name() { return 4; } };
7+
struct S1 { int x; template<class Self, class V> static constexpr void each(Self& s, V& v) { v(s.x); } };
8+
using Bad = APIOf<SlotApi, Slot<A,S1>, APIOf<SlotApi, Slot<A,S1>>, Slot<D,S1>>::Res;
9+
Bad b;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// EXPECT-ERROR: hapi::slot<Tag>: no slot with that tag
2+
#include "../../include/hapi/hapi.h"
3+
using namespace hapi;
4+
struct A { static constexpr int name() { return 1; } };
5+
struct Z { static constexpr int name() { return 26; } };
6+
struct S1 { int x; template<class Self, class V> static constexpr void each(Self& s, V& v) { v(s.x); } };
7+
using State = APIOf<SlotApi, Slot<A,S1>>::Res;
8+
int f(State& s) { return slot<Z>(s).x; }

0 commit comments

Comments
 (0)