@jan pointed out where the magic is happening. There are two useful predicates for mapping variable names in source to the actual variables they become after parsing so they can be used in goal or term expansion: var_property/2 and prolog_load_context/2. I ended up using prolog_load_context/2 in the implementation of my templating engine.
For context, I wanted something very much like quasi quotations but I didnāt want the user to have to explicitly list the variables used in the quotation since the syntax of the quotation could fish them out automatically:
% Test.pl
% Here's an example rule the user might write which checks if
% there is a found item, and prints it out
test :- htnMethod(
if([findItem(X)]),
do([sayText("I found item [X]")])
).
When loaded, the sayText/1
predicate is converted to a sayList/1
predicate which is the result of parsing the string and converting it to something that can be executed when test() is run (with a warning that will be explained further down).
In the next code, notice that findItem(A)
and sayCommand(A)
both use the same variable (A
), which was the original goal I was trying to achieve and am now able to via prolog_load_context/2:
?- consult("Test.pl").
Warning: Test.pl:3:
Warning: Singleton variables: [X]
true.
?- listing(test).
test :-
htnMethod(if([findItem(A)]),
do([sayList([sayString("I found item "),
sayCommand(A)])])).
true.
Here is the full code for this example:
% TermExpansion.pl
goal_expansion(htnMethod(if(IfList), do(DoList)), Replacement) :-
% For performance reasons: do a quick check to see if
% any conversion is needed
nonvar(DoList),
once((
member(X, DoList),
nonvar(X),
X = sayText(_)
)),
!,
% Get the source code variable names and values from the source
% to ensure that the converted code binds to them
prolog_load_context(variable_names, SourceVariables),
% Transform any terms of the form sayText(_) to sayList([...])
convertDoList(SourceVariables, DoList, [], NewDoList),
Replacement = htnMethod(if(IfList), do(NewDoList)).
% SayLanguage.pl
:- use_module(library(dcg/basics)).
% DCG Parser for simple "say" language
%
% The language converts a string like "This is a [Value], you see!"
% into:
% - sayText("String") for all the parts outside of any [] and
% - sayCommand(Term) for any term within a [].
% All variables within Term are unified with variables
% in SourceVariables('Name' = Variable, ...)
% so that they can be used in term expansion and
% match the variables in the source by name
%
% e.g. sayLanguageToList(
% ['Value'=Value],
% "This is a [Value], you see!",
% [sayText("This is a "),
% sayCommand(Value),
% sayText(", you see!")]).
sayLanguageToList(SourceVariables, Text, List) :-
atom_codes(Text, Codes),
phrase(values(List, SourceVariables), Codes).
% "values" handles any sequence of one or more "value"
% (described below)
values([Value|Values], SourceVariables) -->
value(Value, SourceVariables),
values(Values, SourceVariables),
!.
values([Value], SourceVariables) -->
value(Value, SourceVariables).
values([], _) -->
[].
% a "value" is is a text string or [prologTerm]
% First handle a text string:
value(sayString(String), _) -->
string_without([91], Codes),
{
Codes \== [],
string_codes(String, Codes)
}.
% Then handle any Prolog term surrounded by []
value(sayCommand(Term), SourceVariables) -->
[91], string_without([93], Codes), [93],
{
read_term_from_codes(Codes, Term, [variable_names(Vars)]),
% Make sure any variables that exist in source outside
% this code are used if they have the same name
assignTo(Vars, SourceVariables)
}.
% Unify variables in the list ['Name'=OriginalVar, ...] to
% a new variable from the source in the list ['Name'=SourceVar, ...]
% I couldn't figure out a way to do side-effecting
% variable assignments using bagof, findall, foreach, etc
% but recursion worked so:
assignTo([], _).
assignTo([Name=OrigVar | Rest], ListOfAssignments) :-
member(Name=AssignedVar, ListOfAssignments),
OrigVar = AssignedVar,
assignTo(Rest, ListOfAssignments).
Finally, the reason why you get a warning like this:
?- consult("Test.pl").
Warning: Test.pl:3:
Warning: Singleton variables: [X]
true.
is that the parser doesnāt check to see if the goal expansion includes the variables from the source so it thinks it is a singleton. The user can work around it by naming the variable starting with _
followed by a number and then any text like this:
% Test.pl, now with no warnings!
test :- htnMethod(
if([findItem(_1X)]),
do([sayText("I found item [_1X]")])
).
...
?- consult("Test.pl.pl").
true.
?- listing(test).
test :-
htnMethod(if([findItem(A)]),
do([sayList([sayString("I found item "),
sayCommand(A)])])).
true.
Thanks for all the help!