rbs – Rule Based System (Regel-Engine)

Zusammenfassung

rbs ist eine kleine C-Regel-Engine mit einer Ausführungs-Semantik (rbs_step): Gegen dieselbe Faktenbasis werden alle Regeln (Guards) und Effekte (Memory-Operationen) ausgewertet und am Schritt-Ende atomar committet. Zusammen mit sm (bzw. der Brücke rbs_sm) entsteht ein deterministischer, vollständig überprüfbarer Zustandsautomat. Das Wissen liegt deklarativ in Daten, nicht im Code.

rbs ist als Bibliothek aufgebaut (STATIC, Konvention der libraries/-Module); die Beispiel-Anwendung, die die Bibliothek mit Lebenszyklus-Callbacks zeigt, liegt im separaten Projekt rbs_demo.

Einordnung

SchichtOrtAufgabe
rbslibraries/rbs/rbs.cRegel-Engine: Faktenbasis, Memory, Regeln, Effekte
rbs_smlibraries/rbs/rbs_sm.cBrücke: RBS-Schritt + Routing auf Handler-Slots, Schritt-Callback
smlibraries/smminimal-threaded State Machine (Handler-Schleife), inkl. Lebenszyklus-Callbacks
threadinglibraries/threadingThread-Erzeugung/-Joining für sm
rbs_demoapps/rbs_demo (eigenes Repo)Beispiel-App: Regeln, Effekte, Slots, Lebenszyklus

Datenmodell

  • Faktenbasis: int32_t[], ein Slot pro Token. Signum-Kodierung: +X = aktiv, N_X (negierte Form) = „nicht (mehr) aktiv”. Initialisierung setzt alle Slots auf N_X.
  • Memory: double[]-Wertebereich, über Value-Enum indiziert.
  • Terme (struct rbs_term): Faktenterm (nur fact_enum) oder Wertvergleich (memory[value_enum] op operand), kombiniert als UND-Kette mit ZERO-Terminator.
  • Regel (struct rbs_rule): if_termsthen_facts (abzuleitende Fakten) und optional else_facts (gesetzt, wenn der if-Zweig nicht zutrifft; typisch der deklarative Reset, z. B. N_WET). Ein then_facts-Eintrag ist ein bewachtes Setzen (+X).
  • Effekt (struct rbs_effect): Trigger-Fakt aktiv → Memory-Operation (ADD/SUB/MUL/DIV) auf value_enum. Der Trigger bleibt aktiv (zustandsgetriggert, kein Auto-Konsum); zurückgesetzt wird er nur durch eine Regel oder eine externe Faktenquelle.

Schritt-Semantik (Kern)

  1. Alle Regeln gegen die aktuelle Faktenbasis auswerten – kein Regelfakt ist im laufenden Schritt für andere Regeln sichtbar.
  2. Alle Effekte gegen denselben Memory-Snapshot berechnen – keine Kette.
  3. Commit am Schritt-Ende: abgeleitete Fakten (then/else) setzen, Memory schreiben. Wirkt ab dem nächsten Schritt.

Fakten persistieren – sie existieren, bis eine Regel (else_facts mit N_X) oder eine externe Faktenquelle (App-Handler, z. B. Systemzeit) sie explizit zurücksetzt. Es gibt kein automatisches Konsumieren; Effekte feuern, solange ihr Trigger aktiv ist. Beendigung ist Programmlogik der App.

Konsequenz: Die Signum-Kodierung ist bereits die Transition (Diff); es braucht keinen zweiten Faktenpuffer. next = step(current) ist eine reine Funktion; beliebige Umgebungs-Fakten dürfen nur zwischen den Schritten eingespeist werden (geschlossene Welt, keine asynchronen Effekte).

API (Auszug)

struct rbs { int32_t* facts; uint32_t token_count; memory_t memory; uint32_t value_count; };
 
