8.18

5.4 Syntax Tracking🔗ℹ

As macros in a space are expanded, the resulting syntax object acquires an #'origin property to track the history of expansion. Specifically, when the expander dispatches to a macro bound to name, then the result of the macro gets an #'origin property with the use-site name identifier. If a result already has an #'origin property, then the existing value is combined with name using Pair, and an #'origin property value is in general a tree of identifiers. This information is used by DrRacket, for example, to draw binding arrows from uses or names to definitions of names.

When a new macro is defined using a form like annot.macro, bind.macro, or one bound using macro_definer in space.enforest, then the macro may receive arguments that are automatically parsed, depending on the shape of the macro pattern. Specifically, the left-hand argument is always parsed for an infix macro, while the right-hand side is always parsed if its pattern is $ followed by an identifier. A macro automatically propagates an #'origin property value from each automatically parsed argument to the macro’s result. The macro can propagate other #'origin properties explicitly using Syntax.track_origin. That kind of propagation is not needed, however, if a macro expands to a use of other forms that already handle expansion tracking.

For example, the following prefix variant of || does not need specific tracking. Even though its a and b pattern bindings are not automatically parsed, the terms are intact in the resulting syntax, and || will track appropriately, and binding arrows can be drawn for either, String and Int:

annot.macro 'either($a, $b)':

  '($a) || ($b)'

 

1 :: either(String, Int)

The following infix macro receives already-parsed arguments, so even though it pulls apart its arguments and creates a new packed representation of the annotation, the macro expander will record the connection to the a and b expansions:

annot.macro '$a => $b':

  let (a_pred, a_statinfo) = annot_meta.unpack_predicate(a)

  let (b_pred, b_statinfo) = annot_meta.unpack_predicate(b)

  annot_meta.pack_predicate(

    'fun (v): if $a_pred(v) | $b_pred(v) | #true',

    '()'

  )

 

1 :: Int => PosInt

"a" :: Int => PosInt

The following macro needs to use Syntax.track_origin, otherwise no biding arrows will be shown for Int and PosInt in the use of implies:

annot.macro 'implies($(a :: annot_meta.Parsed),

                     $(b :: annot_meta.Parsed))':

  let (a_pred, a_statinfo) = annot_meta.unpack_predicate(a)

  let (b_pred, b_statinfo) = annot_meta.unpack_predicate(b)

  annot_meta.pack_predicate(

    'fun (v): if $a_pred(v) | $b_pred(v) | #true',

    '()'

  ).track_origin([a, b])

 

1 :: implies(Int, PosInt)

Since tracking is often needed when using a packing function like annot_meta.pack_predicate, annot_meta.pack_predicate accepts a ~track argument that provides a slight shorthand and as a reminder to consider the need for tracking.

The annotation examples above show macros working within one space. Tracking is practically always needed when bridging spaces. For example, the rx impleemntation parses a subsequent regexp form, and it uses Syntax.track_origin to connect a returned expression to parsed regexp. Ultimately, all #'origin information must be attached to an expansion or definition form, since those are the only primitive forms. Tracking in expressions is somewhat special, meanwhile, because subexpressions appear intact within an enclosing expression, so origin information does not need to be explicitly lifted to the enclosing expression.