summaryrefslogtreecommitdiff
path: root/src/interactive/scala/tools/nsc/interactive/Pickler.scala
blob: ffd3b7bc642e148b728e1f02bca3ff325f0b2736 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
package scala.tools.nsc.interactive

import Lexer._
import java.io.Writer

/** An abstract class for writing and reading Scala objects to and
 *  from a legible representation. The representation follows the following grammar:
 *  {{{
 *  Pickled = `true` | `false` | `null` | NumericLit | StringLit |
 *            Labelled | Pickled `,` Pickled
 *  Labelled = StringLit `(` Pickled? `)`
 *  }}}
 *
 *  All ...Lit classes are as in JSON. @see scala.tools.nsc.io.Lexer
 *
 *  Subclasses of `Pickler` each can write and read individual classes
 *  of values.
 *
 *  @tparam  T   the type of values handled by this pickler.
 *
 *  These Picklers build on the work of Andrew Kennedy. They are most closely inspired by
 *  Iulian Dragos' picklers for Scala to XML. See:
 *
 *  <a href="http://code.google.com/p/gdata-scala-client/wiki/DevelopersGuide">
 *  http://code.google.com/p/gdata-scala-client/wiki/DevelopersGuide
 *  </a>
 */
abstract class Pickler[T] {

  import Pickler._

  /** Writes value in pickled form
   *  @param  wr   the writer to which pickled form is written
   *  @param  x    the value to write
   */
  def pickle(wr: Writer, x: T)

  /** Reads value from pickled form.
   *
   *  @param  rd   the lexer from which lexemes are read
   *  @return An `UnpickleSuccess value if the current input corresponds to the
   *          kind of value that is unpickled by the current subclass of `Pickler`,
   *          an `UnpickleFailure` value otherwise.
   *  @throws  `Lexer.MalformedInput` if input is invalid, or if
   *          an `Unpickle
   */
  def unpickle(rd: Lexer): Unpickled[T]

  /** A pickler representing a `~`-pair of values as two consecutive pickled
   *  strings, separated by a comma.
   *  @param  that   the second pickler which together with the current pickler makes
   *                 up the pair `this ~ that` to be pickled.
   */
  def ~ [U] (that: => Pickler[U]): Pickler[T ~ U] = seqPickler(this, that)

  /** A pickler that adds a label to the current pickler, using the representation
   *   `label ( <current pickler> )`
   *
   *  @label  the string to be added as a label.
   */
  def labelled(label: String): Pickler[T] = labelledPickler(label, this)

  /** A pickler obtained from the current pickler by a pair of transformer functions
   *  @param   in   the function that maps values handled by the current pickler to
   *                values handled by the wrapped pickler.
   *  @param   out  the function that maps values handled by the wrapped pickler to
   *                values handled by the current pickler.
   */
  def wrapped [U] (in: T => U)(out: U => T): Pickler[U] = wrappedPickler(this)(in)(out)

  /** A conditional pickler obtained from the current pickler.
   *  @param   p   the condition to test to find out whether pickler can handle
   *               some Scala value.
   */
  def cond(p: Any => Boolean): CondPickler[T] = conditionalPickler(this, p)

  /** A conditional pickler handling values of some Scala class. It adds the
   *  class name as a label to the representation of the current pickler and
   *  @param    c     the class of values handled by this pickler.
   */
  def asClass[U <: T](c: Class[U]): CondPickler[T] = this.labelled(c.getName).cond(c isInstance _)
}

