M4sugar makes heavy use of diversions, because it is often the case that
text that must appear early in the output is not discovered until late
in the input. Additionally, some of the topological sorting algorithms
-used in resolving macro dependencies use diversions. Therefore, most
+used in resolving macro dependencies use diversions. However, most
macros should not need to change diversions directly, but rather rely on
higher-level M4sugar macros to manage diversions transparently.
+In the rare case that it is necessary to write a macro that explicitly
+outputs text to a different diversion, it is important to be aware of an
+M4 limitation regarding diversions: text only goes to a diversion if it
+is not part of argument collection. Therefore, any macro that changes
+the current diversion cannot be used as an unquoted argument to another
+macro, but must be expanded at the top level. The macro
+@code{m4_expand} will diagnose any attempt to change diversions, since
+it is generally useful only as an argument to another macro. The
+following example shows what happens when diversion manipulation is
+attempted within macro arguments:
+
+@example
+m4_do([normal text]
+m4_divert_push([KILL])unwanted[]m4_divert_pop([KILL])
+[m4_divert_push([KILL])discarded[]m4_divert_pop([KILL])])dnl
+@result{}normal text
+@result{}unwanted
+@end example
+
+@noindent
+Notice that the unquoted text @code{unwanted} is output, even though it
+was processed while the current diversion was @code{KILL}, because it
+was collected as part of the argument to @code{m4_do}. However, the
+text @code{discarded} disappeared as desired, because the diversion
+changes were single-quoted, and were not expanded until the top-level
+rescan of the output of @code{m4_do}.
+
To make diversion management easier, M4sugar uses the concept of named
diversions. Rather than using diversion numbers directly, it is nicer
to associate a name with each diversion; the diversion number associated
@var{arg}. Additionally, @code{m4_expand} is able to expand text that
would involve an unterminated comment, whereas expanding that same text
as the argument to @code{m4_quote} runs into difficulty in finding the
-end of the argument.
+end of the argument. Since manipulating diversions during argument
+collection is inherently unsafe, @code{m4_expand} issues an error if
+@var{arg} attempts to change the current diversion (@pxref{Diversion
+support}).
@example
m4_define([active], [ACT, IVE])dnl
# this), and should not contain the unlikely delimiters -=<{( or
# )}>=-. It is possible to have unbalanced quoted `(' or `)', as well
# as unbalanced unquoted `)'. m4_expand can handle unterminated
-# comments or dnl on the final line, at the expense of speed, while
-# _m4_expand is faster but must be given a terminated expansion.
+# comments or dnl on the final line, at the expense of speed; it also
+# aids in detecting attempts to incorrectly change the current
+# diversion inside ARG. Meanwhile, _m4_expand is faster but must be
+# given a terminated expansion, and has no safety checks for
+# mis-diverted text.
#
# Exploit that extra unquoted () will group unquoted commas and the
# following whitespace. m4_bpatsubst can't handle newlines inside $1,
# this time with one more `(' in the second argument and in the
# open-quote delimiter. We must also ignore the slop from the
# previous try. The final macro is thus half line-noise, half art.
-m4_define([m4_expand], [m4_chomp(_$0([$1
-]))])
+m4_define([m4_expand],
+[m4_pushdef([m4_divert], _m4_defn([_m4_divert_unsafe]))]dnl
+[m4_pushdef([m4_divert_push], _m4_defn([_m4_divert_unsafe]))]dnl
+[m4_chomp(_$0([$1
+]))_m4_popdef([m4_divert], [m4_divert_push])])
m4_define([_m4_expand], [$0_([$1], [(], -=<{($1)}>=-, [}>=-])])
[m4_expand_once([m4_divert_text([$1], [$2])])])
+# _m4_divert_unsafe(DIVERSION-NAME)
+# ---------------------------------
+# Issue a warning that the attempt to change the current diversion to
+# DIVERSION-NAME is unsafe, because this macro is being expanded
+# during argument collection of m4_expand.
+m4_define([_m4_divert_unsafe],
+[m4_fatal([$0: cannot change diversion to `$1' inside m4_expand])])
+
+
# m4_undivert(DIVERSION-NAME...)
# ------------------------------
# Undivert DIVERSION-NAME. Unlike the M4 version, this requires at