8.12

5.4 Binding Low-Level Protocol🔗ℹ

A binding form using the low-level protocol has four parts:

The first of these functions, which produces static information, is always called first, and its results might be useful to the last three. Operationally, parsing a binding form gets the first function, and then that function reports the other three along with the static information that it computes.

To make binding work both in definition contexts and match search contexts, the check-generating function (second bullet above) must be parameterized over the handling of branches. Toward that end, it receives three extra arguments: the name of an if-like form that we’ll call IF, a success form, and a failure form. The transformer uses the given IF to branch to a block that includes success or just failure. The IF form must be used in tail position with respect to the generated code, where the “then” part of an IF is still in tail position for nesting. The transformer must use failure and only failure in the “else” part of each IF, and it must use success exactly once within a “then” branch of one or more nested IFs.

Unfortunately, there’s one more complication. The result of a macro must be represented as syntax—even a binding macro—and a functions as a first-class compile-time value should not be used as syntax. (Such representation are sometimes called “3-D syntax,” and they’re best avoided.) So, a low-level binding macro must uses a defunctionalized representation of functions. That is, a parsed binding reports a function name for its static-information compile-time function, plus data to be passed to that function, and those two parts form a closure. Among the static-information function’s returns are names for a match-generator, commit-generator, and binding-generator function, plus data to be passed to those functions (effectively: the fused closure for those three functions).

In full detail, a low-level parsed binding result from bind.macro transformer is represented as a syntax object with two parts:

These two pieces are assembled into a parenthesized-tuple syntax object, and then packed with the bind_meta.pack function to turn it into a valid binding expansion (to distinguish the result from a macro expansion in the sense of producing another binding form).

The function bound with bind.infoer will receive two syntax objects: a representation of “downward” static information and the parsed binding’s data. The result must be a single-object tuple with the following parts:

The functions bound with bind.matcher, bind.committer, and bind.binder are called with a syntax-object identifier for the matcher’s input plus the data from the sixth tuple slot. The match-building transformer in addition receives the IF form name, a success form, and a failure form.

Here’s a use of the low-level protocol to implement a fruit pattern, which matches only things that are fruits according to is_fruit:

import:

  rhombus/meta open

bind.macro 'fruit($id)':

  bind_meta.pack('(fruit_infoer,

                   // remember the id:

                   $id)')

bind.infoer 'fruit_infoer($static_info, $id)':

  '("matching(fruit(_))",

    $id,

    // no overall static info:

    (),

    // `id` is bound, `0` means usable as expression, no static info:

    (($id, [0], ())),

    fruit_matcher,

    fruit_committer,

    fruit_binder,

    // binder needs id:

    $id)'

bind.matcher 'fruit_matcher($arg, $id, $IF, $success, $failure)':

  '$IF is_fruit($arg)

   | $success

   | $failure'

bind.binder 'fruit_committer($arg, $id)':

  ''

bind.binder 'fruit_binder($arg, $id)':

  'def $id: $arg'

fun is_fruit(v):

  v == "apple" || v == "banana"

> def fruit(snack) = "apple"

> snack

"apple"

> def fruit(dessert) = "cookie"

def: value does not satisfy annotation

  value: "cookie"

  annotation: matching(fruit(_))

The fruit binding form assumes (without directly checking) that its argument is an identifier, and its infoer discards static information. Binding forms normally need to accomodate other, nested binding forms, instead. A bind.macro transformer with can receive already-parsed sub-bindings as arguments, and the infoer function can use bind_meta.get_info on a parsed binding form to call its internal infoer function. The result is packed static information, which can be unpacked into a tuple syntax object with bind_meta.unpack_info. Normally, bind_meta.get_info should be called only once to avoid exponential work with nested bindings, but bind_meta.unpack_info can used any number of times.

As an example, here’s an infix <&> operator that is similar to &&. It takes two bindings and makes sure a value can be matched to both. The binding forms on either size of <&> can bind variables. The <&> builder is responsible for binding the input name that each sub-binding expects before it deploys the corresponding builder. The only way to find out if a sub-binding matches is to call its builder, providing the same IF and failure that the original builder was given, and possibly extending the success form. A builder must be used in tail position, and it’s success position is a tail position.

bind.macro '$a <&> $b':

  bind_meta.pack('(anding_infoer,

                   ($a, $b))')

bind.infoer 'anding_infoer($static_info, ($a, $b))':

  let a_info = bind_meta.get_info(a, static_info)

  let b_info = bind_meta.get_info(b, static_info)

  def '($a_ann, $a_name, ($a_s_info, ...), ($a_var_info, ...),

        $_, $_, $_, $_)':

    bind_meta.unpack_info(a_info)

  let '($b_ann, $b_name, ($b_s_info, ...), ($b_var_info, ...),

        $_, $_, $_, $_)':

    bind_meta.unpack_info(b_info)

  let ann:

    "and("

      +& Syntax.unwrap(a_ann) +& ", " +& Syntax.unwrap(b_ann)

      +& ")"

  '($ann,

    $a_name,

    ($a_s_info, ..., $b_s_info, ...),

    ($a_var_info, ..., $b_var_info, ...),

    anding_matcher,

    anding_committer,

    anding_binder,

    ($a_info, $b_info))'

bind.matcher 'anding_matcher($in_id, ($a_info, $b_info),

                             $IF, $success, $failure)':

  let '($_, $_, $_, $_,

        $a_matcher, $_, $_, $a_data)':

    bind_meta.unpack_info(a_info)

  let '($_, $_, $_, $_,

        $b_matcher, $_, $_, $b_data)':

    bind_meta.unpack_info(b_info)

  '$a_matcher($in_id, $a_data, $IF,

              $b_matcher($in_id, $b_data, $IF, $success, $failure),

              $failure)'

bind.committer 'anding_committer($in_id, ($a_info, $b_info))':

  let '($_, $_, $_, $_,

        $_, $a_committer, $_, $a_data)':

    bind_meta.unpack_info(a_info)

  let '($_, $_, $_, $_,

        $_, $b_committer, $_, $b_data)':

    bind_meta.unpack_info(b_info)

  '$a_committer($in_id, $a_data)

   $b_committer($in_id, $b_data)'

bind.binder 'anding_binder($in_id, ($a_info, $b_info))':

  let '($_, $_, $_, $_,

        $_, $_, $a_binder, $a_data)':

    bind_meta.unpack_info(a_info)

  let '($_, $_, $_, $_,

        $_, $_, $b_binder, $b_data)':

    bind_meta.unpack_info(b_info)

  '$a_binder($in_id, $a_data)

   $b_binder($in_id, $b_data)'

> def one <&> 1 = 1

> one

1

> def two <&> 1 = 2

def: value does not satisfy annotation

  value: 2

  annotation: and(Any, matching(1))

class Posn(x, y)

> def Posn(0, y) <&> Posn(x, 1) = Posn(0, 1)

> x

0

> y

1

One subtlety here is the syntactic category of IF for a builder call. The IF form might be a definition form, or it might be an expression form, and a builder is expected to work in either case, so a builder call’s category is the same as IF. An IF alternative is written as a block, as is a success form, but the block may be inlined into a definition context.

The <&> infoer is able to just combine any names and “upward” static information that receives from its argument bindings, and it can simply propagate “downward” static information. When a binding operator reflects a composite value with separate binding forms for component values, then upward and downward information needs to be adjusted accordingly.