@@@ all the mess ever
[mLib] / utils / control.h
index fdc7125..97be8b6 100644 (file)
   extern "C" {
 #endif
 
+/*----- Notes on the control operator machinery ---------------------------*
+ *
+ * These macros owe an obvious and immense debt to Simon Tatham's article
+ * `Metaprogramming custom control structures in C', available from
+ * https://www.chiark.greenend.org.uk/~sgtatham/mp/.  The basic tricks are
+ * all Tatham's, as are most of the provided operators.  The focus on
+ * @MC_ACT@ as a significant primitive is probably my main original
+ * contribution.
+ */
+
 /*----- Header files ------------------------------------------------------*/
 
 #ifndef MLIB_MACROS_H
  */
 #define MCTRL__LABEL(tag) GLUE(_mctrl__##tag##__, __LINE__)
 
-/* @FIRSTBRANCH(tag_0) stmt_0@
- * [@MIDBRANCH(tag_i) stmt_i ...@]
- * @LASTBRANCH(tag_n) stmt_n@
- * @GOBRANCH(tag);@
+/* @MC_ACT(stmt)@
+ *
+ * @MC_ACT@ is the main `trick' for constructing these flow-control
+ * operators.  It wraps up a statement as what we call an `action'.  Actions
+ * can be concatenated together to form a valid statement head, i.e., a
+ * sequence of actions can be followed by a statement, called the `body', to
+ * form a single syntactic statement.  The body can be simply `;', so an
+ * action can be treated as a simple statement.  However, if an action
+ * sequence is executed, only the first statement is executed.
+ *
+ * Actions can be labelled, e.g., using @MC_LABEL@, just like statements.  If
+ * control is passed to a label, e.g., by @MC_GOTO@, then the statement
+ * within the following action (only) is executed; the normal flow of control
+ * will then be to the statement following the containing action sequence and
+ * its body.
+ */
+#define MC_ACT(stmt)   if (1) { stmt; } else
+
+/* @MC_LABEL(tag)@
+ * @MC_GOTO(tag);@
+ *
+ * @MC_LABEL@ just establishes a label which can be invoked (only) from the
+ * same top-level macro; and @MC_GOTO@ transfers control to it.
+ *
+ * The @MC_GOTO@ macro is special in that it can be used either as a plain
+ * statement, followed by a semicolon in the usual way, or as a prefix
+ * action in its own right, in place of @MC_ACT@.
+ */
+#define MC_LABEL(tag)  MCTRL__LABEL(tag):
+#define MC_GOTO(tag)   MC_ACT(goto MCTRL__LABEL(tag))
+
+/* @MC_TARGET(tag, stmt) body@
+ * @MC_GOTARGET(tag);@
+ *
+ * Executing @TARGET@ statement executes the @body@, as if the @TARGET@
+ * weren't there.  Executing a @GOTARGET@ transfers control to the @stmt@
+ * (but not normally the @body@).  In either case, the normal flow of control
+ * is then to the following statement.
+ */
+#define MC_TARGET(tag, stmt)                                           \
+                       MC_GOTO(tag##__body)                            \
+  MC_LABEL(tag##__tgt) MC_ACT(stmt)                                    \
+  MC_LABEL(tag##__body)
+#define MC_GOTARGET(tag) do MC_GOTO(tag##__tgt); while (0)
+
+/* @MC_BEFORE(tag, stmt) body@
+ *
+ * Execute @stmt@ and then @body@.
+ */
+#define MC_BEFORE(tag, stmt)                                           \
+                       MC_ACT(stmt; MC_GOTO(tag##__body))              \
+  MC_LABEL(tag##__body)
+
+/* @MC_AFTER(tag, stmt) body@
  *
- * If control enters at the top, then only <stmt_0> is executed, followed by
- * the statement after.  Following @GOBRANCH(tag)@, control continues
- * from the correspondingly tagged statement, and continues with the
- * following statement again.
+ * Execute @body@ and then @stmt@.  If @body@ invokes @break@ then control
+ * immediately transfers to the statement following @MC_AFTER@.  If @body
+ * invokes @continue@, then control returns to @stmt@.
  */
-#define FIRSTBRANCH(tag)  if (1) { goto MCTRL__LABEL(tag); MCTRL__LABEL(tag):
-#define MIDBRANCH(tag)    } else if (0) MCTRL__LABEL(tag): {
-#define LASTBRANCH(tag)   } else MCTRL__LABEL(tag):
-#define GOBRANCH(tag)     goto MCTRL__LABEL(tag)
+#define MC_AFTER(tag, stmt)                                            \
+                       MC_GOTO(tag##__body)                            \
+  MC_LABEL(tag##__end) MC_ACT(stmt)                                    \
+                       for (;;)                                        \
+                         MC_GOTO(tag##__end)                           \
+  MC_LABEL(tag##__body)
 
-/* @BEFORE(tag, stmt_0) stmt_1@
+/* @MC_DOWHILE(tag, cond) body@
  *
- * Execute @stmt_0@ and then @stmt_1@.
+ * Repeatedly execute @body@ until @cond@ evaluates to zero.  The @body@ is
+ * executed once before @cond@ is checked the first time.  The @break@ and
+ * @continue@ statements work within @body@ as one would expect.
  */
-#define BEFORE(tag, stmt)                                              \
-       if (1) { stmt goto MCTRL__LABEL(tag##__before_body); }          \
-       else MCTRL__LABEL(tag##__before_body):
+#define MC_DOWHILE(tag, cond)                                          \
+                       MC_GOTO(tag##__body)                            \
+                       while (cond)                                    \
+  MC_LABEL(tag##__body)
 
-/* @AFTER(tag, stmt_0) stmt_1@
+/* @MC_ALLOWELSE(tag) apodosis_body [else haeresis_body]@
+ * @MC_GOELSE(tag);@
  *
- * Execute @stmt_1@ and then @stmt_0@.  If either statement invokes @break@
- * then control immediately transfers to the statement following @AFTER@.  If
- * either invokes @continue@, then control returns to @stmt_0@.
+ * Executing @MC_ALLOWELSE@ executes @apodosis_body@, but not
+ * @haeresis_body@.  If @MC_GOELSE(tag)@ is executed, then control continues
+ * from @haeresis_body@.
  */
-#define AFTER(tag, stmt)                                               \
-       if (1) goto MCTRL__LABEL(tag##__after_body);                    \
-       else if (1) { MCTRL__LABEL(tag##__after_end): stmt }            \
-       else for (;;)                                                   \
-         if (1) goto MCTRL__LABEL(tag##__after_end);                   \
-         else MCTRL__LABEL(tag##__after_body):
+#define MC_ALLOWELSE(tag)                                              \
+                       MC_GOTO(tag##__body)                            \
+  MC_LABEL(tag##__else)        if (0)                                          \
+  MC_LABEL(tag##__body)
+#define MC_GOELSE(tag) do MC_GOTO(tag##__else); while (0)
 
-/* @WRAP(tag, before, onend, onbreak) stmt@
+/* @MC_WRAP(tag, before, onend, onbreak) body@
  *
- * Execute the @before@ statement, followed by @stmt@.  If @stmt@ invokes
- * @break@, then @onbreak@ is immediately executed; if @stmt@ completes
+ * Execute the @before@ statement, followed by @body@.  If @body@ invokes
+ * @break@, then @onbreak@ is immediately executed; if @body@ completes
  * normally, or invokes @continue@ then @onend@ is immediately executed.
  * Any @break@ and @continue@ in the @before@, @onend@, and @onbreak@
  * statements behave as one would expect from their context.
  */
-#define WRAP(tag, before, onend, onbreak)                              \
-       if (1) { before goto MCTRL__LABEL(tag##__wrap_body); }          \
-       else if (1) { MCTRL__LABEL(tag##__wrap_end): onend }            \
-       else if (1) { MCTRL__LABEL(tag##__wrap_break): onbreak }        \
-       else for (;;)                                                   \
-         if (1) goto MCTRL__LABEL(tag##__wrap_break);                  \
-         else for (;;)                                                 \
-           if (1) goto MCTRL__LABEL(tag##__wrap_end);                  \
-           else MCTRL__LABEL(tag##__wrap_body):
-
-/* @ALLOWELSE(tag, before, onend, onbreak) stmt_0 [else stmt_1]@
- * @GOELSE(tag);@
- *
- * Execute the @before@ statement, followed by @stmt_0@.  If @stmt_0@
- * completes, or invokes @break@ or @continue@, then control continues with
- * the next statement.  If @GOELSE(tag)@ is invoked anywhere in the
- * function, then @before@ is executed, followed by @stmt_1@ (if present).
- * If @stmt_1@ invokes @break@ then control passes to @onbreak@; if @stmt_1@
- * ends normally then control passes to @onend@.  Any @break@ and @continue@
- * in the @before@, @onend@, and @onbreak@ statements behave as one would
- * expect from their context.
- */
-#define ALLOWELSE(tag, before, onend, onbreak)                         \
-       if (1) goto MCTRL__LABEL(tag##__allowelse_body);                \
-       else if (1) MCTRL__LABEL(tag##__allowelse_body_end): ;          \
-       else if (1) { MCTRL__LABEL(tag##__allowelse_else_end): onend }  \
-       else if (1) { MCTRL__LABEL(tag##__allowelse_else_break): onbreak } \
-       else if (1) {                                                   \
-       MCTRL__LABEL(tag##__allowelse_before_else):                     \
-         before goto MCTRL__LABEL(tag##__allowelse_else);              \
-       } else for (;;)                                                 \
-         if (1) goto MCTRL__LABEL(tag##__allowelse_else_break);        \
-         else for (;;)                                                 \
-           if (1) goto MCTRL__LABEL(tag##__allowelse_else_end);        \
-           else MCTRL__LABEL(tag##__allowelse_else): if (0) for (;;)   \
-             if (1) goto MCTRL__LABEL(tag##__allowelse_body_end);      \
-             else MCTRL__LABEL(tag##__allowelse_body):
-#define GOELSE(tag)                                                    \
-       goto MCTRL__LABEL(tag##__allowelse_before_else)
-
-/* @DOWHILE(tag, cond) stmt@
- *
- * Repeatedly execute @stmt@ until @cond@ evaluates to zero.  Execute @stmt@
- * at least once.  The @break@ and @continue@ statements work within @stmt@
- * as one would expect.
- */
-#define DOWHILE(tag, cond)                                             \
-       if (1) goto MCTRL__LABEL(tag##__dowhile_body);                  \
-       else while (cond) MCTRL__LABEL(tag##__dowhile_body):
-
-/* @DECL(tag, decl) stmt@
- *
- * Execute @stmt@ with @decl@ in scope.  If @stmt@ completes or invokes
- * @break@ or @continue@ then control continues with the statement following
- * @DECL@.  Internally, this uses @for@, so it only works in C99 or later, or
- * C++.
+#define MC_WRAP(tag, before, onend, onbreak)                           \
+                       MC_ACT(before; MC_GOTO(tag##__body))            \
+  MC_LABEL(tag##__end) MC_ACT(onend)                                   \
+  MC_LABEL(tag##__brk) MC_ACT(onbreak)                                 \
+                       for (;;)                                        \
+                         MC_GOTO(tag##__brk)                           \
+                         for (;;)                                      \
+                           MC_GOTO(tag##__end)                         \
+  MC_LABEL(tag##__body)
+
+/* @MC_FINALLY(tag, cleanup) body@
+ *
+ * Execute @cleanup@ when @body@ completes or ends with @break@.  In the
+ * latter case, propagate the @break@ to the enclosing context -- for which
+ * it must be syntactically appropriate.
+ *
+ * The @cleanup@ code is duplicated.  If it arrange to have private long-term
+ * state, e.g, by declaring @static@ variables, then the two copies will not
+ * share the same state, so probably don't do this.
+ */
+#define MC_FINALLY(tag, cleanup)                                       \
+       MP_WRAP(tag##__final, { ; } cleanup, { cleanup break; })
+
+/* @MC_DECL(tag, decl) body@
+ *
+ * Execute @body@ with @decl@ in scope.  If @body@ completes or invokes
+ * @continue@ then control continues with the statement following @MC_DECL@;
+ * if it invokes @break@ then it will be restarted without leaving the scope
+ * of @decl@.  Internally, this uses @for@, so it only works in C99 or later,
+ * or C++.
  */
 #if __STDC_VERSION__ >= 199901 || defined(__cplusplus)
-#  define DECL(tag, decl)                                              \
-       for (decl;;)                                                    \
-         if (1) goto MCTRL__LABEL(tag##__decl_body);                   \
-         else if (1) MCTRL__LABEL(tag##__decl_exit): break;            \
-         else for (;;)                                                 \
-           if (1) goto MCTRL__LABEL(tag##__decl_exit);                 \
-           else MCTRL__LABEL(tag##__decl_body):
+#  define MC_DECL(tag, decl)                                           \
+                       for (decl;;)                                    \
+                         MC_GOTO(tag##__body)                          \
+  MC_LABEL(tag##__exit)          MC_ACT(break)                                 \
+                         for (;;)                                      \
+                           MC_GOTO(tag##__exit)                        \
+  MC_LABEL(tag##__body)
 #endif
 
+/* @MC_LOOPELSE(tag, head) loop_body [else else_body]@
+ *
+ * Python-like looping with optional @else@ clause.  @head loop_body@ must be
+ * a syntactically valid @for@, @while@, or @MC_DOWHILE@ loop; if the loop
+ * exits because of @break@ then control continues in the usual way;
+ * otherwise, the @else_body@ (if any) is executed.
+ */
+#define MC_LOOPELSE(tag, head)                                         \
+       MC_TARGET(tag##__exit, { ; })                                   \
+       MC_ALLOWELSE(tag##__else)                                       \
+       MC_AFTER(tag##__after, { MC_GOELSE(tag##__else); })             \
+       head                                                            \
+         MC_WRAP(tag##__body, { ; }, { ; }, { MC_GOTARGET(tag##__exit); })
+
+/* @MC_LOOPBETWEEN(tag, setup, cond, step) loop_body [else else_body]@
+ *
+ * This is essentially a @for@ loop with a twist.  The @setup@, @cond@, and
+ * @step@ arguments are the parts of the @for@ head clause; because of the
+ * limitations of the C macro syntax, they're separated by commas rather than
+ * semicolons.
+ *
+ * The twist is that, once the @loop_body@ has finished, the @step@
+ * expression evaluated, and the @cond@ evaluated and determined to be
+ * nonzero, the @else_body@ (if any) is executed before re-entering the
+ * @loop_body@.  This makes it a useful place to insert any kind of
+ * interstitial material, e.g., printing commas between list items.
+ *
+ * The @cond@ is textually duplicated.  You'll get some code bloat if the
+ * condition is very complex.  If it somehow arranges to have private
+ * long-term state (e.g., as a result of declaring static variables inside
+ * GCC statement expressions), then the two copies will not share this state,
+ * so probably don't do this.
+ *
+ * Note that by the time that the @else_body@ is executed, the decision has
+ * already been made that another iteration will be performed, and, in
+ * particular, the @step@ has occurred.  The @else_body@ is therefore looking
+ * at the next item to be processed, not the item that has just finished
+ * being processed.
+ */
+#define MC_LOOPBETWEEN(tag, setup, cond, step)                         \
+       for (setup;;)                                                   \
+         if (!(cond)) break; else                                      \
+         MC_TARGET(tag##__exit, { break; })                            \
+         for (;;)                                                      \
+           MC_WRAP(tag##__tailwrap, { ; },                             \
+                                    { ; },                             \
+                                    { MC_GOTARGET(tag##__exit); })     \
+           MC_ALLOWELSE(tag##__tail)                                   \
+           MC_WRAP(tag##__bodywrap, { ; },                             \
+                                    { if ((step), !(cond))             \
+                                        MC_GOTARGET(tag##__exit);      \
+                                      else                             \
+                                        MC_GOELSE(tag##__tail); },     \
+                                    { MC_GOTARGET(tag##__exit); })
+
 /*----- That's all, folks -------------------------------------------------*/
 
 #ifdef __cplusplus