object Pickler {
  /** A base class representing unpickler result. It has two subclasses:
   *  `UnpickleSuccess` for successful unpicklings and `UnpickleFailure` for failures,
   *  where a value of the given type `T` could not be unpickled from input.
   *  @tparam  T the type of unpickled values in case of success.
   */
  abstract class Unpickled[+T] {
    /** Transforms success values to success values using given function,
     *  leaves failures alone
     *  @param   f the function to apply.
     */
    def map[U](f: T => U): Unpickled[U] = this match {
      case UnpickleSuccess(x) => UnpickleSuccess(f(x))
      case f: UnpickleFailure => f
    }
    /** Transforms success values to successes or failures using given function,
     *  leaves failures alone.
     *  @param   f the function to apply.
     */
    def flatMap[U](f: T => Unpickled[U]): Unpickled[U] = this match {
      case UnpickleSuccess(x) => f(x)
      case f: UnpickleFailure => f
    }
    /** Tries alternate expression if current result is a failure
     *  @param alt  the alternate expression to be tried in case of failure
     */
    def orElse[U >: T](alt: => Unpickled[U]): Unpickled[U] = this match {
      case UnpickleSuccess(x) => this
      case f: UnpickleFailure => alt
    }

    /** Transforms failures into thrown `MalformedInput` exceptions.
     *  @throws  MalformedInput   if current result is a failure
     */
    def requireSuccess: UnpickleSuccess[T] = this match {
      case s @ UnpickleSuccess(x) => s
      case f: UnpickleFailure =>
        throw new MalformedInput(f.rd, "Unrecoverable unpickle failure:\n"+f.errMsg)
    }
  }

  /** A class representing successful unpicklings
   *  @tparam T       the type of the unpickled value
   *  @param result   the unpickled value
   */
  case class UnpickleSuccess[+T](result: T) extends Unpickled[T]

  /** A class representing unpickle failures
   *  @param msg      an error message describing what failed.
   *  @param rd       the lexer unpickled values were read from (can be used to get
   *                  error position, for instance).
   */
  class UnpickleFailure(msg: => String, val rd: Lexer) extends Unpickled[Nothing] {
    def errMsg = msg
    override def toString = "Failure at "+rd.tokenPos+":\n"+msg
  }

  private def errorExpected(rd: Lexer, msg: => String) =
    new UnpickleFailure("expected: "+msg+"\n" +
                        "found   : "+rd.token,
                        rd)

  private def nextSuccess[T](rd: Lexer, result: T) = {
    rd.nextToken()
    UnpickleSuccess(result)
  }

  /** The implicit `Pickler` value for type `T`. Equivalent to `implicitly[Pickler[T]]`.
   */
  def pkl[T: Pickler] = implicitly[Pickler[T]]

  /** A class representing `~`-pairs */
  case class ~[+S, +T](fst: S, snd: T)

  /** A wrapper class to be able to use `~` s an infix method */
  implicit class TildeDecorator[S](x: S) {
    /** Infix method that forms a `~`-pair. */
    def ~ [T](y: T): S ~ T = new ~ (x, y)
  }

  /** Same as `p.labelled(label)`.
   */
  def labelledPickler[T](label: String, p: Pickler[T]): Pickler[T] = new Pickler[T] {
    def pickle(wr: Writer, x: T) = {
      wr.write(quoted(label))
      wr.write("(")
      p.pickle(wr, x)
      wr.write(")")
    }
    def unpickle(rd: Lexer): Unpickled[T] =
      rd.token match {
        case StringLit(`label`) =>
          rd.nextToken()
          rd.accept('(')
          val result = p.unpickle(rd).requireSuccess
          rd.accept(')')
          result
        case _ =>
          errorExpected(rd, quoted(label)+"(...)")
      }
  }

  /** Same as `p.wrap(in)(out)`
   */
  def wrappedPickler[S, T](p: Pickler[S])(in: S => T)(out: T => S) = new Pickler[T] {
    def pickle(wr: Writer, x: T) = p.pickle(wr, out(x))
    def unpickle(rd: Lexer) = p.unpickle(rd) map in
  }

  /** Same as `p.cond(condition)`
   */
  def conditionalPickler[T](p: Pickler[T], condition: Any => Boolean) = new CondPickler[T](condition) {
    def pickle(wr: Writer, x: T) = p.pickle(wr, x)
    def unpickle(rd: Lexer) = p.unpickle(rd)
  }

