|
| 1 | +package ch05 |
| 2 | + |
| 3 | +enum RegexpT: |
| 4 | + def ++(that: RegexpT): RegexpT = |
| 5 | + Append(this, that) |
| 6 | + |
| 7 | + def orElse(that: RegexpT): RegexpT = |
| 8 | + OrElse(this, that) |
| 9 | + |
| 10 | + def repeat: RegexpT = |
| 11 | + Repeat(this) |
| 12 | + |
| 13 | + def `*`: RegexpT = this.repeat |
| 14 | + |
| 15 | + def matches(input: String): Boolean = |
| 16 | + /* |
| 17 | + Scala's runtimes don't support full tail calls, so calls from a continuation |
| 18 | + to loop or from loop to a continuation will use a stack frame. |
| 19 | + So, instead of making a call, we return a value that reifies the call we want to make. |
| 20 | + This idea is the core of trampolining. |
| 21 | + */ |
| 22 | + // Define a type alias so we can easily write continuations. |
| 23 | + type Continuation = Option[Int] => Call |
| 24 | + |
| 25 | + enum Call: |
| 26 | + case Loop(regexp: RegexpT, index: Int, continuation: Continuation) |
| 27 | + case Continue(index: Option[Int], continuation: Continuation) |
| 28 | + case Done(index: Option[Int]) |
| 29 | + |
| 30 | + def loop(regexp: RegexpT, idx: Int, cont: Continuation): Call = |
| 31 | + regexp match |
| 32 | + case Append(left, right) => |
| 33 | + val k: Continuation = _ match |
| 34 | + case None => Call.Continue(None, cont) |
| 35 | + case Some(i) => Call.Loop(right, i, cont) |
| 36 | + Call.Loop(left, idx, k) |
| 37 | + |
| 38 | + case OrElse(first, second) => |
| 39 | + val k: Continuation = _ match |
| 40 | + case None => Call.Loop(second, idx, cont) |
| 41 | + case some => Call.Continue(some, cont) |
| 42 | + Call.Loop(first, idx, k) |
| 43 | + |
| 44 | + case Repeat(source) => |
| 45 | + val k: Continuation = |
| 46 | + _ match |
| 47 | + case None => Call.Continue(Some(idx), cont) |
| 48 | + case Some(i) => Call.Loop(regexp, i, cont) |
| 49 | + Call.Loop(source, idx, k) |
| 50 | + |
| 51 | + // The following could directly call 'cont' with the Option |
| 52 | + // if Scala had support for full tail calls. |
| 53 | + case Apply(string) => |
| 54 | + Call.Continue( |
| 55 | + Option.when(input.startsWith(string, idx))(idx + string.size), |
| 56 | + cont |
| 57 | + ) |
| 58 | + |
| 59 | + case Empty => |
| 60 | + Call.Continue(None, cont) |
| 61 | + |
| 62 | + def trampoline(next: Call): Option[Int] = |
| 63 | + next match |
| 64 | + case Call.Loop(regexp, index, continuation) => |
| 65 | + trampoline(loop(regexp, index, continuation)) |
| 66 | + case Call.Continue(index, continuation) => |
| 67 | + trampoline(continuation(index)) |
| 68 | + case Call.Done(index) => index |
| 69 | + |
| 70 | + // Check we matched the entire input |
| 71 | + trampoline(loop(this, 0, Call.Done(_))) |
| 72 | + .map(_ == input.size) |
| 73 | + .getOrElse(false) |
| 74 | + |
| 75 | + case Append(left: RegexpT, right: RegexpT) |
| 76 | + case OrElse(first: RegexpT, second: RegexpT) |
| 77 | + case Repeat(source: RegexpT) |
| 78 | + case Apply(string: String) |
| 79 | + case Empty |
| 80 | + |
| 81 | +object RegexpT: |
| 82 | + val empty: RegexpT = Empty |
| 83 | + |
| 84 | + def apply(string: String): RegexpT = |
| 85 | + Apply(string) |
0 commit comments