265: The CFG Language

by Marc Nieper-Wißkirchen

Status

This SRFI is currently in draft status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-265@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.

Abstract

This SRFI defines a language to describe control-flow graphs (CFGs) suitable for formulating iterative and recursive algorithms. Using the notion of a CFG term, this language can be seamlessly embedded in the Scheme language. Complex CFG terms can be composed from simple CFG terms.

The language described in this SRFI is not meant to be directly used in programs but by library authors to build abstractions like loop facilities on top of it.

Issues

  1. Withdraw SRFI 242 as soon as SRFI 265 is finalized. SRFI 265's title deliberately coincides with SRFI 242's title.

  2. The planned SRFI on loops should be submitted during the draft period of this SRFI so that it can point to the loop SRFI.

Table of contents

Rationale

In the Scheme language, procedures (and thus jump labels, according to lambda-the-ultimate philosophy) are first-class entities. In a certain sense, this makes the control-flow graph of a Scheme program a dynamic and not static entity.

The CFG language described in this SRFI, on the other hand, describes a static control-flow graph. For the applications where this is sufficient, this has the advantage that the control-flow graph can be readily statically reasoned about, allowing one, in particular, to define variable scope in terms of it.

The CFG language itself is useful for describing iterative and recursive algorithms more clearly than it is possible in the Scheme language through tail calls and (multiple) return values. Its raison d'être, however, is that more specialized languages like that of a loop facility can be easily built on top of it.

The origin of the CFG language described in this SRFI is Olin Shiver's paper The Anatomy of a Loop: A story of scope and control. In his paper, he makes two important points: The main iterative control construct in Scheme is a tail call. While a tail call as a goto that passes arguments is a pretty powerful construct, it is also as low-level as a goto. His first point is that this implies that it is not the right tool to write down iterative algorithms in a high-level fashion. This fact has stimulated the search for loop facilities allowing one to express iterative algorithms more abstractly and more composably. His second point is that just as the scoping of variables is well-defined in the Scheme language, a well-defined model of (loop) variable scoping is needed for loop facilities as well. For this, he formulates a new scoping rule, namely that binders dominate references.

While not all surface syntax has been adopted from Olin Shiver's paper, the core of the language described here subsumes his conception. The addition of a facility to handle not only iterative but also recursive algorithms is a new invention in this SRFI. This addresses a third perceived shortcoming of the Scheme language. Recursive algorithms in Scheme are based on the fact that Scheme procedures return values, possibly multiple ones. However, as soon as more than one value needs to be returned and each recursion step only needs to modify one of them, a position-only identification of return values becomes unclear and leads to repetition of code. Instead, the CFG language in this SRFI gives names to intermediate results and allows parallel processing of them.

Thanks to Scheme's expressive macro system, the CFG language can be seamlessly implemented in the standard Scheme language, fully respecting Scheme semantics.

The author plans to submit a SRFI describing an extensible loop facility built on the CFG language defined in this SRFI in the future.

Changes from SRFI 242

Examples

We start with two complex examples before we begin with a gentle introduction from the beginning.

The following expression evaluates to an (iterative) procedure that takes a list of integers and returns two values, the number of even and the number of odd values in the list.

(lambda (n*)
  (cfg
      (let f ([n* n*] [e 0] [o 0])
        (branch ([(next n n*)
                   (branch ([(even e) (go f)]
                            [(odd o) (go f)])
                     (if (odd? n)
                         (odd (+ o 1))
                         (even (+ e 1))))]
                  [(done)
                   (return-values [(e o) (values e o)])])
          (if (null? n*)
              (done)
              (next (car n*) (cdr n*)))))
    (values e o)))

The following expression evaluates to a (recursive) procedure that takes a list of integers and returns two values, the sublist of even values and the sublist of odd values.

(lambda (n*)
  (cfg
      (let f ([n* n*])
        (branch ([(next n n*)
                   (branch ([(even)
                              (finally ([e* (cons n e*)])
                                (go f))]
                            [(odd)
                              (finally ([o* (cons n o*)])
                                (go f))])
                     (if (odd? n) (odd) (even)))]
                  [(done)
                   (return-values [(e* o*) (values '() '())])])
          (if (null? n*)
              (done)
              (next (car n*) (cdr n*)))))
    (values e* o*)))

Using the cfg form

A cfg expression lets control flow along the edges of a control-flow graph (CFG) defined by the cfg expression, potentially binding so-called CFG variables, until a return or return-values CFG term is reached. Then, control flows backwards along the edges previously taken, potentially making further CFG-variable bindings. A defer CFG term may call another CFG term after its last CFG term returns. Finally a Scheme expression is evaluated in an environment extended by these bindings, and its values are returned.

