Skip to main content

Rules

Rules are where the computation happens. Each rule says "if these conditions hold, derive these new facts."

Anatomy of a rule

A rule has a head (what to derive) and a body (the conditions), separated by :- and ending with a period:

Head(args...) :- body1, body2, ... .

The classic example — transitive closure:

.decl Arc(x: int32, y: int32)
.decl Tc(x: int32, y: int32)

// Base case: every arc is in the transitive closure
Tc(x, y) :- Arc(x, y).

// Recursive case: extend by one hop
Tc(x, y) :- Tc(x, z), Arc(z, y).

Atoms

An atom is a reference to a relation with arguments. Arguments can be variables, constants, or the _ placeholder:

Edge(x, y)      // two variables
Edge(1, y) // constant + variable
Edge(_, y) // don't care about first column

Positive atoms

A positive atom in the body says "this tuple must exist":

Reach(y) :- Reach(x), Arc(x, y).

Both Reach(x) and Arc(x, y) are positive atoms. The shared variable x is a join key — FlowLog only matches rows where x has the same value in both relations.

Negative atoms

Prefix with ! to negate — "this tuple must not exist":

// Nodes with no outgoing edges
Isolated(x) :- Node(x), !Edge(x, _).
FlowLog uses stratified negation: the negated relation must be fully computed before the rule that negates it runs. The compiler figures out the right evaluation order automatically. If there's a circular dependency through negation, it'll tell you.

Joins

When a variable appears in multiple body atoms, it's a join. FlowLog keeps only the combinations where that variable matches:

// y joins Edge and Node — only matching pairs survive
Match(x, label) :- Edge(x, y), Node(y, label).

Join variables must have compatible types across all atoms they appear in.

Constants in rules

Integer, string, and boolean constants work anywhere — heads, bodies, you name it:

Edge(1, 2).              // fact with integer constants
Name(1, "alice"). // string constant
Active(x) :- Flag(x, True). // boolean constant in body

Remember: boolean literals are capitalized (True, False).

Multiple rules

Multiple rules can derive facts for the same relation. The result is the union of everything derived:

.decl Reach(id: int32)

Reach(y) :- Source(y).
Reach(y) :- Reach(x), Arc(x, y).

Think of it as: each rule contributes rows, and FlowLog takes the union. No duplicates — relations are sets.