Skip to content
This repository was archived by the owner on Feb 20, 2019. It is now read-only.

Commit 6e92d67

Browse files
committed
Merge pull request #160 from scala/ctor-params-no-getter
Deal with constructor parameters properly
2 parents e54885d + 9f8b7f6 commit 6e92d67

File tree

5 files changed

+168
-33
lines changed

5 files changed

+168
-33
lines changed

core/src/main/scala/pickling/Macros.scala

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -108,29 +108,43 @@ trait PicklerMacros extends Macro {
108108
)
109109
""")
110110
} else (nonLoopyFields ++ loopyFields).flatMap(fir => {
111+
// for each field, compute a tree for pickling it
112+
// (or empty list, if impossible)
113+
114+
def pickleLogic(fieldValue: Tree): Tree =
115+
if (fir.tpe.typeSymbol.isEffectivelyFinal) q"""
116+
b.hintStaticallyElidedType()
117+
$fieldValue.pickleInto(b)
118+
""" else q"""
119+
val subPicklee: ${fir.tpe} = $fieldValue
120+
if (subPicklee == null || subPicklee.getClass == classOf[${fir.tpe}]) b.hintDynamicallyElidedType()
121+
subPicklee.pickleInto(b)
122+
"""
123+
124+
def putField(getterLogic: Tree) =
125+
q"builder.putField(${fir.name}, b => ${pickleLogic(getterLogic)})"
126+
127+
// we assume getterLogic is a tree of type Try[${fir.tpe}]
128+
def tryPutField(getterLogic: Tree) = {
129+
val tryName = c.fresh(newTermName("tr"))
130+
val valName = c.fresh(newTermName("value"))
131+
q"""
132+
val $tryName = $getterLogic
133+
if ($tryName.isSuccess) {
134+
val $valName = $tryName.get
135+
builder.putField(${fir.name}, b => ${pickleLogic(Ident(valName))})
136+
}
137+
"""
138+
}
139+
111140
if (sym.isModuleClass) {
112141
Nil
113142
} else if (fir.hasGetter) {
114-
def putField(getterLogic: Tree) = {
115-
def wrap(pickleLogic: Tree) = q"builder.putField(${fir.name}, b => $pickleLogic)"
116-
wrap {
117-
if (fir.tpe.typeSymbol.isEffectivelyFinal) q"""
118-
b.hintStaticallyElidedType()
119-
$getterLogic.pickleInto(b)
120-
""" else q"""
121-
val subPicklee: ${fir.tpe} = $getterLogic
122-
if (subPicklee == null || subPicklee.getClass == classOf[${fir.tpe}]) b.hintDynamicallyElidedType() else ()
123-
subPicklee.pickleInto(b)
124-
"""
125-
}
126-
}
127143
if (fir.isPublic) List(putField(q"picklee.${newTermName(fir.name)}"))
128144
else reflectively("picklee", fir)(fm => putField(q"$fm.get.asInstanceOf[${fir.tpe}]"))
129145
} else {
130-
// NOTE: this means that we've encountered a primary constructor parameter elided in the "constructors" phase
131-
// we can do nothing about that, so we don't serialize this field right now leaving everything to the unpickler
132-
// when deserializing we'll have to use the Unsafe.allocateInstance strategy
133-
Nil
146+
reflectivelyWithoutGetter("picklee", fir)(fvalue =>
147+
tryPutField(q"$fvalue.asInstanceOf[scala.util.Try[${fir.tpe}]]"))
134148
}
135149
})
136150
val endEntry = q"builder.endEntry()"
@@ -259,11 +273,12 @@ trait UnpicklerMacros extends Macro {
259273
// nevertheless don't despair and try to prove whether this is or is not the fact
260274
// i was super scared that string sharing is going to fail due to the same reason, but it did not :)
261275
// in the worst case we can do the same as the interpreted runtime does - just go for allocateInstance
262-
val pendingFields = cir.fields.filter(fir =>
263-
fir.isNonParam ||
264-
(!canCallCtor && fir.isReifiedParam) ||
265-
shouldBotherAboutLooping(fir.tpe)
276+
277+
// pending fields are fields that are restored after instantiation (e.g., through field assignments)
278+
val pendingFields = if (!canCallCtor) cir.fields else cir.fields.filter(fir =>
279+
fir.isNonParam || shouldBotherAboutLooping(fir.tpe)
266280
)
281+
267282
val instantiationLogic = {
268283
if (sym.isModuleClass) {
269284
q"${sym.module}"
@@ -296,7 +311,15 @@ trait UnpicklerMacros extends Macro {
296311
val initPendingFields = pendingFields.flatMap(fir => {
297312
val readFir = readField(fir.name, fir.tpe)
298313
if (fir.isPublic && fir.hasSetter) List(q"$instance.${newTermName(fir.name)} = $readFir".asInstanceOf[Tree])
299-
else if (fir.accessor.isEmpty) List()
314+
else if (fir.accessor.isEmpty) List(q"""
315+
try {
316+
val javaField = $instance.getClass.getDeclaredField(${fir.name})
317+
javaField.setAccessible(true)
318+
javaField.set($instance, $readFir)
319+
} catch {
320+
case e: java.lang.NoSuchFieldException => /* do nothing */
321+
}
322+
""")
300323
else reflectively(instance, fir)(fm => q"$fm.set($readFir)".asInstanceOf[Tree])
301324
})
302325

core/src/main/scala/pickling/Runtime.scala

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,11 @@ class InterpretedUnpicklerRuntime(mirror: Mirror, tag: FastTypeTag[_])(implicit
189189
List[FieldIR]()
190190
} else {
191191
val (nonLoopyFields, loopyFields) = cir.fields.partition(fir => !shouldBotherAboutLooping(fir.tpe))
192-
(nonLoopyFields ++ loopyFields).filter(fir => fir.isNonParam || fir.isReifiedParam)
192+
(nonLoopyFields ++ loopyFields).filter(fir =>
193+
fir.hasGetter || {
194+
// exists as Java field
195+
scala.util.Try(clazz.getDeclaredField(fir.name)).isSuccess
196+
})
193197
}
194198

195199
def fieldVals = pendingFields.map(fir => {
@@ -223,10 +227,19 @@ class InterpretedUnpicklerRuntime(mirror: Mirror, tag: FastTypeTag[_])(implicit
223227
if (shouldBotherAboutSharing(tpe)) registerUnpicklee(inst, preregisterUnpicklee())
224228
val im = mirror.reflect(inst)
225229

230+
//debug(s"pendingFields: ${pendingFields.size}")
231+
//debug(s"fieldVals: ${fieldVals.size}")
232+
226233
pendingFields.zip(fieldVals) foreach {
227234
case (fir, fval) =>
228-
val fmX = im.reflectField(fir.field.get)
229-
fmX.set(fval)
235+
if (fir.hasGetter) {
236+
val fmX = im.reflectField(fir.field.get)
237+
fmX.set(fval)
238+
} else {
239+
val javaField = clazz.getDeclaredField(fir.name)
240+
javaField.setAccessible(true)
241+
javaField.set(inst, fval)
242+
}
230243
}
231244

232245
inst

core/src/main/scala/pickling/RuntimePickler.scala

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package scala.pickling
22

3+
import java.lang.reflect.Field
4+
35
import scala.reflect.runtime.{universe => ru}
46
import ir._
57

@@ -37,7 +39,7 @@ class RuntimePickler(classLoader: ClassLoader, clazz: Class[_])(implicit pf: Pic
3739
import ru._
3840

3941
sealed abstract class Logic(fir: irs.FieldIR, isEffFinal: Boolean) {
40-
def run(builder: PBuilder, im: ru.InstanceMirror): Unit = {
42+
def run(builder: PBuilder, picklee: Any, im: ru.InstanceMirror): Unit = {
4143
val fldMirror = im.reflectField(fir.field.get)
4244
val fldValue: Any = fldMirror.get
4345
val fldClass = if (fldValue != null) fldValue.getClass else null
@@ -83,6 +85,41 @@ class RuntimePickler(classLoader: ClassLoader, clazz: Class[_])(implicit pf: Pic
8385
}
8486
}
8587

88+
sealed class PrivateJavaFieldLogic(fir: irs.FieldIR, field: Field) extends Logic(fir, false) {
89+
override def run(builder: PBuilder, picklee: Any, im: ru.InstanceMirror): Unit = {
90+
field.setAccessible(true)
91+
val fldValue = field.get(picklee)
92+
val fldClass = if (fldValue != null) fldValue.getClass else null
93+
94+
//debug(s"pickling field of type: ${fir.tpe.toString}")
95+
//debug(s"isEffFinal: $isEffFinal")
96+
//debug(s"field value: $fldValue")
97+
//debug(s"field class: ${fldClass.getName}")
98+
99+
// idea: picklers for all fields could be created and cached once and for all.
100+
// however, it depends on whether the type of the field is effectively final or not.
101+
// essentially, we have to emulate the behavior of generated picklers, which make
102+
// the same decision.
103+
val fldPickler = SPickler.genPickler(classLoader, fldClass).asInstanceOf[SPickler[Any]]
104+
//debug(s"looked up field pickler: $fldPickler")
105+
106+
builder.putField(field.getName, b => {
107+
pickleLogic(fldClass, fldValue, b, fldPickler)
108+
})
109+
}
110+
111+
def pickleLogic(fldClass: Class[_], fldValue: Any, b: PBuilder, fldPickler: SPickler[Any]): Unit = {
112+
pickleInto(fldClass, fldValue, b, fldPickler)
113+
}
114+
}
115+
116+
final class PrivateEffectivelyFinalJavaFieldLogic(fir: irs.FieldIR, field: Field) extends PrivateJavaFieldLogic(fir, field) {
117+
override def pickleLogic(fldClass: Class[_], fldValue: Any, b: PBuilder, fldPickler: SPickler[Any]): Unit = {
118+
b.hintStaticallyElidedType()
119+
pickleInto(fldClass, fldValue, b, fldPickler)
120+
}
121+
}
122+
86123
// difference to old runtime pickler: create tag based on fieldClass instead of fir.tpe
87124
def pickleInto(fieldClass: Class[_], fieldValue: Any, builder: PBuilder, pickler: SPickler[Any]): Unit = {
88125
val fieldTag = FastTypeTag.mkRaw(fieldClass, mirror)
@@ -95,16 +132,28 @@ class RuntimePickler(classLoader: ClassLoader, clazz: Class[_])(implicit pf: Pic
95132
new SPickler[Any] {
96133
val format: PickleFormat = pf
97134

98-
val fields: List[Logic] =
99-
cir.fields.filter(_.hasGetter).map { fir =>
100-
if (fir.tpe.typeSymbol.isEffectivelyFinal) new EffectivelyFinalLogic(fir)
101-
else if (fir.tpe.typeSymbol.asType.isAbstractType) new AbstractLogic(fir)
102-
else new DefaultLogic(fir)
103-
}
135+
val fields: List[Logic] = cir.fields.flatMap { fir =>
136+
if (fir.hasGetter)
137+
List(
138+
if (fir.tpe.typeSymbol.isEffectivelyFinal) new EffectivelyFinalLogic(fir)
139+
else if (fir.tpe.typeSymbol.asType.isAbstractType) new AbstractLogic(fir)
140+
else new DefaultLogic(fir)
141+
)
142+
else
143+
try {
144+
val javaField = clazz.getDeclaredField(fir.name)
145+
List(
146+
if (fir.tpe.typeSymbol.isEffectivelyFinal) new PrivateEffectivelyFinalJavaFieldLogic(fir, javaField)
147+
else new PrivateJavaFieldLogic(fir, javaField)
148+
)
149+
} catch {
150+
case e: java.lang.NoSuchFieldException => List()
151+
}
152+
}
104153

105154
def putFields(picklee: Any, builder: PBuilder): Unit = {
106155
val im = mirror.reflect(picklee)
107-
fields.foreach(_.run(builder, im))
156+
fields.foreach(_.run(builder, picklee, im))
108157
}
109158

110159
def pickle(picklee: Any, builder: PBuilder): Unit = {

core/src/main/scala/pickling/Tools.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,18 @@ abstract class Macro { self =>
375375
private var reflectivePrologueEmitted = false // TODO: come up with something better
376376
def reflectively(target: String, fir: FieldIR)(body: Tree => Tree): List[Tree] = reflectively(newTermName(target), fir)(body)
377377

378+
def reflectivelyWithoutGetter(target: String, fir: FieldIR)(body: Tree => Tree): List[Tree] = {
379+
val pickleeName = newTermName(target)
380+
val getFieldValue = q"""
381+
val clazz = $pickleeName.getClass
382+
scala.util.Try(clazz.getDeclaredField(${fir.name})).map { javaField =>
383+
javaField.setAccessible(true)
384+
javaField.get($pickleeName)
385+
}
386+
"""
387+
List(body(getFieldValue))
388+
}
389+
378390
/**
379391
* requires: !fir.accessor.isEmpty
380392
*/
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package scala.pickling.test.ctorparams
2+
3+
import org.scalatest.FunSuite
4+
import scala.pickling._
5+
import json._
6+
7+
class Partitioner(numParts: Int) {
8+
def numPartitions = numParts
9+
override def toString = s"Partitioner($numParts)"
10+
}
11+
12+
class Partitioner2(numParts: Int, val other: String) {
13+
def numPartitions = numParts
14+
override def toString = s"Partitioner2($numParts,$other)"
15+
}
16+
17+
class CtorParamsTest extends FunSuite {
18+
test("runtime pickler") {
19+
val par: Any = new Partitioner(8)
20+
val p: JSONPickle = par.pickle
21+
val up = p.unpickle[Any]
22+
assert(par.toString == up.toString)
23+
}
24+
25+
test("static pickler") {
26+
val par = new Partitioner(8)
27+
val p: JSONPickle = par.pickle
28+
val up = p.unpickle[Partitioner]
29+
assert(par.toString == up.toString)
30+
}
31+
32+
test("ctor param and public getter") {
33+
val par = new Partitioner2(8, "test")
34+
val p: JSONPickle = par.pickle
35+
val up = p.unpickle[Partitioner2]
36+
assert(par.toString == up.toString)
37+
}
38+
}

0 commit comments

Comments
 (0)