|
| 1 | +{% |
| 2 | +laika.versioned = true |
| 3 | +laika.title = "`parsley.macros`" |
| 4 | +parsley.tabname = "Macro Bridges (parsley.macros)" |
| 5 | +laika.site.metadata.description = "This page describes how to use macro bridges to factor code." |
| 6 | +%} |
| 7 | + |
| 8 | +# Macro Bridges (`parsley.macros`, Scala 3) |
| 9 | +The *Parser Bridge* pattern is a technique for decoupling semantic actions from the parser itself. |
| 10 | +The `parsley.macros` package contains a macro that can handle generation of many kinds of bridge |
| 11 | +automatically to help you to get started using the technique straight away if you wish. |
| 12 | + |
| 13 | +@:callout(info) |
| 14 | +*The Scaladoc for this page can be found at [`parsley.macros`](@:api(parsley.macros)).* |
| 15 | +@:@ |
| 16 | + |
| 17 | +## Motivation |
| 18 | +The construction of parser bridges is something that involves a certain amount of boilerplate, |
| 19 | +despite the benefits they provide. The [`parsley.templates`](templates.md) package demonstrates |
| 20 | +one way in which this can be mitigated, producing templating bridge traits which can be mixed |
| 21 | +into a provider of an `apply` on values in exchange for one on parsers. |
| 22 | + |
| 23 | +However, to avoid making the library too opinionated, the `templates` package only exposes |
| 24 | +generators for *pure* parser bridges, which are those that do not incorporate additional metadata. |
| 25 | +To deal with metadata, like positions, it is necessary to define your own template bridge traits. |
| 26 | +In addition, there is still a small amount of boilerplate to be had when using these template traits. |
| 27 | + |
| 28 | +As an example: |
| 29 | + |
| 30 | +```scala mdoc:silent |
| 31 | +import parsley.Parsley |
| 32 | +import parsley.templates.PureParserBridge2 |
| 33 | +import parsley.position.{line, col} |
| 34 | + |
| 35 | +case class Pos(line: Int, col: Int) |
| 36 | +object Pos extends PureParserBridge2[Int, Int, Pos] |
| 37 | + |
| 38 | +val pos: Parsley[Pos] = Pos(line, col) |
| 39 | +``` |
| 40 | + |
| 41 | +Here, we can see a few areas of boilerplate that are not needed: |
| 42 | + |
| 43 | +* There is a mirroring between the types of the arguments to `Pos` and the type parameters for |
| 44 | + `PureParserBridge2`: this has to be kept in sync, which will be enforced by the compiler, but |
| 45 | + it is annoying to change something in two places. We also have to manually ensure the bridge has |
| 46 | + the correct arity, in this case `2`. |
| 47 | +* The strategy for the bridge, namely to use a `pure`-based semantics, is hard-coded in the |
| 48 | + bridge template. If we wanted to add some metadata (more on that later), we would have to remember |
| 49 | + to also adjust the `PureParserBridge2` to be something else. |
| 50 | +* We necessarily have to introduce the companion object, where perhaps we didn't need to before. |
| 51 | + The bridge must be kept in the same file as the bridged class otherwise we will need to implement |
| 52 | + the `apply` ourselves. |
| 53 | + |
| 54 | +To help mitigate this, Scala 3 `parsley` supports a macro-driven bridge generation mechanism that |
| 55 | +addresses the above three points, and provides other functionality on top of them. |
| 56 | + |
| 57 | +## The `bridge` Macro |
| 58 | +The `bridge` macro is exposed via `parsley.macros.bridge`. It takes two main forms: the first is |
| 59 | +`bridge[T]`, which creates a bridge for the type `T`, and `bridge[T, S >: T]`, which constructs a |
| 60 | +bridge for the type `T`, but upcasts the result to construct `S` instead (which is a supertype of `T`). |
| 61 | +To see how easy this is to use, here is the previous example again: |
| 62 | + |
| 63 | +```scala mdoc:silent |
| 64 | +import parsley.macros.bridge |
| 65 | +val Pos2 = bridge[Pos] |
| 66 | + |
| 67 | +val pos2 = Pos2(line, col) |
| 68 | +``` |
| 69 | + |
| 70 | +Here, `Pos2` works the same way as our `Pos` bridge from before, but now all of the three painpoints |
| 71 | +from above have been addressed. In fact, it is now cleaner to separate the class definition and |
| 72 | +bridge definitions in different scopes! That is it! |
| 73 | + |
| 74 | +To be a bit more specific, `bridge` is what is called a *transparent* macro in Scala, which means |
| 75 | +it can return a different type depending on what the macro internally generated. In our case, the |
| 76 | +macro will return something derived from `bridges.ErrorBridge`, so in this case |
| 77 | +`Pos2: bridges.ParserBridge2[Int, Int, Pos]`. If we add or remove arguments, this will automatically |
| 78 | +change the type generated by the macro, so type inference will sort out the boilerplate. |
| 79 | + |
| 80 | +The `bridge` macro works on pretty much anything you can throw at it: |
| 81 | + |
| 82 | +@:todo(seems to be an mdoc bug here on the enum bridges) |
| 83 | +```scala |
| 84 | +case class A(x: Int) |
| 85 | +object B |
| 86 | +enum C { |
| 87 | + case X(x: Int) |
| 88 | + case Y |
| 89 | +} |
| 90 | + |
| 91 | +val a = bridge[A] |
| 92 | +val b = bridge[B.type] |
| 93 | +val x = bridge[C.X, C] // the C is optional here, but I prefer to hide enum cases. |
| 94 | +val y = bridge[C.Y.type, C] // the C is optional here, but I prefer to hide enum cases. |
| 95 | +``` |
| 96 | + |
| 97 | +This also includes parameterless classes, which always produce `ParserSingletonBridge`s: |
| 98 | + |
| 99 | +```scala mdoc:silent |
| 100 | +class Foo() |
| 101 | +class Bar |
| 102 | + |
| 103 | +val foo = bridge[Foo] |
| 104 | +val bar = bridge[Bar] |
| 105 | +``` |
| 106 | + |
| 107 | +Interestingly, for these, we don't treat them the same way we would `object`s, which would always |
| 108 | +return the same thing. Instead, we will actually create a fresh instance (with the `fresh` combinator) |
| 109 | +every time they are parsed: |
| 110 | + |
| 111 | +```scala mdoc |
| 112 | +import parsley.character.item |
| 113 | +import parsley.syntax.zipped.* |
| 114 | +(foo.from(item), foo.from(item)).zipped(_ eq _).parse("aa") |
| 115 | +(bar.from(item), bar.from(item)).zipped(_ eq _).parse("aa") |
| 116 | +``` |
| 117 | + |
| 118 | +This is not something that the pure templated bridge traits can achieve. |
| 119 | + |
| 120 | +### Default Parameters |
| 121 | +Some classes might have default parameters that are not relevant to the parser, but that will |
| 122 | +be relevant for wherever that data is consumed. For example, suppose we are parsing identifiers, |
| 123 | +which will later be assigned a type: during parse time, there is clearly no type to be assigned, |
| 124 | +so it would be good if we could "forget" this for the sake of clean parsing. The `bridge` macro can |
| 125 | +handle this: |
| 126 | + |
| 127 | +```scala mdoc:silent |
| 128 | +enum Type { case NoType } |
| 129 | + |
| 130 | +case class Ident(name: String)(val ty: Type = Type.NoType) |
| 131 | + |
| 132 | +val ident: parsley.bridges.ParserBridge1[String, Ident] = bridge[Ident] |
| 133 | +``` |
| 134 | + |
| 135 | +Here, the `ident` bridge only needs a parser for the `String`, and will place `Type.NoType` into |
| 136 | +the `Ident` as it constructs it. A limitation of this currently is that default parameters must not |
| 137 | +be in the first set of constructor parameters for the class (i.e. notice that constructor is curried). |
| 138 | +This can be lifted in future. |
| 139 | + |
| 140 | +### Metadata Parsing |
| 141 | +One area which the template traits fall short is that you need to construct new templates by hand |
| 142 | +when you want to do anything other than pure bridging. This is because there is a vast design space |
| 143 | +now possible. |
| 144 | + |
| 145 | +Let's suppose we wanted to add position information to our `Ident` class above. Do we want it |
| 146 | +to go as the first parameter? in the second set of parameters? At the end of the first set? So |
| 147 | +many choices are possible here, and so `parsley` doesn't provide any default template trait for it. |
| 148 | +However, the `bridge` macro does have the ability to handle this as long as the user annotates exactly |
| 149 | +what is metadata: |
| 150 | + |
| 151 | +```scala mdoc:nest:fail |
| 152 | +import parsley.macros.{bridge, isMeta} |
| 153 | + |
| 154 | +case class Ident(name: String)(@isMeta val pos: Pos, val ty: Type = Type.NoType) |
| 155 | + |
| 156 | +val ident = bridge[Ident] |
| 157 | +``` |
| 158 | + |
| 159 | +As you can see from the above error message, when we use `@isMeta` to denote something is |
| 160 | +parser-produced metadata, we need to tell `parsley` how we expect it to be parsed. This is done |
| 161 | +via the `ParsableMetadata` typeclass. Let's create one of those: |
| 162 | + |
| 163 | +```scala mdoc:invisible |
| 164 | +import parsley.macros.ParsableMetadata |
| 165 | + |
| 166 | +given ParsableMetadata[Pos] with { |
| 167 | + val meta = pos |
| 168 | +} |
| 169 | +``` |
| 170 | +```scala |
| 171 | +import parsley.macros.ParsableMetadata |
| 172 | + |
| 173 | +given ParsableMetadata[Pos] { |
| 174 | + val meta = pos |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +By doing this, we provide a `meta` parser, which is then what the `bridge` macro will use to |
| 179 | +parse the metadata to add into the class later. By default, `parsley` provides a `ParsableMetadata[(Int, Int)]` |
| 180 | +instance for line and column pairs. |
| 181 | + |
| 182 | +```scala mdoc:silent |
| 183 | +import parsley.macros.{bridge, isMeta} |
| 184 | + |
| 185 | +case class Ident(name: String)(@isMeta val pos: Pos, val ty: Type = Type.NoType) |
| 186 | + |
| 187 | +val ident = bridge[Ident] |
| 188 | +``` |
| 189 | + |
| 190 | +This now works. The order in which this metadata is parsed, relative to the `name` parser, will |
| 191 | +*always* be first. If there is more than one `@isMeta` used, they will be parsed in order of |
| 192 | +appearance, ahead of all other arguments. It remains future work to allow for an annotation that |
| 193 | +allows us to denote after which bridge parameter the metadata is parsed. |
| 194 | + |
| 195 | +### Polymorpic Bridges |
| 196 | +When using template bridge traits, one limitation is that if the class you want to bridge is |
| 197 | +polymorphic, the bridge traits can't be mixed into the companion object! Suppose you tried: |
| 198 | + |
| 199 | +```scala mdoc:fail |
| 200 | +class Poly[A](val x: A) |
| 201 | +object Poly extends parsley.templates.PureParserBridge1[A, Poly[A]] |
| 202 | +``` |
| 203 | + |
| 204 | +Oops! Ok, so what if we just fix `A` to be a specific type? |
| 205 | + |
| 206 | +```scala mdoc:fail |
| 207 | +class Poly[A](val x: A) |
| 208 | +object Poly extends parsley.templates.PureParserBridge1[Int, Poly[Int]] |
| 209 | +``` |
| 210 | + |
| 211 | +It wants an `apply` in the companion object that works on `Int`, but obviously the |
| 212 | +one it sees works generically on `A`. Basically, to use the templates here, we need to |
| 213 | +make it its own object, which also needs implementing `apply` by hand. |
| 214 | + |
| 215 | +Thankfully, `bridge` is undeterred: |
| 216 | + |
| 217 | +```scala mdoc:silent |
| 218 | +class Poly[A](val x: A) |
| 219 | + |
| 220 | +def polyA[A] = bridge[Poly[A]] |
| 221 | +val polyInt = bridge[Poly[Int]] |
| 222 | +``` |
| 223 | + |
| 224 | +No problems either way. In fact, this even works with the `@isMeta` annotation: |
| 225 | + |
| 226 | +```scala mdoc:silent |
| 227 | +class PolyMeta[A](@isMeta val x: A) |
| 228 | + |
| 229 | +def polyMetaA[A: ParsableMetadata] = bridge[PolyMeta[A]] |
| 230 | +val polyMetaPos = bridge[PolyMeta[Pos]] |
| 231 | +``` |
| 232 | + |
| 233 | +### Error Messages |
| 234 | +The parser bridge traits all extend an `ErrorBridge`, which allows for the integration of |
| 235 | +labels and reasons into bridges. The template bridge traits inherit these, so the extender of the |
| 236 | +trait can override `labels` and `reason` and get the right behaviours while keeping them out of the |
| 237 | +parser. This is nice, and the macro supports it under a slightly different mechanism. |
| 238 | + |
| 239 | +The `bridge` we've seen so far is actually not a function, but an object with an `apply[T]` and |
| 240 | +`apply[T, S]`: let's call this a *bridge synthesiser*. It also has two other methods: `label` and `explain`, |
| 241 | +which themselves return a new bridge synthesiser that can incorporate those components. |
| 242 | +If you use `label`, the synthesiser it returns does not itself have a `label` method, but does have |
| 243 | +an `explain` (and vice-versa), so you can build these up "Builder Pattern"-style. As an example: |
| 244 | + |
| 245 | +```scala mdoc:silent |
| 246 | +import parsley.character.digit |
| 247 | +case class Num(n: Int) |
| 248 | + |
| 249 | +val num = bridge.label("number").explain("numbers are made of 1 or more digits")[Num] |
| 250 | +``` |
| 251 | +```scala mdoc:to-string |
| 252 | +num(digit.foldLeft1(0)((n, d) => n * 10 + d.asDigit)).parse("a") |
| 253 | +``` |
| 254 | + |
| 255 | +## Limitations |
| 256 | +Unfortunately, the bridge macro is not perfect. There are still some areas where |
| 257 | +the template bridge traits manage to allow slightly more flexibility. The key is that |
| 258 | +the bridge macro *forces* a specific shape of bridge using all the provided arguments |
| 259 | +to the given types primary constructor as-is. Any modifications you might want to make to these, |
| 260 | +perhaps to adapt to another type, will not be picked up. |
| 261 | + |
| 262 | +For example, *disambiguation bridges* (as described in other pages) involve processing data into |
| 263 | +potentially different sibling types. The bridge macro can't figure this out, so that's a use-case |
| 264 | +that is ruled out. |
| 265 | + |
| 266 | +As another example, *validation bridges* need to inject filtering logic into the bridge, which is |
| 267 | +not currently supported. However, this is something that I believe can be made to work (unlike |
| 268 | +the disambiguation bridges above), so this may be supported by some more synthesisers at a later |
| 269 | +point. |
0 commit comments