prolog

    A small Prolog interpreter in MoonBit: SLD resolution with iterative deepening, cut, negation-as-failure, arithmetic, and list built-ins.

    prolog
    logic-programming
    interpreter
    unification
    slg
    Download zip
    Author
    Version
    0.1.3
    License
    Apache-2.0
    Last updated
    10 days ago
    Downloads
    25

    Dependencies

    #Prolog in MoonBit

    A small, well-tested Prolog interpreter written in MoonBit. It parses Prolog programs, runs queries by SLD resolution, and returns answers as variable bindings.

    #Features

    • Terms: atoms, variables, integers, floats, strings, lists ([a, b|T]), and compound terms, with a parser for the standard operator set (:-, ;, ,, \+, =, \=, ==, \==, is, comparisons, arithmetic operators) and round-trip safe pretty printing.
    • Resolution: bounded depth-first search with iterative deepening (default — fair and terminating on finite programs), cut (!), and negation-as-failure (\+ / not). The engine is iterative, so deep searches use constant stack space.
    • Unification with occurs check.
    • Arithmetic (is/2): + - * / // mod rem abs min max sqrt, and the numeric comparisons =:= =\= < > =< >=.
    • Term inspection: var/1 nonvar/1 atom/1 integer/1 float/1 number/1string/1 atomic/1 compound/1 ground/1 is_list/1.
    • List built-ins: member/2, append/3, reverse/2, length/2, nth0/3, between/3.
    • Output: write/1 and nl/0, collected into the query result.

    #Quick start

    Parse a program and run a query:

    ///|
    test {
    let program_text =
    #|parent(john, mary).
    #|parent(mary, ann).
    #|parent(mary, tom).
    #|ancestor(X, Y) :- parent(X, Y).
    #|ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
    let p = Program::parse(program_text) catch { e => fail(e.message()) }
    assert_eq(p.query("ancestor(john, X)").answer_lines(), [
    "X = mary", "X = ann", "X = tom",
    ])
    }

    Unification, arithmetic, and list built-ins:

    ///|
    test {
    let p = Program::parse("") catch { e => fail(e.message()) }
    assert_eq(p.query("X = f(Y), Y = 2").answer_lines(), ["X = f(2), Y = 2"])
    assert_eq(p.query("X is 1 + 2 * 3").answer_lines(), ["X = 7"])
    assert_eq(p.query("append(X, Y, [1, 2])").answer_lines(), [
    "X = [], Y = [1, 2]", "X = [1], Y = [2]", "X = [1, 2], Y = []",
    ])
    }

    write/1 output is collected into the result:

    ///|
    test {
    let p = Program::parse("") catch { e => fail(e.message()) }
    let r = p.query("(X = 1; X = 2), write(X), nl")
    assert_eq(r.output, "1\n2\n")
    assert_eq(r.answer_lines(), ["X = 1", "X = 2"])
    }

    #Search semantics

    Queries run a bounded search (see Options). By default:

    • iterative deepening: the depth limit grows until a round completes without hitting a bound (or max_depth is reached), so terminating searches produce the same answers, in the same order, as classic depth-first Prolog, while left-recursive programs cannot loop forever;
    • at most max_steps inference steps and max_solutions answers per query; QueryResult::completion reports whether (and why) a bound cut the search space off.

    #Command line

    The cmd/prolog executable loads a program file (or --program text) and answers queries:

    $ moon run cmd/prolog -- --query "member(X, [a, b, c])" X = a X = b X = c

    Or interactively: pipe one query per line (a query may span lines until one ends with .), and halt. quits.

    #Known limitations

    • \+ searches within the current depth budget (bounded negation).
    • Open-list generation is bounded (see length/2, member/2).
    • The occurs check is always on, so X = f(X) fails.
    • Strings are terms, not lists of character codes.

    #License

    Apache-2.0

    PrologError

    pub suberror PrologError {
    Parse(String)
    Eval(String)
    } derive(
    Debug
    )

    Errors raised by the Prolog parser and evaluator.

    PrologError::message

    fn PrologError::message(self : PrologError) -> String

    The human-readable error message.

    Bindings

    pub struct Bindings {
    buckets : Array[Array[(String, Term)]]
    }

    Variable bindings produced by unification. Bindings are persistent: extending them creates a new value, so they can be snapshotted for backtracking safely. Lookups are hash-bucketed for speed.

    Bindings::apply

    fn Bindings::apply(self : Bindings, t : Term) -> Term

    Resolve a term through the bindings, chasing variable chains.

    Bindings::apply_deep

    fn Bindings::apply_deep(self : Bindings, t : Term) -> Term

    Resolve a term fully: dereference the term itself and every subterm.

    Bindings::empty

    fn Bindings::empty() -> Bindings

    Bindings::get

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

    Look up a variable's binding.

    Bindings::names

    fn Bindings::names(self : Bindings) -> Array[String]

    The names of the variables currently bound.

    Clause

    pub struct Clause {
    head : Term
    body : Term
    }

    A clause Head :- Body; facts have body true.

    Completion

    pub enum Completion {
    Complete
    Depth
    Steps
    Solutions
    IncompleteNegation
    } derive(Eq,
    Debug
    )

    Why a search stopped short of full exploration.

    Completion::equal

    fn Completion::equal(Completion, Completion) -> Bool

    Completion::not_equal

    fn Completion::not_equal(x : Completion, y : Completion) -> Bool

    Num

    An arithmetic value: integer or float.

    Num::equal

    fn Num::equal(Num, Num) -> Bool

    Num::not_equal

    fn Num::not_equal(x : Num, y : Num) -> Bool

    Num::to_repr

    Options

    pub struct Options {
    max_depth : Int
    ilds : Bool
    max_steps : Int
    max_solutions : Int
    }

    Search options.

    Options::make

    fn Options::make(max_depth : Int, ilds : Bool, max_steps : Int, max_solutions : Int) -> Options

    Build search options explicitly.

    Options::new

    fn Options::new() -> Options

    Default search options.

    Program

    pub struct Program {
    clauses : Map[String, Array[Clause]]
    }

    A Prolog program: parsed clauses indexed by predicate.

    Program::parse

    fn Program::parse(text : String) -> Program raise PrologError

    Parse a program: a sequence of clauses (facts and rules) ending in ..

    Program::query

    fn Program::query(self : Program, goal : String) -> QueryResult raise PrologError

    Run a query with the default search options.

    Program::query_with

    fn Program::query_with(self : Program, goal : String, opts : Options) -> QueryResult raise PrologError

    Run a query with explicit search options.

    Iterative deepening (default) re-runs bounded depth-first search with increasing depth limits until a round completes without hitting a bound (or max_depth is reached), then returns that round's solutions: the search is fair and terminates on finite programs, while complete searches yield the same answers, in the same order, as plain depth-first search. With ilds = false a single bounded depth-first pass is used. Either way the search is bounded by max_depth, max_steps, and max_solutions; when a bound cuts the search space off, the result's completion reports the bound.

    QueryResult

    pub struct QueryResult {
    solutions : Array[Bindings]
    vars : Array[String]
    output : String
    completion : Completion
    }

    The result of running a query.

    QueryResult::answer_lines

    fn QueryResult::answer_lines(self : QueryResult) -> Array[String]

    Format each solution as one line, e.g. X = 1, Y = f(2); a query without variables yields true.

    QueryResult::completion_name

    fn QueryResult::completion_name(self : QueryResult) -> String

    A short name for the completion reason: "complete", "depth", "steps", "solutions", or "incomplete-negation".

    QueryResult::is_complete

    fn QueryResult::is_complete(self : QueryResult) -> Bool

    True when the search explored everything reachable.

    Term

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

    A Prolog term.

    Term::equal

    fn Term::equal(Term, Term) -> Bool

    Term::free_vars

    fn Term::free_vars(self : Term) -> Array[String]

    Variables occurring in the term, in order of first occurrence.

    Term::is_list

    fn Term::is_list(self : Term) -> Bool

    Is this term a proper list?

    Term::not_equal

    fn Term::not_equal(x : Term, y : Term) -> Bool

    Term::parse

    fn Term::parse(text : String) -> Term raise PrologError

    Parse a single term.

    Term::to_repr

    Term::to_string

    fn Term::to_string(self : Term) -> String

    TokKind

    Lexer token kinds.

    TokKind::equal

    fn TokKind::equal(TokKind, TokKind) -> Bool

    TokKind::not_equal

    fn TokKind::not_equal(x : TokKind, y : TokKind) -> Bool

    TokKind::to_repr

    atom

    fn atom(name : String) -> Term

    Construct an atom term.

    compound

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

    Construct a compound term.

    cons

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

    Build a cons cell [Head | Tail].

    empty_list

    fn empty_list() -> Term

    The empty list [].

    float

    fn float(value : Double) -> Term

    Construct a float term.

    int

    fn int(value : Int) -> Term

    Construct an integer term.

    list

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

    Build a proper list [a, b, c] from elements.

    str

    fn str(value : String) -> Term

    Construct a string term.

    variable

    fn variable(name : String) -> Term

    Construct a variable term.