View source with formatted 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)).   11
   12/** <module> Macro expansion
   13
   14This library defines a  macro  expansion   mechanism  that  operates  on
   15arbitrary terms. Unlike term_expansion/2 and goal_expansion/2, a term is
   16explicitly designed for expansion using the  term `#(Macro)`. Macros are
   17first of all intended to deal with compile time constants. They can also
   18be used to construct terms at compile time.
   19
   20## Defining and using macros {#macros-define-and-use}
   21
   22Macros are defined for  the  current  module   using  one  of  the three
   23constructs below.
   24
   25    #define(Macro, Replacement).
   26    #define(Macro, Replacement) :- Code.
   27    #import(ModuleFile).
   28
   29`Macro` is a _callable  term_,  not   being  define(_,_),  or import(_).
   30`Replacement` is an arbitrary Prolog  term.   `Code`  is  a Prolog _body
   31term_ that _must_ succeed and can be used to dynamically generate (parts
   32of) `Replacement`.
   33
   34The `#import(ModuleFile)` definition makes  all   macros  from the given
   35module available for expansion in the   module it appears. Normally this
   36shall be appear after local macro definitions.
   37
   38A macro is called  using  the  term   `#(Macro)`.  `#`  is  defined as a
   39low-priority (10) prefix operator to  allow   for  `#Macro`.  Macros can
   40appear at the following places:
   41
   42  - An entire sentence (clause)
   43  - Any argument of a compound.  This implies also the head and body of
   44    a clause.
   45  - Anywhere in a list, including as the tail of a list
   46  - As a value for a dict key or as a dict key name.
   47
   48Macros can __not__ appear as name of a compound or tag of a dict. A term
   49`#Macro` appearing in one of the allowed places __must__ have a matching
   50macro defined, i.e., `#Macro`  is  __always__   expanded.  An  error  is
   51emitted if the expansion fails. Macro   expansion is applied recursively
   52and thus, macros may be passed to   macro  arguments and macro expansion
   53may use other macros.
   54
   55Macros are matched to terms  using   _Single  Sided  Unification_ (SSU),
   56implemented using `Head => Body` rules.   This implies that the matching
   57never instantiates variables in the term that is being expanded.
   58
   59Below are some examples. The  first  line   defines  the  macro  and the
   60indented line after show example usage of the macro.
   61
   62```
   63#define(max_width, 100).
   64    W < #max_width
   65
   66#define(calc(Expr), Value) :- Value is Expr.
   67    fact(#calc(#max_width*2)).
   68
   69#define(pt(X,Y), point{x:X, y:Y}).
   70    reply_json(json{type:polygon,
   71                    points:[#pt(0,0), #pt(0,5), #pt(5,0)]}).
   72```
   73
   74Macro expansion expands terms `#(Callable)`.  If   the  argument  to the
   75#-term is not a `callable`, the  #-term   is  not modified. This notably
   76allows for `#(Var)`  as  used  by   library(clpfd)  to  indicate  that a
   77variable is constraint to be an (clp(fd)) integer.
   78
   79
   80## Implementation details {#macros-implementation}
   81
   82A macro `#define(Macro, Expanded) :- Body.`  is, after some basic sanity
   83checks, translated into a rule
   84
   85    '$macro'(Macro, Var), Body => Var = Expanded.
   86
   87The `#import(File)` is translated into `:-   use_module(File, [])` and a
   88_link clause_ that links the macro expansion  from the module defined in
   89`File` to the current module.
   90
   91Macro expansion is realised by creating a clause for term_expansion/2 in
   92the current module.  This  clause  results   from  expanding  the  first
   93`#define` or `#import` definition. Thus, if   macros  are defined before
   94any other local definition for term_expansion/2   it  is executed as the
   95first step. The macro expansion fails if no macros were encounted in the
   96term, allowing other term_expansion rules local   to  the module to take
   97effect. In other words, a term  holding   macros  is  not subject to any
   98other term expansion local  to  the  module.   It  is  subject  to  term
   99expansion defined in module `user` and  `system` that is performed after
  100the local expansion is completed.
  101
  102
  103## Predicates {#macros-predicates}
  104
  105*/
  106
  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(_)).
  163
  164%!  include_macros(+M, +Macro, -Expanded) is semidet.
  165%
  166%   Include macros from another module. This   predicate is a helper for
  167%   `#import(File)`. It calls '$macro'/2 in  M,   but  fails silently in
  168%   case Macro is not defined in  M  as   it  may  be defined in another
  169%   imported macro file or further down in the current file.
  170
  171include_macros(M, Macro, Expanded) :-
  172    catch(M:'$macro'(Macro, Expanded),
  173          error(existence_error(matching_rule,
  174                                M:'$macro'(Macro,_)),_),
  175          fail).
  176
  177%!  expand_macros(+Module, +TermIn, -TermOut, +PosIn, -PosOut) is semidet.
  178%
  179%   Perform macro expansion on  TermIn  with   layout  PosIn  to produce
  180%   TermOut with layout PosOut. The transformation   is performed if the
  181%   current load context module is Module (see prolog_load_context/2).
  182%
  183%   This predicate is not intended for direct usage.
  184
  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).
  236
  237%!  macro_position(-Position) is det.
  238%
  239%   True when Position is the position of  the macro. Position is a term
  240%   `File:Line:LinePos`. If `File` is unknown it is unified with `-`. If
  241%   Line and/or LinePos are  unknown  they   are  unified  with  0. This
  242%   predicate can be used in the body   of a macro definition to provide
  243%   the source location. The example below defines `#pp(Var)` to print a
  244%   variable together with the variable name and source location.
  245%
  246%   ```
  247%   #define(pp(Var), print_message(debug, dump_var(Pos, Name, Var))) :-
  248%       (   var_property(Var, name(Name))
  249%       ->  true
  250%       ;   Name = 'Var'
  251%       ),
  252%       macro_position(Pos).
  253%
  254%   :- multifile prolog:message//1.
  255%   prolog:message(dump_var(Pos,Name,Var)) -->
  256%       [ url(Pos), ': ',
  257%         ansi([fg(magenta),bold], '~w', [Name]), ' = ',
  258%         ansi(code, '~p', [Var])
  259%       ].
  260%   ```
  261
  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).
  290
  291%!  fix_pos_shape(+TermIn, +TermOut, +PosIn, -PosOut) is det.
  292%
  293%   Fixup PosIn to be a position term that is compatible to Term.
  294%
  295%   @bug This predicate is largely unimplemented.
  296
  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.
  307
  308%! foldsubterms_pos(:Goal, +TermIn, -TermOut, +PosIn, -PosOut,
  309%!                  +State0, -State) is det.
  310%
  311%  As  foldsubterms/5,  but  also  transforms   the  layout  term.  This
  312%  predicate may later be moved to  e.g. library(prolog_code) to make it
  313%  publically available.
  314
  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 ]])