  /** Same as `p ~ q`
   */
  def seqPickler[T, U](p: Pickler[T], q: => Pickler[U]) = new Pickler[T ~ U] {
    lazy val qq = q
    def pickle(wr: Writer, x: T ~ U) = {
      p.pickle(wr, x.fst)
      wr.write(',')
      q.pickle(wr, x.snd)
    }
    def unpickle(rd: Lexer) =
      for (x <- p.unpickle(rd); y <- { rd.accept(','); qq.unpickle(rd).requireSuccess })
      yield x ~ y
  }

  /** Same as `p | q`
   */
  def eitherPickler[T, U <: T, V <: T](p: CondPickler[U], q: => CondPickler[V]) =
    new CondPickler[T](x => p.canPickle(x) || q.canPickle(x)) {
      lazy val qq = q
      override def tryPickle(wr: Writer, x: Any): Boolean =
        p.tryPickle(wr, x) || qq.tryPickle(wr, x)
      def pickle(wr: Writer, x: T) =
        require(tryPickle(wr, x),
                "no pickler found for "+x+" of class "+x.getClass.getName)
      def unpickle(rd: Lexer) = p.unpickle(rd) orElse qq.unpickle(rd)
    }

  /** A conditional pickler for singleton objects. It represents these
   *  with the object's underlying class as a label.
   *  Example: Object scala.None would be represented as `scala.None$()`.
   */
  def singletonPickler[T <: AnyRef](x: T): CondPickler[T] =
    unitPickler
      .wrapped { _ => x } { x => () }
      .labelled (x.getClass.getName)
      .cond (x eq _.asInstanceOf[AnyRef])

  /** A pickler the handles instances of classes that have an empty constructor.
   *  It represents than as `$new ( <name of class> )`.
   *  When unpickling, a new instance of the class is created using the empty
   *  constructor of the class via `Class.forName(<name of class>).newInstance()`.
   */
  def javaInstancePickler[T <: AnyRef]: Pickler[T] =
    (stringPickler labelled "$new")
      .wrapped { name => Class.forName(name).newInstance().asInstanceOf[T] } { _.getClass.getName }

  /** A picklers that handles iterators. It pickles all values
   *  returned by an iterator separated by commas.
   *  When unpickling, it always returns an `UnpickleSuccess` containing an iterator.
   *  This iterator returns 0 or more values that are obtained by unpickling
   *  until a closing parenthesis, bracket or brace or the end of input is encountered.
   *
   *  This means that iterator picklers should not be directly followed by `~`
   *  because the pickler would also read any values belonging to the second
   *  part of the `~`-pair.
   *
   *  What's usually done instead is that the iterator pickler is wrapped and labelled
   *  to handle other kinds of sequences.
   */
  implicit def iterPickler[T: Pickler]: Pickler[Iterator[T]] = new Pickler[Iterator[T]] {
    lazy val p = pkl[T]
    def pickle(wr: Writer, xs: Iterator[T]) {
      var first = true
      for (x <- xs) {
        if (first) first = false else wr.write(',')
        p.pickle(wr, x)
      }
    }
    def unpickle(rd: Lexer): Unpickled[Iterator[T]] = UnpickleSuccess(new Iterator[T] {
      var first = true
      def hasNext = {
        val t = rd.token
        t != EOF && t != RParen && t != RBrace && t != RBracket
      }
      def next(): T = {
        if (first) first = false else rd.accept(',')
        p.unpickle(rd).requireSuccess.result
      }
    })
  }

  /** A pickler that handles values that can be represented as a single token.
   *  @param   kind   the kind of token representing the value, used in error messages
   *                  for unpickling.
   *  @param  matcher A partial function from tokens to handled values. Unpickling
   *                  succeeds if the matcher function is defined on the current token.
   */
  private def tokenPickler[T](kind: String)(matcher: PartialFunction[Token, T]) = new Pickler[T] {
    def pickle(wr: Writer, x: T) = wr.write(x.toString)
    def unpickle(rd: Lexer) =
      if (matcher isDefinedAt rd.token) nextSuccess(rd, matcher(rd.token))
      else errorExpected(rd, kind)
  }

