-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Copy pathObjects.scala
1808 lines (1551 loc) · 74.2 KB
/
Objects.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 Denotations.Denotation
import StdNames.*
import Names.TermName
import NameKinds.OuterSelectName
import NameKinds.SuperAccessorName
import Decorators.*
import ast.tpd.*
import util.{ SourcePosition, NoSourcePosition }
import config.Printers.init as printer
import reporting.StoreReporter
import reporting.trace as log
import typer.Applications.*
import Errors.*
import Trace.*
import Util.*
import scala.collection.immutable.ListSet
import scala.collection.mutable
import scala.annotation.tailrec
import scala.annotation.constructorOnly
import dotty.tools.dotc.core.Flags.AbstractOrTrait
/** Check initialization safety of static objects
*
* The problem is illustrated by the example below:
*
* class Foo(val opposite: Foo)
* case object A extends Foo(B) // A -> B
* case object B extends Foo(A) // B -> A
*
* In the code above, the initialization of object `A` depends on `B` and vice versa. There is no
* correct way to initialize the code above. The current checker issues a warning for the code
* above.
*
* At the high-level, the analysis has the following characteristics:
*
* 1. The check enforces the principle of "initialization-time irrelevance", which means that the
* time when an object is initialized should not change program semantics. For that purpose, it
* enforces the following rule:
*
* The initialization of a static object should not directly or indirectly read or write
* mutable state of another static object.
*
* This principle not only put initialization of static objects on a solid foundation, but also
* avoids whole-program analysis.
*
* 2. The design is based on the concept of "cold aliasing" --- a cold alias may not be actively
* used during initialization, i.e., it's forbidden to call methods or access fields of a cold
* alias. Method arguments are cold aliases by default unless specified to be sensitive. Method
* parameters captured in lambdas or inner classes are always cold aliases.
*
* 3. It is inter-procedural and flow-sensitive.
*
* 4. It is object-sensitive by default and parameter-sensitive on-demand.
*
* 5. The check is modular in the sense that each object is checked separately and there is no
* whole-program analysis. However, the check is not modular in terms of project boundaries.
*
*/
class Objects(using Context @constructorOnly):
val immutableHashSetNode: Symbol = requiredClass("scala.collection.immutable.SetNode")
// TODO: this should really be an annotation on the rhs of the field initializer rather than the field itself.
val SetNode_EmptySetNode: Symbol = Denotations.staticRef("scala.collection.immutable.SetNode.EmptySetNode".toTermName).symbol
val immutableHashSet: Symbol = requiredModule("scala.collection.immutable.HashSet")
val HashSet_EmptySet: Symbol = Denotations.staticRef("scala.collection.immutable.HashSet.EmptySet".toTermName).symbol
val immutableVector: Symbol = requiredModule("scala.collection.immutable.Vector")
val Vector_EmptyIterator: Symbol = immutableVector.requiredValue("emptyIterator")
val immutableMapNode: Symbol = requiredModule("scala.collection.immutable.MapNode")
val MapNode_EmptyMapNode: Symbol = immutableMapNode.requiredValue("EmptyMapNode")
val immutableHashMap: Symbol = requiredModule("scala.collection.immutable.HashMap")
val HashMap_EmptyMap: Symbol = immutableHashMap.requiredValue("EmptyMap")
val immutableLazyList: Symbol = requiredModule("scala.collection.immutable.LazyList")
val LazyList_empty: Symbol = immutableLazyList.requiredValue("_empty")
val whiteList: Set[Symbol] = Set(SetNode_EmptySetNode, HashSet_EmptySet, Vector_EmptyIterator, MapNode_EmptyMapNode, HashMap_EmptyMap, LazyList_empty)
// ----------------------------- abstract domain -----------------------------
/** Syntax for the data structure abstraction used in abstract domain:
*
* ve ::= ObjectRef(class) // global object
* | OfClass(class, vs[outer], ctor, args, env) // instance of a class
* | OfArray(object[owner], regions)
* | Fun(..., env) // value elements that can be contained in ValueSet
* vs ::= ValueSet(ve) // set of abstract values
* Bottom ::= ValueSet(Empty)
* val ::= ve | Cold | vs // all possible abstract values in domain
* Ref ::= ObjectRef | OfClass // values that represent a reference to some (global or instance) object
* ThisValue ::= Ref | Cold // possible values for 'this'
*
* refMap = Ref -> ( valsMap, varsMap, outersMap ) // refMap stores field informations of an object or instance
* valsMap = valsym -> val // maps immutable fields to their values
* varsMap = valsym -> addr // each mutable field has an abstract address
* outersMap = class -> val // maps outer objects to their values
*
* arrayMap = OfArray -> addr // an array has one address that stores the join value of every element
*
* heap = addr -> val // heap is mutable
*
* env = (valsMap, Option[env]) // stores local variables in the residing method, and possibly outer environments
*
* addr ::= localVarAddr(regions, valsym, owner)
* | fieldVarAddr(regions, valsym, owner) // independent of OfClass/ObjectRef
* | arrayAddr(regions, owner) // independent of array element type
*
* regions ::= List(sourcePosition)
*/
sealed abstract class Value:
def show(using Context): String
/** ValueElement are elements that can be contained in a RefSet */
sealed abstract class ValueElement extends Value
/**
* A reference caches the values for outers and immutable fields.
*/
sealed abstract class Ref(
valsMap: mutable.Map[Symbol, Value],
varsMap: mutable.Map[Symbol, Heap.Addr],
outersMap: mutable.Map[ClassSymbol, Value])
extends ValueElement:
protected val vals: mutable.Map[Symbol, Value] = valsMap
protected val vars: mutable.Map[Symbol, Heap.Addr] = varsMap
protected val outers: mutable.Map[ClassSymbol, Value] = outersMap
def isObjectRef: Boolean = this.isInstanceOf[ObjectRef]
def klass: ClassSymbol
def valValue(sym: Symbol): Value = vals(sym)
def varAddr(sym: Symbol): Heap.Addr = vars(sym)
def outerValue(cls: ClassSymbol): Value = outers(cls)
def hasVal(sym: Symbol): Boolean = vals.contains(sym)
def hasVar(sym: Symbol): Boolean = vars.contains(sym)
def hasOuter(cls: ClassSymbol): Boolean = outers.contains(cls)
def initVal(field: Symbol, value: Value)(using Context) = log("Initialize " + field.show + " = " + value + " for " + this, printer) {
assert(!field.is(Flags.Mutable), "Field is mutable: " + field.show)
assert(!vals.contains(field), "Field already set: " + field.show)
vals(field) = value
}
def initVar(field: Symbol, addr: Heap.Addr)(using Context) = log("Initialize " + field.show + " = " + addr + " for " + this, printer) {
assert(field.is(Flags.Mutable), "Field is not mutable: " + field.show)
assert(!vars.contains(field), "Field already set: " + field.show)
vars(field) = addr
}
def initOuter(cls: ClassSymbol, value: Value)(using Context) = log("Initialize outer " + cls.show + " = " + value + " for " + this, printer) {
assert(!outers.contains(cls), "Outer already set: " + cls)
outers(cls) = value
}
/** A reference to a static object */
case class ObjectRef(klass: ClassSymbol)
extends Ref(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty):
val owner = klass
def show(using Context) = "ObjectRef(" + klass.show + ")"
/**
* Represents values that are instances of the specified class.
*
* Note that the 2nd parameter block does not take part in the definition of equality.
*/
case class OfClass private (
klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)(
valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr], outersMap: mutable.Map[ClassSymbol, Value])
extends Ref(valsMap, varsMap, outersMap):
def widenedCopy(outer: Value, args: List[Value], env: Env.Data): OfClass =
new OfClass(klass, outer, ctor, args, env)(this.valsMap, this.varsMap, this.outersMap)
def show(using Context) =
val valFields = vals.map(_.show + " -> " + _.show)
"OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ", vals = " + valFields + ")"
object OfClass:
def apply(
klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)(
using Context
): OfClass =
val instance = new OfClass(klass, outer, ctor, args, env)(
valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty
)
instance.initOuter(klass, outer)
instance
/**
* Represents arrays.
*
* Note that the 2nd parameter block does not take part in the definition of equality.
*
* Different arrays are distinguished by the context. Currently the default context is the static
* object whose initialization triggers the creation of the array.
*
* In the future, it is possible that we introduce a mechanism for end-users to mark the context.
*
* @param owner The static object whose initialization creates the array.
*/
case class OfArray(owner: ClassSymbol, regions: Regions.Data)(using @constructorOnly ctx: Context, @constructorOnly trace: Trace) extends ValueElement:
val klass: ClassSymbol = defn.ArrayClass
val addr: Heap.Addr = Heap.arrayAddr(regions, owner)
def show(using Context) = "OfArray(owner = " + owner.show + ")"
/**
* Represents a lambda expression
* @param klass The enclosing class of the anonymous function's creation site
*/
case class Fun(code: Tree, thisV: ThisValue, klass: ClassSymbol, env: Env.Data) extends ValueElement:
def show(using Context) = "Fun(" + code.show + ", " + thisV.show + ", " + klass.show + ")"
/**
* Represents a set of values
*
* It comes from `if` expressions.
*/
case class ValueSet(values: ListSet[ValueElement]) extends Value:
def show(using Context) = values.map(_.show).mkString("[", ",", "]")
/** A cold alias which should not be used during initialization.
*
* Cold is not ValueElement since RefSet containing Cold is equivalent to Cold
*/
case object Cold extends Value:
def show(using Context) = "Cold"
val Bottom = ValueSet(ListSet.empty)
/** Possible types for 'this' */
type ThisValue = Ref | Cold.type
/** Checking state */
object State:
class Data:
// objects under check
private[State] val checkingObjects = new mutable.ArrayBuffer[ObjectRef]
private[State] val checkedObjects = new mutable.ArrayBuffer[ObjectRef]
private[State] val pendingTraces = new mutable.ArrayBuffer[Trace]
end Data
def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last.klass
private def doCheckObject(classSym: ClassSymbol)(using ctx: Context, data: Data) =
val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
var count = 0
given Cache.Data = new Cache.Data
@tailrec
def iterate()(using Context): ObjectRef =
count += 1
given Trace = Trace.empty.add(classSym.defTree)
given Env.Data = Env.emptyEnv(tpl.constr.symbol)
given Heap.MutableData = Heap.empty()
given returns: Returns.Data = Returns.empty()
given regions: Regions.Data = Regions.empty // explicit name to avoid naming conflict
val obj = ObjectRef(classSym)
log("Iteration " + count) {
data.checkingObjects += obj
init(tpl, obj, classSym)
assert(data.checkingObjects.last.klass == classSym, "Expect = " + classSym.show + ", found = " + data.checkingObjects.last.klass)
data.checkingObjects.remove(data.checkingObjects.size - 1)
}
val hasError = ctx.reporter.pendingMessages.nonEmpty
if cache.hasChanged && !hasError then
cache.prepareForNextIteration()
iterate()
else
data.checkedObjects += obj
obj
end iterate
val reporter = new StoreReporter(ctx.reporter)
val obj = iterate()(using ctx.fresh.setReporter(reporter))
for warning <- reporter.pendingMessages do
ctx.reporter.report(warning)
obj
end doCheckObject
def checkObjectAccess(clazz: ClassSymbol)(using data: Data, ctx: Context, pendingTrace: Trace): ObjectRef =
val index = data.checkingObjects.indexOf(ObjectRef(clazz))
if index != -1 then
if data.checkingObjects.size - 1 > index then
// Only report errors for non-trivial cycles, ignore self cycles.
val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, acc) => acc ++ a }
val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n")
val cycle = data.checkingObjects.slice(index, data.checkingObjects.size)
val pos = clazz.defTree.sourcePos.focus
report.warning("Cyclic initialization: " + cycle.map(_.klass.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, pos)
end if
data.checkingObjects(index)
else
val objOpt = data.checkedObjects.find(_.klass == clazz)
objOpt match
case Some(obj) => obj
case None =>
data.pendingTraces += pendingTrace
val obj = doCheckObject(clazz)
data.pendingTraces.remove(data.pendingTraces.size - 1)
obj
end checkObjectAccess
end State
/** Environment for parameters */
object Env:
abstract class Data:
private[Env] def getVal(x: Symbol)(using Context): Option[Value]
private[Env] def getVar(x: Symbol)(using Context): Option[Heap.Addr]
def widen(height: Int)(using Context): Data
def level: Int
def show(using Context): String
/** Local environments can be deeply nested, therefore we need `outer`.
*
* For local variables in rhs of class field definitions, the `meth` is the primary constructor.
*/
private case class LocalEnv
(private[Env] val params: Map[Symbol, Value], meth: Symbol, outer: Data)
(valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr])
(using Context)
extends Data:
val level = outer.level + 1
if (level > 3)
report.warning("[Internal error] Deeply nested environment, level = " + level + ", " + meth.show + " in " + meth.enclosingClass.show, meth.defTree)
private[Env] val vals: mutable.Map[Symbol, Value] = valsMap
private[Env] val vars: mutable.Map[Symbol, Heap.Addr] = varsMap
private[Env] def getVal(x: Symbol)(using Context): Option[Value] =
if x.is(Flags.Param) then params.get(x)
else vals.get(x)
private[Env] def getVar(x: Symbol)(using Context): Option[Heap.Addr] =
vars.get(x)
def widen(height: Int)(using Context): Data =
new LocalEnv(params.map(_ -> _.widen(height)), meth, outer.widen(height))(this.vals, this.vars)
def show(using Context) =
"owner: " + meth.show + "\n" +
"params: " + params.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" +
"vals: " + vals.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" +
"vars: " + vars.map(_.show + " ->" + _).mkString("{", ", ", "}") + "\n" +
"outer = {\n" + outer.show + "\n}"
end LocalEnv
object NoEnv extends Data:
val level = 0
private[Env] def getVal(x: Symbol)(using Context): Option[Value] =
throw new RuntimeException("Invalid usage of non-existent env")
private[Env] def getVar(x: Symbol)(using Context): Option[Heap.Addr] =
throw new RuntimeException("Invalid usage of non-existent env")
def widen(height: Int)(using Context): Data = this
def show(using Context): String = "NoEnv"
end NoEnv
/** An empty environment can be used for non-method environments, e.g., field initializers.
*
* The owner for the local environment for field initializers is the primary constructor of the
* enclosing class.
*/
def emptyEnv(meth: Symbol)(using Context): Data =
new LocalEnv(Map.empty, meth, NoEnv)(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty)
def valValue(x: Symbol)(using data: Data, ctx: Context, trace: Trace): Value =
data.getVal(x) match
case Some(theValue) =>
theValue
case _ =>
report.warning("[Internal error] Value not found " + x.show + "\nenv = " + data.show + ". " + Trace.show, Trace.position)
Bottom
def getVal(x: Symbol)(using data: Data, ctx: Context): Option[Value] = data.getVal(x)
def getVar(x: Symbol)(using data: Data, ctx: Context): Option[Heap.Addr] = data.getVar(x)
def of(ddef: DefDef, args: List[Value], outer: Data)(using Context): Data =
val params = ddef.termParamss.flatten.map(_.symbol)
assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size)
assert(ddef.symbol.owner.isClass ^ (outer != NoEnv), "ddef.owner = " + ddef.symbol.owner.show + ", outer = " + outer + ", " + ddef.source)
new LocalEnv(params.zip(args).toMap, ddef.symbol, outer)(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty)
def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit =
assert(!x.isOneOf(Flags.Param | Flags.Mutable), "Only local immutable variable allowed")
data match
case localEnv: LocalEnv =>
assert(!localEnv.vals.contains(x), "Already initialized local " + x.show)
localEnv.vals(x) = value
case _ =>
throw new RuntimeException("Incorrect local environment for initializing " + x.show)
def setLocalVar(x: Symbol, addr: Heap.Addr)(using data: Data, ctx: Context): Unit =
assert(x.is(Flags.Mutable, butNot = Flags.Param), "Only local mutable variable allowed")
data match
case localEnv: LocalEnv =>
assert(!localEnv.vars.contains(x), "Already initialized local " + x.show)
localEnv.vars(x) = addr
case _ =>
throw new RuntimeException("Incorrect local environment for initializing " + x.show)
/**
* Resolve the environment owned by the given method.
*
* The method could be located in outer scope with intermixed classes between its definition
* site and usage site.
*
* Due to widening, the corresponding environment might not exist. As a result reading the local
* variable will return `Cold` and it's forbidden to write to the local variable.
*
* @param meth The method which owns the environment
* @param thisV The value for `this` of the enclosing class where the local variable is referenced.
* @param env The local environment where the local variable is referenced.
*
* @return the environment and value for `this` owned by the given method.
*/
def resolveEnv(meth: Symbol, thisV: ThisValue, env: Data)(using Context): Option[(ThisValue, Data)] = log("Resolving env for " + meth.show + ", this = " + thisV.show + ", env = " + env.show, printer) {
env match
case localEnv: LocalEnv =>
if localEnv.meth == meth then Some(thisV -> env)
else resolveEnv(meth, thisV, localEnv.outer)
case NoEnv =>
thisV match
case ref: OfClass =>
ref.outer match
case outer : ThisValue =>
resolveEnv(meth, outer, ref.env)
case _ =>
// TODO: properly handle the case where ref.outer is ValueSet
None
case _ =>
None
}
def withEnv[T](env: Data)(fn: Data ?=> T): T = fn(using env)
end Env
/** Abstract heap for mutable fields
*/
object Heap:
abstract class Addr:
/** The static object which owns the mutable slot */
def owner: ClassSymbol
def getTrace: Trace = Trace.empty
/** The address for mutable fields of objects. */
private case class FieldAddr(regions: Regions.Data, field: Symbol, owner: ClassSymbol)(trace: Trace) extends Addr:
override def getTrace: Trace = trace
/** The address for mutable local variables . */
private case class LocalVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol) extends Addr
/** Immutable heap data used in the cache.
*
* We need to use structural equivalence so that in different iterations the cache can be effective.
*
* TODO: speed up equality check for heap.
*/
opaque type Data = Map[Addr, Value]
/** Store the heap as a mutable field to avoid threading it through the program. */
class MutableData(private[Heap] var heap: Data):
private[Heap] def writeJoin(addr: Addr, value: Value): Unit =
heap.get(addr) match
case None =>
heap = heap.updated(addr, value)
case Some(current) =>
val value2 = value.join(current)
if value2 != current then
heap = heap.updated(addr, value2)
end MutableData
def empty(): MutableData = new MutableData(Map.empty)
def contains(addr: Addr)(using mutable: MutableData): Boolean =
mutable.heap.contains(addr)
def read(addr: Addr)(using mutable: MutableData): Value =
mutable.heap(addr)
def writeJoin(addr: Addr, value: Value)(using mutable: MutableData): Unit =
mutable.writeJoin(addr, value)
def localVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol): Addr =
LocalVarAddr(regions, sym, owner)
def fieldVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol)(using Trace): Addr =
FieldAddr(regions, sym, owner)(summon[Trace])
def arrayAddr(regions: Regions.Data, owner: ClassSymbol)(using Trace, Context): Addr =
FieldAddr(regions, defn.ArrayClass, owner)(summon[Trace])
def getHeapData()(using mutable: MutableData): Data = mutable.heap
/** Cache used to terminate the check */
object Cache:
case class Config(thisV: Value, env: Env.Data, heap: Heap.Data)
case class Res(value: Value, heap: Heap.Data)
class Data extends Cache[Config, Res]:
def get(thisV: Value, expr: Tree)(using Heap.MutableData, Env.Data): Option[Value] =
val config = Config(thisV, summon[Env.Data], Heap.getHeapData())
super.get(config, expr).map(_.value)
def cachedEval(thisV: ThisValue, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using Heap.MutableData, Env.Data): Value =
val config = Config(thisV, summon[Env.Data], Heap.getHeapData())
val result = super.cachedEval(config, expr, cacheResult, default = Res(Bottom, Heap.getHeapData())) { expr =>
Res(fun(expr), Heap.getHeapData())
}
result.value
end Cache
/**
* Region context for mutable states
*
* By default, the region context is empty.
*/
object Regions:
opaque type Data = List[SourcePosition]
val empty: Data = Nil
def extend(pos: SourcePosition)(using data: Data): Data = pos :: data
def exists(pos: SourcePosition)(using data: Data): Boolean = data.indexOf(pos) >= 0
def show(using data: Data, ctx: Context): String = data.map(_.show).mkString("[", ", ", "]")
inline def cache(using c: Cache.Data): Cache.Data = c
/**
* Handle return statements in methods and non-local returns in functions.
*/
object Returns:
private class ReturnData(val method: Symbol, val values: mutable.ArrayBuffer[Value])
opaque type Data = mutable.ArrayBuffer[ReturnData]
def empty(): Data = mutable.ArrayBuffer()
def installHandler(meth: Symbol)(using data: Data): Unit =
data.addOne(ReturnData(meth, mutable.ArrayBuffer()))
def popHandler(meth: Symbol)(using data: Data): Value =
val returnData = data.remove(data.size - 1)
assert(returnData.method == meth, "Symbol mismatch in return handlers, expect = " + meth + ", found = " + returnData.method)
returnData.values.join
def handle(meth: Symbol, value: Value)(using data: Data, trace: Trace, ctx: Context): Unit =
data.findLast(_.method == meth) match
case Some(returnData) =>
returnData.values.addOne(value)
case None =>
report.warning("[Internal error] Unhandled return for method " + meth + " in " + meth.owner.show + ". Trace:\n" + Trace.show, Trace.position)
type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Heap.MutableData, Regions.Data, Returns.Data, Trace) ?=> T
// --------------------------- domain operations -----------------------------
case class ArgInfo(value: Value, trace: Trace, tree: Tree)
extension (a: Value)
def join(b: Value): Value =
(a, b) match
case (Cold, _) => Cold
case (_, Cold) => Cold
case (Bottom, b) => b
case (a, Bottom) => a
case (ValueSet(values1), ValueSet(values2)) => ValueSet(values1 ++ values2)
case (a : ValueElement, ValueSet(values)) => ValueSet(values + a)
case (ValueSet(values), b : ValueElement) => ValueSet(values + b)
case (a : ValueElement, b : ValueElement) => ValueSet(ListSet(a, b))
def widen(height: Int)(using Context): Value =
if height == 0 then Cold
else
a match
case Bottom => Bottom
case ValueSet(values) =>
values.map(ref => ref.widen(height)).join
case Fun(code, thisV, klass, env) =>
Fun(code, thisV.widenRefOrCold(height), klass, env.widen(height - 1))
case ref @ OfClass(klass, outer, _, args, env) =>
val outer2 = outer.widen(height - 1)
val args2 = args.map(_.widen(height - 1))
val env2 = env.widen(height - 1)
ref.widenedCopy(outer2, args2, env2)
case _ => a
def filterType(tpe: Type)(using Context): Value =
tpe match
case t @ SAMType(_, _) if a.isInstanceOf[Fun] => a // if tpe is SAMType and a is Fun, allow it
case _ =>
val baseClasses = tpe.baseClasses
if baseClasses.isEmpty then a
else filterClass(baseClasses.head) // could have called ClassSymbol, but it does not handle OrType and AndType
def filterClass(sym: Symbol)(using Context): Value =
if !sym.isClass then a
else
val klass = sym.asClass
a match
case Cold => Cold
case ref: Ref => if ref.klass.isSubClass(klass) then ref else Bottom
case ValueSet(values) => values.map(v => v.filterClass(klass)).join
case arr: OfArray => if defn.ArrayClass.isSubClass(klass) then arr else Bottom
case fun: Fun =>
if klass.isOneOf(AbstractOrTrait) && klass.baseClasses.exists(defn.isFunctionClass) then fun else Bottom
extension (value: Ref | Cold.type)
def widenRefOrCold(height : Int)(using Context) : Ref | Cold.type = value.widen(height).asInstanceOf[ThisValue]
extension (values: Iterable[Value])
def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) }
def widen(height: Int): Contextual[List[Value]] = values.map(_.widen(height)).toList
/** Handle method calls `e.m(args)`.
*
* @param value The value for the receiver.
* @param meth The symbol of the target method (could be virtual or abstract method).
* @param args Arguments of the method call (all parameter blocks flatten to a list).
* @param receiver The type of the receiver.
* @param superType The type of the super in a super call. NoType for non-super calls.
* @param needResolve Whether the target of the call needs resolution?
*/
def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", this = " + value.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) {
value.filterClass(meth.owner) match
case Cold =>
report.warning("Using cold alias. " + Trace.show, Trace.position)
Bottom
case Bottom =>
Bottom
case arr: OfArray =>
val target = resolve(defn.ArrayClass, meth)
if target == defn.Array_apply || target == defn.Array_clone then
if arr.addr.owner == State.currentObject then
Heap.read(arr.addr)
else
errorReadOtherStaticObject(State.currentObject, arr.addr)
Bottom
else if target == defn.Array_update then
assert(args.size == 2, "Incorrect number of arguments for Array update, found = " + args.size)
if arr.addr.owner != State.currentObject then
errorMutateOtherStaticObject(State.currentObject, arr.addr)
else
Heap.writeJoin(arr.addr, args.tail.head.value)
Bottom
else
// Array.length is OK
Bottom
case ref: Ref =>
val isLocal = !meth.owner.isClass
val target =
if !needResolve then
meth
else if superType.exists then
meth
else if meth.name.is(SuperAccessorName) then
ResolveSuper.rebindSuper(ref.klass, meth)
else
resolve(ref.klass, meth)
if target.isOneOf(Flags.Method) then
if target.owner == defn.ArrayModuleClass && target.name == nme.apply then
val arr = OfArray(State.currentObject, summon[Regions.Data])
Heap.writeJoin(arr.addr, args.map(_.value).join)
arr
else if target.hasSource then
val cls = target.owner.enclosingClass.asClass
val ddef = target.defTree.asInstanceOf[DefDef]
val meth = ddef.symbol
val (thisV : ThisValue, outerEnv) =
if meth.owner.isClass then
(ref, Env.NoEnv)
else
Env.resolveEnv(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv)
val env2 = Env.of(ddef, args.map(_.value), outerEnv)
extendTrace(ddef) {
given Env.Data = env2
cache.cachedEval(ref, ddef.rhs, cacheResult = true) { expr =>
Returns.installHandler(meth)
val res = cases(expr, thisV, cls)
val returns = Returns.popHandler(meth)
res.join(returns)
}
}
else
Bottom
else if target.exists then
select(ref, target, receiver, needResolve = false)
else
if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then
report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position)
Bottom
else
// This is possible due to incorrect type cast.
// See tests/init/pos/Type.scala
Bottom
case Fun(code, thisV, klass, env) =>
// meth == NoSymbol for poly functions
if meth.name == nme.tupled then
value // a call like `fun.tupled`
else
code match
case ddef: DefDef =>
if meth.name == nme.apply then
given Env.Data = Env.of(ddef, args.map(_.value), env)
extendTrace(code) { eval(ddef.rhs, thisV, klass, cacheResult = true) }
else
// The methods defined in `Any` and `AnyRef` are trivial and don't affect initialization.
if meth.owner == defn.AnyClass || meth.owner == defn.ObjectClass then
value
else
// In future, we will have Tasty for stdlib classes and can abstractly interpret that Tasty.
// For now, return `Cold` to ensure soundness and trigger a warning.
Cold
end if
end if
case _ =>
// by-name closure
given Env.Data = env
extendTrace(code) { eval(code, thisV, klass, cacheResult = true) }
case ValueSet(vs) =>
vs.map(v => call(v, meth, args, receiver, superType)).join
}
/** Handle constructor calls `<init>(args)`.
*
* @param value The value for the receiver.
* @param ctor The symbol of the target method.
* @param args Arguments of the constructor call (all parameter blocks flatten to a list).
*/
def callConstructor(value: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("call " + ctor.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) {
value match
case ref: Ref =>
if ctor.hasSource then
val cls = ctor.owner.enclosingClass.asClass
val ddef = ctor.defTree.asInstanceOf[DefDef]
val argValues = args.map(_.value)
given Env.Data = Env.of(ddef, argValues, Env.NoEnv)
if ctor.isPrimaryConstructor then
val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) }
else
extendTrace(ddef) { // The return values for secondary constructors can be ignored
Returns.installHandler(ctor)
eval(ddef.rhs, ref, cls, cacheResult = true)
Returns.popHandler(ctor)
}
else
// no source code available
Bottom
case _ =>
report.warning("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + value + Trace.show, Trace.position)
Bottom
}
/** Handle selection `e.f`.
*
* @param value The value for the receiver.
* @param field The symbol of the target field (could be virtual or abstract).
* @param receiver The type of the receiver.
* @param needResolve Whether the target of the selection needs resolution?
*/
def select(value: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + value.show, printer, (_: Value).show) {
value.filterClass(field.owner) match
case Cold =>
report.warning("Using cold alias", Trace.position)
Bottom
case ref: Ref =>
val target = if needResolve then resolve(ref.klass, field) else field
if target.is(Flags.Lazy) then
given Env.Data = Env.emptyEnv(target.owner.asInstanceOf[ClassSymbol].primaryConstructor)
if target.hasSource then
val rhs = target.defTree.asInstanceOf[ValDef].rhs
eval(rhs, ref, target.owner.asClass, cacheResult = true)
else
Bottom
else if target.exists then
def isNextFieldOfColonColon: Boolean = ref.klass == defn.ConsClass && target.name.toString == "next"
if target.isOneOf(Flags.Mutable) && !isNextFieldOfColonColon then
if ref.hasVar(target) then
val addr = ref.varAddr(target)
if addr.owner == State.currentObject then
Heap.read(addr)
else
errorReadOtherStaticObject(State.currentObject, addr)
Bottom
else if ref.isObjectRef && ref.klass.hasSource then
report.warning("Access uninitialized field " + field.show + ". " + Trace.show, Trace.position)
Bottom
else
// initialization error, reported by the initialization checker
Bottom
else if ref.hasVal(target) then
ref.valValue(target)
else if ref.isObjectRef && ref.klass.hasSource then
report.warning("Access uninitialized field " + field.show + ". " + Trace.show, Trace.position)
Bottom
else
// initialization error, reported by the initialization checker
Bottom
else
if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then
report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position)
Bottom
else
// This is possible due to incorrect type cast.
// See tests/init/pos/Type.scala
Bottom
case fun: Fun =>
report.warning("[Internal error] unexpected tree in selecting a function, fun = " + fun.code.show + Trace.show, fun.code)
Bottom
case arr: OfArray =>
report.warning("[Internal error] unexpected tree in selecting an array, array = " + arr.show + Trace.show, Trace.position)
Bottom
case Bottom =>
if field.isStaticObject then ObjectRef(field.moduleClass.asClass)
else Bottom
case ValueSet(values) =>
values.map(ref => select(ref, field, receiver)).join
}
/** Handle assignment `lhs.f = rhs`.
*
* @param lhs The value of the object to be mutated.
* @param field The symbol of the target field.
* @param rhs The value to be assigned.
* @param rhsTyp The type of the right-hand side.
*/
def assign(lhs: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + lhs.show + ", rhs = " + rhs.show, printer, (_: Value).show) {
lhs.filterClass(field.owner) match
case fun: Fun =>
report.warning("[Internal error] unexpected tree in assignment, fun = " + fun.code.show + Trace.show, Trace.position)
case arr: OfArray =>
report.warning("[Internal error] unexpected tree in assignment, array = " + arr.show + " field = " + field + Trace.show, Trace.position)
case Cold =>
report.warning("Assigning to cold aliases is forbidden. " + Trace.show, Trace.position)
case Bottom =>
case ValueSet(values) =>
values.foreach(ref => assign(ref, field, rhs, rhsTyp))
case ref: Ref =>
if ref.hasVar(field) then
val addr = ref.varAddr(field)
if addr.owner != State.currentObject then
errorMutateOtherStaticObject(State.currentObject, addr)
else
Heap.writeJoin(addr, rhs)
else
report.warning("Mutating a field before its initialization: " + field.show + ". " + Trace.show, Trace.position)
end match
Bottom
}
/**
* Handle new expression `new p.C(args)`.
* The actual instance might be cached without running the constructor.
* See tests/init-global/pos/cache-constructor.scala
*
* @param outer The value for `p`.
* @param klass The symbol of the class `C`.
* @param ctor The symbol of the target constructor.
* @param args The arguments passsed to the constructor.
*/
def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", outer = " + outer + ", args = " + args.map(_.value.show), printer, (_: Value).show) {
outer.filterClass(klass.owner) match
case _ : Fun | _: OfArray =>
report.warning("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position)
Bottom
case outer: (Ref | Cold.type | Bottom.type) =>
if klass == defn.ArrayClass then
args.head.tree.tpe match
case ConstantType(Constants.Constant(0)) =>
// new Array(0)
Bottom
case _ =>
val arr = OfArray(State.currentObject, summon[Regions.Data])
Heap.writeJoin(arr.addr, Bottom)
arr
else
// Widen the outer to finitize the domain. Arguments already widened in `evalArgs`.
val (outerWidened, envWidened) =
outer match
case _ : Bottom.type => // For top-level classes
(Bottom, Env.NoEnv)
case thisV : (Ref | Cold.type) =>
if klass.owner.isClass then
if klass.owner.is(Flags.Package) then
report.warning("[Internal error] top-level class should have `Bottom` as outer, class = " + klass.show + ", outer = " + outer.show + ", " + Trace.show, Trace.position)
(Bottom, Env.NoEnv)
else
(thisV.widenRefOrCold(1), Env.NoEnv)
else
// klass.enclosingMethod returns its primary constructor
Env.resolveEnv(klass.owner.enclosingMethod, thisV, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv)
val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened)
callConstructor(instance, ctor, args)
case ValueSet(values) =>
values.map(ref => instantiate(ref, klass, ctor, args)).join
}
/** Handle local variable definition, `val x = e` or `var x = e`.
*
* @param sym The symbol of the variable.
* @param value The value of the initializer.
*/
def initLocal(sym: Symbol, value: Value): Contextual[Unit] = log("initialize local " + sym.show + " with " + value.show, printer) {
if sym.is(Flags.Mutable) then
val addr = Heap.localVarAddr(summon[Regions.Data], sym, State.currentObject)
Env.setLocalVar(sym, addr)
Heap.writeJoin(addr, value)
else
Env.setLocalVal(sym, value)
}
/** Read local variable `x`.
*
* @param thisV The value for `this` where the variable is used.
* @param sym The symbol of the variable.
*/
def readLocal(thisV: ThisValue, sym: Symbol): Contextual[Value] = log("reading local " + sym.show, printer, (_: Value).show) {
def isByNameParam(sym: Symbol) = sym.is(Flags.Param) && sym.info.isInstanceOf[ExprType]
Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match
case Some(thisV -> env) =>
if sym.is(Flags.Mutable) then
// Assume forward reference check is doing a good job
given Env.Data = env
Env.getVar(sym) match
case Some(addr) =>
if addr.owner == State.currentObject then
Heap.read(addr)
else
errorReadOtherStaticObject(State.currentObject, addr)
Bottom
end if
case _ =>
// Only vals can be lazy
report.warning("[Internal error] Variable not found " + sym.show + "\nenv = " + env.show + ". " + Trace.show, Trace.position)
Bottom
else
given Env.Data = env