prolog

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.

prolog
edsl
logic-programming
logic
moonbit
moon add amistozy/prolog@0.1.7
Download zip
Author
Version
0.1.7
License
Apache-2.0
Last updated
14 minutes ago
Downloads
15
README

#amistozy/prolog

A Prolog EDSL (embedded domain-specific language) in MoonBit: build Prolog terms, clauses and programs as ordinary MoonBit values, then run SLD resolution with backtracking to enumerate answers.

The design is inspired by Scryer Prolog (see reference/scryer-prolog): its Term representation, right-nested conjunction (a, b), and answer bindings follow the same shape.

#Quick start

///|
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")
}

Consumers of the package can drop the @prolog. prefix with a using declaration (types use the type keyword):

///|
using @prolog {
atom,
compound,
fact,
rule,
variable,
program,
solve_first,
type PrologError,
}

#Terms

///|
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)")
}

Important: every call to variable(name) creates a brand-new logic variable. A rule's head and body must share the same variable values:

///|
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")
}

Lists can be built either as list([...]) or as cons chains (cons, list_tail); both representations unify with each other. The | operator is reserved for the Prolog disjunction ;.

#Querying

  • program.solve(goals) / [Program::solve] — a lazy iterator of [Answer]s
  • program.solve_first(goals) — the first answer, if any
  • program.solve_all(goals) — all answers (careful with infinite programs)

An [Answer] reports the bindings of the query's named variables, rendered as X = john, Y = mary.

///|
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")
}

#Writing programs in Prolog syntax

Instead of (or alongside) the builder API, the package can parse plain Prolog source text:

///|
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")
}

  • parse_term("parent(john, X)") — one term (variables with the same name share one variable, like in Prolog)
  • parse_clause("ancestor(X, Y) :- parent(X, Y).") — one clause
  • parse_program(src) — a whole program (.-separated clauses, % and /* */ comments)

Supported syntax: variables, atoms (incl. quoted '...'), integers, floats (including 1.5e-2), base literals (0x1F, 0o17, 0b101), char codes (0'a, 0'\n), strings, lists with tails ([a, b | T]), compound terms, {G} (DCG goals), DCG rules (head --> body), and the usual ISO operators with their precedences (:-, ;, ,, ->, =, is, =.., + - * / // div mod, @< ..., unary - and \+).

#Builtins and standard library

Builtin predicates (they take precedence over clauses with the same name):

  • control: true, fail, ! (cut), ,/;/-> (if-then-else) are handled structurally; (A -> B ; C) commits to B once A succeeds
  • constraints: dif/2 (delayed disequality, cf. Scryer's library(dif))
  • unification: =, \=, ==, \==
  • arithmetic: is, <, >, =</<=, >=, =:=, =\=; functors + - * / // div mod ^ abs max min sqrt (with ISO semantics: / is float division, // truncates, div/mod floor)
  • meta: not/1 and \+ (with a cut-local scope), call/1..8, ignore/1, once/1, repeat/0, forall/2, findall/3, bagof/3, setof/3 (with ^ existential quantification), copy_term/2, term_variables/2
  • term inspection: functor/3, arg/3, =../2, ground/1
  • term ordering (standard order, cf. Scryer's TermOrderCategory): compare/3, sort/2, msort/2, @<, @>, @=<, @>=
  • atoms: atom_length/2, atom_concat/3 (enumerates splits), atom_codes/2, atom_chars/2, sub_atom/5
  • numbers: number_codes/2, number_chars/2, atom_number/2, char_code/2
  • DCG: phrase/2, phrase/3 (grammar rules are expanded at parse time, see below)
  • type tests: var, nonvar, atom, integer, float, number, atomic, string, compound, list
  • output: write(X), writeln(X) (simplified to println)

[stdlib] provides classic predicates as ordinary clauses, so they stay fully relational:

///|
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")
}

member/2, append/3, length/2, reverse/2, between/3, nth0/3, nth1/3, last/2, sum_list/2, max_list/2, min_list/2, select/3, flatten/2, permutation/2 are available in any argument direction, e.g. append(A, B, [1, 2]) enumerates all splits. Also included: maplist/2..4, foldl/4 (via call/N), memberchk/2, selectchk/3, succ/2, plus/3, numlist/3, prefix/2, suffix/2, same_length/2.

#Definite clause grammars (DCGs)

Grammar rules are expanded into ordinary clauses at parse time, following Scryer's library(dcgs): Head --> Body becomes Head(S0, S) :- Body'(S0, S), with [a, b] terminals, (A, B) sequencing, (A ; B) alternatives, {G} plain goals, ! cuts, call(G) and phrase(...) handled as in Scryer. Run a grammar with phrase/2 or phrase/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]")
}