  /** A pickler for values of type `Long`, represented as integer literals */
  implicit val longPickler: Pickler[Long] =
    tokenPickler("integer literal") { case IntLit(s) => s.toLong }

  /** A pickler for values of type `Int`, represented as integer literals */
  implicit val intPickler: Pickler[Int] = longPickler.wrapped { _.toInt } { _.toLong }

  /** A conditional pickler for the boolean value `true` */
  private val truePickler =
    tokenPickler("boolean literal") { case TrueLit => true } cond { _ == true }

  /** A conditional pickler for the boolean value `false` */
  private val falsePickler =
    tokenPickler("boolean literal") { case FalseLit => false } cond { _ == false }

  /** A pickler for values of type `Boolean`, represented as the literals `true` or `false`. */
  implicit def booleanPickler: Pickler[Boolean] = truePickler | falsePickler

  /** A pickler for values of type `Unit`, represented by the empty character string */
  implicit val unitPickler: Pickler[Unit] = new Pickler[Unit] {
    def pickle(wr: Writer, x: Unit) {}
    def unpickle(rd: Lexer): Unpickled[Unit] = UnpickleSuccess(())
  }

  /** A pickler for values of type `String`, represented as string literals */
  implicit val stringPickler: Pickler[String] = new Pickler[String] {
    def pickle(wr: Writer, x: String) = wr.write(if (x == null) "null" else quoted(x))
    def unpickle(rd: Lexer) = rd.token match {
      case StringLit(s) => nextSuccess(rd, s)
      case NullLit => nextSuccess(rd, null)
      case _ => errorExpected(rd, "string literal")
    }
  }

  /** A pickler for pairs, represented as `~`-pairs */
  implicit def tuple2Pickler[T1: Pickler, T2: Pickler]: Pickler[(T1, T2)] =
    (pkl[T1] ~ pkl[T2])
      .wrapped { case x1 ~ x2 => (x1, x2) } { case (x1, x2) => x1 ~ x2 }
      .labelled ("tuple2")

  /** A pickler for 3-tuples, represented as `~`-tuples */
  implicit def tuple3Pickler[T1, T2, T3](implicit p1: Pickler[T1], p2: Pickler[T2], p3: Pickler[T3]): Pickler[(T1, T2, T3)] =
    (p1 ~ p2 ~ p3)
      .wrapped { case x1 ~ x2 ~ x3 => (x1, x2, x3) } { case (x1, x2, x3) => x1 ~ x2 ~ x3 }
      .labelled ("tuple3")

  /** A pickler for list values */
  implicit def listPickler[T: Pickler]: Pickler[List[T]] =
    iterPickler[T] .wrapped { _.toList } { _.iterator } .labelled ("scala.List")
}

/** A subclass of Pickler can indicate whether a particular value can be pickled by instances
 *  of this class.
 *  @param canPickle   The predicate that indicates whether a given value
 *                     can be pickled by instances of this class.
 */
abstract class CondPickler[T](val canPickle: Any => Boolean) extends Pickler[T] {
  import Pickler._

  /** Pickles given value `x` if possible, as indicated by `canPickle(x)`.
   */
  def tryPickle(wr: Writer, x: Any): Boolean = {
    val result = canPickle(x)
    if (result) pickle(wr, x.asInstanceOf[T])
    result
  }

  /** A pickler obtained from this pickler and an alternative pickler.
   *  To pickle a value, this pickler is tried first. If it cannot handle
   *  the object (as indicated by its `canPickle` test), then the
   *  alternative pickler is tried.
   *  To unpickle a value, this unpickler is tried first. If it cannot read
   *  the input (as indicated by a `UnpickleFailure` result), then the
   *  alternative pickler is tried.
   *  @tparam V    The handled type of the returned pickler.
   *  @tparam U    The handled type of the alternative pickler.
   *  @param that The alternative pickler.
   */
  def | [V >: T, U <: V] (that: => CondPickler[U]): CondPickler[V] =
    eitherPickler[V, T, U](this, that)
}