Term and goal expansion
Logtalk supports a term and goal expansion mechanism that can be used to define source-to-source transformations. Two common uses are the definition of language extensions and domain-specific languages.
Logtalk improves upon the term-expansion mechanism found on some Prolog systems by providing the user with fine-grained control on if, when, and how expansions are applied. It allows declaring in a source file itself which expansions, if any, will be used when compiling it. It allows declaring which expansions will be used when compiling a file using compile and loading predicate options. It also allows defining a default expansion for all source files. It defines a concept of hook objects that can be used as building blocks to create custom and reusable expansion workflows with explicit and well-defined semantics. It prevents the simple act of loading expansion rules affecting subsequent compilation of files. It prevents conflicts between groups of expansion rules of different origins. It avoids a set of buggy expansion rules from breaking other sets of expansion rules.
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. Note that, unlike Prolog systems also providing these two predicates, they are not declared as multifile predicates in the protocol. This design decision is key for giving the programmer full control of the expansion process and preventing the problems inflicting most of the Prolog systems that provide a term-expansion mechanism.
An example of an object defining expansion rules:
:- 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 or called automatically by the compiler when compiling a source file (see the section below on hook objects).
In the case of source files referenced in include/1
directives, expansions are only applied automatically when the directives are
found in source files, not when used as arguments in the create_object/4,
create_protocol/3, and create_category/4,
predicates. This restriction prevents inconsistent results when, for example,
an expansion is defined for a predicate with clauses found in both an included
file and as argument in a call to the create_object/4
predicate.
Clauses for the term_expansion/2
predicate are called until one of them
succeeds. The returned expansion can be a single term or a list of terms
(including the empty list). 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 was not the result 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.
Goal-expansion applies to goal arguments of control constructs, meta-arguments
in built-in or user
defined meta-predicates, meta-arguments in local
user-defined meta-predicates, meta-arguments in meta-predicate messages when
static binding is possible, and initialization/1
, if/1
, and elif/1
directives.
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.
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 when using hook objects (discussed next).
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. Hook objects must be compiled and loaded prior to being used to expand a source file.
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).
To use multiple hook objects in the same source file, simply write each directive before the block of code that it should handle. For example:
:- object(h1,
implements(expanding)).
term_expansion((:- public(a/0)), (:- public(b/0))).
term_expansion(a, b).
:- end_object.
:- object(h2,
implements(expanding)).
term_expansion((:- public(a/0)), (:- public(c/0))).
term_expansion(a, c).
:- end_object.
:- set_logtalk_flag(hook, h1).
:- object(s1).
:- public(a/0).
a.
:- end_object.
:- set_logtalk_flag(hook, h2).
:- object(s2).
:- public(a/0).
a.
:- end_object.
| ?- {h1, h2, s}.
...
| ?- s1::b.
yes
| ?- s2::c.
yes
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.
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.
Virtual source file terms and loading context
When using a 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 language 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 (with the exception of the
initialization/1
, if/1
, and elif/1
directives, where the goal
argument is expanded to improve code portability across backends).
Default compiler expansion workflow
When compiling a source file,
the compiler will first try, by default,
the source file-specific hook object specified using a local
set_logtalk_flag/2
directive, if defined. If that expansion fails,
it tries the hook object specified using the hook/1
compiler option
in the logtalk_compile/2
or logtalk_load/2
goal that compiles
or loads the file, if defined. If that expansion fails, it tries the
default hook object, if defined. If that expansion also fails, the
compiler tries the Prolog dialect-specific expansion rules found
in the adapter file (which are used to support non-standard
Prolog features).
User defined expansion workflows
Sometimes we have multiple hook objects that we need to combine and use in the compilation of a source file. Logtalk includes a hook_flows library that supports two basic expansion workflows: a pipeline of hook objects, where the expansion results from a hook object are fed 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. There is also a hook_objects library that provides convenient hook objects for defining custom expansion workflows. This library includes a hook object that can be used to restore the default expansion workflow used by the compiler.
For example, assuming that you want to apply the Prolog backend-specific expansion rules defined in its adapter file, using the backend_adapter_hook library object, passing the resulting terms to your own expansion when compiling a source file, we could use the goal:
| ?- logtalk_load(
source,
[hook(hook_pipeline([backend_adapter_hook, my_expansion]))]
).
As a second example, we can prevent expansion of a source file using the library object identity_hook by adding as the first term in a source file the directive:
:- set_logtalk_flag(hook, identity_hook).
The file will be compiled as-is as any hook object (specified as a compiler option or as a default hook object) and any backend adapter expansion rules are overridden by the directive.
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. When defining a custom workflow, the library object
prolog_module_hook/1 can be used as a
workflow step. For example, assuming a module functions
defining expansion
rules that we want to use:
| ?- logtalk_load(
source,
[hook(hook_set([prolog_module_hook(functions), my_expansion]))]
).
But note that Prolog module libraries may provide definitions of the expansion
predicates that are not compatible with the Logtalk compiler. In particular,
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 by some of the
supported backend Prolog compilers.
Debugging expansions
The term_expansion/2
and goal_expansion/2
predicates can be
debugged like any other object predicates. Note
that expansions can often be manually tested by sending
expand_term/2 and expand_goal/2
messages to a hook object with the term or goal whose expansion you want to
check as argument. An alternative to the debugging tools is to use a
monitor for the runtime messages that call the predicates. For example,
assume a expansions_debug.lgt
file with the contents:
:- initialization(
define_events(after, edcg, _, _, expansions_debug)
).
:- object(expansions_debug,
implements(monitoring)).
after(edcg, term_expansion(T,E), _) :-
writeq(term_expansion(T,E)), nl.
:- end_object.
We can use this monitor to help debug the expansion rules of the
edcg library when applied to the edcgs
example using
the queries:
| ?- {expansions_debug}.
...
| ?- set_logtalk_flag(events, allow).
yes
| ?- {edcgs(loader)}.
...
term_expansion(begin_of_file,begin_of_file)
term_expansion((:-object(gemini)),[(:-object(gemini)),(:-op(1200,xfx,-->>))])
term_expansion(acc_info(castor,A,B,C,true),[])
term_expansion(pass_info(pollux),[])
term_expansion(pred_info(p,1,[castor,pollux]),[])
term_expansion(pred_info(q,1,[castor,pollux]),[])
term_expansion(pred_info(r,1,[castor,pollux]),[])
term_expansion((p(A)-->>B is A+1,q(B),r(B)),(p(A,C,D,E):-B is A+1,q(B,C,F,E),r(B,F,D,E)))
term_expansion((q(A)-->>[]),(q(A,B,B,C):-true))
term_expansion((r(A)-->>[]),(r(A,B,B,C):-true))
term_expansion(end_of_file,end_of_file)
...
This solution does not require compiling the edcg
hook object in debug
mode or access to its source code (e.g., to modify its expansion rules to
emit debug messages). We could also simply use the user
pseudo-object
as the monitor object:
| ?- assertz((
after(_, term_expansion(T,E), _) :-
writeq(term_expansion(T,E)), nl
)).
yes
| ?- define_events(after, edcg, _, Sender, user).
yes
Another alternative is to use a pipeline of hook objects with the library
hook_pipeline/1
and write_to_stream_hook
objects to write the
expansion results to a file. For example, using the unique.lgt
test
file from the edcgs
library directory:
| ?- {hook_flows(loader), hook_objects(loader)}.
...
| ?- open('unique_expanded.lgt', write, Stream),
logtalk_compile(
unique,
[hook(hook_pipeline([edcg,write_to_stream_hook(Stream,[quoted(true)])]))]
),
close(Stream).
...
The generated unique_expanded.lgt
file will contain the clauses resulting
from the expansion of the EDCG rules found in the unique.lgt
file by the
edcg
hook object expansion.