WIP
This commit is contained in:
parent
e24876b8f5
commit
e1562beacb
@ -99,6 +99,7 @@ fact Parent(alice, bob).
|
|||||||
rule Parent(?X, ?Y) -> Ancestor(?X, ?Y).
|
rule Parent(?X, ?Y) -> Ancestor(?X, ?Y).
|
||||||
run.
|
run.
|
||||||
query Ancestor(?X, ?Y)?
|
query Ancestor(?X, ?Y)?
|
||||||
|
explain Ancestor(alice, carol)?
|
||||||
show facts
|
show facts
|
||||||
show rules
|
show rules
|
||||||
reset
|
reset
|
||||||
@ -109,6 +110,7 @@ Rules:
|
|||||||
|
|
||||||
- facts and rules end with `.`
|
- facts and rules end with `.`
|
||||||
- queries end with `?`
|
- queries end with `?`
|
||||||
|
- `explain ... ?` shows one derivation tree per matching answer
|
||||||
- variables are prefixed with `?`
|
- variables are prefixed with `?`
|
||||||
- quoted constants are supported, for example `"alice smith"`
|
- quoted constants are supported, for example `"alice smith"`
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ pub enum Command {
|
|||||||
Rule(Rule),
|
Rule(Rule),
|
||||||
Run,
|
Run,
|
||||||
Query(Vec<Atom>),
|
Query(Vec<Atom>),
|
||||||
|
Explain(Vec<Atom>),
|
||||||
ShowFacts,
|
ShowFacts,
|
||||||
ShowRules,
|
ShowRules,
|
||||||
Reset,
|
Reset,
|
||||||
@ -82,6 +83,11 @@ pub fn parse_command(input: &str) -> Result<Command, String> {
|
|||||||
return Ok(Command::Query(atoms));
|
return Ok(Command::Query(atoms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(rest) = strip_keyword(trimmed, "explain") {
|
||||||
|
let atoms = parse_atom_list(trim_suffix(rest, '?')?)?;
|
||||||
|
return Ok(Command::Explain(atoms));
|
||||||
|
}
|
||||||
|
|
||||||
Err("unknown command; try `help`".to_string())
|
Err("unknown command; try `help`".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,6 +367,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_explain_command() {
|
||||||
|
let command = parse_command("explain Ancestor(alice, carol)?").unwrap();
|
||||||
|
match command {
|
||||||
|
Command::Explain(atoms) => assert_eq!(atoms.len(), 1),
|
||||||
|
other => panic!("unexpected command: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_script_reports_line_numbers() {
|
fn parse_script_reports_line_numbers() {
|
||||||
let error = parse_script("help\nbogus\nrun.").unwrap_err();
|
let error = parse_script("help\nbogus\nrun.").unwrap_err();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
//! Frontend utilities for interacting with chase-rs through scripts, a REPL, or a GUI.
|
//! Frontend utilities for interacting with chase-rs through scripts, a REPL, or a GUI.
|
||||||
|
|
||||||
pub mod language;
|
pub mod language;
|
||||||
|
pub mod provenance;
|
||||||
pub mod repl;
|
pub mod repl;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
|
|||||||
264
src/frontend/provenance.rs
Normal file
264
src/frontend/provenance.rs
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
//! Provenance tracking for frontend query explanations.
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use crate::chase::substitution::unify_atom;
|
||||||
|
use crate::{Atom, ChaseResult, Instance, Rule, Substitution, Term};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_STEPS: usize = 10_000;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MaterializedState {
|
||||||
|
pub result: ChaseResult,
|
||||||
|
provenance: HashMap<Atom, Derivation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Derivation {
|
||||||
|
Input,
|
||||||
|
Derived {
|
||||||
|
rule_index: usize,
|
||||||
|
rule: Rule,
|
||||||
|
premises: Vec<Atom>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct NullGenerator {
|
||||||
|
counter: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NullGenerator {
|
||||||
|
fn fresh(&mut self) -> Term {
|
||||||
|
let id = self.counter;
|
||||||
|
self.counter += 1;
|
||||||
|
Term::Null(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
struct Trigger {
|
||||||
|
rule_index: usize,
|
||||||
|
frontier_bindings: Vec<(String, Term)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Trigger {
|
||||||
|
fn new(rule_index: usize, rule: &Rule, subst: &Substitution) -> Self {
|
||||||
|
let frontier = rule.frontier_variables();
|
||||||
|
let mut bindings: Vec<_> = frontier
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|var| subst.get(&var).map(|term| (var, term.clone())))
|
||||||
|
.collect();
|
||||||
|
bindings.sort_by(|left, right| left.0.cmp(&right.0));
|
||||||
|
Self {
|
||||||
|
rule_index,
|
||||||
|
frontier_bindings: bindings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct PendingFact {
|
||||||
|
fact: Atom,
|
||||||
|
derivation: Derivation,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn materialize(base_instance: Instance, rules: &[Rule]) -> MaterializedState {
|
||||||
|
let mut instance = base_instance;
|
||||||
|
let mut provenance = instance
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|fact| (fact, Derivation::Input))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
let mut null_gen = NullGenerator::default();
|
||||||
|
let mut applied_triggers = HashSet::new();
|
||||||
|
let mut steps = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if steps >= DEFAULT_MAX_STEPS {
|
||||||
|
return MaterializedState {
|
||||||
|
result: ChaseResult {
|
||||||
|
instance,
|
||||||
|
steps,
|
||||||
|
terminated: false,
|
||||||
|
},
|
||||||
|
provenance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let derived = chase_step(
|
||||||
|
&instance,
|
||||||
|
rules,
|
||||||
|
&mut null_gen,
|
||||||
|
&mut applied_triggers,
|
||||||
|
&provenance,
|
||||||
|
);
|
||||||
|
|
||||||
|
if derived.is_empty() {
|
||||||
|
return MaterializedState {
|
||||||
|
result: ChaseResult {
|
||||||
|
instance,
|
||||||
|
steps,
|
||||||
|
terminated: true,
|
||||||
|
},
|
||||||
|
provenance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for pending in derived {
|
||||||
|
if instance.add(pending.fact.clone()) {
|
||||||
|
provenance.entry(pending.fact).or_insert(pending.derivation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
steps += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn explain_atom(atom: &Atom, provenance: &MaterializedState) -> String {
|
||||||
|
let mut lines = vec![atom.to_string()];
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
render_derivation(atom, provenance, 1, &mut seen, &mut lines);
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_matches(instance: &Instance, body: &[Atom]) -> Vec<Substitution> {
|
||||||
|
if body.is_empty() {
|
||||||
|
return vec![Substitution::new()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results = vec![Substitution::new()];
|
||||||
|
|
||||||
|
for body_atom in body {
|
||||||
|
let mut new_results = Vec::new();
|
||||||
|
|
||||||
|
for subst in &results {
|
||||||
|
let pattern = subst.apply_atom(body_atom);
|
||||||
|
for fact in instance.facts_for_predicate(&pattern.predicate) {
|
||||||
|
if let Some(next_subst) = unify_atom(&pattern, fact) {
|
||||||
|
let mut combined = subst.clone();
|
||||||
|
for (var, term) in next_subst.iter() {
|
||||||
|
combined.bind(var.clone(), term.clone());
|
||||||
|
}
|
||||||
|
new_results.push(combined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = new_results;
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaterializedState {
|
||||||
|
pub fn provenance_for(&self, atom: &Atom) -> Option<&Derivation> {
|
||||||
|
self.provenance.get(atom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chase_step(
|
||||||
|
instance: &Instance,
|
||||||
|
rules: &[Rule],
|
||||||
|
null_gen: &mut NullGenerator,
|
||||||
|
applied_triggers: &mut HashSet<Trigger>,
|
||||||
|
provenance: &HashMap<Atom, Derivation>,
|
||||||
|
) -> Vec<PendingFact> {
|
||||||
|
let mut pending = Vec::new();
|
||||||
|
|
||||||
|
for (rule_index, rule) in rules.iter().enumerate() {
|
||||||
|
let matches = find_matches(instance, &rule.body);
|
||||||
|
|
||||||
|
for subst in matches {
|
||||||
|
let trigger = Trigger::new(rule_index, rule, &subst);
|
||||||
|
if applied_triggers.contains(&trigger) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.existential_variables().is_empty() {
|
||||||
|
let head_facts = rule
|
||||||
|
.head
|
||||||
|
.iter()
|
||||||
|
.map(|atom| subst.apply_atom(atom))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if head_facts.iter().all(|fact| instance.contains(fact)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applied_triggers.insert(trigger);
|
||||||
|
|
||||||
|
let premises = rule
|
||||||
|
.body
|
||||||
|
.iter()
|
||||||
|
.map(|atom| subst.apply_atom(atom))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for fact in apply_rule_head(rule, &subst, null_gen) {
|
||||||
|
if instance.contains(&fact) || provenance.contains_key(&fact) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pending.push(PendingFact {
|
||||||
|
fact,
|
||||||
|
derivation: Derivation::Derived {
|
||||||
|
rule_index: rule_index + 1,
|
||||||
|
rule: rule.clone(),
|
||||||
|
premises: premises.clone(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pending
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_rule_head(rule: &Rule, subst: &Substitution, null_gen: &mut NullGenerator) -> Vec<Atom> {
|
||||||
|
let mut extended_subst = subst.clone();
|
||||||
|
|
||||||
|
for variable in rule.existential_variables() {
|
||||||
|
extended_subst.bind(variable, null_gen.fresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.head
|
||||||
|
.iter()
|
||||||
|
.map(|atom| extended_subst.apply_atom(atom))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_derivation(
|
||||||
|
atom: &Atom,
|
||||||
|
state: &MaterializedState,
|
||||||
|
depth: usize,
|
||||||
|
seen: &mut HashSet<Atom>,
|
||||||
|
lines: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
let indent = " ".repeat(depth);
|
||||||
|
if !seen.insert(atom.clone()) {
|
||||||
|
lines.push(format!("{}already shown", indent));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match state.provenance_for(atom) {
|
||||||
|
Some(Derivation::Input) => {
|
||||||
|
lines.push(format!("{}input fact", indent));
|
||||||
|
}
|
||||||
|
Some(Derivation::Derived {
|
||||||
|
rule_index,
|
||||||
|
rule,
|
||||||
|
premises,
|
||||||
|
}) => {
|
||||||
|
lines.push(format!(
|
||||||
|
"{}derived by rule #{}: {}",
|
||||||
|
indent, rule_index, rule
|
||||||
|
));
|
||||||
|
for premise in premises {
|
||||||
|
lines.push(format!("{}premise: {}", indent, premise));
|
||||||
|
render_derivation(premise, state, depth + 1, seen, lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
lines.push(format!("{}no provenance recorded", indent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::chase::substitution::unify_atom;
|
use crate::{Atom, Instance, Rule, Substitution};
|
||||||
use crate::{Atom, ChaseResult, Instance, Rule, Substitution, chase};
|
|
||||||
|
|
||||||
use super::language::{Command, parse_script};
|
use super::language::{Command, parse_script};
|
||||||
|
use super::provenance::{MaterializedState, explain_atom, find_matches, materialize};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
base_instance: Instance,
|
base_instance: Instance,
|
||||||
rules: Vec<Rule>,
|
rules: Vec<Rule>,
|
||||||
materialized: Option<ChaseResult>,
|
materialized: Option<MaterializedState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
@ -56,6 +56,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
Command::Run => Ok(self.run_chase()),
|
Command::Run => Ok(self.run_chase()),
|
||||||
Command::Query(query) => Ok(self.run_query(&query)),
|
Command::Query(query) => Ok(self.run_query(&query)),
|
||||||
|
Command::Explain(query) => Ok(self.explain_query(&query)),
|
||||||
Command::ShowFacts => Ok(self.show_facts()),
|
Command::ShowFacts => Ok(self.show_facts()),
|
||||||
Command::ShowRules => Ok(self.show_rules()),
|
Command::ShowRules => Ok(self.show_rules()),
|
||||||
Command::Reset => {
|
Command::Reset => {
|
||||||
@ -71,20 +72,20 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_chase(&mut self) -> String {
|
fn run_chase(&mut self) -> String {
|
||||||
let result = chase(self.base_instance.clone(), &self.rules);
|
let state = materialize(self.base_instance.clone(), &self.rules);
|
||||||
let message = if result.terminated {
|
let message = if state.result.terminated {
|
||||||
format!(
|
format!(
|
||||||
"Chase completed in {} step(s); {} fact(s) available.",
|
"Chase completed in {} step(s); {} fact(s) available.",
|
||||||
result.steps,
|
state.result.steps,
|
||||||
result.instance.len()
|
state.result.instance.len()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"Chase stopped after {} step(s); result may be incomplete.",
|
"Chase stopped after {} step(s); result may be incomplete.",
|
||||||
result.steps
|
state.result.steps
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
self.materialized = Some(result);
|
self.materialized = Some(state);
|
||||||
message
|
message
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +118,46 @@ impl Session {
|
|||||||
rendered.join("\n")
|
rendered.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn explain_query(&self, query: &[Atom]) -> String {
|
||||||
|
let instance = self.active_instance();
|
||||||
|
let matches = find_matches(instance, query);
|
||||||
|
if matches.is_empty() {
|
||||||
|
return if self.materialized.is_none() && !self.rules.is_empty() {
|
||||||
|
"0 explanations. Run `run.` first to trace derived answers.".to_string()
|
||||||
|
} else {
|
||||||
|
"0 explanations".to_string()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let variables = query_variables(query);
|
||||||
|
let mut sections = Vec::new();
|
||||||
|
|
||||||
|
for (index, subst) in matches.iter().enumerate() {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
lines.push(format!("match {}", index + 1));
|
||||||
|
if !variables.is_empty() {
|
||||||
|
lines.push(format!(" {}", format_substitution(subst, &variables)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for atom in query.iter().map(|atom| subst.apply_atom(atom)) {
|
||||||
|
lines.push(format!(" answer atom: {}", atom));
|
||||||
|
if let Some(state) = &self.materialized {
|
||||||
|
for detail in explain_atom(&atom, state).lines().skip(1) {
|
||||||
|
lines.push(format!(" {}", detail));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.push(" input fact".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.push(lines.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut output = vec![format!("{} explanation(s)", sections.len())];
|
||||||
|
output.extend(sections);
|
||||||
|
output.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
fn show_facts(&self) -> String {
|
fn show_facts(&self) -> String {
|
||||||
let facts = sorted_render(self.active_instance().iter());
|
let facts = sorted_render(self.active_instance().iter());
|
||||||
if facts.is_empty() {
|
if facts.is_empty() {
|
||||||
@ -141,7 +182,7 @@ impl Session {
|
|||||||
|
|
||||||
fn active_instance(&self) -> &Instance {
|
fn active_instance(&self) -> &Instance {
|
||||||
if let Some(result) = &self.materialized {
|
if let Some(result) = &self.materialized {
|
||||||
&result.instance
|
&result.result.instance
|
||||||
} else {
|
} else {
|
||||||
&self.base_instance
|
&self.base_instance
|
||||||
}
|
}
|
||||||
@ -154,6 +195,7 @@ fact Parent(alice, bob).
|
|||||||
rule Parent(?X, ?Y) -> Ancestor(?X, ?Y).
|
rule Parent(?X, ?Y) -> Ancestor(?X, ?Y).
|
||||||
run.
|
run.
|
||||||
query Ancestor(?X, ?Y)?
|
query Ancestor(?X, ?Y)?
|
||||||
|
explain Ancestor(alice, bob)?
|
||||||
show facts
|
show facts
|
||||||
show rules
|
show rules
|
||||||
reset
|
reset
|
||||||
@ -188,35 +230,6 @@ fn format_substitution(subst: &Substitution, variables: &[String]) -> String {
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_matches(instance: &Instance, body: &[Atom]) -> Vec<Substitution> {
|
|
||||||
if body.is_empty() {
|
|
||||||
return vec![Substitution::new()];
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut results = vec![Substitution::new()];
|
|
||||||
|
|
||||||
for body_atom in body {
|
|
||||||
let mut new_results = Vec::new();
|
|
||||||
|
|
||||||
for subst in &results {
|
|
||||||
let pattern = subst.apply_atom(body_atom);
|
|
||||||
for fact in instance.facts_for_predicate(&pattern.predicate) {
|
|
||||||
if let Some(next_subst) = unify_atom(&pattern, fact) {
|
|
||||||
let mut combined = subst.clone();
|
|
||||||
for (var, term) in next_subst.iter() {
|
|
||||||
combined.bind(var.clone(), term.clone());
|
|
||||||
}
|
|
||||||
new_results.push(combined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results = new_results;
|
|
||||||
}
|
|
||||||
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -248,4 +261,24 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(output.ends_with("true"));
|
assert!(output.ends_with("true"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explain_query_shows_rule_trace() {
|
||||||
|
let mut session = Session::new();
|
||||||
|
let output = session
|
||||||
|
.execute_script(
|
||||||
|
"fact Parent(alice, bob).\n\
|
||||||
|
fact Parent(bob, carol).\n\
|
||||||
|
rule Parent(?X, ?Y) -> Ancestor(?X, ?Y).\n\
|
||||||
|
rule Ancestor(?X, ?Y), Parent(?Y, ?Z) -> Ancestor(?X, ?Z).\n\
|
||||||
|
run.\n\
|
||||||
|
explain Ancestor(alice, carol)?",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(output.contains("explanation(s)"));
|
||||||
|
assert!(output.contains("derived by rule #2"));
|
||||||
|
assert!(output.contains("premise: Ancestor(alice, bob)"));
|
||||||
|
assert!(output.contains("input fact"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -248,12 +248,12 @@ fact Parent(bob, carol).
|
|||||||
rule Parent(?X, ?Y) -> Ancestor(?X, ?Y).
|
rule Parent(?X, ?Y) -> Ancestor(?X, ?Y).
|
||||||
rule Ancestor(?X, ?Y), Parent(?Y, ?Z) -> Ancestor(?X, ?Z).
|
rule Ancestor(?X, ?Y), Parent(?Y, ?Z) -> Ancestor(?X, ?Z).
|
||||||
run.
|
run.
|
||||||
query Ancestor(?X, ?Y)?</textarea>
|
explain Ancestor(alice, carol)?</textarea>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="primary" id="execute">Execute</button>
|
<button class="primary" id="execute">Execute</button>
|
||||||
<button class="secondary" id="reset">Reset Session</button>
|
<button class="secondary" id="reset">Reset Session</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="sample">Try <code>show facts</code>, <code>show rules</code>, or boolean queries like <code>query Parent(alice, bob)?</code>.</p>
|
<p class="sample">Try <code>query Ancestor(?X, ?Y)?</code>, <code>explain Ancestor(alice, carol)?</code>, or boolean queries like <code>query Parent(alice, bob)?</code>.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="output">
|
<section class="output">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user