The same expansion is available programmatically: [dcg_rule] builds a clause from a grammar rule, [Term::dcg_body] expands a grammar body against two list arguments.

#dif/2 disequality constraints

dif(X, Y) succeeds when X and Y can be shown to be different and fails when they are identical; when the terms are not yet comparable the constraint is delayed and re-checked after every binding, so X = b fails after dif(X, b). Constraints are undone on backtracking, and \=/2 keeps its ISO "not unifiable" meaning.

#Laziness

Solutions are produced lazily, so infinite programs can be explored with take:

///|
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))")
}

#Semantics notes

  • Subst (the substitution passed to unify/deref/resolve) is a persistent, immutable hash map (moonbitlang/core/immut/hashmap): binding a variable returns a new substitution and never mutates the old one, so sharing a substitution across branches is always safe.
  • Unification performs the occur check and treats numbers numerically (1 = 1.0 succeeds).
  • dif/2 constraints are re-checked after every binding; they are snapshotted and undone together with the choice points.
  • Undefined predicates simply fail (no error), like many small Prologs.
  • solve_all on a program with infinitely many answers will not terminate; use solve with take/next instead.
  • Terms with unknown arity/name render plainly; atoms with special characters are not quoted; integral floats render with a trailing .0 so they round-trip as floats.

#
Subst

A substitution maps variable ids to terms.

Subst is backed by a persistent (immutable) hash map (moonbitlang/core/immut/hashmap): every binding step produces a new map that shares structure with the old one, so a substitution value can be shared safely across branches and backtracking snapshots are free.

#
ParseError

pub(all) suberror ParseError {
UnexpectedChar(pos~ : Int, ch~ : Char)
UnexpectedEof(pos~ : Int)
UnclosedString(pos~ : Int)
InvalidNumber(pos~ : Int, text~ : String)
} derive(
Debug
)

Errors produced by the parser.

#
PrologError

pub(all) suberror PrologError {
InvalidHead(term~ : Term)
} derive(
Debug
)

Errors raised while building a [Program].

#
Alt

How to resume from a choice point during backtracking.

#
Answer

pub struct Answer {
bindings : Map[String, Term]
order : Array[String]
} derive(
Debug
)

One answer of a query: the bindings of the query's variables, keyed by name (anonymous variables starting with _ are skipped).

order records the names in order of first appearance in the query, so answers render deterministically as X = john, Y = mary regardless of hash-map iteration order.
impl Show for Answer

#
Answer::from_subst

fn Answer::from_subst(subst :
HashMap
[Int, Term], goals : Array[Term]) -> Answer

Builds an [Answer] from a substitution and the query it answers. Bindings that just say X = X (an unbound query variable) are omitted.

#
Answer::get

fn Answer::get(self : Answer, name : String) -> Term?

The binding of the query variable named name, if any.

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)
}

#
Choice

A suspended search state that backtracking can resume.

#
Clause

pub struct Clause {
head : Term
body : Term
} derive(
Debug
)

A Prolog clause: head :- body.

Build one with the Clause constructor or [Clause::fact] (a clause whose body is true).

#
Clause::Clause

fn Clause::Clause(head : Term, body : Term) -> Clause

Builds a rule head :- body, where body is a single goal (use the & operator to build conjunctions, | for disjunctions).

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)")
}

#
Clause::fact

fn Clause::fact(head : Term) -> Clause

A fact head. (a clause with body true).

#
FStack

Immutable goal stack. Pushing creates a new cell, so frames pushed before a choice point are automatically snapshotted by reference.

#
Frame

A goal frame on the goal stack: the goal plus the cut mark of the clause body it belongs to. The mark is the length of the choice-point stack when the enclosing clause was entered; ! truncates the stack to it.

#
Machine

The SLD resolution machine. subst maps are never mutated in place, so each choice point holds an implicit snapshot of the substitution.

diffs holds the active dif/2 disequality constraints; like choices, it is truncated on backtracking (see [Choice::diffs_len]).

#
Parser

#
Program

pub struct Program {
clauses : Map[(String, Int), Array[Clause]]
} derive(
Debug
)

A logic program: a set of clauses indexed by predicate key (functor name, arity).

Build one with the Program constructor and run queries with [Program::solve].

#
Program::Program

fn Program::Program(clauses : Array[Clause]) -> Program raise PrologError

Builds a program from clauses, e.g.

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")
}

#
Program::add

