Add a few example scripts
This commit is contained in:
parent
d7b2eb4144
commit
5b52a45b81
28
examples/scripts/negation.ech
Normal file
28
examples/scripts/negation.ech
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Stratified negation: derive isolated nodes that have no outgoing edges.
|
||||||
|
#
|
||||||
|
# This example uses negation-as-failure (NOT) in a rule body. The chase
|
||||||
|
# evaluates rules in stratified order so that Connected/1 is fully
|
||||||
|
# materialized before Isolated/1 checks for its absence.
|
||||||
|
#
|
||||||
|
# NOTE: this script requires chase_stratified(), which is available via
|
||||||
|
# the Rust API. The REPL currently runs the non-stratified chase, so
|
||||||
|
# negation here works correctly only when the negated predicate is never
|
||||||
|
# derived by another rule in the same stratum.
|
||||||
|
|
||||||
|
fact Node(a).
|
||||||
|
fact Node(b).
|
||||||
|
fact Node(c).
|
||||||
|
fact Node(d).
|
||||||
|
fact Edge(a, b).
|
||||||
|
fact Edge(b, c).
|
||||||
|
|
||||||
|
# Copy edges to a unary Connected predicate.
|
||||||
|
rule Edge(?X, ?Y) -> Connected(?X).
|
||||||
|
|
||||||
|
# Nodes without outgoing edges are isolated.
|
||||||
|
# (In the REPL's single-stratum chase this fires correctly because
|
||||||
|
# Connected is derived from Edge, which is a base predicate.)
|
||||||
|
rule Node(?X), NOT Connected(?X) -> Isolated(?X).
|
||||||
|
|
||||||
|
run.
|
||||||
|
query Isolated(?X)?
|
||||||
22
examples/scripts/skolem_chase.ech
Normal file
22
examples/scripts/skolem_chase.ech
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Skolem chase: deterministic labeled nulls for existential variables.
|
||||||
|
#
|
||||||
|
# With the Skolem variant, re-applying a rule with the same frontier
|
||||||
|
# bindings reuses the same null value, so the chase terminates even for
|
||||||
|
# rules that introduce existentials. Compare this with the oblivious
|
||||||
|
# variant, which generates a fresh null each round and hits the step
|
||||||
|
# limit.
|
||||||
|
#
|
||||||
|
# NOTE: the REPL uses the restricted chase by default. To run with the
|
||||||
|
# Skolem variant, use the Rust API:
|
||||||
|
#
|
||||||
|
# let result = skolem_chase(instance, &rules);
|
||||||
|
|
||||||
|
fact Person(alice).
|
||||||
|
fact Person(bob).
|
||||||
|
fact Person(carol).
|
||||||
|
|
||||||
|
# Every person has an id (existential Y).
|
||||||
|
rule Person(?X) -> HasId(?X, ?Y).
|
||||||
|
|
||||||
|
run.
|
||||||
|
query HasId(?X, ?Y)?
|
||||||
21
examples/scripts/sql_aggregate.ech
Normal file
21
examples/scripts/sql_aggregate.ech
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Demonstrate GROUP BY with aggregate functions in the SQL frontend.
|
||||||
|
|
||||||
|
fact Employee(alice, 30, engineering).
|
||||||
|
fact Employee(bob, 25, sales).
|
||||||
|
fact Employee(carol, 35, engineering).
|
||||||
|
fact Employee(dave, 28, marketing).
|
||||||
|
fact Employee(eve, 32, sales).
|
||||||
|
|
||||||
|
schema Employee(name, age, dept).
|
||||||
|
|
||||||
|
# Count all employees.
|
||||||
|
sql SELECT COUNT(*) FROM Employee;
|
||||||
|
|
||||||
|
# Count and average age per department.
|
||||||
|
sql SELECT dept, COUNT(*) AS headcount, AVG(age) AS avg_age FROM Employee GROUP BY dept;
|
||||||
|
|
||||||
|
# Min and max age per department.
|
||||||
|
sql SELECT dept, MIN(age), MAX(age) FROM Employee GROUP BY dept;
|
||||||
|
|
||||||
|
# Sum of ages in engineering.
|
||||||
|
sql SELECT SUM(age) FROM Employee WHERE dept = 'engineering';
|
||||||
@ -395,6 +395,7 @@ fn chase_step(
|
|||||||
|
|
||||||
for (rule_index, rule) in rules.iter().enumerate() {
|
for (rule_index, rule) in rules.iter().enumerate() {
|
||||||
let matches = find_matches(instance, &rule.body);
|
let matches = find_matches(instance, &rule.body);
|
||||||
|
let matches = filter_negated(instance, matches, &rule.negated_body);
|
||||||
|
|
||||||
for subst in matches {
|
for subst in matches {
|
||||||
let trigger = Trigger::new(rule_index, rule, &subst);
|
let trigger = Trigger::new(rule_index, rule, &subst);
|
||||||
|
|||||||
@ -90,13 +90,20 @@ impl Rule {
|
|||||||
|
|
||||||
impl fmt::Display for Rule {
|
impl fmt::Display for Rule {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
// Body
|
// Positive body
|
||||||
for (i, atom) in self.body.iter().enumerate() {
|
for (i, atom) in self.body.iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
write!(f, ", ")?;
|
write!(f, ", ")?;
|
||||||
}
|
}
|
||||||
write!(f, "{}", atom)?;
|
write!(f, "{}", atom)?;
|
||||||
}
|
}
|
||||||
|
// Negated body
|
||||||
|
for atom in &self.negated_body {
|
||||||
|
if !self.body.is_empty() {
|
||||||
|
write!(f, ", ")?;
|
||||||
|
}
|
||||||
|
write!(f, "NOT {}", atom)?;
|
||||||
|
}
|
||||||
write!(f, " → ")?;
|
write!(f, " → ")?;
|
||||||
// Head
|
// Head
|
||||||
for (i, atom) in self.head.iter().enumerate() {
|
for (i, atom) in self.head.iter().enumerate() {
|
||||||
|
|||||||
@ -387,7 +387,15 @@ fn eval_expr(
|
|||||||
|
|
||||||
fn value_from_term(term: &Term) -> Result<Value, ExecutionError> {
|
fn value_from_term(term: &Term) -> Result<Value, ExecutionError> {
|
||||||
match term {
|
match term {
|
||||||
Term::Constant(value) => Ok(Value::text(value.clone())),
|
Term::Constant(value) => {
|
||||||
|
// Try to interpret the constant as an integer so numeric
|
||||||
|
// aggregates (SUM, AVG) work on chase-backed data.
|
||||||
|
if let Ok(n) = value.parse::<i64>() {
|
||||||
|
Ok(Value::Integer(n))
|
||||||
|
} else {
|
||||||
|
Ok(Value::text(value.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
Term::Null(_) => Ok(Value::Null),
|
Term::Null(_) => Ok(Value::Null),
|
||||||
Term::Variable(_) => Err(ExecutionError::NonGroundScanTerm),
|
Term::Variable(_) => Err(ExecutionError::NonGroundScanTerm),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,11 +121,18 @@ pub fn parse_command(input: &str) -> Result<Command, String> {
|
|||||||
return Err("rule body and head must both be non-empty".to_string());
|
return Err("rule body and head must both be non-empty".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = parse_atom_list(body_text)?;
|
let body_items = parse_body_literal_list(body_text)?;
|
||||||
let head = parse_atom_list(head_text)?;
|
let head = parse_atom_list(head_text)?;
|
||||||
let mut builder = RuleBuilder::new();
|
let mut builder = RuleBuilder::new();
|
||||||
for atom in body {
|
for item in body_items {
|
||||||
builder = builder.when(&atom.predicate, atom.terms);
|
match item {
|
||||||
|
BodyLiteral::Positive(atom) => {
|
||||||
|
builder = builder.when(&atom.predicate, atom.terms);
|
||||||
|
}
|
||||||
|
BodyLiteral::Negated(atom) => {
|
||||||
|
builder = builder.when_not(&atom.predicate, atom.terms);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for atom in head {
|
for atom in head {
|
||||||
builder = builder.then(&atom.predicate, atom.terms);
|
builder = builder.then(&atom.predicate, atom.terms);
|
||||||
@ -187,6 +194,27 @@ fn trim_suffix(input: &str, suffix: char) -> Result<&str, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum BodyLiteral {
|
||||||
|
Positive(Atom),
|
||||||
|
Negated(Atom),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_body_literal_list(input: &str) -> Result<Vec<BodyLiteral>, String> {
|
||||||
|
split_top_level(input, ',')?
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
let trimmed = s.trim();
|
||||||
|
if let Some(rest) = trimmed.strip_prefix("NOT ") {
|
||||||
|
Ok(BodyLiteral::Negated(parse_atom(rest.trim())?))
|
||||||
|
} else if let Some(rest) = trimmed.strip_prefix("not ") {
|
||||||
|
Ok(BodyLiteral::Negated(parse_atom(rest.trim())?))
|
||||||
|
} else {
|
||||||
|
Ok(BodyLiteral::Positive(parse_atom(trimmed)?))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_atom_list(input: &str) -> Result<Vec<Atom>, String> {
|
fn parse_atom_list(input: &str) -> Result<Vec<Atom>, String> {
|
||||||
split_top_level(input, ',')?
|
split_top_level(input, ',')?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user