forked from scala/scala3
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Semantic.scala
1587 lines (1342 loc) · 58.4 KB
/
Semantic.scala
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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package dotty.tools.dotc
package transform
package init
import core._
import Contexts._
import Symbols._
import Types._
import StdNames._
import NameKinds.OuterSelectName
import ast.tpd._
import util.EqHashMap
import config.Printers.init as printer
import reporting.trace as log
import Errors._
import scala.collection.mutable
import scala.annotation.tailrec
import scala.annotation.constructorOnly
object Semantic {
// ----- Domain definitions --------------------------------
/** Abstract values
*
* Value = Hot | Cold | Warm | ThisRef | Fun | RefSet
*
* Cold
* ┌──────► ▲ ◄────┐ ◄────┐
* │ │ │ │
* │ │ │ │
* | │ │ │
* | │ │ │
* ThisRef Warm Fun RefSet
* │ ▲ ▲ ▲
* │ │ │ │
* | │ │ │
* ▲ │ │ │
* │ │ │ │
* └─────────┴───────┴───────┘
* Hot
*
* The diagram above does not reflect relationship between `RefSet`
* and other values. `RefSet` represents a set of values which could
* be `ThisRef`, `Warm` or `Fun`. The following ordering applies for
* RefSet:
*
* R_a ⊑ R_b if R_a ⊆ R_b
*
* V ⊑ R if V ∈ R
*
*/
sealed abstract class Value {
def show: String = this.toString()
def isHot = this == Hot
def isCold = this == Cold
def isWarm = this.isInstanceOf[Warm]
def isThisRef = this.isInstanceOf[ThisRef]
}
/** A transitively initialized object */
case object Hot extends Value
/** An object with unknown initialization status */
case object Cold extends Value
sealed abstract class Ref extends Value {
def klass: ClassSymbol
def outer: Value
}
/** A reference to the object under initialization pointed by `this` */
case class ThisRef(klass: ClassSymbol) extends Ref {
val outer = Hot
}
/** An object with all fields initialized but reaches objects under initialization
*
* We need to restrict nesting levels of `outer` to finitize the domain.
*/
case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref {
/** If a warm value is in the process of populating parameters, class bodies are not executed. */
private var populatingParams: Boolean = false
def isPopulatingParams = populatingParams
/** Ensure that outers and class parameters are initialized.
*
* Fields in class body are not initialized.
*
* We need to populate class parameters and outers for warm values for the
* following cases:
*
* - Widen an already checked warm value to another warm value without
* corresponding object
*
* - Using a warm value from the cache, whose corresponding object from
* the last iteration have been remove due to heap reversion
* {@see Cache.prepareForNextIteration}
*
* After populating class parameters and outers, it is possible to lazily
* compute the field values in class bodies when they are accessed.
*/
private def populateParams(): Contextual[this.type] = log("populating parameters", printer, (_: Warm).objekt.toString) {
assert(!populatingParams, "the object is already populating parameters")
populatingParams = true
val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
this.callConstructor(ctor, args.map(arg => ArgInfo(arg, EmptyTree)), tpl)
populatingParams = false
this
}
def ensureObjectExistsAndPopulated(): Contextual[this.type] =
if cache.containsObject(this) then this
else this.ensureFresh().populateParams()
}
/** A function value */
case class Fun(expr: Tree, thisV: Ref, klass: ClassSymbol, env: Env) extends Value
/** A value which represents a set of addresses
*
* It comes from `if` expressions.
*/
case class RefSet(refs: List[Fun | Ref]) extends Value
// end of value definition
/** The abstract object which stores value about its fields and immediate outers.
*
* Semantically it suffices to store the outer for `klass`. We cache other outers
* for performance reasons.
*
* Note: Object is NOT a value.
*/
case class Objekt(val klass: ClassSymbol, val fields: Map[Symbol, Value], val outers: Map[ClassSymbol, Value]) {
def field(f: Symbol): Value = fields(f)
def outer(klass: ClassSymbol) = outers(klass)
def hasOuter(klass: ClassSymbol) = outers.contains(klass)
def hasField(f: Symbol) = fields.contains(f)
}
/** The environment for method parameters
*
* For performance and usability, we restrict parameters to be either `Cold`
* or `Hot`.
*
* Despite that we have environment for evaluating expressions in secondary
* constructors, we don't need to put environment as the cache key. The
* reason is that constructor parameters are determined by the value of
* `this` --- it suffices to make the value of `this` as part of the cache
* key.
*
* This crucially depends on the fact that in the initialization process
* there can be exactly one call to a specific constructor for a given
* receiver. However, once we relax the design to allow non-hot values to
* methods and functions, we have to put the environment as part of the cache
* key. The reason is that given the same receiver, a method or function may
* be called with different arguments -- they are not decided by the receiver
* anymore.
*/
object Env {
opaque type Env = Map[Symbol, Value]
val empty: Env = Map.empty
def apply(bindings: Map[Symbol, Value]): Env = bindings
def apply(ddef: DefDef, args: List[Value])(using Context): Env =
val params = ddef.termParamss.flatten.map(_.symbol)
assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size)
params.zip(args).toMap
extension (env: Env)
def lookup(sym: Symbol)(using Context): Value = env(sym)
def getOrElse(sym: Symbol, default: Value)(using Context): Value = env.getOrElse(sym, default)
def union(other: Env): Env = env ++ other
def isHot: Boolean = env.values.forall(_ == Hot)
}
type Env = Env.Env
inline def env(using env: Env) = env
inline def withEnv[T](env: Env)(op: Env ?=> T): T = op(using env)
import Env._
object Promoted {
class PromotionInfo {
var isCurrentObjectPromoted: Boolean = false
val values = mutable.Set.empty[Value]
override def toString(): String = values.toString()
}
/** Values that have been safely promoted */
opaque type Promoted = PromotionInfo
/** Note: don't use `val` to avoid incorrect sharing */
def empty: Promoted = new PromotionInfo
extension (promoted: Promoted)
def isCurrentObjectPromoted: Boolean = promoted.isCurrentObjectPromoted
def promoteCurrent(thisRef: ThisRef): Unit = promoted.isCurrentObjectPromoted = true
def contains(value: Value): Boolean = promoted.values.contains(value)
def add(value: Value): Unit = promoted.values += value
def remove(value: Value): Unit = promoted.values -= value
end extension
}
type Promoted = Promoted.Promoted
import Promoted._
inline def promoted(using p: Promoted): Promoted = p
/** Interpreter configuration
*
* The (abstract) interpreter can be seen as a push-down automaton
* that transits between the configurations where the stack is the
* implicit call stack of the meta-language.
*
* It's important that the configuration is finite for the analysis
* to terminate.
*
* For soundness, we need to compute fixed point of the cache, which
* maps configuration to evaluation result.
*
* Thanks to heap monotonicity, heap is not part of the configuration.
*
* This class is only used for the purpose of documentation.
*/
case class Config(thisV: Value, expr: Tree)
/** Cache used to terminate the analysis
*
* A finitary configuration is not enough for the analysis to
* terminate. We need to use cache to let the interpreter "know"
* that it can terminate.
*
* For performance reasons we use curried key.
*
* Note: It's tempting to use location of trees as key. That should
* be avoided as a template may have the same location as its single
* statement body. Macros may also create incorrect locations.
*
*/
object Cache {
opaque type CacheStore = mutable.Map[Value, EqHashMap[Tree, Value]]
private type Heap = Map[Ref, Objekt]
class Cache {
private var last: CacheStore = mutable.Map.empty
private var current: CacheStore = mutable.Map.empty
private val stable: CacheStore = mutable.Map.empty
private var changed: Boolean = false
/** Abstract heap stores abstract objects
*
* The heap serves as cache of summaries for warm objects and is shared for checking all classes.
*
* The fact that objects of `ThisRef` are stored in heap is just an engineering convenience.
* Technically, we can also store the object directly in `ThisRef`.
*
* The heap contains objects of two conceptually distinct kinds.
*
* - Objects that are also in `heapStable` are flow-insensitive views of already initialized objects that are
* cached for reuse in analysis of later classes. These objects and their fields should never change; this is
* enforced using assertions.
*
* - Objects that are not (yet) in `heapStable` are the flow-sensitive abstract state of objects being analyzed
* in the current iteration of the analysis of the current class. Their fields do change flow-sensitively: more
* fields are added as fields become initialized. These objects are valid only within the current iteration and
* are removed when moving to a new iteration of analyzing the current class. When the analysis of a class
* reaches a fixed point, these now stable flow-sensitive views of the object at the end of the constructor
* of the analyzed class now become the flow-insensitive views of already initialized objects and can therefore
* be added to `heapStable`.
*/
private var heap: Heap = Map.empty
/** Used to revert heap to last stable heap. */
private var heapStable: Heap = Map.empty
def hasChanged = changed
def contains(value: Value, expr: Tree) =
current.contains(value, expr) || stable.contains(value, expr)
def apply(value: Value, expr: Tree) =
if current.contains(value, expr) then current(value)(expr)
else stable(value)(expr)
/** Copy the value of `(value, expr)` from the last cache to the current cache
* (assuming it's `Hot` if it doesn't exist in the cache).
*
* Then, runs `fun` and update the caches if the values change.
*/
def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result): Contextual[Result] =
val assumeValue: Value =
if last.contains(value, expr) then
last.get(value, expr)
else
last.put(value, expr, Hot)
Hot
end if
current.put(value, expr, assumeValue)
val actual = fun
if actual.value != assumeValue then
this.changed = true
last.put(value, expr, actual.value)
current.put(value, expr, actual.value)
else
// It's tempting to cache the value in stable, but it's unsound.
// The reason is that the current value may depend on other values
// which might change.
//
// stable.put(value, expr, actual)
()
end if
actual
end assume
/** Commit current cache to stable cache. */
private def commitToStableCache() =
current.foreach { (v, m) =>
// It's useless to cache value for ThisRef.
if v.isWarm then m.iterator.foreach { (e, res) =>
stable.put(v, e, res)
}
}
/** Prepare cache for the next iteration
*
* 1. Reset changed flag.
*
* 2. Reset current cache (last cache already synced in `assume`).
*
* 3. Revert heap if instable.
*
*/
def prepareForNextIteration()(using Context) =
this.changed = false
this.current = mutable.Map.empty
this.heap = this.heapStable
/** Prepare for checking next class
*
* 1. Reset changed flag.
*
* 2. Commit current cache to stable cache if not changed.
*
* 3. Update stable heap if not changed.
*
* 4. Reset last cache.
*/
def prepareForNextClass()(using Context) =
if this.changed then
this.changed = false
this.heap = this.heapStable
else
this.commitToStableCache()
this.heapStable = this.heap
this.last = mutable.Map.empty
this.current = mutable.Map.empty
def updateObject(ref: Ref, obj: Objekt) =
assert(!this.heapStable.contains(ref))
this.heap = this.heap.updated(ref, obj)
def containsObject(ref: Ref) = heap.contains(ref)
def getObject(ref: Ref) = heap(ref)
}
extension (cache: CacheStore)
def contains(value: Value, expr: Tree) = cache.contains(value) && cache(value).contains(expr)
def get(value: Value, expr: Tree): Value = cache(value)(expr)
def remove(value: Value, expr: Tree) = cache(value).remove(expr)
def put(value: Value, expr: Tree, result: Value): Unit = {
val innerMap = cache.getOrElseUpdate(value, new EqHashMap[Tree, Value])
innerMap(expr) = result
}
end extension
}
import Cache._
inline def cache(using c: Cache): Cache = c
/** Result of abstract interpretation */
case class Result(value: Value, errors: Seq[Error]) {
def show(using Context) = value.show + ", errors = " + errors.map(_.toString)
def ++(errors: Seq[Error]): Result = this.copy(errors = this.errors ++ errors)
def +(error: Error): Result = this.copy(errors = this.errors :+ error)
def ensureHot(msg: String, source: Tree): Contextual[Result] =
this ++ value.promote(msg, source)
def select(f: Symbol, source: Tree): Contextual[Result] =
value.select(f, source) ++ errors
def call(meth: Symbol, args: List[ArgInfo], superType: Type, source: Tree): Contextual[Result] =
value.call(meth, args, superType, source) ++ errors
def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] =
value.callConstructor(ctor, args, source) ++ errors
def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] =
value.instantiate(klass, ctor, args, source) ++ errors
}
// ----- Checker State -----------------------------------
/** The state that threads through the interpreter */
type Contextual[T] = (Env, Context, Trace, Promoted, Cache) ?=> T
// ----- Error Handling -----------------------------------
object Trace {
opaque type Trace = Vector[Tree]
val empty: Trace = Vector.empty
extension (trace: Trace)
def add(node: Tree): Trace = trace :+ node
def toVector: Vector[Tree] = trace
}
type Trace = Trace.Trace
import Trace._
def trace(using t: Trace): Trace = t
inline def withTrace[T](t: Trace)(op: Trace ?=> T): T = op(using t)
// ----- Operations on domains -----------------------------
extension (a: Value)
def join(b: Value): Value =
(a, b) match
case (Hot, _) => b
case (_, Hot) => a
case (Cold, _) => Cold
case (_, Cold) => Cold
case (a: (Fun | Warm | ThisRef), b: (Fun | Warm | ThisRef)) =>
if a == b then a else RefSet(a :: b :: Nil)
case (a: (Fun | Warm | ThisRef), RefSet(refs)) =>
if refs.exists(_ == a) then b: Value // fix pickling test
else RefSet(a :: refs)
case (RefSet(refs), b: (Fun | Warm | ThisRef)) =>
if refs.exists(_ == b) then a: Value // fix pickling test
else RefSet(b :: refs)
case (RefSet(refs1), RefSet(refs2)) =>
val diff = refs2.filter(ref => refs1.forall(_ != ref))
RefSet(refs1 ++ diff)
/** Conservatively approximate the value with `Cold` or `Hot` */
def widenArg: Value =
a match
case _: Ref | _: Fun => Cold
case RefSet(refs) => refs.map(_.widenArg).join
case _ => a
extension (values: Seq[Value])
def join: Value =
if values.isEmpty then Hot
else values.reduce { (v1, v2) => v1.join(v2) }
def widenArgs: List[Value] = values.map(_.widenArg).toList
extension (ref: Ref)
def objekt: Contextual[Objekt] =
// TODO: improve performance
ref match
case warm: Warm => warm.ensureObjectExistsAndPopulated()
case _ =>
cache.getObject(ref)
def ensureObjectExists()(using Cache): ref.type =
if cache.containsObject(ref) then
printer.println("object " + ref + " already exists")
ref
else
ensureFresh()
def ensureFresh()(using Cache): ref.type =
val obj = Objekt(ref.klass, fields = Map.empty, outers = Map(ref.klass -> ref.outer))
printer.println("reset object " + ref)
cache.updateObject(ref, obj)
ref
/** Update field value of the abstract object
*
* Invariant: fields are immutable and only set once
*/
def updateField(field: Symbol, value: Value): Contextual[Unit] = log("set field " + field + " of " + ref + " to " + value) {
val obj = objekt
// We may reset the outers or params of a populated warm object.
// This is the case if we need access the field of a warm object, which
// requires population of parameters and outers; and later create an
// instance of the exact warm object, which requires initialization check.
//
// See tests/init/neg/unsound1.scala
assert(!obj.hasField(field) || field.is(Flags.ParamAccessor) && obj.field(field) == value, field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + ref)
val obj2 = obj.copy(fields = obj.fields.updated(field, value))
cache.updateObject(ref, obj2)
}
/** Update the immediate outer of the given `klass` of the abstract object
*
* Invariant: outers are immutable and only set once
*/
def updateOuter(klass: ClassSymbol, value: Value): Contextual[Unit] = log("set outer " + klass + " of " + ref + " to " + value) {
val obj = objekt
// See the comment in `updateField` for setting the value twice.
assert(!obj.hasOuter(klass) || obj.outer(klass) == value, klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + ref)
val obj2 = obj.copy(outers = obj.outers.updated(klass, value))
cache.updateObject(ref, obj2)
}
end extension
extension (value: Value)
def select(field: Symbol, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("select " + field.show + ", this = " + value, printer, (_: Result).show) {
if promoted.isCurrentObjectPromoted then Result(Hot, Nil)
else value match {
case Hot =>
Result(Hot, Errors.empty)
case Cold =>
val error = AccessCold(field, source, trace.toVector)
Result(Hot, error :: Nil)
case ref: Ref =>
val target = if needResolve then resolve(ref.klass, field) else field
val trace1 = trace.add(source)
if target.is(Flags.Lazy) then
given Trace = trace1
val rhs = target.defTree.asInstanceOf[ValDef].rhs
eval(rhs, ref, target.owner.asClass, cacheResult = true)
else
val obj = ref.objekt
if obj.hasField(target) then
Result(obj.field(target), Nil)
else if ref.isInstanceOf[Warm] then
assert(obj.klass.isSubClass(target.owner))
if target.is(Flags.ParamAccessor) then
// possible for trait parameters
// see tests/init/neg/trait2.scala
//
// return `Hot` here, errors are reported in checking `ThisRef`
Result(Hot, Nil)
else if target.hasSource then
val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs
eval(rhs, ref, target.owner.asClass, cacheResult = true)
else
val error = CallUnknown(field, source, trace.toVector)
Result(Hot, error :: Nil)
else
val error = AccessNonInit(target, trace.add(source).toVector)
Result(Hot, error :: Nil)
case fun: Fun =>
report.error("unexpected tree in selecting a function, fun = " + fun.expr.show, source)
Result(Hot, Nil)
case RefSet(refs) =>
val resList = refs.map(_.select(field, source))
val value2 = resList.map(_.value).join
val errors = resList.flatMap(_.errors)
Result(value2, errors)
}
}
def call(meth: Symbol, args: List[ArgInfo], superType: Type, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("call " + meth.show + ", args = " + args, printer, (_: Result).show) {
def checkArgs = args.flatMap(_.promote)
def isSyntheticApply(meth: Symbol) =
meth.is(Flags.Synthetic)
&& meth.owner.is(Flags.Module)
&& meth.owner.companionClass.is(Flags.Case)
// fast track if the current object is already initialized
if promoted.isCurrentObjectPromoted then Result(Hot, Nil)
else value match {
case Hot =>
if isSyntheticApply(meth) then
val klass = meth.owner.companionClass.asClass
instantiate(klass, klass.primaryConstructor, args, source)
else
Result(Hot, checkArgs)
case Cold =>
val error = CallCold(meth, source, trace.toVector)
Result(Hot, error :: checkArgs)
case ref: Ref =>
val isLocal = !meth.owner.isClass
val target =
if !needResolve then
meth
else if superType.exists then
resolveSuper(ref.klass, superType, meth)
else
resolve(ref.klass, meth)
if target.isOneOf(Flags.Method) then
val trace1 = trace.add(source)
if target.hasSource then
given Trace = trace1
val cls = target.owner.enclosingClass.asClass
val ddef = target.defTree.asInstanceOf[DefDef]
val argErrors = checkArgs
// normal method call
if argErrors.nonEmpty && isSyntheticApply(meth) then
val klass = meth.owner.companionClass.asClass
val outerCls = klass.owner.lexicallyEnclosingClass.asClass
val outer = resolveOuterSelect(outerCls, ref, 1, source)
outer.instantiate(klass, klass.primaryConstructor, args, source)
else
withEnv(if isLocal then env else Env.empty) {
eval(ddef.rhs, ref, cls, cacheResult = true) ++ argErrors
}
else if ref.canIgnoreMethodCall(target) then
Result(Hot, Nil)
else
// no source code available
val error = CallUnknown(target, source, trace.toVector)
Result(Hot, error :: checkArgs)
else
// method call resolves to a field
val obj = ref.objekt
if obj.hasField(target) then
Result(obj.field(target), Nil)
else
value.select(target, source, needResolve = false)
case Fun(body, thisV, klass, env) =>
// meth == NoSymbol for poly functions
if meth.name.toString == "tupled" then Result(value, Nil) // a call like `fun.tupled`
else
withEnv(env) {
eval(body, thisV, klass, cacheResult = true) ++ checkArgs
}
case RefSet(refs) =>
val resList = refs.map(_.call(meth, args, superType, source))
val value2 = resList.map(_.value).join
val errors = resList.flatMap(_.errors)
Result(value2, errors)
}
}
def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = log("call " + ctor.show + ", args = " + args, printer, (_: Result).show) {
// init "fake" param fields for the secondary constructor
def addParamsAsFields(env: Env, ref: Ref, ctorDef: DefDef) = {
val paramSyms = ctorDef.termParamss.flatten.map(_.symbol)
paramSyms.map { acc =>
val value = env.lookup(acc)
ref.updateField(acc, value)
printer.println(acc.show + " initialized with " + value)
}
}
value match {
case Hot | Cold | _: RefSet | _: Fun =>
report.error("unexpected constructor call, meth = " + ctor + ", value = " + value, source)
Result(Hot, Nil)
case ref: Warm if ref.isPopulatingParams =>
val trace1 = trace.add(source)
if ctor.hasSource then
given Trace = trace1
val cls = ctor.owner.enclosingClass.asClass
val ddef = ctor.defTree.asInstanceOf[DefDef]
given Env = Env(ddef, args.map(_.value).widenArgs)
if ctor.isPrimaryConstructor then
val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
init(tpl, ref, cls)
else
addParamsAsFields(env, ref, ddef)
val initCall = ddef.rhs match
case Block(call :: _, _) => call
case call => call
eval(initCall, ref, cls)
end if
else
Result(Hot, Nil)
case ref: Ref =>
val trace1 = trace.add(source)
if ctor.hasSource then
given Trace = trace1
val cls = ctor.owner.enclosingClass.asClass
val ddef = ctor.defTree.asInstanceOf[DefDef]
given Env = Env(ddef, args.map(_.value).widenArgs)
if ctor.isPrimaryConstructor then
val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) }
Result(ref, res.errors)
else
addParamsAsFields(env, ref, ddef)
eval(ddef.rhs, ref, cls, cacheResult = true)
else if ref.canIgnoreMethodCall(ctor) then
Result(Hot, Nil)
else
// no source code available
val error = CallUnknown(ctor, source, trace.toVector)
Result(Hot, error :: Nil)
}
}
/** Handle a new expression `new p.C` where `p` is abstracted by `value` */
def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = log("instantiating " + klass.show + ", value = " + value + ", args = " + args, printer, (_: Result).show) {
val trace1 = trace.add(source)
if promoted.isCurrentObjectPromoted then Result(Hot, Nil)
else value match {
case Hot =>
val buffer = new mutable.ArrayBuffer[Error]
val args2 = args.map { arg =>
val errors = arg.promote
buffer ++= errors
if errors.isEmpty then Hot
else arg.value.widenArg
}
if buffer.isEmpty then
Result(Hot, Errors.empty)
else
val outer = Hot
val warm = Warm(klass, outer, ctor, args2).ensureObjectExists()
val argInfos2 = args.zip(args2).map { (argInfo, v) => argInfo.copy(value = v) }
val res = warm.callConstructor(ctor, argInfos2, source)
Result(warm, res.errors)
case Cold =>
val error = CallCold(ctor, source, trace1.toVector)
Result(Hot, error :: Nil)
case ref: Ref =>
given Trace = trace1
// widen the outer to finitize the domain
val outer = ref match
case warm @ Warm(_, _: Warm, _, _) =>
// the widened warm object might not exist in the heap
warm.copy(outer = Cold).ensureObjectExistsAndPopulated()
case _ => ref
val argsWidened = args.map(_.value).widenArgs
val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) }
val warm = Warm(klass, outer, ctor, argsWidened).ensureObjectExists()
val res = warm.callConstructor(ctor, argInfos2, source)
Result(warm, res.errors)
case Fun(body, thisV, klass, env) =>
report.error("unexpected tree in instantiating a function, fun = " + body.show, source)
Result(Hot, Nil)
case RefSet(refs) =>
val resList = refs.map(_.instantiate(klass, ctor, args, source))
val value2 = resList.map(_.value).join
val errors = resList.flatMap(_.errors)
Result(value2, errors)
}
}
end extension
extension (ref: Ref)
def accessLocal(tmref: TermRef, klass: ClassSymbol, source: Tree): Contextual[Result] =
val sym = tmref.symbol
def default() = Result(Hot, Nil)
if sym.is(Flags.Param) && sym.owner.isConstructor then
// if we can get the field from the Ref (which can only possibly be
// a secondary constructor parameter), then use it.
if (ref.objekt.hasField(sym))
Result(ref.objekt.field(sym), Errors.empty)
// instances of local classes inside secondary constructors cannot
// reach here, as those values are abstracted by Cold instead of Warm.
// This enables us to simplify the domain without sacrificing
// expressiveness nor soundess, as local classes inside secondary
// constructors are uncommon.
else if sym.isContainedIn(klass) then
Result(env.lookup(sym), Nil)
else
// We don't know much about secondary constructor parameters in outer scope.
// It's always safe to approximate them with `Cold`.
Result(Cold, Nil)
else if sym.is(Flags.Param) then
default()
else
sym.defTree match {
case vdef: ValDef =>
// resolve this for local variable
val enclosingClass = sym.owner.enclosingClass.asClass
val thisValue2 = resolveThis(enclosingClass, ref, klass, source)
thisValue2 match {
case Hot => Result(Hot, Errors.empty)
case Cold => Result(Cold, Nil)
case ref: Ref => eval(vdef.rhs, ref, enclosingClass)
case _ =>
report.error("unexpected defTree when accessing local variable, sym = " + sym.show + ", defTree = " + sym.defTree.show, source)
default()
}
case _ => default()
}
end extension
// ----- Promotion ----------------------------------------------------
extension (ref: Ref)
/** Whether the object is fully assigned
*
* It means all fields and outers are set. For performance, we don't check
* outers here, because Scala semantics ensure that they are always set
* before any user code in the constructor.
*
* Note that `isFullyFilled = true` does not mean we can use the
* object freely, as its fields or outers may still reach uninitialized
* objects.
*/
def isFullyFilled: Contextual[Boolean] = log("isFullyFilled " + ref, printer) {
val obj = ref.objekt
ref.klass.baseClasses.forall { klass =>
!klass.hasSource || {
val nonInits = klass.info.decls.filter { member =>
!member.isOneOf(Flags.Method | Flags.Lazy | Flags.Deferred)
&& !member.isType
&& !obj.hasField(member)
}
printer.println("nonInits = " + nonInits)
nonInits.isEmpty
}
}
}
end extension
extension (thisRef: ThisRef)
def tryPromoteCurrentObject: Contextual[Boolean] = log("tryPromoteCurrentObject ", printer) {
if promoted.isCurrentObjectPromoted then
true
else if thisRef.isFullyFilled then
// If we have all fields initialized, then we can promote This to hot.
promoted.promoteCurrent(thisRef)
true
else
false
}
extension (value: Value)
/** Promotion of values to hot */
def promote(msg: String, source: Tree): Contextual[List[Error]] = log("promoting " + value + ", promoted = " + promoted, printer) {
if promoted.isCurrentObjectPromoted then Nil else
value.match
case Hot => Nil
case Cold => PromoteError(msg, source, trace.toVector) :: Nil
case thisRef: ThisRef =>
if thisRef.tryPromoteCurrentObject then Nil
else PromoteError(msg, source, trace.toVector) :: Nil
case warm: Warm =>
if promoted.contains(warm) then Nil
else {
promoted.add(warm)
val errors = warm.tryPromote(msg, source)
if errors.nonEmpty then promoted.remove(warm)
errors
}
case fun @ Fun(body, thisV, klass, env) =>
if promoted.contains(fun) then Nil
else
val res = withEnv(env) { eval(body, thisV, klass) }
val errors2 = res.value.promote(msg, source)
if (res.errors.nonEmpty || errors2.nonEmpty)
UnsafePromotion(msg, source, trace.toVector, res.errors ++ errors2) :: Nil
else
promoted.add(fun)
Nil
case RefSet(refs) =>
refs.flatMap(_.promote(msg, source))
}
end extension
extension (warm: Warm)
/** Try early promotion of warm objects
*
* Promotion is expensive and should only be performed for small classes.
*
* 1. for each concrete method `m` of the warm object:
* call the method and promote the result
*
* 2. for each concrete field `f` of the warm object:
* promote the field value
*
* If the object contains nested classes as members, the checker simply
* reports a warning to avoid expensive checks.
*
* TODO: we need to revisit whether this is needed once we make the
* system more flexible in other dimentions: e.g. leak to
* methods or constructors, or use ownership for creating cold data structures.
*/
def tryPromote(msg: String, source: Tree): Contextual[List[Error]] = log("promote " + warm.show + ", promoted = " + promoted, printer) {
val classRef = warm.klass.appliedRef
if classRef.memberClasses.nonEmpty || !warm.isFullyFilled then
return PromoteError(msg, source, trace.toVector) :: Nil
val buffer = new mutable.ArrayBuffer[Error]
warm.klass.baseClasses.exists { klass =>
klass.hasSource && klass.info.decls.exists { member =>
if !member.isType && !member.isConstructor && member.hasSource && !member.is(Flags.Deferred) then
if member.is(Flags.Method) then
val trace2 = trace.add(source)
locally {
given Trace = trace2
val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, EmptyTree))
val res = warm.call(member, args, superType = NoType, source = member.defTree)
buffer ++= res.ensureHot(msg, source).errors
}
else
val res = warm.select(member, source)
buffer ++= res.ensureHot(msg, source).errors
buffer.nonEmpty
}
}
if buffer.isEmpty then Nil
else UnsafePromotion(msg, source, trace.toVector, buffer.toList) :: Nil
}
end extension
// ----- Policies ------------------------------------------------------
extension (value: Ref)
/** Can the method call on `value` be ignored?
*
* Note: assume overriding resolution has been performed.
*/
def canIgnoreMethodCall(meth: Symbol)(using Context): Boolean =
val cls = meth.owner
cls == defn.AnyClass ||
cls == defn.AnyValClass ||
cls == defn.ObjectClass
// ----- Work list ---------------------------------------------------
case class Task(value: ThisRef)
class WorkList private[Semantic]() {
private var pendingTasks: List[Task] = Nil
def addTask(task: Task): Unit =
if !pendingTasks.contains(task) then pendingTasks = task :: pendingTasks
/** Process the worklist until done */
final def work()(using Cache, Context): Unit =
for task <- pendingTasks
do doTask(task)
/** Check an individual class
*
* This method should only be called from the work list scheduler.
*/
private def doTask(task: Task)(using Cache, Context): Unit = {
val thisRef = task.value
val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
val paramValues = tpl.constr.termParamss.flatten.map(param => param.symbol -> Hot).toMap
@tailrec
def iterate(): Unit = {
given Promoted = Promoted.empty
given Trace = Trace.empty
given Env = Env(paramValues)