A Prolog EDSL in MoonBit: build terms, clauses and programs as ordinary MoonBit values and run SLD resolution with backtracking. Includes a Prolog syntax parser, DCG rules, dif/2 constraints and a relational standard library, modeled after Scryer Prolog.
///|
test {
// 1. build a program from facts and rules
let p = Program([
Clause::fact(compound("parent", [atom("john"), atom("mary")])),
Clause::fact(compound("parent", [atom("john"), atom("jane")])),
Clause::fact(compound("parent", [atom("mary"), atom("bob")])),
])
// 2. query it with logic variables
let x = variable("X")
let answers = p.solve([compound("parent", [x, variable("_")])]).to_array()
assert_eq(answers.length(), 3)
assert_eq(answers[0].to_string(), "X = john")
assert_eq(answers[1].to_string(), "X = john")
assert_eq(answers[2].to_string(), "X = mary")
// 3. or enumerate lazily
let first = p.solve_first([compound("parent", [x, variable("_")])])
assert_eq(first.unwrap().to_string(), "X = john")
}///|
using @prolog {
atom,
compound,
fact,
rule,
variable,
program,
solve_first,
type PrologError,
}///|
test {
// Term("...") parses Prolog syntax directly
assert_eq(Term("parent(john, X)").to_string(), "parent(john, X)")
assert_eq(Term("[1, 2 | T]").to_string(), "[1, 2 | T]")
let x = variable("X")
assert_eq(x.to_string(), "X")
assert_eq(atom("john").to_string(), "john")
assert_eq(int(42).to_string(), "42")
assert_eq(float(1.5).to_string(), "1.5")
assert_eq(empty_list().to_string(), "[]")
assert_eq(list([int(1), int(2)]).to_string(), "[1, 2]")
assert_eq(cons(int(1), variable("T")).to_string(), "[1 | T]")
assert_eq(compound("f", [x, int(1)]).to_string(), "f(X, 1)")
// operator sugar: `|` is disjunction `;`, `&` is conjunction `,`,
// `+ - * / %` build arithmetic terms, `-x` unary negation
assert_eq(cons(int(1), cons(int(2), empty_list())).to_string(), "[1, 2]")
assert_eq((x & atom("true")).to_string(), "X, true")
assert_eq((x | atom("true")).to_string(), "(X; true)")
assert_eq((x + int(1)).to_string(), "(X + 1)")
}///|
test {
// X and Y are the same variable in head and body:
let x = variable("X")
let y = variable("Y")
let z = variable("Z")
let p = Program([
Clause::fact(compound("parent", [atom("john"), atom("mary")])),
Clause::fact(compound("parent", [atom("mary"), atom("bob")])),
// ancestor(X, Y) :- parent(X, Y).
Clause(compound("ancestor", [x, y]), compound("parent", [x, y])),
// ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
Clause(
compound("ancestor", [x, y]),
compound("parent", [x, z]) & compound("ancestor", [z, y]),
),
])
let y2 = variable("Y")
let answers = p.solve([compound("ancestor", [atom("john"), y2])]).to_array()
assert_eq(answers.length(), 2)
assert_eq(answers[0].to_string(), "Y = mary")
assert_eq(answers[1].to_string(), "Y = bob")
}///|
test {
let p = Program([
Clause::fact(compound("parent", [atom("john"), atom("mary")])),
Clause::fact(compound("parent", [atom("mary"), atom("bob")])),
])
let x = variable("X")
let y = variable("Y")
let answers = p.solve([compound("parent", [x, y])]).to_array()
assert_eq(answers.length(), 2)
assert_eq(answers[0].to_string(), "X = john, Y = mary")
assert_eq(answers[1].to_string(), "X = mary, Y = bob")
}///|
test {
let src =
#|parent(john, mary). parent(john, jane). parent(mary, bob).
#|ancestor(X, Y) :- parent(X, Y).
#|ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
#|
let p = parse_program(src)
let y = variable("Y")
let answers = p.solve([compound("ancestor", [atom("john"), y])]).to_array()
assert_eq(answers.length(), 3)
assert_eq(answers[2].to_string(), "Y = bob")
}///|
test {
let lib = Program::stdlib()
let x = variable("X")
let answers = lib
.solve([compound("member", [x, list([int(1), int(2), int(3)])])])
.to_array()
assert_eq(answers.length(), 3)
assert_eq(answers[2].to_string(), "X = 3")
}///|
test {
let src =
#|as --> [].
#|as --> [a], as.
#|
let p = parse_program(src)
let l = variable("L")
let answers = p
.solve([compound("phrase", [atom("as"), l])])
.take(3)
.to_array()
assert_eq(answers[0].to_string(), "L = []")
assert_eq(answers[1].to_string(), "L = [a]")
assert_eq(answers[2].to_string(), "L = [a, a]")
}///|
test {
let n = variable("N")
let p = Program([
Clause::fact(compound("nat", [int(0)])),
Clause(compound("nat", [compound("s", [n])]), compound("nat", [n])),
])
let x = variable("X")
let first3 = p.solve([compound("nat", [x])]).take(3).to_array()
assert_eq(first3[2].to_string(), "X = s(s(0))")
}pub(all) suberror ParseError {
UnexpectedChar(pos~ : Int, ch~ : Char)
UnexpectedEof(pos~ : Int)
UnclosedString(pos~ : Int)
InvalidNumber(pos~ : Int, text~ : String)
} derive(Debug)test {
let x = variable("X")
let p = Program([Clause::fact(compound("p", [atom("a")]))])
let a = p.solve_first([compound("p", [x])]).unwrap()
assert_eq(a.get("X"), Some(atom("a")))
assert_eq(a.get("Y"), None)
}test {
let x = variable("X")
let c = Clause(compound("p", [x]), compound("q", [x]))
inspect(c.head.to_string(), content="p(X)")
inspect(c.body.to_string(), content="q(X)")
}test {
let x = variable("X")
let y = variable("Y")
let p = Program([
Clause::fact(compound("parent", [atom("john"), atom("mary")])),
// ancestor(X, Y) :- parent(X, Y).
Clause(compound("ancestor", [x, y]), compound("parent", [x, y])),
])
let qx = variable("X")
let answers = p.solve([compound("parent", [qx, variable("_")])]).to_array()
assert_eq(answers.length(), 1)
assert_eq(answers[0].to_string(), "X = john")
}test {
let p = Program([
Clause::fact(compound("parent", [atom("john"), atom("mary")])),
Clause::fact(compound("parent", [atom("john"), atom("jane")])),
])
let x = variable("X")
let answers = p.solve([compound("parent", [x, variable("_")])]).to_array()
assert_eq(answers.length(), 2)
assert_eq(answers[0].to_string(), "X = john")
}test {
let p = Program::stdlib()
let x = variable("X")
let answers = p
.solve([compound("member", [x, list([int(1), int(2), int(3)])])])
.to_array()
assert_eq(answers.length(), 3)
assert_eq(answers[0].to_string(), "X = 1")
assert_eq(answers[2].to_string(), "X = 3")
}test {
inspect(
@prolog.Term("parent(john, X)").to_string(),
content="parent(john, X)",
)
inspect(@prolog.Term("[1, 2]").to_string(), content="[1, 2]")
inspect(@prolog.Term("X + 1").to_string(), content="(X + 1)")
}test {
assert_true(atom("a").compare_terms(int(1)) > 0)
assert_eq(int(1).compare_terms(int(1)), 0)
// standard order: floats sort before integers, so Int(1) > Float(1.0)
assert_true(int(1).compare_terms(float(1.0)) > 0)
}test {
let x = variable("X")
let y = variable("Y")
let s : Subst = @immut_hashmap.HashMap([])
let s2 = match x.unify(y, s) {
Some(s2) => s2
None => abort("unify failed")
}
let s3 = match y.unify(atom("a"), s2) {
Some(s3) => s3
None => abort("unify failed")
}
assert_eq(x.deref(s3).to_string(), "a")
}test {
assert_eq(compound("parent", [atom("john")]).head_key(), Some(("parent", 1)))
assert_eq(atom("true").head_key(), Some(("true", 0)))
assert_true(int(1).head_key() is None)
}test {
let x = variable("X")
let s = x.unify(atom("a"), @immut_hashmap.HashMap([]))
assert_true(s is Some(_))
}test {
let x = variable("X")
let clause_head = compound("parent", [x, atom("mary")])
let clause_body = compound("likes", [x, atom("mary")])
assert_eq(clause_head.to_string(), "parent(X, mary)")
assert_eq(clause_body.to_string(), "likes(X, mary)")
}test {
let p = parse_program("as --> []. as --> [a], as.")
let l = variable("L")
let answers = p
.solve([compound("phrase", [atom("as"), l])])
.take(3)
.to_array()
assert_eq(answers[0].to_string(), "L = []")
assert_eq(answers[1].to_string(), "L = [a]")
assert_eq(answers[2].to_string(), "L = [a, a]")
// parsing a fixed sequence
let ok = p
.solve([compound("phrase", [atom("as"), list([atom("a"), atom("a")])])])
.to_array()
assert_eq(ok.length(), 1)
}test {
let c = parse_clause("ancestor(X, Y) :- parent(X, Y).")
inspect(c.head.to_string(), content="ancestor(X, Y)")
inspect(c.body.to_string(), content="parent(X, Y)")
inspect(parse_clause("likes(john, mary).").body.to_string(), content="true")
}test {
let p = parse_program(
"parent(john, mary). parent(john, jane).\n% ancestor rule\nancestor(X, Y) :- parent(X, Y).",
)
let x = variable("X")
let answers = p.solve([compound("parent", [x, variable("_")])]).to_array()
assert_eq(answers.length(), 2)
}test {
inspect(parse_term("parent(john, X)").to_string(), content="parent(john, X)")
inspect(parse_term("1 + 2 * 3").to_string(), content="(1 + (2 * 3))")
inspect(parse_term("[a, b | T]").to_string(), content="[a, b | T]")
inspect(parse_term("(a, b) ; c").to_string(), content="(a, b; c)")
inspect(parse_term("0x1F").to_string(), content="31")
}test {
let a = variable("X")
let b = variable("X")
let a_ref = match a {
Term::Var(x) => x
_ => abort("expected a variable")
}
let b_ref = match b {
Term::Var(x) => x
_ => abort("expected a variable")
}
assert_true(a_ref.id != b_ref.id)
assert_eq(a_ref.name, "X")
}A Prolog EDSL in MoonBit: build terms, clauses and programs as ordinary MoonBit values and run SLD resolution with backtracking. Includes a Prolog syntax parser, DCG rules, dif/2 constraints and a relational standard library, modeled after Scryer Prolog.