κ Rule Engine — Algorithm Extraction
Language-agnostic pseudocode extracted from Phoenix Python source.
Source files: dsl.py (198L), engine.py (353L), state_machine.py (726L), policy.py (5L).
1. Full EBNF Grammar
RuleSet = { Rule } ;
Rule = "rule" IDENTIFIER "{" GuardBlock EffectBlock "}" ;
GuardBlock = "guards" "{" { GuardClause } "}" ;
GuardClause = ( Expression | "else" ) "->" Action ;
Action = "admit" | "reject" STRING ;
EffectBlock = "effects" "{" { EffectCall } "}" ;
EffectCall = IDENTIFIER "(" ArgList ")" ;
ArgList = Arg { "," Arg } ;
Arg = Expression | STRING ;
Expression = OrExpr ;
OrExpr = AndExpr { "or" AndExpr } ;
AndExpr = NotExpr { "and" NotExpr } ;
NotExpr = [ "not" ] Comparison ;
Comparison = Additive { CompOp Additive } ;
CompOp = "==" | "!=" | "<" | ">" | "<=" | ">=" ;
Additive = Multiplicative { ("+" | "-") Multiplicative } ;
Multiplicative = Unary { ("*" | "/" | "%") Unary } ;
Unary = [ "-" ] Primary ;
Primary = INTEGER | "true" | "false" | Variable | FuncCall | "(" Expression ")" ;
Variable = "$" DotPath ;
DotPath = IDENTIFIER { "." IDENTIFIER } ;
FuncCall = IDENTIFIER "(" [ ArgList ] ")" ;
INTEGER = DIGIT { DIGIT } ;
STRING = '"' { CHAR } '"' ;
IDENTIFIER = LETTER { LETTER | DIGIT | "_" } ;
Critical constraints:
- No floating-point literals —
3.14is a syntax error - All
/performs integer division (truncate toward zero) - No
import,require, or external references inside rule bodies - Variables are read-only within a rule — mutations collected separately
2. AST Node Types
| Node Type | Fields | Semantics |
|---|---|---|
| RuleNode | name: string, guards: GuardClause[], effects: EffectCall[] | Top-level rule definition |
| GuardClause | condition: Expression or null (else), action: “admit”|”reject”, reason: string? | Guard evaluation; first match wins |
| EffectCall | function: string, args: Expression[] | Side-effect invocation |
| BinaryOp | op: string, left: Expression, right: Expression | Arithmetic or comparison |
| UnaryOp | op: “-“, operand: Expression | Numeric negation |
| LogicalOp | op: “and”|”or”|”not”, operands: Expression[] | Boolean logic |
| IntLiteral | value: int64 | Integer constant |
| BoolLiteral | value: bool | true or false |
| StringLiteral | value: string | Quoted string constant |
| VarRef | path: string[] | Variable dereference (e.g. $actor.rep) |
| FuncCall | name: string, args: Expression[] | Built-in function call |
3. 8 Built-in Functions
BPS Constants
BPS_100_PERCENT = 10000 // 100% = 10000 basis points
BPS_1_PERCENT = 100 // 1% = 100 basis points
BPS_50_PERCENT = 5000 // 50% = 5000 basis points
Function Definitions
| Function | Signature | Implementation | Notes |
|---|---|---|---|
decay(v, r) |
(int64, int64) → int64 | (v * (10000 - r)) / 10000 |
Apply rate-bps decay. r=500 → 5% loss |
diminishing(v, k) |
(int64, int64) → int64 | (v * k) / (k + v) |
k defaults to 1000. Returns diminishing returns value |
bps_mul(v, b) |
(int64, int64) → int64 | (v * b) / 10000 |
Multiply by basis-point fraction |
bps_div(v, b) |
(int64, int64) → int64 | (v * 10000) / b |
Divide by basis-point fraction |
isqrt(n) |
int64 → int64 | Newton’s method (see below) | Integer square root, no float |
ilog2(n) |
int64 → int64 | bit_length(n) - 1 |
Integer base-2 logarithm |
rep(node) |
node → int64 | lookup reputation score | Returns node’s total reputation |
min(a, b) |
(int64, int64) → int64 | a < b ? a : b |
Minimum of two values |
max(a, b) |
(int64, int64) → int64 | a > b ? a : b |
Maximum of two values |
abs(a) |
int64 → int64 | a < 0 ? -a : a |
Absolute value |
clamp(v, lo, hi) |
(int64, int64, int64) → int64 | max(lo, min(v, hi)) |
Clamp value to range |
cap(v, m) |
(int64, int64) → int64 | min(v, m) |
Cap value at maximum |
isqrt Algorithm (Newton’s Method, Integer-Only)
function isqrt(n: int64) -> int64:
if n < 0: error("negative input")
if n == 0: return 0
x = n
y = (x + 1) / 2
while y < x:
x = y
y = (x + n / x) / 2
return x
ilog2 Algorithm
function ilog2(n: int64) -> int64:
if n <= 0: return 0 // defined as 0 for edge case
return bit_length(n) - 1
// bit_length: number of bits needed to represent n (e.g., 8 → 4 bits → ilog2=3)
4. BPS Integer Math
All arithmetic uses 64-bit signed integers. No floats permitted.
Range: -2^63 to 2^63 - 1 (= -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807)
Examples
decay(1000, 500) = 1000 * (10000 - 500) / 10000 = 1000 * 9500 / 10000 = 950
bps_mul(5000, 2000) = 5000 * 2000 / 10000 = 1000 // 20% of 5000
bps_div(1000, 2000) = 1000 * 10000 / 2000 = 5000 // 1000 is 20% of what?
diminishing(500, 1000) = 500 * 1000 / (1000 + 500) = 333 // diminishing returns
isqrt(100) = 10
isqrt(101) = 10 // floor
ilog2(8) = 3
ilog2(1024) = 10
Overflow Protection
// Before any multiplication that might overflow:
function safe_mul(a: int64, b: int64) -> int64:
if b != 0 and abs(a) > MAX_INT64 / abs(b):
error("multiplication overflow")
return a * b
// Division by zero protection:
function safe_div(a: int64, b: int64) -> int64:
if b == 0: error("division by zero")
return a / b // truncate toward zero (not floor)
5. Rule Execution Flow
function execute_rule(rule: RuleNode, context: ReadOnlyState) -> RuleResult:
mutations = []
node_budget = 0
MAX_NODES = 10000 // AST node evaluation budget per rule
// Phase 1: Guard evaluation — first matching guard wins
admitted = false
for guard in rule.guards:
node_budget += count_ast_nodes(guard.condition)
if node_budget > MAX_NODES:
error("rule budget exceeded")
if guard.condition == null: // else clause
match = true
else:
match = evaluate_expr(guard.condition, context)
if match:
if guard.action == "reject":
return RuleResult { status: REJECTED, reason: guard.reason }
admitted = true
break // first match wins; remaining guards ignored
if not admitted:
return RuleResult { status: REJECTED, reason: "NO_MATCH" }
// Phase 2: Effect collection — no state changes yet
for effect in rule.effects:
node_budget += count_ast_nodes(effect)
if node_budget > MAX_NODES:
error("rule budget exceeded")
mutation = collect_effect(effect, context)
mutations.append(mutation)
return RuleResult { status: ADMITTED, mutations: mutations }
// Mutations are applied atomically after all rules run (collect-then-apply)
Collect-then-Apply Atomicity
function process_event(event: Event, rules: RuleNode[], state: State) -> TransitionResult:
snapshot = state.read_only_snapshot() // immutable view for rule eval
all_mutations = []
for rule in rules:
result = execute_rule(rule, snapshot)
if result.status == ADMITTED:
all_mutations.extend(result.mutations)
// Validate: detect conflicting mutations on same field
if has_conflicts(all_mutations):
error("conflicting mutations — cannot apply")
// Apply atomically
new_state = apply_mutations(state, all_mutations)
proof = generate_merkle_proof(state.root, new_state.root, all_mutations)
return TransitionResult { new_state, proof, mutations: all_mutations }
6. Rule Execution Order
Fixed and deterministic across all implementations:
1. Admission rules — Can this action enter the system?
2. StateTransition — What state changes result?
3. Consequence rules — What reputation / token effects?
4. Promotion rules — Level-ups, unlocks, or role changes?
Within each category: alphabetical by rule name (ensures determinism)
7. TransitionType Enum (13 Values)
From state_machine.py:
enum TransitionType:
COMMITMENT_CREATE // New commitment/obligation created
COMMITMENT_ACCEPT // Counterparty accepts commitment
SETTLEMENT_COMPLETE // Contract fulfilled
SETTLEMENT_FAIL // Contract failed or abandoned
DISPUTE_OPEN // Dispute raised against a commitment
DISPUTE_RESOLVE // Dispute concluded
GOVERNANCE_PROPOSE // New governance proposal submitted
GOVERNANCE_VOTE // Vote cast on proposal
IDENTITY_CREATE // New identity registered
IDENTITY_UPDATE // Identity attributes updated
FORK_CREATE // New fork created
FORK_MERGE // Fork merged back to parent
REPUTATION_DECAY // Epoch-triggered reputation decay
Each transition type maps to a rule set in the registry and a state machine step.
8. Rule Engine process_action() Algorithm
From engine.py:
// Named rule registry (the PhoenixDSL / RuleEngine class)
RULE_REGISTRY = {
"AcceptCommitment": RuleNode { ... },
"Yield": RuleNode { ... },
"Schism": RuleNode { ... },
"InvitePeer": RuleNode { ... },
// ... additional named rules
}
function process_action(action_name: string, actor: Node, context: ReadOnlyState) -> ActionResult:
rule = RULE_REGISTRY.get(action_name)
if rule == null:
error("unknown action: " + action_name)
// Inject actor into context
eval_context = context.with_binding("actor", actor)
result = execute_rule(rule, eval_context)
if result.status == REJECTED:
return ActionResult { allowed: false, reason: result.reason }
return ActionResult {
allowed: true,
mutations: result.mutations,
rep_delta: extract_rep_deltas(result.mutations),
}
9. Policy Gating: check_policy()
From engine.py + policy.py. Policies are pre-guards that run before named rules.
// P1-P13 policy definitions (from policy.py)
enum PolicyId: P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13
struct Policy:
id: PolicyId
predicate: Expression // DSL expression (pure, no side effects)
rejection: string // reason string on failure
function check_policy(policy_id: PolicyId, actor: Node, context: ReadOnlyState) -> PolicyResult:
policy = POLICIES[policy_id]
ctx = context.with_binding("actor", actor)
if not evaluate_expr(policy.predicate, ctx):
return PolicyResult { admitted: false, reason: policy.rejection }
return PolicyResult { admitted: true }
function check_all_policies(action_name: string, actor: Node, context: ReadOnlyState) -> PolicyResult:
applicable = get_applicable_policies(action_name)
for policy_id in applicable:
result = check_policy(policy_id, actor, context)
if not result.admitted:
return result // first policy failure short-circuits
return PolicyResult { admitted: true }
10. ReadOnlyState Interface
interface ReadOnlyState:
reputation: Map<NodeId, Map<Domain, int64>>
tokens: Map<NodeId, TokenRecord[]>
stakes: Map<NodeId, int64>
epoch: int64
event_count: int64
fork_id: bytes32
rule_version: string
function with_binding(name: string, value: any) -> ReadOnlyState
// Returns new context with name bound; original unchanged
11. Dependencies
| Module | Interaction |
|---|---|
| η Proof Store | Merkle proofs for each state transition |
| θ Consensus | Validates rule evaluation is deterministic and agreed |
| λ Reputation | Rule engine computes reputation deltas via apply_decay + action_table |
| ι State Fork | Rule version conflicts trigger automatic fork creation |
| μ Integrity | Sentinel can dampen effects (advisory, not blocking) |
Links
| [[concepts/κ-rule-engine | κ Rule Engine]] · [[spec/S11-rule-engine | S11 Rule Engine Spec]] · [[reference/dsl | DSL Reference]] · [[reference/extractions/theta-consensus-extraction | θ Extraction]] · [[reference/extractions/lambda-reputation-extraction | λ Extraction]] |