fn Program::add(self : Program, c : Clause) -> Unit raise PrologError

Adds a clause to the program. Raises [PrologError::InvalidHead] when the head is not an atom or a compound term.

#
Program::solve

fn Program::solve(self : Program, goals : Array[Term]) -> Iter[Answer]

Runs a query against the program, yielding each solution as an [Answer]. The iterator is lazy: solutions are produced on demand, and infinite solution spaces can be explored with take/next.

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")
}

#
Program::solve_all

fn Program::solve_all(self : Program, goals : Array[Term]) -> Array[Answer]

All solutions of a query. Use with care on programs with infinite solution spaces (use [solve] with take/next instead).

#
Program::solve_first

fn Program::solve_first(self : Program, goals : Array[Term]) -> Answer?

The first solution of a query, if any.

#
Program::solve_subst

All solutions of the query (a conjunction of goals) as a lazy iterator of raw substitutions.

#
Program::stdlib

fn Program::stdlib() -> Program

A small library of classic list / arithmetic predicates, implemented as ordinary Prolog clauses so they stay fully relational (usable in any argument direction):

  • member/2, append/3, length/2, reverse/2, between/3
  • nth0/3, nth1/3, last/2, sum_list/2, max_list/2, min_list/2
  • select/3, flatten/2, permutation/2
  • maplist/2..4, foldl/4, memberchk/2, selectchk/3 (need call/2+)
  • succ/2, plus/3, numlist/3, prefix/2, suffix/2, same_length/2

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")
}

#
Term

pub(all) enum Term {
Int(Int)
Float(Double)
Atom(String)
Str(String)
Var(VarRef)
List(Array[Term])
Compound(String, Array[Term])
} derive(
Debug
)

A Prolog term, modeled after Scryer Prolog's Term enum (reference/scryer-prolog/src/machine/lib_machine/mod.rs).

  • atoms: Atom("john") or [atom]
  • variables: Var(ref) or [variable]
  • integers: Int(42) or [int]
  • floats: Float(1.5) or [float]
  • strings: Str("text") or [str]
  • proper lists: List([...]) or [list]
  • compound terms: Compound("f", [a, b]) or [compound]

Lists are also represented as cons cells '.'(H, T) (built by [cons] or [list_tail]), so list([1, 2, 3]) and cons(1, cons(2, cons(3, empty_list()))) unify with each other.
impl Add for Term
impl BitAnd for Term
impl BitOr for Term
impl Div for Term
impl Eq for Term
impl Mod for Term
impl Mul for Term
impl Neg for Term
impl Show for Term
impl Sub for Term

#
Term::Term

fn Term::Term(text : String) -> Term raise ParseError

Parses a term from Prolog syntax: Term("parent(john, X)"), Term("[1, 2 | T]"), Term("X is 2 * 3"). Variables with the same name share one variable, like in Prolog source text.

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)")
}

#
Term::compare_terms

fn Term::compare_terms(self : Term, other : Term) -> Int

Compares two resolved terms, returning a negative / zero / positive int (like compare/3). Unbound variables compare by their id.

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)
}

#
Term::dcg_body

fn Term::dcg_body(self : Term, s0 : Term, s : Term) -> Term

Expands a DCG body self (a grammar construct) into an ordinary goal relating the two list arguments s0 (input) and s (remaining output). See the module docs for the translation rules.

#
Term::deref

Follows the binding chain of self, returning the current value of the variable (or the term itself if it is not a bound variable).

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")
}

#
Term::dif

fn Term::dif(self : Term, other : Term) -> Term

Builds the goal dif(self, other): a disequality constraint that delays the decision until the two terms are comparable (see [Machine::dif]).

#
Term::eq

fn Term::eq(self : Term, other : Term) -> Term

Builds the goal self = other (unification).

#
Term::head_key

fn Term::head_key(self : Term) -> (String, Int)?

The predicate key of a callable term: (name, arity).

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)
}

#
Term::identical

fn Term::identical(self : Term, other : Term) -> Term

Builds the goal self == other (identical terms).

#
Term::is_

fn Term::is_(self : Term, expr : Term) -> Term

Builds the goal self is expr (arithmetic evaluation).

#
Term::neq

fn Term::neq(self : Term, other : Term) -> Term

Builds the goal self \= other (non-unifiability).

#
Term::not

fn Term::not(self : Term) -> Term

Builds the goal not(self).

#
Term::not_identical

fn Term::not_identical(self : Term, other : Term) -> Term

Builds the goal self \== other.

#
Term::resolve

Fully resolves a term: bound variables are replaced by their values recursively, unbound variables stay as they are.

