Typed Datalog: literals, filters, aggregates

gestaltdb.reasoning.terms defines strict immutable literal values; gestaltdb.reasoning.typed evaluates finite n-ary positive Datalog over entities and literals with kind-strict comparisons, normalized multi-head rules, a single-pass stratified aggregation stratum and explicitly expanded predicate variables. The binary entity engine, its digests and its public behavior are unchanged: Literal never implicitly converts to an entity string, and unexpanded predicate variables fail closed.

from gestaltdb.reasoning import (
    Comparison, GenericEngine, GenericProgram, GenericRule,
    Literal, ReasoningLimits, TypedAtom, TypedFact,
)
from gestaltdb.reasoning.model import Variable

x, y = Variable("x"), Variable("y")
adult = GenericRule("adult", [TypedAtom("age", [x, y])],
                    [Comparison(">=", y, Literal("integer", 18))],
                    TypedAtom("adult", [x]))
result = GenericEngine().evaluate(
    GenericProgram([adult]),
    [TypedFact("age", ["alice", Literal("integer", 30)]),
     TypedFact("age", ["bob", Literal("integer", 12)])],
    limits=ReasoningLimits(),
)
assert TypedFact("adult", ["alice"]) in result
assert TypedFact("adult", ["bob"]) not in result

Literals and normalization

Kinds are boolean, integer, float, text, bytes, instant (integer microseconds) and null. Construction validates exact Python types — bool is not an integer, 1.0 is not an integer literal — and from_lexical parses canonical forms strictly ("007" is integer 7, "1e3" is float 1000.0, empty text is allowed, only the empty lexical form is null). Anything else fails with ReasoningError; there is no silent coercion.

Equality is kind-strict: 1 (integer) never equals 1.0 (float) or "1" (text). Null is an explicit marker and equals itself, so grouping and joins stay deterministic. NaN follows IEEE semantics and is unequal to everything including itself. Ordering (compare) applies only within one kind, excluding nulls and NaN; anything else is incomparable (None), never a ranked guess.

Comparisons in rule bodies

Comparison supports == != < <= > >= over literal operands. Variables must be bound by relational body atoms (range-restriction is enforced at construction); unbound variables fail closed. Filters are checked incrementally as their variables bind, pruning the join before deeper traversal. Incomparable pairs — cross-kind values, nulls, NaN involvement, or entity bindings reaching a literal comparison — filter the row out instead of erroring, consistent with the non-explosive open-world default. A comparison against an entity is therefore never silently true.

Multi-head rules, aggregates and predicate variables

MultiHeadRule holds a finite non-empty head conjunction and normalizes to equivalent single-head rules with deterministic rule_id#index IDs. AggregateRule computes one non-recursive stratum after the positive fixpoint completes: count tallies rows per group; sum/min/max consume integer/float literals, skipping nulls, and fail closed on other kinds, mixed kinds or NaN. Targets must be fresh variables, heads are sinks no rule body may read, and aggregate bodies may not read other aggregate heads — recursion through aggregation is rejected, not silently unrolled. Groups with no aggregatable values produce no head fact for that group.

Predicate variables range only over an explicit finite domain via expand_predicate_variables (Cartesian product in first-occurrence order, deterministic suffixed IDs). Unexpanded programs fail closed, as do empty domains, non-string domains and variables shared between predicate and term positions. There is no open-world predicate invention.

Measured equivalence and cost

The generic engine reuses the proven delta algorithm — per-occurrence pivots, smallest-bound-bucket ordering, set semantics — extended with positional indexes and incremental filter pruning. Forty generated programs (mixed arities, repeated atoms, constants, filters, recursion) match an independent grounding oracle exactly, and repeated runs are deterministic.

On the RE-01 analytical workloads translated 1:1 to binary typed rules (entities stay entities; no literals or filters), both engines reach identical closures in identical iteration counts: hierarchy N=256 completes 1280 facts in 5 rounds, recursive N=1024 completes 9227 facts in 6 rounds, dense N=128 completes 8128 facts in 8 rounds. The generic engine costs roughly 1.5–1.7x wall time there (3.7→5.6ms, 86→144ms, 535→918ms medians) for positional indexing with no filter work to amortize it; see benchmarks/results/reasoning-re07-typed.json. The generic engine offers no witnesses and no incremental maintenance; those remain with the binary engine and its single-witness traces. Recursive and lattice aggregation stay explicitly deferred.

Scoped negation lives in Scoped closed-world negation: explicit closed predicate sets, stratified anti-joins and scope-carrying absence witnesses.