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)).
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 141errorhas_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(_)).
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).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).
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).
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.
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 423systemterm_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 437prologerror_message(domain_error(macro, Macro)) --> 438 [ 'Invalid macro: ~p'-[Macro] ]. 439prologerror_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_colourterm_colours(#define(_Macro, _Replacement), 450 expanded - [ expanded - [ classify, classify ]]). 451prolog_colourterm_colours((#define(_Macro, _Replacement) :- _Body), 452 neck(:-) - [ expanded - [ expanded - [ classify, classify ]], 453 body 454 ]). 455prolog_colourterm_colours(#import(_File), 456 expanded - [ expanded - [ file ]])
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.
Macro is a callable term, not being
define(_,_), orimport(_). 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 => Bodyrules. 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 acallable, 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 ruleThe `#
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#importdefinition. 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 moduleuserandsystem` that is performed after the local expansion is completed.Predicates