The general form of a cfg expression is

(cfg cfg term result expression)

where the cfg term describes the CFG and result expression is an arbitrary Scheme expression.

The simplest CFG is just one consisting of an empty return CFG term:

(return)

For example, we have

(cfg (return) (+ 1 2))3

as the control flow along this CFG, which has no edges, immediately stops, leading on to the evaluation of the result expression.

Return and return-values CFG terms are used to bind CFG variables. Their syntax is:

(return [variable expression] …)

(return-values [formals expression] …)

Return is the single-value counterpart of return-values. When control enters either term, the (Scheme) expressions are evaluated to yield values that are bound as CFG variables to the respective variables or formals. For example, return-values supports a dotted formals:

(cfg
    (return-values [(x . y) (values 1 2 3)])
  (list x y)(1 (2 3))

A more complicated example is the following:

(let ([x 1])
  (cfg
      (defer ([(finish y)
                (return [y y])])
          (let-values ([(y) (+ x 2)])
            (finish y))
        (return [x (+ x 1)]))
    (list x y)))(2 4)

A defer CFG term calls its last CFG term first. After this CFG term has returned, the Scheme expression is evaluated with the CFG variables visible in the returned CFG state. Each target, such as finish above, is bound to a procedure in this expression. The expression must tail-call exactly one target. Calling (finish y) resumes forward control with the corresponding CFG term.

In the example, the last CFG term binds the CFG variable x to 2. This binding is visible in (+ x 2), so finish is invoked with 4. The corresponding CFG term receives the binding of x as part of the current CFG state. The target formal y is an ordinary Scheme variable, lexically scoped over the corresponding target CFG term and bound to the explicitly passed value. The return term then defines a CFG variable also named y.

A binding made by the last CFG term is also directly visible in the corresponding CFG term:

(cfg
    (defer ([(finish)
              (return [result (+ r 1)])])
      (finish)
      (return [r 8]))
  result)9

The general syntax of a defer CFG term is:

(defer ([(target . formals) cfg term] …) expression cfg term)

Targets use ordinary procedure-call semantics. Thus, several arguments can be passed with (finish x y), and a list of arguments can be passed with (apply finish argument-list).

When a defer term is only needed to bind CFG variables after its last CFG term returns, the derived finally and finally-values forms are more concise. Their syntax is:

(finally ([variable expression] ...) cfg term)

(finally-values ([formals expression] ...) cfg term)

The CFG term is called first. After it returns, the expressions are evaluated in an unspecified order with its visible CFG-variable bindings, and their values are bound as CFG variables in parallel. Thus, the earlier example can be written more simply as:

(let ([x 1])
  (cfg
      (finally ([y (+ x 2)])
        (return [x (+ x 1)]))
    (list x y)))(2 4)

While defer CFG terms are used to perform actions after their last CFG term returns, branch CFG terms perform actions during the initial forward flow through the CFG. The syntax of a branch CFG term is given by

(branch ([(target . formals) cfg term] …) expression)

Each target is bound to a procedure accepting arguments according to its formals. When the forward control flow through the CFG reaches a branch CFG term, the expression is evaluated and must tail-call one of the targets. Targets use ordinary procedure-call semantics and can, for example, be passed to apply. Control flow then proceeds with the corresponding cfg term:

(let ([x 1] [y 2])
  (cfg
      (defer ([(finish y)
                (return [y y])])
          (finish (+ x 3))
        (branch ([(e) (return)])
          (begin
            (set! x y)
            (e))))
    (list x y)))(2 5)

In this example, the forward control flow passes through the defer CFG term before it reaches the branch block. The expression is evaluated in the environment where x is bound to 1 and y is bound to 2. The tail call to e lets the control flow continue with the empty return CFG term, from where it flows backward. The backward control flow passes through the branch CFG term before the defer expression is evaluated. It invokes finish with the result of (+ x 3), and the corresponding CFG term binds the CFG variable y to that result, 5.

Branch CFG terms are used to create CFGs with branches. For this, more than one cfg term has to be provided. Consider the following example:

(let ([x 1] [y 2])
  (cfg
      (branch ([(e1) (return)]
               [(e2) (return [x 3])])
        (if (odd? y) (e1) (e2)))
    (+ x 10)11

Here, the control flow exiting the branch CFG term can proceed along two possible edges, e1 and e2, depending on the parity of y in this example. It stands out that the value of the example expression is not 13, as one might have expected, but 11. In other words, the binding of x that is visible in (+ x 10) is the let binding of x and not the CFG-variable binding introduced in the second return CFG term. To understand this, we have to introduce the rule by which the scope of definitions made by both return forms is determined. During backward flow, a CFG term (and, likewise, the result expression) is in the scope of such a CFG variable if and only if a definition of the variable post-dominates the CFG term, that is if every possible control-flow path starting at the CFG term and ending at a CFG term in either return form passes through such a definition. Such a binding does not exist during the original forward flow before its definition. It is visible during backward flow and, when a defer term resumes forward control, enters the corresponding CFG term as part of the same CFG state. In the example above, there is a (statically possible) control-flow path from the entry block to the first return CFG term that does not pass through a definition of the CFG variable x. Compare with the following two examples:

(let ([x 1] [y 2])
  (cfg
      (branch ([(e1) (return [x #f])]
               [(e2) (return [x 3])])
        (if (odd? y) (e1) (e2)))
    (+ x 10)13
(let ([x 1] [y 2])
  (cfg
      (finally ([x 3])
        (branch ([(e1) (return)]
                 [(e2) (return)])
          (if (odd? y) (e1) (e2))))
    (+ x 10)))13

So far, we haven't talked about the formals that appear in a branch CFG term. Using these, we can bind CFG variables during forward control flow. The value to which a CFG variable is bound when control flow leaves along an edge of a branch CFG term is passed as an argument to the tail-called target that corresponds to the edge:

(let ([x 1])
  (cfg
      (branch ([(e x) (return [y (+ x 3)])])
        (e (+ x 2)))
    y)6

In (+ x 2), the variable x is let-bound to 1, and in (+ x 3) the binding as a CFG variable to (+ 1 2) is visible.

While the scoping rule for definitions made by both return forms is based on the post-dominance relation, the scoping rule for definitions made on forward edges is based on the dominance relation: A CFG term is in the scope of a CFG variable if definitions of this variable dominate the CFG term, that is if all possible control flow paths starting at the entry of the whole CFG and ending at the CFG block pass through a definition of the variable, which happens along an edge exiting a branch CFG term. Such a binding remains in the CFG state during backward flow until the CFG term called with that binding returns. A CFG term corresponding to a target of a defer CFG term is called with the CFG state returned by the last CFG term unchanged. The target's formals are ordinary Scheme variables, lexically scoped over that target CFG term; they are not bindings in the CFG state.

In the following example, the definitions of the CFG variable x do not dominate the expression (+ x 3):

(let ([x 1] [y 2])
  (cfg
      (branch ([(e1 x) (return [y (+ x 2)])]
               [(e2) (return [y (+ x 3)])])
        (if (odd? y) (e1 5) (e2)))
    y))4

The expressions in both return forms and in defer CFG terms are evaluated with all CFG variables visible at that point. A later binding shadows an earlier same-named binding, independently of which CFG term made either binding. In the following example, x is the outer Scheme variable bound to 1 in (+ x 1). The forward CFG binding is 2 in (+ x 3). The returned CFG binding is 5 in (+ x 2); the target call passes 7, which is visible as an ordinary Scheme formal in the target term. The subsequent return defines a CFG binding of x to 7, so x is 7 in (+ x 4):

(let ([x 1])
  (cfg
      (branch ([(e x)
                 (defer ([(finish x)
                           (return [x x])])
                     (finish (+ x 2))
                   (return [x (+ x 3)]))])
        (e (+ x 1)))
    (+ x 4)))11

Other CFG terms that can be used to define CFG variables on forward flow are let-values and let. Their syntax is:

(let-values ([formals expression] ...) cfg term)

(let ([variable expression] ...) cfg term)

(let label ([variable expression] ...) cfg term)

When the forward control flow reaches a let-values CFG term, the expressions are evaluated in the same environment in which the expression of a branch CFG term would be evaluated. Then, all formals are bound in parallel, and the CFG-variable bindings become visible in the cfg term, with which the forward control flow proceeds. The let CFG term is the single-value variant of let-values. These terms are analogous to the Scheme expressions with the same names, but bind CFG variables instead of Scheme variables. In the named let form, label is recursively bound in the cfg term, and control initially proceeds to it with the new CFG-variable bindings.

(let ([x 1] [y 2])
  (cfg
      (let-values ([(x y) (values y x)])
        (return-values [(x y) (values x y)]))
    (list x y)))(2 1)

When each expression produces exactly one value, the same bindings can be written with let:

(let ([x 1] [y 2])
  (cfg
      (let ([x y]
            [y x])
        (return-values [(x y) (values x y)]))
    (list x y)))(2 1)

In order to construct more interesting examples, we need to be able to introduce loops in control-flow graphs. This is possible with the use of letrec CFG terms, which have the following syntax:

(letrec ([label cfg term] …) body cfg term)

where each label is an identifier. A letrec CFG term binds the labels to the cfg terms, and this binding is visible within the cfg terms and the body cfg term, much like the semantics of the letrec expression for Scheme code. When (forward) control flow enters a letrec CFG term, the control flow is passed to the body cfg term.

Control can flow to the cfg term using go CFG terms with the corresponding label. The syntax of a go CFG term is:

(go label)

Labels occupy a different namespace than variables (of any kind) or keywords. Labels are lexically scoped.

(let ([x 1])
  (cfg
      (letrec ([x (return [x x])])
        (go x))
    x))1

In the following example (adapted from Olin Shiver's paper), the return CFG term is dominated by definitions of the CFG variable y. When the control flows through the label la, the CFG variable y is bound before going to la; otherwise, when the control flows through the label lb, the CFG variable y is bound before going to lj:

(let ([x 1]
      [f (lambda (y) (set! x y))]
      [g (lambda (y) (set! x (- y 1)))])
  (cfg
      (letrec ([lj (return [y (+ y 10)])]
               [la (branch ([(e1) (go lj)])
                     (begin (f y) (e1)))]
               [lb (branch ([(e1 y) (go lj)])
                     (begin (g x) (e1 (* x x))))])
        (branch ([(e1 y) (go la)]
                 [(e2) (go lb)])
          (if (odd? x)
              (e1 (+ x 1))
              (e2))))
    (list x y)))(2 12)

A CFG term corresponding to a defer target can use go to restart forward flow at a statically bound label:

(let ([retry? #t])
  (cfg
      (letrec ([l
                 (defer ([(again) (go l)]
                          [(done) (return)])
                   (if retry?
                       (begin
                         (set! retry? #f)
                         (again))
                       (done))
                   (return [result 42]))])
        (go l))
    result))42

The last CFG term returns before the expression is evaluated. On its first evaluation, again re-enters the CFG term at the same static label. On the second, done calls the corresponding empty return CFG term, which adds no new CFG-variable bindings, so the binding returned by the last CFG term remains visible.

Let* is another binding construct for CFG labels; it has a similar relation to letrec as Scheme's let* has to Scheme's letrec. In particular, the binding of a label by let* is not visible within the CFG term bound by the label but only further to the right. The syntax of let* does not differ from the letrec syntax:

(let* ([label cfg term] …) body cfg term)

The semantics of let* can be explained through the following rule: Replace all occurrences of (go label) in body cfg term by cfg term when the binding of the corresponding label is not (lexically) shadowed by another label binding.

(cfg
    (let* ([l (return [x 42])]
           [l (go l)])
      (go l))
  x)42

In principle, the same replacement semantics works for letrec but may give an infinite tree in that case.

The next CFG term that needs to be discussed is permute. Its general syntax is:

(permute ([label cfg term] …) body cfg term)

When the forward control flow enters a permute CFG term, the cfg terms together with their labels are dynamically permuted, and control then passes to the then first CFG term. Within the lexical scope of this CFG term, the corresponding label is bound so that when it is jumped to, control proceeds to the then second cfg term and so on until the then final cfg term is reached whose accompanying label is bound so that when called, control finally proceeds to the body cfg term term.

(cfg
    (permute ([p (let ([x 10])
                   (go p))]
              [p (let ([y 20])
                   (go p))])
      (return [z (list x y)]))
  z)(10 20)

So far, the permute CFG term just looks like an inside-out let* form up to the permutation that is involved. The permute CFG term's purpose comes from the non-determinism it introduces. In the above example, the sequencing of the two let CFG terms is not determined, i.e. whether the forward control flow first passes through the lexically first let CFG term and then through the lexically second, or vice versa. In the above example, it does not matter. In general, the non-determinism shows up through the binding of CFG variables. The definition of x by the first let CFG term does not dominate the second let CFG term (more precisely, the CFG term it describes), because a possible control flow (coming from a different sequencing) coming from the entry passes the second CFG term before the first. This is demonstrated in the following two examples:

(let ([x 1])
  (cfg
      (permute ([p (let ([x 2])
                     (go p))]
                [p (let ([y x])
                     (go p))])
        (return [z (list x y)]))
    z))(2 1)
(let ([x 1])
  (cfg
      (permute ([p (finally ([y x])
                       (go p))]
                [p (finally ([x 2])
                       (go p))])
        (return))
    (list x y)))(2 1)

The final CFG term that needs to be discussed is permute/tail. It is like permute except that the permutation property of permute/tail CFG terms also propagates to the bodies of let* and letrec forms and through going to labels:

(let ([x 1] [y 10])
  (cfg
      (permute/tail ([p (let ([x 2])
                          (go p))])
        (letrec ()
          (let* ([p (permute ([p (let ([z (list x y)])
                                     (go p))])
                        (return-values [(x y z) (values x y z)]))])
            (permute/tail ([p (let ([y x])
                                (go p))])
              (go p)))))
    (list x y z)))(2 1 (1 10))

The final concept we need to explain are macros for the cfg form. Like define-syntax is used to define Scheme macros, we use the define-cfg-syntax definition here, which is an ordinary Scheme definition and of the following form:

(define-cfg-syntax cfg keyword transformer expression)

When the expander processes a cfg form, it expands CFG macro uses, which look like Scheme macro uses except that a cfg keyword is used instead of a (Scheme) keyword. CFG macros are expanded by the usual Scheme macro expansion algorithm. An example is worth a thousand words:

(define-cfg-syntax loop
  (lambda (stx)
    (syntax-case stx ()
      [(_ n-expr lp-lbl loop-cfg-term body-cfg-term)
       (identifier? #'lp-lbl)
       #'(let lp-lbl ([n n-expr])
           (branch ([(loop n) loop-cfg-term]
                    [(done) body-cfg-term])
             (if (zero? n)
                 (done)
                 (loop (- n 1)))))])))

(cfg
    (let ([n 0])
      (loop 10 next
          (let ([n (+ n 2)])
            (go next))
        (return [n n])))
  n)20

The example above also demonstrates the usual hygiene provisions of (Scheme) macros; the variable n introduced in the macro is effectively renamed so that it doesn't shadow the variable n in the macro use. This hygiene is also the reason why the label next has to be given as an argument to the macro. One can actually get rid of this as the following example shows:

(define-cfg-label next)
(define-cfg-syntax loop
  (lambda (stx)
    (syntax-case stx ()
      [(_ n-expr loop-cfg-term body-cfg-term)
       #'(let ([n n-expr])
           (letrec ([(next)
                     (branch ([(loop n) loop-cfg-term]
                              [(done) body-cfg-term])
                       (if (zero? n)
                           (done)
                           (loop (- n 1))))])
             (go (next))))])))

(cfg
    (let ([n 0])
      (loop 10
          (let ([n (+ n 2)])
            (go (next)))
        (return [n n])))
  n)20

Here, we used the define-cfg-label definition, whose syntax is simply:

(define-cfg-label free-label)

(Free-label is syntactically an identifier.) This definition binds free-label in the label binding space to a fresh label and (free-label) is replaced by this label in letrec, let*, and go CFG terms where free-label is in scope.

Specification

The identifiers defined in this section are exported by the (srfi :265 cfg) and the (srfi :265) libraries in case of an R6RS system and by the (srfi 265) library in case of an R7RS system.

Remark: This section is a formal account of the syntax and semantics of the cfg form. It should not be misunderstood as a gentle introduction, which was given in the previous section.

Preliminary notions

In order to describe the computation model of the CFG language, which is not the same computation model underlying Scheme (but can be expressed in the latter as the portable implementation shows), a couple of primitive notions have to be introduced, that is the grammatical context in which they are used. Their semantic meaning only follows from the context in which they are used below.

  1. A CFG term is a syntactic construct in the CFG language that can be called within a pending set and with a CFG state. The result of this call is again a CFG state.

  2. A CFG state is a sequence of bindings of CFG variables to Scheme values.

  3. A pending set is a finite set of pairs consisting of a CFG label and a CFG term.

  4. A CFG label is an identifier.

  5. A CFG variable is defined in a CFG term and is associated with an identifier. The target formals of branch and the formals of let-values, as well as the variables of let, define CFG variables on forward flow. The formals of return-values and finally-values, as well as the variables of return and finally, define CFG variables on backward flow. These are definitions of the same kind of variable. The target formals of defer are ordinary, lexically scoped Scheme variables and do not define CFG variables.

  6. A CFG term occurs in the scope of a CFG variable if the CFG term will potentially only be called with CFG states in which the CFG variable is bound.

  7. The visible CFG-variable bindings of a CFG term in a CFG state are those bindings in the CFG state whose CFG variables are in the scope of the CFG term and which are not shadowed by later bindings in the CFG state associated with the same identifier.

By abuse of language, extending an environment by the visible CFG-variable bindings in a CFG state means extending the environment by binding the identifiers associated with the visible CFG-variable bindings to fresh locations initially holding the values to which the CFG variables are bound in the CFG state.

Entry format

Each entry is of one of two categories, “syntax” or “cfg syntax”. The first category is as in R6RS and R7RS, while an entry of the second category describes a syntactic construct in the CFG language.

A label is either a free label reference of the form (free label) or a simple label of the form identifier. The corresponding CFG label is the identifier.

Syntactically, a free label is an identifier.

CFG Expressions

(cfg cfg term result expression)

Syntax: Cfg term is an arbitrary CFG term. Result expression is an arbitrary Scheme expression.

Semantics: During expand-time, the CFG macros (see below) occurring in the cfg term are recursively expanded in lexical order so that no macro uses remain. Then, free label references (see below) are expanded so that only simple labels remain.

A cfg expression is evaluated by first calling the cfg term within an empty pending set and with an empty CFG state. The environment of the cfg expression is then extended by binding the visible CFG variables of the returned CFG state. Finally, the result expression is evaluated in this extended environment and its values returned as the results of the cfg expression.

The simplest cfg expression just returns some value(s). (Both return forms and the other cfg terms are described in the next subsection.) For example:

(cfg (return) 'done)done

Using a return-values cfg term, CFG variables can be bound that can be used in the result expression:

(let ([x 1])
  (cfg (return-values [(x y) (values (+ x 1) (+ x 2))])
    (list x y)))(2 3)

CFG variables can also be bound on the forward edges of branch terms:

(let ([x 1])
  (cfg (branch ([(e x) (return [res x])])
         (e (+ x 1)))
    res))2

Branch CFG terms can have more than one successor. During evaluation, one control path is (dynamically) chosen:

(let ([x 1])
  (cfg (branch ([(e1) (return [res 'even])]
                [(e2 a) (return [res a])])
         (if (even? x) (e1) (e2 'odd)))
    res))odd

Due to the scope of a CFG variable defined on forward flow, the return CFG term in the second branch cannot simply be factored into an enclosing defer CFG term because its Scheme expression is evaluated after the CFG term called with the binding of a has returned, so that variable is no longer in scope:

(let ([a 'outer]
      [x 1])
  (cfg (defer ([(finish res)
                  (return [res res])])
           (finish a)
         (branch ([(e1) (return [res 'even])]
                  [(e2 a) (return)])
           (if (even? x) (e1) (e2 'odd)))
      res))outer

And due to the post-dominance rule for the definition made by return, the enclosing defer cannot simply be left out because the entry block would no longer be post-dominated by a definition of the CFG variable res:

(let ([res 'outer]
      [x 1])
  (cfg (branch ([(e1) (return)]
                [(e2 a) (return [res a])])
         (if (even? x) (e1) (e2 'odd)))
    res))outer

The main reason for defining CFG variables on forward flow is that the CFG language allows writing loops. This can be done particularly conveniently with named let:

(cfg (let f ([x 1] [a 1])
       (branch ([(e1) (return [res a])]
                [(e2 x a) (go f)])
         (if (> x 6)
             (e1)
             (e2 (+ x 1) (* a x)))))
  res)720

Finally, permute and permute/tail CFG terms can be used to create sequences of CFG terms so that CFG variables introduced in these blocks do not have the other blocks of this sequence in scope (comparable to scoping rules of the let expression of Scheme):

(let ([x 'outer] [y 'outer])
  (cfg (let* ([c (permute ([p (finally ([y 'inner])
                                  (let ([a x]) (go p)))])
                     (return [a a]))])
         (permute/tail ([(p) (finally ([b y])
                               (let ([x 'inner]) (go p)))])
           (go c)))
    (list a b)))(outer outer)

(Using permute/tail, permutation sequences carry forward across calls to labels introduced by let* and letrec.)

Primitive CFG terms

The following entries describe the primitive CFG terms. Simple labels do not occupy the same namespace as keywords and all kind of variables. That is, within the same scope, an identifier can be bound as a simple label and as a variable or keyword, and local bindings of either kind do not shadow other bindings of the other kind.

(branch ([(target . formals) cfg term] …) expression)

There must be at least one target. Each target must be an identifier, and the targets must be pairwise distinct. It is a syntax violation if a branch CFG term may potentially be called within a non-empty pending set.

The branch CFG term defines a CFG variable on forward flow for each variable in the formals.

Each target is hygienically bound as a variable whose value is a procedure accepting arguments according to its formals. The region of these bindings is the expression.

When the branch CFG term is called with a CFG state, the environment of the surrounding cfg expression is extended by the visible CFG-variable bindings. The expression is then evaluated in tail context in this extended environment. Exactly one target must be called, and the call must be a tail call; the implementation is not required to detect a violation of this requirement. When a target is called, the CFG state is extended by binding the CFG variables associated with the formals to the arguments of the call, and the corresponding cfg term is then tail-called with the extended CFG state. The new bindings remain in the CFG state during forward flow and on the subsequent backward path until the corresponding CFG term returns, at which point they are removed from the state. The resulting CFG state is then returned.

The branch CFG term potentially calls each cfg term, which is relevant for the definition of scope.

(return-values [formals expression] …)

It is a syntax violation if a return-values CFG term may potentially be called with a non-empty pending set.

The return-values CFG term defines a CFG variable on backward flow for each variable in each formals.

When the return-values CFG term is called with a CFG state, the environment of the surrounding cfg expression is extended by the visible CFG-variable bindings. The expressions are then evaluated in an unspecified order in this extended environment. The CFG-variable bindings defined by the return-values CFG term are not in scope in any of these expressions; an earlier binding associated with the same identifier may nevertheless be visible there. None of the expressions is required to be in tail context. The CFG state is then extended with bindings of the CFG variables associated with each formals to the values received from evaluating the corresponding expression, and the extended state is returned, beginning backward control flow. In particular, (return-values) adds no bindings and returns the state it received.

(letrec ([label cfg term] …) body cfg term)

It is a syntax violation if a label occurs more than once.

Each label is bound to the corresponding cfg term. These bindings are visible in all cfg terms and in the body cfg term and shadow lexically earlier bindings of label there.

When the letrec CFG term is called within a pending set and a CFG state, it tail-calls the body cfg term within the pending set and with this CFG state and the resulting CFG state is returned.

(let* ([label cfg term] …) body cfg term)

Each label is bound to the corresponding cfg term. These bindings are visible in all lexically following cfg terms, respectively, and in the body cfg term and shadow lexically earlier bindings of label there.

When the letrec CFG term is called within a pending set and a CFG state, it tail calls the body cfg term with the pending set and this CFG state and the resulting CFG state is returned.

(go label)

It is an undefined violation if label is not bound as a label.

When the go CFG term is called within a pending set and with a CFG state, the CFG term to which label is bound is tail-called within the pending set and with the CFG state and the resulting CFG state is returned.

(defer ([(target . formals) cfg term] …) expression cfg term)

There must be at least one target. Each target must be an identifier, and the targets must be pairwise distinct. It is a syntax violation if a defer CFG term may potentially be called within a non-empty pending set.

Each variable in a target's formals is an ordinary Scheme variable. Its lexical region is the corresponding cfg term. It is not visible in the expression, the last cfg term, or a CFG term corresponding to another target, and it does not define a CFG variable.

Each target is hygienically bound as a variable whose value is a procedure accepting arguments according to its formals. The region of these bindings is the expression. Exactly one target must be called while evaluating the expression, and the call must be a tail call; the implementation is not required to detect a violation of this requirement.

When the defer CFG term is called with a CFG state, the last cfg term is called within an empty pending set and with this CFG state. This call is not in tail context. After it returns a CFG state, the environment of the surrounding cfg expression is extended by the visible CFG-variable bindings of the returned state. The expression is evaluated in tail context in this environment.

When a target is called, its formals are bound to the arguments according to ordinary Scheme procedure-call semantics, and the corresponding cfg term is called within an empty pending set and with the CFG state returned by the last CFG term unchanged. This call is not in tail context. Thus, bindings made by return-values in the last CFG term enter the corresponding CFG term without having to be passed through the target's formals. Dynamic extents established while evaluating the expression remain active while the corresponding CFG term is called.

When the corresponding CFG term returns, the procedure call finishes and the ordinary lexical bindings of its formals cease to be in scope. The returned CFG state is returned by the defer term. A later binding produced by the corresponding CFG term shadows a visible same-named binding produced by the last CFG term.

The defer CFG term potentially calls the last cfg term and every cfg term corresponding to a target, which is relevant for the definition of scope.

(permute ([label cfg term] …) body cfg term)

When a permute CFG term is called within a pending set and in a CFG state, the [label cfg term] pairs are added to the pending set and the pairs of the pending set are then non-deterministically ordered into a list. All but the last label of the pairs are bound to the cfg term of the following pair and the label of the last pair is bound to the body cfg term. Each such binding of a label is visible in the cfg term of the label's pair and shadows lexically earlier bindings of the label.

The cfg term of the first pair is then tail-called within an empty pending set and with the CFG state and the resulting CFG state is returned.

(permute/tail ([label cfg term] …) body cfg term)

When a permute/tail CFG term is called within a pending set and in a CFG state, the [label cfg term] pairs are added to the pending set. The body cfg term is then tail-called within the extended pending set and with the CFG state and the resulting CFG state is returned.

Tail contexts

If a cfg expression is in tail context, the result expression is in tail context as well.

Derived CFG terms

The following entries describe CFG terms that can be converted into primitive CFG terms.

Although let and let-values are derived CFG terms, their identifiers are already bound as Scheme keywords. An implementation must therefore recognize these bindings directly instead of defining them through define-cfg-syntax.

(return [variable expression] ...)

Effectively equivalent to (return-values [(variable) expression] ...).

(let-values ([formals expression] ...) cfg term)

It is a syntax violation if a let-values term may potentially be called within a non-empty pending set.

The let-values CFG term defines a CFG variable on forward flow for each variable in the formals.

When the let-values CFG term is called with a CFG state, the environment of the surrounding cfg expression is extended by the visible CFG-variable bindings. The expressions are then evaluated in an unspecified order in this extended environment to yield values. The CFG state is then extended by binding the CFG variables associated with the formals to the corresponding values, and the cfg term is then tail-called with the extended CFG state. The new bindings remain in the CFG state until the called CFG term returns, at which point they are removed, and the resulting CFG state is returned.

It is a syntax violation if the variables over all formals are not pairwise different.

(let ([variable expression] ...) cfg term)
(let label ([variable expression] ...) cfg term)

The first form is effectively equivalent to (let-values ([(variable) expression] ...) cfg term).

The second form is effectively equivalent to (letrec ([label cfg term]) (let ([variable expression] ...) (go label))).

(finally-values ([formals expression] ...) cfg term)

It is a syntax violation if a finally-values term may potentially be called within a non-empty pending set.

The finally-values CFG term defines a CFG variable on backward flow for each variable in the formals.

When the finally-values CFG term is called with a CFG state, the cfg term is called within an empty pending set and with that CFG state. This call is not in tail context. When the CFG term returns, the environment of the surrounding cfg expression is extended by the CFG-variable bindings visible in the returned CFG state. The expressions are then evaluated in an unspecified order in this extended environment to yield values. The returned CFG state is extended by binding the CFG variables associated with the formals to the corresponding values, and the extended CFG state is returned. The new bindings are not visible in any of the expressions.

It is a syntax violation if the variables over all formals are not pairwise different.

(finally ([variable expression] ...) cfg term)

Effectively equivalent to (finally-values ([(variable) expression] ...) cfg term).

CFG syntax and label definitions

The Scheme syntax described in this section are definitions, which may appear anywhere other definitions may appear.

Keyword bindings established by these definitions are visible throughout the body in which they appear, except where shadowed by other bindings, and nowhere else, just like variable bindings established by define. All bindings established by a set of definitions are visible within the definitions themselves.

(cfg keyword datum …)
(cfg keyword datum … . datum)
cfg keyword

Before executing a cfg expression, these CFG macro uses are expanded by the syntax expander into core CFG terms just as Scheme macro uses are expanded into core forms. In particular, a CFG transformer is like a Scheme transformer.

(define-cfg-syntax cfg keyword transformer expression)

Binds cfg keyword to the value transformer expression, which must evaluate, at macro-expansion time, to a CFG transformer.

(define-cfg-label free label)

Binds the free label to a fresh label.

Whenever (free label) appears as a label, it is expanded into the fresh label to which it is bound before the containing cfg expression is executed.

The following example defines a CFG keyword that can be used like let to unconditionally bind CFG variables:

(define-cfg-syntax simple-let
  (lambda (stx)
    (syntax-case stx ()
      [(_ ([id init] ...) cfg)
       (for-all identifier? #'(id ...))
       #'(branch ([(e id ...) cfg])
           (e init ...))])))

(cfg (simple-let ([x 1] [y 2])
       (return [res (+ x y)]))
  res)3

A probably more useful CFG macro is the following one:

(define-cfg-syntax return-variables
  (lambda (stx)
    (syntax-case stx ()
      [(_ return-var ...)
       (for-all identifier? #'(return-var ...))
       #'(return [return-var return-var] ...)])))

(cfg (simple-let ([x 1]) (return-variables x))
  x)1

The label definition feature is demonstrated in the following example:

(define-cfg-label p)
(define-syntax permuting
  (lambda (stx)
    (syntax-case stx ()
      [(_ cfg-term ... result-expr)
       #'(cfg (permute ([(p) cfg-term] ...)
                (return [res result-expr]))
           res)])))

(permuting (simple-let ([x 99]) (go (p))) x)99

Implementation

The sample implementation is a portable R6RS implementation written without knowledge of Olin Shiver's original code.

The sample implementation in the git repository is configured for Chez Scheme.

Git repository for the sample implementation.

Acknowledgements

This SRFI would not exist if there hadn't been Olin Shiver's paper The Anatomy of a Loop. In fact, almost all of the mental effort needed for this SRFI was already provided by him. This does not imply that he does or does not endorse this SRFI.

It was Jens Axel Søgaard who reminded me of Olin Shiver's paper, which I had once read but then forgotten about.

During the draft period, Wolfgang Corcoran-Mathe read the specification thoroughly and found a number of small bugs and inconsistencies, which could then be fixed.

This SRFI is a direct descendant of SRFI 242, by the same author.

© 2025 Marc Nieper-Wißkirchen.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Editor: Arthur A. Gleckler