View source with raw comments or as raw
    1:- module(macros,
    2          [ macro_position/1,           % -Position
    3                                        % private
    4            expand_macros/5,            % +M, +In, -Out, +P0, -P
    5            include_macros/3,           % +M,+Macro,-Expanded
    6            op(10, fx, #)
    7          ]).    8:- use_module(library(terms)).    9:- use_module(library(error)).   10:- use_module(library(lists)).

Macro expansion

This library defines a macro expansion mechanism that operates on arbitrary terms. Unlike term_expansion/2 and goal_expansion/2, a term is explicitly designed for expansion using the term #(Macro). Macros are first of all intended to deal with compile time constants. They can also be used to construct terms at compile time.

Defining and using macros

Macros are defined for the current module using one of the three constructs below.

#define(Macro, Replacement).
#define(Macro, Replacement) :- Code.
#import(ModuleFile).

Macro is a callable term, not being define(_,_), or import(_). Replacement is an arbitrary Prolog term. Code is a Prolog body term that must succeed and can be used to dynamically generate (parts of) Replacement.

The `#import(ModuleFile)` definition makes all macros from the given module available for expansion in the module it appears. Normally this shall be appear after local macro definitions.

A macro is called using the term #(Macro). # is defined as a low-priority (10) prefix operator to allow for `#Macro`. Macros can appear at the following places:

Macros can not appear as name of a compound or tag of a dict. A term `#Macro` appearing in one of the allowed places must have a matching macro defined, i.e., `#Macro` is always expanded. An error is emitted if the expansion fails. Macro expansion is applied recursively and thus, macros may be passed to macro arguments and macro expansion may use other macros.

Macros are matched to terms using Single Sided Unification (SSU), implemented using Head => Body rules. This implies that the matching never instantiates variables in the term that is being expanded.

Below are some examples. The first line defines the macro and the indented line after show example usage of the macro.

#define(max_width, 100).
    W < #max_width

#define(calc(Expr), Value) :- Value is Expr.
    fact(#calc(#max_width*2)).

#define(pt(X,Y), point{x:X, y:Y}).
    reply_json(json{type:polygon,
                    points:[#pt(0,0), #pt(0,5), #pt(5,0)]}).

Macro expansion expands terms #(Callable). If the argument to the #-term is not a callable, the #-term is not modified. This notably allows for #(Var) as used by library(clpfd) to indicate that a variable is constraint to be an (clp(fd)) integer.

Implementation details

A macro `#define(Macro, Expanded) :- Body.` is, after some basic sanity checks, translated into a rule

'$macro'(Macro, Var), Body => Var = Expanded.

The `#import(File)` is translated into :- use_module(File, []) and a link clause that links the macro expansion from the module defined in File to the current module.

Macro expansion is realised by creating a clause for term_expansion/2 in the current module. This clause results from expanding the first `#define or #import definition. Thus, if macros are defined before any other local definition for term_expansion/2 it is executed as the first step. The macro expansion fails if no macros were encounted in the term, allowing other term_expansion rules local to the module to take effect. In other words, a term holding macros is not subject to any other term expansion local to the module. It is subject to term expansion defined in module user and system` that is performed after the local expansion is completed.

Predicates

  107define_macro((#define(From, To)), Clauses) =>
  108    valid_macro(From),
  109    Clause0 = ('$macro'(From, Expansion) => Expansion = To),
  110    prepare_module(Clause0, Clauses).
  111define_macro((#define(From, To) :- Cond), Clauses) =>
  112    valid_macro(From),
  113    Clause0 = ('$macro'(From, Expansion), Cond => Expansion = To),
  114    prepare_module(Clause0, Clauses).
  115define_macro((#import(File)), Clauses) =>
  116    use_module(File, []),
  117    source_file_property(File, module(M)),
  118    Clause0 = ('$macro'(Macro, Expansion), include_macros(M, Macro, Expansion)
  119                  => true),
  120    prepare_module(Clause0, Clauses).
  121
  122define_macro(_, _) =>
  123    fail.
  124
  125valid_macro(Macro), reserved_macro(Macro) =>
  126    domain_error(macro, Macro).
  127valid_macro(Macro), callable(Macro) =>
  128    true.
  129valid_macro(Macro), is_dict(Macro) =>
  130    true.
  131valid_macro(_Macro) =>
  132    fail.
  133
  134reserved_macro(define(_,_)) => true.
  135reserved_macro(import(_)) => true.
  136reserved_macro(_) => fail.
  137
  138:- multifile
  139    error:has_type/2.  140
  141error:has_type(macro, Term) :-
  142    (   callable(Term)
  143    ->  true
  144    ;   is_dict(Term)
  145    ),
  146    \+ reserved_macro(Term).
  147
  148prepare_module(Clause0, Clauses) :-
  149    prolog_load_context(module, M),
  150    (   is_prepared_module(M)
  151    ->  Clauses = Clause0
  152    ;   Clauses = [ (:- multifile(('$macro'/2,term_expansion/4))),
  153                    (term_expansion(In, PIn, Out, Pout) :-
  154                        expand_macros(M, In, Out, PIn, Pout)),
  155                    expand_macros,
  156                    Clause0
  157                  ]
  158    ).
  159
  160is_prepared_module(M) :-
  161    current_predicate(M:expand_macros/0),
  162    \+ predicate_property(M:expand_macros, imported_from(_)).
 include_macros(+M, +Macro, -Expanded) is semidet
Include macros from another module. This predicate is a helper for `#import(File)`. It calls '$macro'/2 in M, but fails silently in case Macro is not defined in M as it may be defined in another imported macro file or further down in the current file.
  171include_macros(M, Macro, Expanded) :-
  172    catch(M:'$macro'(Macro, Expanded),
  173          error(existence_error(matching_rule,
  174                                M:'$macro'(Macro,_)),_),
  175          fail).
 expand_macros(+Module, +TermIn, -TermOut, +PosIn, -PosOut) is semidet
Perform macro expansion on TermIn with layout PosIn to produce TermOut with layout PosOut. The transformation is performed if the current load context module is Module (see prolog_load_context/2).

This predicate is not intended for direct usage.

  185expand_macros(M, T0, T, P0, P) :-
  186    prolog_load_context(module, M),
  187    \+ is_define(T0),
  188    expand_macros(M, T0, T, P0, P, _State0, _State),
  189    T \== T0.
  190
  191is_define(#Macro), reserved_macro(Macro) => true.
  192is_define((#Macro :- _)), reserved_macro(Macro) => true.
  193is_define(_) => fail.
  194
  195:- meta_predicate
  196    foldsubterms_pos(6, +, -, +, -, +, -).  197
  198expand_macros(M, T0, T, P0, P, State0, State) :-
  199    foldsubterms_pos(expand_macro(M), T0, T, P0, P, State0, State).
  200
  201expand_macro(M, #Macro, T, P0, P, State0, State) =>
  202    valid_macro(Macro),
  203    arg_pos(1, P0, P1),
  204    call_macro(M, Macro, Expanded, P1, P2),
  205    expand_macros(M, Expanded, T, P2, P, State0, State).
  206expand_macro(_, \#(T0), T, P0, P, State0, State) =>
  207    arg_pos(1, P0, P),
  208    T = T0, State = State0.
  209expand_macro(_, _, _, _, _, _, _) =>
  210    fail.
  211
  212call_macro(M, Macro, Expanded, P0, P) :-
  213    b_setval('$macro_position', P0),
  214    catch(M:'$macro'(Macro, Expanded),
  215          error(existence_error(matching_rule, _), _),
  216          macro_failed(Macro, P0)),
  217    fix_pos_shape(Macro, Expanded, P0, P),
  218    b_setval('$macro_position', 0).
  219
  220macro_failed(Macro, TermPos) :-
  221    macro_error_position(TermPos, Pos),
  222    throw(error(existence_error(macro, Macro), Pos)).
  223
  224macro_error_position(TermPos, Position) :-
  225    macro_position(TermPos, AtMacro),
  226    !,
  227    prolog_load_context(stream, Input),
  228    stream_position_to_position_term(Input, AtMacro, Position).
  229macro_error_position(_, _).
  230
  231stream_position_to_position_term(Stream, StreamPos,
  232                                 stream(Stream, Line, LinePos, CharNo)) :-
  233    stream_position_data(line_count, StreamPos, Line),
  234    stream_position_data(line_position, StreamPos, LinePos),
  235    stream_position_data(char_count, StreamPos, CharNo).
 macro_position(-Position) is det
True when Position is the position of the macro. Position is a term File:Line:LinePos. If File is unknown it is unified with -. If Line and/or LinePos are unknown they are unified with 0. This predicate can be used in the body of a macro definition to provide the source location. The example below defines `#pp(Var)` to print a variable together with the variable name and source location.
#define(pp(Var), print_message(debug, dump_var(Pos, Name, Var))) :-
    (   var_property(Var, name(Name))
    ->  true
    ;   Name = 'Var'
    ),
    macro_position(Pos).

:- multifile prolog:message//1.
prolog:message(dump_var(Pos,Name,Var)) -->
    [ url(Pos), ': ',
      ansi([fg(magenta),bold], '~w', [Name]), ' = ',
      ansi(code, '~p', [Var])
    ].
  262macro_position(File:Line:LinePos) :-
  263    prolog_load_context(file, File),
  264    !,
  265    (   b_getval('$macro_position', TermPos),
  266        macro_position(TermPos, StreamPos)
  267    ->  stream_position_data(line_count, StreamPos, Line),
  268        stream_position_data(line_position, StreamPos, LinePos)
  269    ;   Line = 0,
  270        LinePos = 0
  271    ).
  272macro_position((-):0:0).
  273
  274macro_position(TermPos, AtMacro) :-
  275    compound(TermPos),
  276    arg(1, TermPos, MacroStartCharCount),
  277    integer(MacroStartCharCount),
  278    prolog_load_context(stream, Input),
  279    stream_property(Input, reposition(true)),
  280    stream_property(Input, position(Here)),
  281    prolog_load_context(term_position, ClauseStart),
  282    stream_position_data(char_count, ClauseStart, ClauseStartCharCount),
  283    MacroStartCharCount >= ClauseStartCharCount,
  284    $,
  285    set_stream_position(Input, ClauseStart),
  286    Skip is MacroStartCharCount - ClauseStartCharCount,
  287    forall(between(1, Skip, _), get_char(Input, _)),
  288    stream_property(Input, position(AtMacro)),
  289    set_stream_position(Input, Here).
 fix_pos_shape(+TermIn, +TermOut, +PosIn, -PosOut) is det
Fixup PosIn to be a position term that is compatible to Term.
bug
- This predicate is largely unimplemented.
  297fix_pos_shape(_, _, P0, _), var(P0) =>
  298    true.
  299fix_pos_shape(_, V, P0, P),
  300    atomic(V),
  301    compound(P0), compound_name_arity(P0, _, Arity), Arity >= 2 =>
  302    P = F-T,
  303    arg(1, P0, F),
  304    arg(2, P0, T).
  305fix_pos_shape(_, _, P0, P) =>
  306    P = P0.
 foldsubterms_pos(:Goal, +TermIn, -TermOut, +PosIn, -PosOut, +State0, -State) is det
As foldsubterms/5, but also transforms the layout term. This predicate may later be moved to e.g. library(prolog_code) to make it publically available.
  315foldsubterms_pos(Goal, Term1, Term2, P1, P2, State0, State) :-
  316    call(Goal, Term1, Term2, P1, P2, State0, State),
  317    !.
  318foldsubterms_pos(Goal, Term1, Term2, P1, P2, State0, State) :-
  319    is_dict(Term1),
  320    !,
  321    pos_parts(dict, P1, P2, VPos1, VPos2),
  322    dict_pairs(Term1, Tag, Pairs1),
  323    fold_dict_pairs(Pairs1, Pairs2, VPos1, VPos2, Goal, State0, State),
  324    dict_pairs(Term2, Tag, Pairs2).
  325foldsubterms_pos(Goal, Term1, Term2, P1, P2, State0, State) :-
  326    nonvar(Term1), Term1 = [_|_],       % [] is not a list
  327    !,
  328    pos_parts(list, P1, P2, list(Elms1,Tail1), list(Elms2,Tail2)),
  329    fold_list(Term1, Term2, Elms1, Elms2, Tail1, Tail2, Goal, State0, State).
  330foldsubterms_pos(Goal, Term1, Term2, P1, P2, State0, State) :-
  331    compound(Term1),
  332    !,
  333    pos_parts(compound, P1, P2, ArgPos1, ArgPos2),
  334    same_functor(Term1, Term2, Arity),
  335    foldsubterms_(1, Arity, Goal, Term1, Term2, ArgPos1, ArgPos2, State0, State).
  336foldsubterms_pos(_, Term, Term, P, P, State, State).
  337
  338:- det(fold_dict_pairs/7).  339fold_dict_pairs([], [], KVPos, KVPos, _, State, State).
  340fold_dict_pairs([K0-V0|T0], [K-V|T1], KVPos0, KVPos, Goal, State0, State) :-
  341    (   nonvar(KVPos0),
  342        selectchk(key_value_position(F,T,SF,ST,K0,KP0,VP0), KVPos0,
  343                  key_value_position(F,T,SF,ST,K, KP, VP),  KVPos1)
  344    ->  true
  345    ;   true
  346    ),
  347    foldsubterms_pos(Goal, K0, K, KP0, KP, State0, State1),
  348    foldsubterms_pos(Goal, V0, V, VP0, VP, State1, State2),
  349    fold_dict_pairs(T0, T1, KVPos1, KVPos, Goal, State2, State).
  350
  351:- det(fold_list/9).  352fold_list(Var0, Var, EP, EP, TP0, TP, Goal, State0, State) :-
  353    var(Var0),
  354    !,
  355    foldsubterms_pos(Goal, Var0, Var, TP0, TP, State0, State).
  356fold_list([], [], [], [], TP, TP, _, State, State) :-
  357    !.
  358fold_list([H0|T0], [H|T], [EP0|EPT0], [EP1|EPT1], TP1, TP2, Goal, State0, State) :-
  359    !,
  360    foldsubterms_pos(Goal, H0, H, EP0, EP1, State0, State1),
  361    fold_list(T0, T, EPT0, EPT1, TP1, TP2, Goal, State1, State).
  362fold_list(T0, T, EP, EP, TP0, TP, Goal, State0, State) :-
  363    foldsubterms_pos(Goal, T0, T, TP0, TP, State0, State).
  364
  365:- det(foldsubterms_/9).  366foldsubterms_(I, Arity, Goal, Term1, Term2, PosIn, PosOut, State0, State) :-
  367    I =< Arity,
  368    !,
  369    (   PosIn = [AP1|APT1]
  370    ->  PosOut = [AP2|APT2]
  371    ;   true
  372    ),
  373    arg(I, Term1, A1),
  374    arg(I, Term2, A2),
  375    foldsubterms_pos(Goal, A1, A2, AP1, AP2, State0, State1),
  376    I2 is I+1,
  377    foldsubterms_(I2, Arity, Goal, Term1, Term2, APT1, APT2, State1, State).
  378foldsubterms_(_, _, _, _, _, _, [], State, State).
  379
  380:- det(pos_parts/5).  381pos_parts(_, Var, _, _, _), var(Var) => true.
  382pos_parts(Type, parentheses_term_position(F,T,In), PosOut, SubIn, SubOut) =>
  383    PosOut = parentheses_term_position(F,T,Out),
  384    pos_parts(Type, In, Out, SubIn, SubOut).
  385pos_parts(compound, term_position(From, To, FFrom, FTo, SubPos),
  386          PosOut, SubIn, SubOut) =>
  387    PosOut = term_position(From, To, FFrom, FTo, SubOut),
  388    SubIn = SubPos.
  389pos_parts(compound, brace_term_position(From, To, ArgPos0),
  390          PosOut, SubIn, SubOut) =>
  391    PosOut = brace_term_position(From, To, ArgPos),
  392    SubIn = [ArgPos0],
  393    SubOut = [ArgPos].
  394pos_parts(list, list_position(From, To, Elms, Tail),
  395          PosOut, SubIn, SubOut) =>
  396    PosOut = list_position(From, To, Elms1, Tail1),
  397    SubIn = list(Elms, Tail),
  398    SubOut = list(Elms1, Tail1).
  399pos_parts(dict, dict_position(From, To, TagFrom, TagTo, KVPosIn),
  400          PosOut, SubIn, SubOut) =>
  401    PosOut = dict_position(From, To, TagFrom, TagTo, SubOut),
  402    SubIn = KVPosIn.
  403pos_parts(_, _, _, _, _) =>
  404    true.                               % mismatch term and pos
  405
  406arg_pos(_, TermPos, _), var(TermPos) => true.
  407arg_pos(I, parentheses_term_position(_,_,TP), AP) =>
  408    arg_pos(I, TP, AP).
  409arg_pos(I, term_position(_,_,_,_,APL), AP) =>
  410    ignore(nth1(I, APL, AP)).
  411arg_pos(1, brace_term_position(_,_,TPA), AP) =>
  412    AP = TPA.
  413arg_pos(_,_,_) =>
  414    true.
  415
  416		 /*******************************
  417		 *             REGISTER		*
  418		 *******************************/
  419
  420% Hook to deal with #define and #import if this library was loaded into
  421% this context.
  422
  423system:term_expansion(In, Out) :-
  424    is_define(In),
  425    prolog_load_context(module, M),
  426    predicate_property(M:expand_macros(_,_,_,_,_), imported_from(macros)),
  427    $,
  428    define_macro(In, Out).
  429
  430
  431		 /*******************************
  432		 *            MESSAGES		*
  433		 *******************************/
  434
  435:- multifile prolog:error_message//1.  436
  437prolog:error_message(domain_error(macro, Macro)) -->
  438    [ 'Invalid macro: ~p'-[Macro] ].
  439prolog:error_message(existence_error(macro, Macro)) -->
  440    [ 'Failed to expand macro: ~p'-[Macro] ].
  441
  442
  443		 /*******************************
  444		 *         IDE SUPPORT		*
  445		 *******************************/
  446
  447:- multifile prolog_colour:term_colours/2.  448
  449prolog_colour:term_colours(#define(_Macro, _Replacement),
  450                           expanded - [ expanded - [ classify, classify ]]).
  451prolog_colour:term_colours((#define(_Macro, _Replacement) :- _Body),
  452                           neck(:-) - [ expanded - [ expanded - [ classify, classify ]],
  453                                        body
  454                                      ]).
  455prolog_colour:term_colours(#import(_File),
  456                           expanded - [ expanded - [ file ]])