predicates!() { /* proc-macro */ }
Expand description

Define foreign predicates written in rust for use in prolog.

The predicates! macro takes an arbitrary amount of predicate definitions. These definitions may be semidet or nondet. Optionally, a visibility specifier like pub may be used to change the visibility of the generated functions. These definitions look somewhat like ordinary rust functions. However, their argument list is completely untyped, as each argument is known to be a &Term, except for the first argument which is a context object. As there always needs to be a context to call a predicate, this first argument is required, even if it is unused.

For each definition, a registration function will be generated. This function will be named register_<name>, where name is the name of the defined predicate, and this function will take zero arguments. After calling this function, the predicate is registered and may be used from prolog code.

Each definition may optionally be annotated with #[module("<module>")] and/or #[name("<name>")] To change the module this predicate will be registered in, or the name of the predicate in prolog. By default, the predicate name will be the definition name, and the module will be the context module at the time of generation. For foreign libraries, this context module is whatever module did the first import of this library. Otherwise it’s usually ‘user’.

Semideterministic predicates

The first kind of predicate that you can define is a semidet predicate. Semidet, or semideterministic, means that this predicate is only going to have one result, and it could either be success or failure. Note that this also covers the deterministic case - to implement a deterministic predicate, just ensure that your predicate does not fail.

Semidet predicates return a PrologResult<()>, which also happens to be the type returned by most of the functions in the swipl library. This means you can easily handle failure and exception of things you call using rust’s ? syntax, or make use of the various combinators that are defined on context objects and in the result module.

Examples

predicates! {
    semidet fn unify_with_foo(context, term) {
        let atom = context.new_atom("foo");
        term.unify(&atom)
    }

    #[module("some_module")]
    #[name("some_alternate_name")]
    pub semidet fn term_is_42(_context, term) {
        let num: u64 = term.get::<u64>()?;

        into_prolog_result(num == 42)
    }

    semidet fn just_fail(_context) {
        Err(PrologError::Failure)
    }

    pub semidet fn throw_if_not_42(_context, term) {
        let num: u64 = term.get::<u64>()?;
        if num != 42 {
            context.raise_exception(&term!{context: error(this_is_not_the_answer, _)})
        } else {
            Ok(())
        }
    }
}

To register these defined predicates, their register function has to be called:

register_unify_with_foo();
register_term_is_42();
register_just_fail();
register_throw_if_not_42();

Nondeterministic predicates

Nondet or nondeterministic predicates are a bit more complex to implement. Instead of just one block which returns success or failure, nondet predicates are implemented with two bodies, a setup block and a call block.

In the setup block, you create a state object which will be available in the call block. The call block is then called with this state object. As long as the call block returns true, the predicate call is considered to still have choice points and will be called unless the caller does a cut, which will clean up the state object automatically.

The state type

Nondeterministic predicate definitions require you to specify a type argument as part of the function signature. This specifies the type of the state object, and is required to implement the auto-traits Send and Unpin.

Setup

The setup block is called at the start of a predicate invocation. It is to return a PrologResult<Option<StateObject>>, where StateObject is your state object type.

You can return from this block in three ways:

  • Return an exception or failure. The predicate will error or fail accordingly and the call block will not be invoked.
  • Return None. The call block will also not be invoked, but the predicate will return success. This is useful to handle predicate inputs which allow your predicate to behave in a semidet manner.
  • Return Some(object). This returns a state object for use in the call block. After this, the call block will be invoked.

Call

The call block is called each time the next result is required from this predicate. This happens on the first call to this predicate (except if the setup returned early as described above), and subsequently upon backtracking. The call block is given a mutable borrow of the state object, and is therefore able to both inspect and modify it.

you can return from this block in three ways:

  • Return an exception or failure. Thep redicate will error or fail accordingly, and the call block will not be invoked again.
  • Return false, signaling that this was the last succesful call to this predicate.
  • Return true, signaling that there’s more results available upon backtracking.

After exception, failure or returning false to signal the last succesful call, the state object will be cleaned up automatically.

Examples

predicates!{
    nondet fn unify_with_bar_baz<Vec<String>>(context, term) {
        setup => {
            Ok(Some(vec!["bar", "baz"]))
        },
        call(v) => {
            let next = v.pop().unwrap();
            let atom = context.new_atom(next);
            term.unify(&atom)?;

            Ok(!v.is_empty())
        }
    }

    nondet fn fail_early<()>(_context) {
        setup => {
            Err(PrologError::Failure)
        },
        call(_) => {
            // We never get here
        }
    }

    nondet fn succeed_early<()>(_context) {
        setup => {
            Ok(None)
        },
        call(_) => {
            // We never get here
        }
    }
}