X-Git-Url: https://git.distorted.org.uk/~mdw/mLib/blobdiff_plain/e6591bec0938b76b8c02083eb6ba101aba4e1b7e..b64eb60f6c1fdb12f3922e04913e137199838807:/utils/control.h diff --git a/utils/control.h b/utils/control.h index 433d57b..97be8b6 100644 --- a/utils/control.h +++ b/utils/control.h @@ -32,6 +32,16 @@ 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 @@ -47,115 +57,200 @@ */ #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 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) 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 for (;;) \ - if (1) { stmt break; } \ - 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