#
Term::unify

Robinson unification under substitution s. Returns Some(s') with the new substitution when the two terms unify, None otherwise. The occur check is performed, so cyclic bindings are rejected.

Numbers unify numerically: Int(1) and Float(1.0) unify. Proper lists (built from List, [] or cons cells) unify regardless of which representation each side uses.

test {
let x = variable("X")
let s = x.unify(atom("a"), @immut_hashmap.HashMap([]))
assert_true(s is Some(_))
}

#
TokItem

#
VarRef

pub(all) struct VarRef {
id : Int
name : String
} derive(
Debug
)

Prolog variables: a unique id plus a display name.

Two occurrences of the same VarRef value denote the same logic variable. Every call to [variable] allocates a brand-new id, so writing variable("X") twice produces two different variables; share one value through a let binding instead:

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)")
}

#
atom

fn atom(name : String) -> Term

An atom, e.g. atom("john").

#
compound

fn compound(functor : String, args : Array[Term]) -> Term

A compound term, e.g. compound("parent", [atom("john"), atom("mary")]).

#
cons

fn cons(head : Term, tail : Term) -> Term

The list constructor [H | T]; use [cons] (or [list_tail]) to build lists as cons chains — the | operator is reserved for the Prolog disjunction ;.

#
dcg_rule

fn dcg_rule(head : Term, body : Term) -> Clause

Definite clause grammars (DCGs), following Scryer Prolog's library(dcgs) design (reference/scryer-prolog/src/lib/dcgs.pl).

A grammar rule Head --> Body is expanded at clause-construction time into an ordinary clause Head(S0, S) :- Body'(S0, S), where the body is translated with two extra list arguments:

  • [a, b] (a terminal list) becomes S0 = [a, b | S]
  • (A, B) becomes A'(S0, S1), B'(S1, S)
  • (A ; B) becomes a disjunction of the expanded branches
  • {G} (a plain Prolog goal) becomes G, S0 = S
  • ! becomes !, S0 = S
  • call(G) becomes call(G, S0, S)
  • phrase(Body, ...) keeps its arguments and gains S0, S
  • \+ G becomes \+ phrase(G, S0, _), S0 = S
  • a variable body becomes phrase(Var, S0, S)
  • anything else is a nonterminal: NT becomes NT(S0, S)

[parse_program] / [parse_clause] expand --> clauses automatically; the same expansion is available programmatically via [dcg_rule] and [Term::dcg_body], and at runtime via the phrase/2 and phrase/3 builtins.

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)
}

#
empty_list

fn empty_list() -> Term

The empty list [].

#
float

fn float(d : Double) -> Term

A float, e.g. float(1.5).

#
int

fn int(i : Int) -> Term

An integer term, e.g. int(42) (or the variant Term::Int(42)).

#
list

fn list(elems : Array[Term]) -> Term

A proper list, e.g. list([1, 2, 3]) or list([]).

#
list_tail

fn list_tail(elems : Array[Term], tail : Term) -> Term

A list with a non-empty tail: [e1, ..., en | tail].

#
parse_clause

fn parse_clause(text : String) -> Clause raise ParseError

Parses one clause: head :- body. or a bare fact head. (the trailing period is optional).

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")
}

#
parse_program

fn parse_program(text : String) -> Program raise ParseError

Parses a whole program (a sequence of clauses separated by .), e.g.

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)
}

#
parse_term

fn parse_term(text : String) -> Term raise ParseError

A parser for a practical subset of Prolog syntax: terms, clauses and whole programs (see reference/scryer-prolog/src/parser for the full Scryer parser; this one covers the common cases with ISO operator precedences).

Supported syntax:

  • variables: X, _Foo, _ (each _ is a fresh anonymous variable)
  • atoms: foo, quoted 'foo bar', !
  • numbers: 42, -3, 1.5, 1.5e-2, 0x1F, 0o17, 0b101 (a leading - builds -(N)), char codes 0'a, 0'\n
  • strings: "hello" (with \", \\, \n, \t escapes)
  • lists: [], [a, b], [a, b | T]
  • compound terms and operators: f(a, b), a + b * c, (a, b) ; c, X is 2 * 3, X =.. [f, a], {G} (DCG goals)
  • clauses: head :- body. / fact. — with % and /* */ comments; DCG rules head --> body. are expanded via [dcg_rule]

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")
}

#
str

fn str(s : String) -> Term

A Prolog string (double-quoted text), e.g. str("hello").

#
variable

fn variable(name : String) -> Term

Allocates a fresh logic variable with the given display name.

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")
}