Term and goal expansion

Logtalk supports the term and goal expansion mechanism also found in some Prolog systems. This macro mechanism is used to define source-to-source transformations. Two common uses are the definition of language extensions and domain-specific languages.

Defining expansions

Term and goal expansions are defined using, respectively, the predicates term_expansion/2 and goal_expansion/2, which are declared in the expanding built-in protocol. For example:

:- object(an_object,
    implements(expanding)).

    term_expansion(ping, pong).
    term_expansion(
        colors,
        [white, yellow, blue, green, read, black]
    ).

    goal_expansion(a, b).
    goal_expansion(b, c).
    goal_expansion(X is Expression, true) :-
        catch(X is Expression, _, fail).

:- end_object.

These predicates can be explicitly called using the expand_term/2 and expand_goal/2 built-in methods.

Clauses for the term_expansion/2 predicate are called until of them succeeds. The returned expansion can be a single term or a list of terms. For example:

| ?- an_object::expand_term(ping, Term).

Term = pong
yes

| ?- an_object::expand_term(colors, Colors).

Colors = [white, yellow, blue, green, read, black]
yes

When no term_expansion/2 clause applies, the same term that we are trying to expand is returned:

| ?- an_object::expand_term(sounds, Sounds).

Sounds = sounds
yes

Clauses for the goal_expansion/2 predicate are recursively called on the expanded goal until a fixed point is reached. For example:

| ?- an_object::expand_goal(a, Goal).

Goal = c
yes

| ?- an_object::expand_goal(X is 3+2*5, Goal).

X = 13,
Goal = true
yes

When no goal_expansion/2 clause applies, the same goal that we are trying to expand is returned:

| ?- an_object::expand_goal(3 =:= 5, Goal).

Goal = (3=:=5)
yes

The goal-expansion mechanism prevents an infinite loop when expanding a goal by checking that a goal to be expanded does not resulted from a previous expansion of the same goal. For example, consider the following object:

:- object(fixed_point,
    implements(expanding)).

    goal_expansion(a, b).
    goal_expansion(b, c).
    goal_expansion(c, (a -> b; c)).

:- end_object.

The expansion of the goal a results in the goal (a -> b; c) with no attempt to further expand the a, b, and c goals as they have already been expanded.

Term and goal expansion predicates can also be used when compiling a source file as described below.

Expanding grammar rules

A common term expansion is the translation of grammar rules into predicate clauses. This transformation is performed automatically by the compiler when a source file entity defines grammar rules. It can also be done explicitly by calling the expand_term/2 built-in method. For example:

| ?- logtalk::expand_term((a --> b, c), Clause).

Clause = (a(A,B) :- b(A,C), c(C,B))
yes

Note that the default translation of grammar rules can be overridden by defining clauses for the term_expansion/2 predicate.

Hook objects

Term and goal expansion of a source file during its compilation is performed by using hook objects. A hook object is simply an object implementing the expanding built-in protocol and defining clauses for the term and goal expansion hook predicates.

To compile a source file using a hook object, we can use the hook compiler flag in the second argument of the logtalk_compile/2 and logtalk_load/2 built-in predicates. For example:

| ?- logtalk_load(source_file, [hook(hook_object)]).
...

In alternative, we can use a set_logtalk_flag/2 directive in the source file itself. For example:

:- set_logtalk_flag(hook, hook_object).

It is also possible to define a default hook object by defining a global value for the hook flag by calling the set_logtalk_flag/2 predicate. For example:

| ?- set_logtalk_flag(hook, hook_object).

yes

Note that, due to the set_logtalk_flag/2 directive being local to a source, file, using it to specify a hook object will override any defined default hook object or any hook object specified as a logtalk_compile/2 or logtalk_load/2 predicate compiler option for compiling or loading the source file.

When compiling a source file, the compiler will first try the source file specific hook object, if defined. If that fails, it tries the default hook object, if defined. If that also fails, the compiler tries the Prolog dialect specific expansion predicate definitions if defined in the adapter file.

Note

Clauses for the term_expansion/2 and goal_expansion/2 predicates defined within an object or a category are never used in the compilation of the object or the category itself.

When using an hook object to expand the terms of a source file, two virtual file terms are generated: begin_of_file and end_of_file. These terms allow the user to define term-expansions before and after the actual source file terms.

Logtalk also provides a logtalk_load_context/2 built-in predicate that can be used to access the compilation/loading context when performing expansions. The logtalk built-in object also provides a set of predicates that can be useful, notably when adding Logtalk support for languages extensions originally developed for Prolog.

As an example of using the virtual terms and the logtalk_load_context/2 predicate, assume that you want to convert plain Prolog files to Logtalk by wrapping the Prolog code in each file using an object (named after the file) that implements a given protocol. This could be accomplished by defining the following hook object:

:- object(wrapper(_Protocol_),
    implements(expanding)).

    term_expansion(begin_of_file, (:- object(Name,implements(_Protocol_)))) :-
        logtalk_load_context(file, File),
        os::decompose_file_name(File,_ , Name, _).

    term_expansion(end_of_file, (:- end_object)).

:- end_object.

Assuming e.g. my_car.pl and lease_car.pl files to be wrapped and a car_protocol protocol, we could then load them using:

| ?- logtalk_load(
         ['my_car.pl', 'lease_car.pl'],
         [hook(wrapper(car_protocol))]
     ).

yes

Note

When a source file also contains plain Prolog directives and predicates, these are term-expanded but not goal-expanded.

Bypassing expansions

Terms and goals wrapped by the {}/1 control construct are not expanded. For example:

| ?- an_object::expand_term({ping}, Term).

Term = {ping}
yes

| ?- an_object::expand_goal({a}, Goal).

Goal = {a}
yes

This also applies to source file terms and source file goals.

Combining multiple expansions

Sometimes we have multiple hook objects that we need to use in the compilation of a source file. The Logtalk library includes support for two basic expansion workflows: a pipeline of hook objects, where the expansion results from a hook object are feed to the next hook object in the pipeline, and a set of hook objects, where expansions are tried until one of them succeeds. These workflows are implemented as parametric objects allowing combining them to implement more sophisticated expansion workflows.

Using Prolog defined expansions

In order to use clauses for the term_expansion/2 and goal_expansion/2 predicates defined in plain Prolog, simply specify the pseudo-object user as the hook object when compiling source files. When using backend Prolog compilers that support a module system, it can also be specified a module containing clauses for the expanding predicates as long as the module name doesn’t coincide with an object name. But note that Prolog module libraries may provide definitions of the expansion predicates that are not compatible with the Logtalk compiler. Specially when setting the hook object to user, be aware of any Prolog library that is loaded, possibly by default or implicitly by the Prolog system, that may be contributing definitions of the expansion predicates. It is usually safer to define a specific hook object for combining multiple expansions in a fully controlled way.

Note

The user object declares term_expansion/2 and goal_expansion/2 as multifile and dynamic predicates. This helps in avoiding predicate existence errors when compiling source files with the hook flag set to user as these predicates are only natively declared in some of the supported backend Prolog compilers.