Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# Changelog

## 0.7.0 (not yet tagged)
## 0.8.0 (not yet tagged)

### Added
- **`Expand<O>` / `Expansion<Children,Queried,Selected,Validates,Searched>` / `IsContainer<O>`**: 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<OO...>::Drop<n>`: the chain without its first `n` elements.
- **`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
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
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`
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/`
Expand All @@ -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<n>`; GCC/Clang only, not built by CI.

## 0.7.0

### Added
- **`Expand<O>` / `Expansion<Children,Queried,Selected,Validates,Searched>` / `IsContainer<O>`**: 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<OO...>::Drop<n>`: 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).
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tag, S>` 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 <hapi/hapi.h>
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<SlotApi, Slot<Pos, PosSlot>, Slot<Speed, SpeedSlot>>::Res;

State s{};
slot<Pos>(s).x = 1; slot<Speed>(s).x = 2;
```

* **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
`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<Tag, S, Contract>` takes a third parameter, a template `Contract<Below, R>` 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<class Below, class R> struct Counted : Below { unsigned steps = 0; void step() { ++steps; } };
using Counting = APIOf<SlotApi, Slot<Pos, PosSlot, Counted>>::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<Q>(object)`.
Expand Down
26 changes: 26 additions & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,32 @@ template<typename... II>
struct Expand<Box<II...>> : Expansion<Chain<II...>, /*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<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 |
| `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 ...`) |
| `HasSlot<Tag, R>` | 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<Tag, S>` | the slot as a base class of the state; `slot_<Tag>(...)` converts to it |
| `SlotBase<Below, R>` | 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<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
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:
Expand Down
1 change: 1 addition & 0 deletions include/hapi/hapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#pragma once
#include "hapi/rules.h"
#include "hapi/meta.h"
#include "hapi/slots.h"

namespace hapi {
// ====================== APIOf ======================--
Expand Down
74 changes: 74 additions & 0 deletions include/hapi/slots.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @file slots.h
* @brief Slots: state composed along a chain and addressed by tag.
*
* hapi::Slot<Tag, S> 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::SlotApi, hapi::Slot<A, SlotA>, hapi::Slot<B, SlotB>>::Res;
* hapi::slot<A>(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<class Below, class R> struct Counted : Below { unsigned steps = 0; void step() { ++steps; } };
* hapi::Slot<A, SlotA, Counted>
*
* 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<class Tag, class S> struct SlotOf : S {};
template<class Tag, class S> constexpr S& slot_(SlotOf<Tag,S>& f) { return f; }
template<class Tag, class S> constexpr const S& slot_(const SlotOf<Tag,S>& f) { return f; }

template<class T> T& slot_decl();
template<class Tag, class R, class = void> struct HasSlot : std::false_type {};
template<class Tag, class R> struct HasSlot<Tag, R, std::void_t<decltype(slot_<Tag>(slot_decl<R>()))>> : std::true_type {};

/// @brief the slot of Tag in a state
template<class Tag, class R> constexpr decltype(auto) slot(R& r) {
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)");
return slot_<Tag>(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<class V> constexpr void each(V&) const {} };
struct SlotApi { using Res = SlotRoot; };

/// @brief what a state adds when no Contract asks for more: nothing
template<class Below, class R> struct SlotBase : Below {};

/// @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
/// 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 Tag, class S, template<class, class> class Contract = SlotBase>
struct Slot {
template<class O> struct Part : O {
static_assert(!HasSlot<Tag, typename O::Res>::value, "hapi::Slot: two Parts claim the same tag");
struct HAPI_EMPTY_BASES Res : Contract<typename O::Res, Res>, SlotOf<Tag,S> {
constexpr S& me() { return slot_<Tag>(*this); }
constexpr const S& me() const { return slot_<Tag>(*this); }
template<class V> HAPI_SLOT_EACH_INLINE constexpr void each(V& v) { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); }
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); }
};
};
};
}
2 changes: 1 addition & 1 deletion library.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
79 changes: 77 additions & 2 deletions single/hapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----
Expand Down Expand Up @@ -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<Tag, S> 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::SlotApi, hapi::Slot<A, SlotA>, hapi::Slot<B, SlotB>>::Res;
* hapi::slot<A>(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<class Below, class R> struct Counted : Below { unsigned steps = 0; void step() { ++steps; } };
* hapi::Slot<A, SlotA, Counted>
*
* 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<class Tag, class S> struct SlotOf : S {};
template<class Tag, class S> constexpr S& slot_(SlotOf<Tag,S>& f) { return f; }
template<class Tag, class S> constexpr const S& slot_(const SlotOf<Tag,S>& f) { return f; }

template<class T> T& slot_decl();
template<class Tag, class R, class = void> struct HasSlot : std::false_type {};
template<class Tag, class R> struct HasSlot<Tag, R, std::void_t<decltype(slot_<Tag>(slot_decl<R>()))>> : std::true_type {};

/// @brief the slot of Tag in a state
template<class Tag, class R> constexpr decltype(auto) slot(R& r) {
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)");
return slot_<Tag>(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<class V> constexpr void each(V&) const {} };
struct SlotApi { using Res = SlotRoot; };

/// @brief what a state adds when no Contract asks for more: nothing
template<class Below, class R> struct SlotBase : Below {};

/// @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
/// 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 Tag, class S, template<class, class> class Contract = SlotBase>
struct Slot {
template<class O> struct Part : O {
static_assert(!HasSlot<Tag, typename O::Res>::value, "hapi::Slot: two Parts claim the same tag");
struct HAPI_EMPTY_BASES Res : Contract<typename O::Res, Res>, SlotOf<Tag,S> {
constexpr S& me() { return slot_<Tag>(*this); }
constexpr const S& me() const { return slot_<Tag>(*this); }
template<class V> HAPI_SLOT_EACH_INLINE constexpr void each(V& v) { O::Res::each(v); v.layer(Tag::name()); S::each(me(), v); }
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); }
};
};
};
}
// ---- end hapi/slots.h ----

namespace hapi {
// ====================== APIOf ======================--
Expand Down
8 changes: 8 additions & 0 deletions tests/negative/slots_dup_tag.cpp
Original file line number Diff line number Diff line change
@@ -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<class Self, class V> static constexpr void each(Self& s, V& v) { v(s.x); } };
struct S2 { int y; template<class Self, class V> static constexpr void each(Self& s, V& v) { v(s.y); } };
using Bad = APIOf<SlotApi, Slot<A,S1>, Slot<A,S2>>::Res;
Bad b;
9 changes: 9 additions & 0 deletions tests/negative/slots_dup_tag_nested.cpp
Original file line number Diff line number Diff line change
@@ -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<class Self, class V> static constexpr void each(Self& s, V& v) { v(s.x); } };
using Bad = APIOf<SlotApi, Slot<A,S1>, APIOf<SlotApi, Slot<A,S1>>, Slot<D,S1>>::Res;
Bad b;
8 changes: 8 additions & 0 deletions tests/negative/slots_missing_tag.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// EXPECT-ERROR: hapi::slot<Tag>: 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<class Self, class V> static constexpr void each(Self& s, V& v) { v(s.x); } };
using State = APIOf<SlotApi, Slot<A,S1>>::Res;
int f(State& s) { return slot<Z>(s).x; }
Loading
Loading