add_user_printer : (string * term * userprinter) -> unit
The user-supplied function may choose not to print anything for the given term and hand back control to the standard printer by raising the exception term_pp_types.UserPP_Failed. All other exceptions will propagate to the top-level. If the system printer receives the UserPP_Failed exception, it prints out the term using its standard algorithm, but will again attempt to call the user function on any sub-terms that match the pattern.
The type userprinter is an abbreviation defined in term_grammar to be
type userprinter = type_grammar.grammar * term_grammar.grammar -> PPBackend.t -> sysprinter -> term_pp_types.ppstream_funs -> (grav * grav * grav) -> int -> term -> uprinter
datatype grav = Top | RealTop | Prec of (int * string)
type sysprinter = (grav * grav * grav) -> int -> term -> uprinter
Though there are existing functions add_string, add_break etc. that can be used to manipulate pretty-printing streams, users should prefer instead to use the functions that are provided in the triple with the sysprinter. This then gives them access to functions that can prevent inadvertent symbol merges.
The grav type is used to let pretty-printers know a little about the context in which a term is to be printed out. The triple of gravities is given in the order “parent”, “left” and “right”. The left and right gravities specify the precedence of any operator that might be attempting to “grab” arguments from the left and right. For example, the term
(p /\ (if q then r else s)) ==> t
p /\ (if q then r else s) ==> t
The grav constructors Top and RealTop indicate a context analogous to the top of the term, where there is no binding competition. The constructor RealTop is reserved for situations where the term really is the top of the tree; Top is used for analogous situations such when the term is enclosed in parentheses. (In the conditional expression above, the printing of q will have Top gravities to the left and right.)
The Prec constructor for gravity values takes both a number indicating precedence level and a string corresponding to the token that has this precedence level. This string parameter is of most importance in the parent gravity (the first component of the triple) where it can be useful in deciding whether or not to print parentheses and whether or not to begin fresh pretty-printing blocks. For example, tuples in the logic look better if they have parentheses around the topmost instance of the comma-operator, regardless of whether or not this is required according to precedence considerations. By examining the parent gravity, a printer can determine more about the term’s context. (Note that the parent gravity will also be one or other of the left and right gravities; but it is not possible to tell which.)
The integer parameter to both the system printing function and the user printing function is the depth of the term. The system printer will stop printing a term if the depth ever reaches exactly zero. Each time it calls itself recursively, the depth parameter is reduced by one. It starts out at the value stored in Globals.max_print_depth. Setting the latter to ~1 will ensure that all of a term is always printed.
Finally, the string parameter to the add_user_printer function is a string corresponding to the ML function. Best practice is probably to define the printing function in an independent structure and to then have the string be of the form "module.fnname". This parameter is not present in the accompanying temp_add_user_printer, as this latter function does not affect the grammar exported to disk with export_theory.
- fun myprint Gs B sys (ppfns:term_pp_types.ppstream_funs) gravs d t = let open Portable term_pp_types smpp val (str,brk) = (#add_string ppfns, #add_break ppfns); val (l,r) = dest_conj t in str "CONJ:" >> brk (1,0) >> sys (Top, Top, Top) (d - 1) l >> brk (1,0) >> str "and then" >> brk(1,0) >> sys (Top, Top, Top) (d - 1) r >> str "ENDCONJ" end handle HOL_ERR _ => raise term_pp_types.UserPP_Failed; > val myprint = fn : 'a -> 'b -> (grav * grav * grav -> int -> term -> (term_pp_types.printing_info,'c)smpp.t) -> term_pp_types.ppstream_funs -> 'd -> int -> term -> (term_pp_types.printing_info,unit)smpp.t - temp_add_user_printer ("myprint", ``p /\ q``, myprint); > val it = () : unit - ``p ==> q /\ r``; > val it = ``p ==> CONJ: q and then r ENDCONJ`` : term
The next approach to printing conjunctions is not possible with add_rule. This example uses the styling and blocking functions to create part of its effect. These functions (ustyle and ublock respectively) are higher-order functions that take printers as arguments and cause the arguments to be printed with a particular governing style (ustyle), or indented to reveal block structure (ublock).
- fun myprint2 Gs B sys (ppfns:term_pp_types.ppstream_funs) (pg,lg,rg) d t = let open Portable term_pp_types PPBackEnd smpp val {add_string,add_break,ublock,ustyle,...} = ppfns val (l,r) = dest_conj t fun delim wrap body = case pg of Prec(_, "CONJ") => body | _ => wrap body in delim (fn bod => ublock CONSISTENT 0 (ustyle [Bold] (add_string "CONJ") >> add_break (1,2) >> ublock INCONSISTENT 0 bod >> add_break (1,0) >> ustyle [Bold] (add_string "ENDCONJ"))) (sys (Prec(0, "CONJ"), Top, Top) (d - 1) l >> add_string "," >> add_break (1,0) >> sys (Prec(0, "CONJ"), Top, Top) (d - 1) r) end handle HOL_ERR _ => raise term_pp_types.UserPP_Failed; - temp_add_user_printer ("myprint2", ``p /\ q``, myprint2); - ``p /\ q /\ r /\ s /\ t /\ u /\ p /\ p /\ p /\ p /\ p /\ p /\ p /\ p /\ p /\ p/\ p /\ p /\ q /\ r /\ s /\ t /\ u /\ v /\ (w /\ x) /\ (p \/ q) /\ r``; > val it = ``CONJ p, q, r, s, t, u, p, p, p, p, p, p, p, p, p, p, p, p, q, r, s, t, u, v, w, x, p \/ q, r ENDCONJ`` : term
A better approach (and certainly a more direct one) would probably be to call strip_conj and print all of the conjuncts in one fell swoop. Additionally, this example demonstrates how easy it is to conceal genuine syntactic structure with a pretty-printer. Finally, it shows how styles can be used.