rbs & sm – Regel-Engine als Zustandsautomat
Zusammenfassung
rbs (Rule-Based System) plus sm (State Machine) bilden zusammen einen deterministischen, vollständig überprüfbaren Zustandsautomaten: Das Wissen steckt deklarativ in Daten (Regeln, Effekte, Zustands-Slots), die Engine ist ein kleiner, fehlerfreier Kern. Ein Programm in diesem Muster ist selbst das Modell der Zustände und Übergänge – es gibt keine versteckten impliziten Zustände (Globale, Bestellungen, Zufall) mehr. Fehlerklassen werden durch das Design ausgeschlossen, nicht durch Disziplin verhindert.
Zentrales Design-Prinzip: jeder Schritt ist eine reine Funktion next = step(current) – alle Regeln werden gegen dieselbe Faktenbasis ausgewertet, Änderungen wirken erst ab dem nächsten Schritt. Nur bewachte Übergänge (Diffs) verändern den Zustand, nichts wirkt asynchron von außen hinein.
Architektur (drei Schichten)
| Schicht | Datei | Aufgabe |
|---|---|---|
sm | libraries/sm | Generische, minimal-threaded State-Machine: Handler-Schleife bis false; Lebenszyklus-Callbacks sm_on_start/sm_on_stop |
rbs | libraries/rbs/rbs.c | Regel-Engine: Faktenbasis, Memory, Regeln (Guards → then/else-Fakten), Effekte (Memory-Ops) |
rbs_sm | libraries/rbs/rbs_sm.c | Brücke: RBS-Schritte + Marker-Routing auf Handler (Slots), Schritt-Callback |
sm (State-Machine)
typedef bool (*sm_state_handler_t)(sm_state_t next_state, void* user_data);
struct sm_state { sm_state_handler_t state_function; };
void sm_run(sm_state_t next_state, void* user_data);sm_run startet einen Thread, ruft die Handler-Schleife auf (solange ein Handler true liefert) und joined ihn wieder. Ein Handler ist der „Zustandskörper” – er entscheidet, ob die Maschine weiterläuft.
rbs (Regel-Engine)
- Faktenbasis:
int32_t[], ein Slot pro Token. Signum-Kodierung:+X= aktiv,-X(N_X) = „nicht (mehr) aktiv”. Initialisierung setzt alle aufN_X. - Regeln:
if_terms(Fakten-Terme und/oder Wertvergleichememory[value] op operand) →then_facts(abzuleitende Fakten, mitZERO-Terminator). Optional einelse_facts-Zweig: wird gesetzt, wenn derif-Zweig nicht zutrifft (typisch der deklarative Reset, z. B.N_WET,N_UMBRELLA). - Effekte: Trigger-Fakt aktiv → Memory-Operation (
ADD/SUB/MUL/DIV). Der Trigger bleibt aktiv (zustandsgetriggert, kein Auto-Konsum); er wird nur durch eine Regel oder eine externe Faktenquelle zurückgesetzt. - Ein API, eine Semantik:
rbs_step(). Es gibt bewusst keine weiteren „Regeln ausführen”-Funktionen – zwei parallele Semantiken wären eine wartbare Falle.
void rbs_step(struct rbs* rbs,
const rbs_rule_t rules, size_t rule_count,
const rbs_effect_t effects, size_t effect_count);rbs_sm (Brücke)
rbs_sm verbindet beide: Ein Schritt = rbs_step auf der Faktenbasis, danach wird der erste aktive Slot gesucht (slots[i].fact ist gesetzt) und dessen Handler geroutet. on_step (optional) wird nach jedem Schritt mit der Schrittnummer aufgerufen – z. B. für den Trennstrich der Step-Grenzen in der Konsole. Der Zähler ticks protokolliert die Schritte.
Zusätzlich kennt die App-FSM zwei Lebenszyklus-Hooks als
sm-Callbacks (weak Defaults in sm.c, stark definiert in der App):
sm_on_start(Konstruktor): läuft genau einmal vor der Zustandsschleife und baut die initiale Welt auf (Buffer anlegen, externe Ausgangslage, z. B. Regen/Bewölkung setzen, Welt-KonstantenAGE/MONEY).sm_on_stop(Destruktor): läuft genau einmal nach Terminierung der Schleife, z. B. um die Endfakten zu bilanzieren und die Buffer freizugeben.
Über sm_core_t (und daraus das rbs_sm) besteht Zugriff auf Fakten/Memory.
Damit wird die Faktenbasis vollständig aus den Lebenszyklus-Hooks versorgt:
Der Konstruktor stellt die Ausgangslage und die Speicherverwaltung bereit, die
Slot-Handler liefern laufenden externen Input, der Destruktor bilanziert das
Ergebnis; main speist dann keine Fakten mehr ein und übernimmt keine
Speicherverwaltung, sondern nur Welt-Konstanten und die statische Verkabelung.
Kern-Konzept: Schritt-Semantik
- Alle Regeln auswerten gegen die AKTUELLE Basis (kein Regel-Effekt, kein Regelfakt sichtbar im selben Schritt – sonst Sequenz-Kette, der klassische Denkfehler).
- Alle Effekte auswerten gegen denselben Memory-Stand (Snapshot, keine Kette).
- Commit am Schritt-Ende: abgeleitete Fakten (
then/else) setzen, Memory schreiben. Alles wirkt ab dem nächsten Schritt.
Fakten persistieren – sie existieren, bis sie explizit zurückgesetzt werden (durch eine Regel oder eine externe Faktenquelle). Es gibt kein automatisches Konsumieren/Verschwinden von Fakten.
Es sind nur Merk-Arrays der Größe O(Regeln) + O(Effekte) nötig – kein zweiter Faktenpuffer, denn die Signum-Kodierung ist bereits die Diff/Transition. Ein then_facts ist ein bewachter Diff, else_facts der bewachte Rücksetzer (N_X), ein abgeschlossener Schritt ein committeter Diff; der Lauf ist eine Diff-Log.
Zustandsgetriggert, kein Flanken-/Konsum-Mechanik
Die Maschine ist zustandsgetriggert: Solange ein Guard (z. B. AGE > 18 bzw. ein Fakt) wahr bleibt, bleibt der abgeleitete Zustand aktiv und Effekte feuern konsequent in jedem Schritt – es werden keine Fakten konsumiert. Ein Fakt verschwindet nur dann, wenn eine Regel (via else_facts) oder eine externe Faktenquelle (App-Handler, z. B. Systemzeit) ihn explizit zurück- bzw. neusetzt. Beendigung ist Programmlogik der App (externer Input + Zähler, oder ein Handler liefert letztlich false); die Engine selbst läuft, bis die Welt keinen aktiven Zustands-Marker mehr kennt.
Geschlossene Welt
rbs_step liest ausschließlich facts + memory – kein I/O, keine Concurrency, keine Umgebungs-Zugriffe im Kern. Die Außenwelt darf nur zwischen den Schritten Fakten einspeisen (z. B. RAIN, CLOUDY setzen), nie währenddessen. Damit ist der Zustandsraum endlich und aufzählbar: alle erreichbaren Zustände lassen sich exhaustiv durchlaufen, jede Transition einzeln prüfen. „Perfektes Design ersetzt riesige Gehirne”: Die Engine ist der dumme Muskel, das Wissen liegt als Statik in den Daten.
Beispiel: Wetter → UMBRELLA → Erwachsener bezahlt
struct rbs_term if_weather[] = { {RAIN}, {CLOUDY}, {ZERO} };
enum token then_weather[] = { WET, ZERO }; /* Regen/bewoelkt -> nass */
enum token else_weather[] = { N_WET, ZERO }; /* sonst trocken */
struct rbs_term if_wet[] = { {WET}, {ZERO} };
enum token then_wet[] = { UMBRELLA, ZERO }; /* nass -> Schirm noetig */
enum token else_wet[] = { N_UMBRELLA, ZERO }; /* trocken -> kein Schirm */
struct rbs_term if_adult[] = { {AGE, GT, 18}, {ZERO} };
enum token then_adult[] = { ADULT, PAY, ZERO }; /* Erwachsener bezahlt */
struct rbs_effect effects[] = { { PAY, MONEY, SUB, 10 } };
/* Externe Faktenquelle (z. B. Wettersensor): meldet das Ende des Regens. */
static bool _app_handle_wet(sm_state_t next, void* ud) {
rbs_sm_t fsm = ud;
rbs_set_fact(fsm->rbs->facts, fsm->rbs->token_count, rbs_invert_token(RAIN));
rbs_set_fact(fsm->rbs->facts, fsm->rbs->token_count, rbs_invert_token(CLOUDY));
return rbs_sm_advance(fsm, next);
}
/* Konstruktor (sm-Callback): baut die Speicherverwaltung und die initiale
* Welt (Regen + Bewoelkung) vor der Schleife auf. */
callback void sm_on_start(sm_core_t core) {
rbs_sm_t fsm = core->user_data;
fsm->rbs->facts = rbs_create_facts_buffer(fsm->rbs->token_count);
fsm->rbs->memory = rbs_create_memory_buffer(fsm->rbs->value_count);
rbs_set_fact(fsm->rbs->facts, fsm->rbs->token_count, RAIN);
rbs_set_fact(fsm->rbs->facts, fsm->rbs->token_count, CLOUDY);
}
/* Destruktor (sm-Callback): bilanziert die Endfakten und gibt die Buffer auf. */
callback void sm_on_stop(sm_core_t core) {
rbs_sm_t fsm = core->user_data;
if (rbs_is_fact(fsm->rbs->facts, fsm->rbs->token_count, UMBRELLA)) { /* ... */ }
free(fsm->rbs->facts);
free(fsm->rbs->memory);
}
struct rbs_sm_slot slots[] = {
{ .fact = WET, .handler = _app_handle_wet }, /* externer Input */
{ .fact = ADULT, .handler = _app_handle_adult },/* liefert false */
};
struct rbs rbs = { .facts = NULL, .token_count = TOKEN_COUNT, .memory = NULL, .value_count = VALUE_COUNT };
struct rbs_sm fsm = { .rbs = &rbs, .rules = rules, .rule_count = ..., .effects = effects, .effect_count = ...,
.slots = slots, .slot_count = ..., .on_step = _app_on_step };
rbs_sm_run(&fsm); /* sm_on_start / sm_on_stop laufen im Thread */Ausgabe (Schritt-Callback on_step):
--- Step 1 ---
WET: true (Regen, Schirm noetig) UMBRELLA: false, MONEY: 100
--- Step 2 ---
WETTER: regen klaert auf -> RAIN/CLOUDY negiert
ADULT: true (erwachsen -> es wird bezahlt) UMBRELLA: true, MONEY: 90
Im Schritt 1 wird WET abgeleitet und UMBRELLA (via else_wet) negiert; erst im
Schritt 2, wenn WET aktiv ist, setzt if_wet UMBRELLA. Die Ausgangslage (Regen +
Bewölkung) wird nicht mehr in main gesetzt, sondern vom Konstruktor sm_on_start
aufgebaut; der Regenschauer endet durch die externe Faktenquelle
(_app_handle_wet negiert RAIN/CLOUDY); dadurch greift else_weather
(N_WET) und der WET-Marker verschwindet – ohne jegliches „Konsumieren”. PAY
wird abgezogen, solange es aktiv ist; die App beendet sich als echte Software über
externe Ereignisse/Zähler und einen Handler, der false liefert, und bilanziert
danach im Destruktor sm_on_stop die Endfakten.
Vorteile
- Vollständig überprüfbar: endlicher Zustandsraum, Übergänge sind Daten → exhaustiv test-/durchlaufbar; bei garantierter Engine korrektes Verhalten.
- Constraints statt Konventionen:
rbs_stepist der einzige Mutationspfad; es gibt keinen falschen Weg, der gewählt werden könnte. - Deterministisch: gleiche Basis → gleicher Nachfolger; keine Races, keine Zwischenzustände.
- Einfach & wartbar: reines C, ~300 Zeilen Kern, keine externen Abhängigkeiten außer
sm/threading/api. - Architektur = Modell: das ausführbare Programm ist die Spezifikation, keine Diskrepanz zwischen beidem.
Konventionen (Projekt)
- Bibliotheken immer mit spitzen Klammern einbinden:
#include <sm/sm.h>,#include <rbs/rbs.h>. - Typedefs
X_tsind immer Pointer auf das Struct. - Regeln/Effekte/If-Terme als
[]-Arrays, Terme mit{ ZERO }-Terminator. - Effekt-Code bleibt nebenwirkungsfrei (nur
facts/memory), damit Handler Teil des überprüfbaren Systems bleiben. - Speicherverwaltung und Ausgangslage gehören in die Lebenszyklus-Callbacks
sm_on_start/sm_on_stop, nicht inmain.
Quellen
- Quellcode:
libraries/rbs/{rbs.c,rbs_sm.c},libraries/rbs/rbs/{rbs.h,rbs_sm.h},libraries/rbs/test/ libraries/sm/sm/{sm.h,sm.c}(inkl.libraries/threading,api)- Beispiel-App:
apps/rbs_demo(Repo deppenkaiser/rbs_demo) - Tests:
ctestim Build-Ordner (rbs_test,rbs_effects_test,rbs_sm_test,rbs_else_test)
Verwandte Artikel
- rbs – Rule Based System
- sm – minimal threaded State Machine
- rbs_demo – Beispiel-App
- Master/Slave-System mit MCP-Gateway – steuert dieses Wiki und orchestriert Slave-Agenten