void rbs_step(struct rbs* rbs,
              const rbs_rule_t rules, size_t rule_count,
              const rbs_effect_t effects, size_t effect_count);
int32_t* rbs_create_facts_buffer(uint32_t token_count);
bool rbs_is_fact(int32_t* facts, uint32_t token_count, int32_t fact);
void rbs_set_fact(int32_t* facts, uint32_t token_count, int32_t fact);
bool rbs_compare(memory_t memory, uint32_t value_count, int32_t value, enum operation op, double operand);

struct rbs trägt die Puffergrößen mit; die Fakten-/Vergleichsfunktionen verwerfen Zugriffe mit magnitude >= token_count bzw. value_enum >= value_count (Bounds-Guards, kein OOB).

signedness: Fakten-Vergleich erfolgt auf das Vorzeichen (N_X-Form ist falsch, +X wahr). Das ist „Constraints statt Konventionen”: rbs_step ist der einzige Mutationspfad.

rbs_sm – Brücke zur State Machine

rbs_sm_slot { .fact, .handler }: Nach dem RBS-Schritt wird der erste aktive Slot gesucht und dessen Handler geroutet. Der Handler ist eine externe Faktenquelle (z. B. Systemzeit/Wettersensor), die Fakten hinzufügen oder zurücksetzen darf; er „konsumiert” keine Fakten. Beendigung = kein Markerslot mehr gesetzt (bzw. Handler liefert false) – Programmlogik der App. on_step (optional) wird je Schritt mit der Schrittnummer aufgerufen; ticks zählt die Schritte.

typedef struct rbs_sm_slot { int32_t fact; sm_state_handler_t handler; } *rbs_sm_slot_t;
 
struct rbs_sm {
    struct rbs* rbs;
    rbs_rule_t rules; size_t rule_count;
    rbs_effect_t effects; size_t effect_count;
    rbs_sm_slot_t slots; size_t slot_count;
    void (*on_step)(struct rbs_sm* fsm, uint32_t tick);   /* optional */
    uint32_t ticks;
};
 
void rbs_sm_init(rbs_sm_t fsm, struct rbs* rbs,
                 const rbs_rule_t rules, size_t rule_count,
                 const rbs_effect_t effects, size_t effect_count,
                 const rbs_sm_slot_t slots, size_t slot_count);
bool rbs_sm_advance(rbs_sm_t fsm, sm_state_t current);
void rbs_sm_run(rbs_sm_t fsm);   /* blockierend bis Beendigung */

Der Lebenszyklus der App-FSM ist nicht mehr Teil von struct rbs_sm: Er liegt in den sm-Callbacks sm_on_start/sm_on_stop (Konstruktor/Destruktor, weak, überschreibbar in der App) — siehe rbs_demo.

Nutzung

#include <rbs/rbs.h>
#include <rbs/rbs_sm.h>
#include <sm/sm.h>
 
struct rbs_term if_adult[] = { { .comparison = true, .value_enum = AGE, .op = GT, .operand = 18 }, { .comparison = false, .fact_enum = ZERO } };
enum token then_adult[] = { ADULT, PAY, ZERO };
struct rbs_effect effects[] = { { .trigger_fact_enum = PAY, .value_enum = MONEY, .op = SUB, .operand = 10 } };
 
struct rbs rbs = { .facts = fact_buffer, .token_count = TOKEN_COUNT,
                   .memory = mel, .value_count = VALUE_COUNT };
rbs_step(&rbs, rules, rule_count, effects, effect_count);

Build & Test

cmake -S . -B build && cmake --build build && ctest --test-dir build

Struktur wie sm/logging: rbs.c/rbs_sm.c im Repo-Root, Header in rbs/ (rbs/rbs.h, rbs/rbs_sm.h); Einbindung per add_subdirectory(../../libraries/rbs …) mit PUBLIC-Links auf sm, logging, m (api/threading transitiv).

Quellen

Verwandte Artikel