Rule Bodies and Safety
Unification ended with a sibling rule that listed ann as ann’s own sibling.
Nothing in the rule said that its two people had to differ, and a relation atom has no way to say
it. The program below adds that condition. age(p, a) records that person p is a years old, and
each rule ends with a condition that is an ordinary Bridger expression:
relation parent : (String, String);
relation age : (String, Int);
relation sibling : (String, String);
relation adult : (String);
rule sibling(x, y) :- parent(p, x) and parent(p, y) and x != y;
rule adult(p) :- age(p, a) and a >= 18;
fn main() {
add parent("tom", "bob"); add parent("tom", "liz");
add parent("bob", "ann"); add parent("bob", "pat");
add parent("pat", "jim");
add age("tom", 71); add age("bob", 44); add age("liz", 40);
add age("pat", 19); add age("ann", 17); add age("jim", 1);
println(solutions sibling(?x, ?y));
println(solutions adult(?p));
}
It prints:
[("ann", "pat"), ("bob", "liz"), ("liz", "bob"), ("pat", "ann")]
["bob", "liz", "pat", "tom"]
If a body can hold any expression, other rules come to mind. note below prints a name and returns
true. With next_age, related, noted, and childless declared as relations, these rules all
parse, but Bridger rejects each of them before main runs:
fn note(p: String) -> Bool { println(p); true }
rule next_age(p, b) :- age(p, a) and b == a + 1;
rule related(x, y) :- parent(x, z);
rule noted(p) :- age(p, a) and note(p);
rule childless(p) :- age(p, a) and not parent(p, ?);
next_age is rejected because logic variable b “is not bound by any generator in this rule”, and
related with the same message for y. noted and childless are rejected with “this filter is
not pure”. These rejections raise the following questions:
- What gives a variable its values?
x != yuses values thatparent(p, x)andparent(p, y)found.b == a + 1looks as if it could supply a value forb, yet it is rejected. - What would a rule mean if a head variable had no source of values?
relatedsays nothing abouty, so which facts would it derive? - What may a condition do? A condition can run many times, in an order and a number of rounds
that the engine chooses. What would a program see if a condition printed or read a
refcell? - Why can a body ask for a fact to be present and never for one to be absent? Whether someone is
childless looks like a simple question to ask of
parent.
The problem with related first surfaced in logic-based database query languages, before
Datalog-style rules were introduced.
Queries with no finite answer
A query written in the predicate calculus describes the values it wants. Nothing in the notation limits what that description may be, so it can ask for every that is not a parent:
If ranges over all strings, the answer is infinite. If ranges over the values in the database, the answer is finite but depends on facts unrelated to the question: recording the age of a new person adds that person to the answer. Abiteboul, Hull, and Vianu’s Foundations of Databases trace this concern in the database literature back at least to Kuhns’s work of 1967, which studied the formulas whose answers depend only on the stored relations.
No test can pick out exactly those formulas. Di Paola proved in 1969 that the question is undecidable. The response was a series of syntactic conditions, each accepting only formulas with the property and each turning away some others that have it too. Codd’s relational calculus of 1972 restricted its variables to ranges over stored relations. Jeffrey Ullman’s Principles of Database Systems defined safe formulas. Jean-Marie Nicolas’s range-restricted formulas (1982) require each variable to be tied to a relation in a form that can be read off the formula. Allen Van Gelder and Rodney Topor compared several of these classes in 1991 and gave translations from the formulas they accept into the relational algebra.
Datalog’s definition carries the same idea. Each variable in a rule’s head must also appear in its body, so a rule with no body must have a ground head. The word safety has stayed attached to conditions of this kind.
Generators and filters
A conjunct in a rule body is one of two things. A generator is a relation atom, such as
parent(p, x). Matching it against the facts binds its variables, as in
Unification. A filter is any other expression, such as x != y. It is evaluated under a substitution and yields true or false, which keeps or drops that
substitution. A filter binds no variable, whatever its shape, and that is why b == a + 1 leaves
b without a value. Here == is the comparison it is everywhere else in the language: it needs
both sides to have values already.
Firing a rule therefore works as it did in the last two chapters, with one extra step. Carry the
substitutions through the generators, then keep the ones under which every filter is true. The
operator from Evaluating relations gains one condition.
Writing a rule’s generators as and its filters as :
is the program’s top-level environment extended by the substitution. For sibling,
the generators produce nine substitutions, ann paired with ann among them, and x != y keeps the
four whose two people differ. Applying each to the head gives the four facts the program printed.
The filter does not disturb the argument for monotonicity. If , a substitution that finds its generators’ facts in finds them in . A filter’s result depends on and nothing else, so a filter that accepted for accepts it for . Every fact derived from is derived from , and is still monotone. This argument assumes that a filter’s result depends only on , a requirement that Purity enforces.
Binding belongs to the body as a whole, and the position of a filter does not limit what it can see. This rule has the filter first and derives the same four facts:
rule sibling(x, y) :- x != y and parent(p, x) and parent(p, y);
Wherever x != y sits in the body, the generators supply the substitutions before any filter runs.
The order of filters among themselves can still matter when one of them can get stuck, as Conjunct
order shows.
Safety
A rule is range-restricted when every variable in its head and every variable in its filters is bound by a generator of that rule. A filter may also refer to top-level constants and functions, which are visible everywhere. Writing for the variables a rule’s generators mention, and for the top-level names, the conditions are:
A rule that meets them is safe. related fails the first condition: y appears in the head but
in no generator. rule big(n) :- n > 100; fails it too, and so does a rule with no body and a
variable in its head, rule everyone(x);.
What an unsafe rule would mean
Reading related(x, y) :- parent(x, z) logically gives: for all x, y, and z, if x is a
parent of z then related(x, y). Taken at its word, related("tom", y) holds for every string
y, so the relation would have infinitely many facts, which solutions cannot list.
Limiting y to the strings in the database makes the set finite, but at the price the calculus
query paid: unrelated facts change the answer. add age("sue", 3) would then add related("tom", "sue"), related("bob", "sue"), and related("pat", "sue"), from a fact about someone’s age in a
rule that mentions neither ages nor sue.
An engine computing has a more immediate problem. Applying a substitution to related(x, y)
leaves y in place, but Bridger’s matching depends on every
fact being ground.
What safety guarantees
Take a safe rule and any substitution that satisfies its body. Every variable in the head is bound by a generator, so it has a value taken from a fact. Every other term in the head is a literal. So the derived fact is ground, and each value in it was already in a fact or written in the program. By induction over the rounds, every value in appears in or in the head of a rule.
That is the premise of the count in Why the rounds stop: finitely many values, so finitely many possible facts, so finitely many rounds. Filters add nothing to the count, since they contribute no values. The bound comes from the rule’s shape, and it holds for any facts the program may add while it runs.
Purity
The meaning of a query is a set determined by and
. The engine is free to compute it in naive or semi-naive rounds, to choose the order of a
body’s generators, and to reuse a closure until the next add or clear. Each of those freedoms
relies on filters having no effect and depending on nothing except the substitution.
Suppose this rule were accepted, with limit a top-level ref:
ref limit = 18;
rule adult(p) :- age(p, a) and a >= deref limit;
After limit := 65, is unchanged. An engine that reuses its closure would give the old
answer, while one that recomputes for every query would give a new one. Since the filter’s result
depends on the store, is no longer a function of its set of facts. A filter that printed would
print once for each substitution the strategy happened to try. The previous
chapter counted 31 rule instances for naive rounds and
9 for semi-naive ones on the same rules, and an effect in a filter would turn that difference into
visible output.
A filter must therefore be pure: free of effects, and a deterministic function of the variables the generators bind.
The purity judgment
The judgment reads: given the set of functions known to be pure, is pure. Literals and variables are pure, an operator is pure when its operands are, and a call is pure when it names a function in and its arguments are pure:
The other effect-free forms, such as if, match, tuples, and field access, are pure when their
parts are. Some forms have no purity rule, so any expression containing one is impure: ref,
deref, :=, while, for, add, clear, solutions, and a query. Calls to input and output
primitives are impure as well, and so is to_string, because converting a ref cell to a string
reads its current contents. A call through a parameter, a local, or a computed closure has no
instance, because the analysis cannot tell what it will run. The language
reference has the complete list.
Which functions are pure
comes from the program. Let be the smallest set of function names such that
whenever f’s body contains a form with no purity rule, or calls a function in ; is every
function outside it. This is another least fixpoint: start from the empty set, add what the
condition requires, and stop when nothing changes. Take these functions:
fn plain(a: Int) -> Bool = a > 1;
fn note(p: String) -> Bool { println(p); true }
fn check(p: String) -> Bool = note(p);
fn both(a: Int, p: String) -> Bool = plain(a) and check(p);
| step | added to | because |
|---|---|---|
| 1 | note | its body calls println |
| 2 | check | it calls note |
| 3 | both | it calls check |
| 4 | none | stop |
plain stays out, so holds only plain, and a > 1 and plain(a) are acceptable filters.
A rule whose filter is both(a, x) is rejected, and the error follows the calls that put both in
:
the effect is reached through `both` → `check` → `note` → `println`
The steps in the table are the rounds of a relational program. has_effect records the functions
whose bodies directly contain an impure form, as note’s contains println, and calls records
the call graph. The rules have the same shape as ancestor:
relation calls : (String, String);
relation has_effect : (String);
relation impure : (String);
rule impure(f) :- has_effect(f);
rule impure(f) :- calls(f, g) and impure(g);
fn main() {
add has_effect("note");
add calls("check", "note");
add calls("both", "plain");
add calls("both", "check");
println(solutions impure(?f));
}
It prints ["both", "check", "note"], the set in the table. Datalog’s use in program
analysis rests on this correspondence. The checker you build
computes the same set directly and also keeps the path for the error.
Filters must finish
A pure function can still run forever. fn spin(n: Int) -> Bool = spin(n); has no effects, but
calling it in a filter would hang the engine, whose rounds rely on every filter returning. So no
function a filter reaches may recurse, directly or through others, and Bridger rejects this filter
with is recursive, so a filter calling it might not terminate:
fn is_even(n: Int) -> Bool = if n == 0 { true } else { is_odd(n - 1) };
fn is_odd(n: Int) -> Bool = if n == 0 { false } else { is_even(n - 1) };
rule even_aged(x) :- age(x, a) and is_even(a);
Like the conditions in the calculus, this check accepts only some of the programs that would work.
On the ages above, is_even always returns, but on a negative argument it counts away from zero.
The check looks at the call graph and never at the arguments. a % 2 == 0 tests the same thing with
no recursion. For the same reason, a filter may not reach a function that takes a function-typed
parameter, since the analysis cannot follow what such a function will call. The prelude’s own map,
filter, fold, and the functions written over them are trusted to finish.
A filter can still get stuck: 100 / (a - 1) > 0 divides by zero when a is 1. Termination and
reaching a value are separate properties, and these checks establish only the first. A filter that
gets stuck makes the query, and the program, stuck.
Bridger’s rule bodies
Telling generators from filters
parent(p, x) and is_adult(a) have the same syntax, so the parser cannot sort them. The
distinction comes from name resolution: a conjunct is a generator when it is a whole call whose
callee names a declared relation, and every other conjunct is a filter. Bridger’s single namespace
for top-level names guarantees that no name is both a relation and a function.
A body is its and-chain however it is bracketed, so age(x, a) and (parent(x, y) and a > 18) has
three conjuncts. A generator’s arguments must be variables or literals, as a head’s must, so age(p, a - 1) is rejected with a relation's argument in a rule body must be a variable or a literal.
A relation applied anywhere else is a query, and a query is impure. So a body reads the database
only through generators standing as whole conjuncts. (a < 18 or a >= 65) is a good filter, and
with the facts above it selects ann, jim, and tom. parent(x, y) or parent(y, x) is a single
conjunct and therefore a filter, with no generator to bind x. Two rules say the same thing and are
accepted, since a relation with several rules reads as an
“or”:
rule related(x, y) :- parent(x, y);
rule related(x, y) :- parent(y, x);
What a filter can see
A rule is a top-level definition and closes over the top-level environment, as a fn body does. It
sees the variables its generators bind and the program’s top-level names, but never the locals where
a query is asked:
let voting_age = 18;
rule adult(p) :- age(p, a) and a >= voting_age;
Moving voting_age into main as a let makes the rule an error, since no generator binds
voting_age. If a rule could read a query’s locals, one set of facts and rules could have a
different meaning at each query site. A generator-bound variable of the same name as a top-level
definition shadows it inside the rule.
Typing a rule
Rules are checked by the same type checker as the rest of the program. A generator gives each of its
variables the type of that column. Where a variable appears in several columns, those types must
agree, so age(x, a) and parent(x, a) is rejected at the second a with type mismatch: expected String, found Int. The column’s declared type is the expected type, and the type a already has
in the rule’s context is the found type. Head terms are checked against the head relation’s
columns. Each filter is checked against Bool with the rule’s variables in the context, so a > "ten" and a filter of a + 1 are both type errors.
Conjunct order
Generators and filters can be written in any order without changing which facts a body derives.
Filters evaluate left to right, as operands do everywhere else in the language: for each
substitution the generators produce, the language
reference runs a rule’s filters in the order written
and stops at the first that yields false. Since a substitution that fails one filter derives
nothing, this order makes no difference when no filter gets stuck. It does matter when one can. With
jim at age 1, this rule finds its answer, because a != 1 removes jim before the division runs:
rule r(x) :- age(x, a) and a != 1 and 100 / (a - 1) > 0;
With the two filters swapped, the query is stuck with division by zero.
Building it
The checks are yours, in relations/check.rs. The provided driver calls them once per rule when the
program is loaded, before main:
struct Rule { head: Atom, body: Vec<Expr>, span: Span }
pub fn check_generators(program: &Program, rule: &Rule)
-> Result<Vec<bool>, RuleError>;
pub fn check_range_restriction(program: &Program, rule: &Rule, generators: &[bool])
-> Result<(), RuleError>;
pub fn check_pure(program: &Program, rule: &Rule, generators: &[bool])
-> Result<(), RuleError>;
body holds every conjunct as an ordinary Expr, so a generator is an Expr::Call that has not
yet been recognised as one. check_generators classifies the conjuncts, answering with one bool
per conjunct, and checks that the head and each generator name a declared relation with the right
number of arguments and that each generator’s arguments are terms. The driver hands that answer to
the other two checks, so neither has to resolve relation names again. check_range_restriction
collects the variables the generators bind and compares the head’s and the filters’ variables
against them. check_pure computes the impure set over the program’s call graph, then judges each
filter, and also rejects a filter that reaches a recursive function or one with a function-typed
parameter. The failures are the RuleError variants in the language
reference. Checker::check_rule, in
types/check.rs, gives the rule its types as in Typing a rule.
The engine from Evaluating relations gains the filter step. After the
generators have been joined, the filters are evaluated by eval_expr left to right for each
remaining substitution, in the program’s root environment (env.root()) extended with that
substitution’s bindings. true moves on to the next filter, and false drops the substitution
without evaluating the filters after it. A rule with filters and no generators starts from the one
empty substitution, so rule r(1) :- 2 > 1; derives its fact once; a join that began with no
substitutions at all would derive nothing. Filters read no facts, so semi-naive rounds choose a
delta among a rule’s generators alone.
The design space
Absence in a body
Bridger’s rejected childless rule asks for a fact to be absent. To see what that does to the
rounds, suppose person holds recorded facts for the six people, and suppose this version were
accepted:
rule has_child(x) :- parent(x, y);
rule childless(x) :- person(x) and not has_child(x);
Run the rounds of the previous chapter on the family:
| round | new has_child facts | new childless facts |
|---|---|---|
| 1 | tom, bob, pat | all six: has_child is still empty when the body reads it |
| 2 | none | none |
The rounds stop with childless("tom") in the set beside has_child("tom"). Rounds only add, so
nothing withdraws the fact derived too early. has lost its monotonicity: a larger set of facts
can give fewer consequences, and the chain no longer leads to
the intended meaning.
Some programs lose the meaning itself. With a person named tom, consider:
rule stays(x) :- person(x) and not leaves(x);
rule leaves(x) :- person(x) and not stays(x);
The set holding person("tom") and stays("tom") is closed: tom stays, so the second rule does not
fire. The set holding person("tom") and leaves("tom") is closed for the mirror-image reason.
Neither contains the other, and the only set inside both, person("tom") alone, is not closed,
since it fires both rules. With two minimal closed sets and nothing closed inside both, these rules
have no least closed set to serve as .
Stratification separates the two programs. Give each relation a layer number such that a rule’s
head is in a layer at least as high as each relation it uses positively, and strictly higher than
each relation it negates. childless can go above has_child. stays would have to be above
leaves and leaves above stays, so no numbering exists for them. A program with a numbering is
stratified: a cycle among its relations never passes through a negation. Evaluation computes one
layer at a time, each to its own least fixpoint, so a negated relation is complete before anything
reads its absence. Several researchers proposed this independently in the 1980s, among them Chandra
and Harel and Apt, Blair, and
Walker, who showed that the meaning does not
depend on which numbering is chosen. Negated atoms also need safety: x must be bound by a positive
generator, or not has_child(x) asks for every string that has no child, the calculus query from
the start of this chapter.
Reading absence as falsehood was itself a decision. Keith Clark’s negation as
failure and Raymond Reiter’s closed-world
assumption, both published in 1978, examine when a
system may conclude that what it cannot derive is false. Prolog’s \+ succeeds when the search for
its goal fails. Datalog systems such as Soufflé offer stratified negation and reject a program with
no numbering. SQL restricts recursion in a similar direction: SQLite, for example, rejects a
recursive query that refers to itself inside NOT IN. Other semantics assign a meaning to programs
like stays and leaves, at the cost of a meaning that is no longer a plain least fixpoint.
Bridger leaves negation over relations out of rules, but a host program can do the layering itself. A query in ordinary code is free to be negated, because the closure it reads is complete by the time it is asked:
relation person : (String);
relation has_child : (String);
relation childless : (String);
rule person(x) :- age(x, a);
rule has_child(x) :- parent(x, y);
fn find_childless() {
clear childless;
for person(?x) {
if not has_child(x) { add childless(x); }
}
}
After find_childless(), solutions childless(?x) is ["ann", "jim", "liz"]. Host code’s
statement order is exactly the order between layers. The result is a snapshot: after add parent("jim", "sue") the query still lists jim, until find_childless() runs again and gives
["ann", "liz"]. A stratified rule would stay current by itself. The host version keeps every
rule’s meaning a least fixpoint and places the sequencing where a program already has sequence.
Computing values in a body
next_age asks a body to compute a value. Datalog systems such as Soufflé read something like b == a + 1 as a binding, with = giving a new variable the result of the arithmetic. That reading gives
up the bound on values. From the single fact person("tom"), this rule, were it allowed, would
derive "tomtom", then "tomtomtomtom", doubling the string each round until memory ran out:
rule person(x) :- person(y) and x == y ++ y;
Arithmetic gives a slower version of the same problem: b == a + 1 would add a new integer each
round, stopping only when the 64-bit integers wrapped around. A filter that limited the string’s
length would make the rule stop, but telling those two apart is a harder problem than checking that
a variable appears in a generator. Ramakrishnan, Bancilhon, and
Silberschatz (1987) studied conditions under which recursive
rules over such computed relations are safe. Prolog’s is evaluates arithmetic during the search
and leaves termination to the programmer, as with its other control.
Bridger’s terms remain variables and literals, and the computation happens in host code:
for age(?p, ?a) { add next_age(p, a + 1); }
That for runs once over a fixed set of solutions and adds six facts. A host loop around it could
still run forever, but that keeps non-termination in the part of the language where it already
exists. The rules themselves keep the guarantee.
Aggregates
A rule that counted a person’s descendants would run into both of the preceding problems. Suppose a
rule could hold count in its head, and watch tom’s descendants arrive in the rounds of the
previous chapter: two after round 1, four after round 2, five after round 3.
Facts are never withdrawn, so descendants("tom", 2), descendants("tom", 4), and
descendants("tom", 5) would all hold. A count of a set that can grow is not monotone unless the
count waits for the set to be complete. The counts are also values no fact contains, so they fall
outside the finite set of values safety establishes.
Systems that allow aggregates usually place them under the layering discipline, so that a counted
relation is complete first. SQL’s GROUP BY works over a complete input, and SQLite rejects an
aggregate over a query’s own recursive reference with recursive aggregate queries not supported.
Other work keeps recursion through an aggregate by asking which aggregates can be treated as
growing. Kenneth Ross and Yehoshua Sagiv’s monotonic
aggregation (1992) gave such an account, and
Flix lets rules compute over ordered values that climb
toward a fixed point, alongside sets of facts. Datafun
records monotonicity in its types, so that a checker can confirm a recursive definition grows.
Bridger declines aggregates in rules and computes them in host code over a finished answer:
len(solutions ancestor("tom", ?d)) is 5. Querying
relations covers that style.
When order decides safety
Prolog has no static check of this kind, and the order of a clause’s conditions determines whether they have values to work with. These two clauses differ only in order:
sibling(X, Y) :- parent(P, X), parent(P, Y), X \= Y.
sibling(X, Y) :- X \= Y, parent(P, X), parent(P, Y).
The first behaves like Bridger’s rule. In the second, X \= Y runs first, on two unbound variables,
and it succeeds only when they cannot be unified. Unbound variables can, so the clause finds nothing
and reports no error. N is M + 1 with M unbound raises an error at run time. Prolog programmers
learn which conditions need which variables bound.
Bridger’s check is made once, before the program runs, and asks only whether a generator somewhere
binds the variable. In return, a query that finishes derives the same facts wherever its filters are
written; reordering them can only change whether a stuck filter is reached. The check also rejects
more than Prolog would: a program cannot use == to bind.
Tracking effects
Prolog’s clause bodies may call write, and a condition re-run on backtracking prints again.
Programmers use this for tracing, and accept that the output describes the search. Bridger’s
analysis takes the opposite position for rule bodies, and its form is a choice too. Function types
record no effects, so the checker works out purity from
the whole program’s call graph, for the one place the semantics needs it. Languages such as Haskell
record effects in types, which makes purity visible at every call and requires every signature in a
chain to say so. Bridger’s needs no annotations and reports a witness path, though a programmer
reading a signature cannot tell from it whether the function may be used in a filter.
Further reading
Safety in query languages: Abiteboul, Hull, and Vianu’s Foundations of Databases, freely available from the authors, treats the problem and its syntactic conditions in chapter 5, “Adding Negation: Algebra and Calculus”, whose exercises work through several of the classes. Van Gelder and Topor’s “Safety and Translation of Relational Calculus” (1991) compares the conditions and translates accepted queries into the algebra.
Negation: chapter 15 of Foundations of Databases develops stratified semantics and the well-founded semantics, which gives a meaning to programs with no stratification, using three truth values. Clark’s “Negation as Failure” and Reiter’s “On Closed World Data Bases” both appear in Gallaire and Minker’s Logic and Data Bases (1978).
Beyond sets of facts: Madsen, Yee, and Lhoták’s “From Datalog to Flix” (2016) defines rules over lattices and applies them to program analysis. Arntzenius and Krishnaswami’s “Datafun: A Functional Datalog” (2016) combines a typed functional language with least fixed points, tracking monotonicity in the type system.
Concept checks
In rule elder_sibling(x, y) :- parent(p, x) and parent(p, y) and age(x, a) and age(y, b) and a > b;, which conjuncts bind which variables, and what does the rule derive for the family?
The four relation atoms are generators: they bind p, x, y, a, and b. a > b is a filter,
which binds nothing but reads two variables the generators bound. The head’s x and y are bound,
so the rule is safe. It derives ("bob", "liz"), since 44 is greater than 40, and ("pat", "ann"),
since 19 is greater than 17. The filter also removes each person paired with themselves, so no x != y is needed.
Bridger rejects rule copy(x) :- parent(y, z) and x == y;, even though x could only ever equal y. What could be accepted instead, and what would it take to accept this form?
rule copy(y) :- parent(y, z); says the same thing and is safe. To accept the original, the check
would have to treat an equality with one bound side as a binding. For a plain variable that is
sound, since the value still comes from a fact, and some Datalog systems do it. The same reading for
x == y ++ y, however, lets a rule produce values no fact contains, so a language that adopts it
must decide where to draw the line and how to keep the guarantee that is finite.
Bridger keeps one reading of ==, as a test.
In a program, f calls g, g calls f and println, and h calls nothing. Compute I. Which error would each be in a filter?
Step one adds g, whose body calls println. Step two adds f, which calls g. The next step
adds nothing, so and holds h. A filter calling f or g is reported as not
pure, with a witness path ending at println, while h is accepted. Had g not called println,
the same two functions would be outside , since the least fixpoint adds only what an effect
forces in, and the filter would then be rejected for recursion. Purity and termination are separate
conditions on a filter.
Can the relations person, has_child, childless be given layer numbers? Can stays and leaves? What does the answer say about the existence of a meaning?
The first can: person and has_child in layer 0, childless in layer 1, since it negates
has_child. Computing layer 0 to its fixpoint and then layer 1 gives one answer. stays negates
leaves and leaves negates stays, so each would have to be strictly above the other, and no
numbering exists. Those rules have two minimal closed sets, neither contained in the other, so there
is no least one. Stratification picks out programs for which a layer-by-layer meaning exists. A
language that accepts the others needs a semantics of a different kind.
find_childless gives a stale answer after an add, and a stratified rule would not. What does Bridger get in exchange for making programmers do the layering themselves?
Every rule set keeps a single least-fixpoint meaning, which the rounds compute with no analysis of
dependencies between relations, and stays monotone, which is what lets an engine continue from
a closure after an add. The non-monotone step is visible in the program as a not in ordinary
code, in a sequence the programmer wrote. The cost is that the programmer must rerun that step
whenever its inputs change, which a stratified engine would do by itself, and that the answer is
held as stored data, apart from any rule that defines it.