Skip to content

Commit fc56707

Browse files
committed
Merge branch 'bridge-macro' into staging/5.0
2 parents 444e869 + 0fde2e4 commit fc56707

17 files changed

Lines changed: 870 additions & 504 deletions

File tree

docs/api-guide/directory.conf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ laika.navigationOrder = [
22
Parsley.md
33
character.md
44
combinator.md
5-
generic.md
5+
templates.md
6+
macros.md
67
syntax.md
78
position.md
89
state.md

docs/api-guide/macros.md

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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

Comments
 (0)