Skip to content

Commit 7d43b44

Browse files
authored
Merge pull request #15659 from dotty-staging/init-docs
Improve documentation and error messages for safe initialization
2 parents af16d56 + ad1dc9c commit 7d43b44

18 files changed

+185
-68
lines changed

compiler/src/dotty/tools/dotc/transform/init/Errors.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ object Errors:
8282

8383
case class AccessCold(field: Symbol, trace: Seq[Tree]) extends Error:
8484
def show(using Context): String =
85-
"Access field on a value with an unknown initialization status." + stacktrace()
85+
"Access field " + field.show + " on a cold object." + stacktrace()
8686

8787
case class CallCold(meth: Symbol, trace: Seq[Tree]) extends Error:
8888
def show(using Context): String =
89-
"Call method on a value with an unknown initialization." + stacktrace()
89+
"Call method " + meth.show + " on a cold object." + stacktrace()
9090

9191
case class CallUnknown(meth: Symbol, trace: Seq[Tree]) extends Error:
9292
def show(using Context): String =

compiler/src/dotty/tools/dotc/transform/init/Semantic.scala

+8-7
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ object Semantic:
5656
case ThisRef(klass) =>
5757
"ThisRef[" + klass.show + "]"
5858
case Warm(klass, outer, ctor, args) =>
59-
"Warm[" + klass.show + "] { outer = " + outer.show + ", args = " + args.map(_.show).mkString("(", ", ", ")") + " }"
59+
val argsText = if args.nonEmpty then ", args = " + args.map(_.show).mkString("(", ", ", ")") else ""
60+
"Warm[" + klass.show + "] { outer = " + outer.show + argsText + " }"
6061
case Fun(expr, thisV, klass) =>
6162
"Fun { this = " + thisV.show + ", owner = " + klass.show + " }"
6263
case RefSet(values) =>
@@ -1080,7 +1081,7 @@ object Semantic:
10801081
eval(body, thisV, klass)
10811082
}
10821083
given Trace = Trace.empty.add(body)
1083-
res.promote("The function return value is not fully initialized.")
1084+
res.promote("The function return value is not fully initialized. Found = " + res.show + ". ")
10841085
}
10851086
if errors.nonEmpty then
10861087
reporter.report(UnsafePromotion(msg, trace.toVector, errors.head))
@@ -1124,12 +1125,12 @@ object Semantic:
11241125
withTrace(Trace.empty) {
11251126
val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, Trace.empty))
11261127
val res = warm.call(member, args, receiver = NoType, superType = NoType)
1127-
res.promote("Cannot prove that the return value of " + member + " is fully initialized.")
1128+
res.promote("Cannot prove that the return value of " + member.show + " is fully initialized. Found = " + res.show + ". ")
11281129
}
11291130
else
11301131
withTrace(Trace.empty) {
11311132
val res = warm.select(member)
1132-
res.promote("Cannot prove that the field " + member + " is fully initialized.")
1133+
res.promote("Cannot prove that the field " + member.show + " is fully initialized. Found = " + res.show + ". ")
11331134
}
11341135
end for
11351136
end for
@@ -1230,7 +1231,7 @@ object Semantic:
12301231
/** Utility definition used for better error-reporting of argument errors */
12311232
case class ArgInfo(value: Value, trace: Trace):
12321233
def promote: Contextual[Unit] = withTrace(trace) {
1233-
value.promote("Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.")
1234+
value.promote("Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.\nFound = " + value.show + ". ")
12341235
}
12351236

12361237
/** Evaluate an expression with the given value for `this` in a given class `klass`
@@ -1395,14 +1396,14 @@ object Semantic:
13951396
case Match(selector, cases) =>
13961397
val res = eval(selector, thisV, klass)
13971398
extendTrace(selector) {
1398-
res.ensureHot("The value to be matched needs to be fully initialized.")
1399+
res.ensureHot("The value to be matched needs to be fully initialized. Found = " + res.show + ". ")
13991400
}
14001401
eval(cases.map(_.body), thisV, klass).join
14011402

14021403
case Return(expr, from) =>
14031404
val res = eval(expr, thisV, klass)
14041405
extendTrace(expr) {
1405-
res.ensureHot("return expression must be fully initialized.")
1406+
res.ensureHot("return expression must be fully initialized. Found = " + res.show + ". ")
14061407
}
14071408

14081409
case WhileDo(cond, body) =>

docs/_docs/reference/other-new-features/safe-initialization.md

+121-14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ nightlyOf: https://docs.scala-lang.org/scala3/reference/other-new-features/safe-
66

77
Scala 3 implements experimental safe initialization check, which can be enabled by the compiler option `-Ysafe-init`.
88

9+
The design and implementation of the initialization checker is described in the
10+
paper _Safe object initialization, abstractly_ [3].
11+
912
## A Quick Glance
1013

1114
To get a feel of how it works, we first show several examples below.
@@ -105,8 +108,8 @@ By _reasonable usage_, we include the following use cases (but not restricted to
105108

106109
## Principles
107110

108-
To achieve the goals, we uphold three fundamental principles:
109-
_stackability_, _monotonicity_ and _scopability_.
111+
To achieve the goals, we uphold the following fundamental principles:
112+
_stackability_, _monotonicity_, _scopability_ and _authority_.
110113

111114
Stackability means that all fields of a class are initialized at the end of the
112115
class body. Scala enforces this property in syntax by demanding that all fields
@@ -159,41 +162,144 @@ Monotonicity is based on a well-known technique called _heap monotonic
159162
typestate_ to ensure soundness in the presence of aliasing
160163
[1]. Roughly speaking, it means initialization state should not go backwards.
161164

162-
Scopability means that there are no side channels to access to partially constructed objects. Control effects like coroutines, delimited
165+
Scopability means that there are no side channels to access to partially
166+
constructed objects. Control effects like coroutines, delimited
163167
control, resumable exceptions may break the property, as they can transport a
164168
value upper in the stack (not in scope) to be reachable from the current scope.
165169
Static fields can also serve as a teleport thus breaks this property. In the
166170
implementation, we need to enforce that teleported values are transitively
167171
initialized.
168172

169-
The principles enable _local reasoning_ of initialization, which means:
173+
The three principles above contribute to _local reasoning about initialization_,
174+
which means:
170175

171176
> An initialized environment can only produce initialized values.
172177
173178
For example, if the arguments to an `new`-expression are transitively
174179
initialized, so is the result. If the receiver and arguments in a method call
175180
are transitively initialized, so is the result.
176181

182+
Local reasoning about initialization gives rise to a fast initialization
183+
checker, as it avoids whole-program analysis.
184+
185+
The principle of authority goes hand-in-hand with monotonicity: the principle
186+
of monotonicity stipulates that initialization states cannot go backwards, while
187+
the principle of authority stipulates that the initialization states may not
188+
go forward at arbitrary locations due to aliasing. In Scala, we may only
189+
advance initialization states of objects in the class body when a field is
190+
defined with a mandatory initializer or at local reasoning points when the object
191+
becomes transitively initialized.
192+
193+
## Abstract Values
194+
195+
There are three fundamental abstractions for initialization states of objects:
196+
197+
- __Cold__: A cold object may have uninitialized fields.
198+
- __Warm__: A warm object has all its fields initialized but may reach _cold_ objects.
199+
- __Hot__: A hot object is transitively initialized, i.e., it only reaches warm objects.
200+
201+
In the initialization checker, the abstraction `Warm` is refined to handle inner
202+
classes and multiple constructors:
203+
204+
- __Warm[C] { outer = V, ctor, args = Vs }__: A warm object of class `C`, where the immediate outer of `C` is `V`, the constructor is `ctor` and constructor arguments are `Vs`.
205+
206+
The initialization checker checks each concrete class separately. The abstraction `ThisRef`
207+
represents the current object under initialization:
208+
209+
- __ThisRef[C]__: The current object of class `C` under initialization.
210+
211+
The initialization state of the current object is stored in the abstract heap as an
212+
abstract object. The abstract heap also serves as a cache for the field values
213+
of warm objects. `Warm` and `ThisRef` are "addresses" of the abstract objects stored
214+
in the abstract heap.
215+
216+
Two more abstractions are introduced to support functions and conditional
217+
expressions:
218+
219+
- __Fun(e, V, C)__: An abstract function value where `e` is the code, `V` is the
220+
abstract value for `this` inside the function body and the function is located
221+
inside the class `C`.
222+
223+
- __Refset(Vs)__: A set of abstract values `Vs`.
224+
225+
A value `v` is _effectively hot_ if any of the following is true:
226+
227+
- `v` is `Hot`.
228+
- `v` is `ThisRef` and all fields of the underlying object are assigned.
229+
- `v` is `Warm[C] { ... }` and
230+
1. `C` does not contain inner classes; and
231+
2. Calling any method on `v` encounters no initialization errors and the method return value is _effectively hot_; and
232+
3. Each field of `v` is _effectively hot_.
233+
- `v` is `Fun(e, V, C)` and calling the function encounters no errors and the
234+
function return value is _effectively hot_.
235+
- The root object (refered by `ThisRef`) is _effectively hot_.
236+
237+
An effectively hot value can be regarded as transitively initialized thus can
238+
be safely leaked via method arguments or as RHS of an reassignment.
239+
The initialization checker tries to promote non-hot values to effectively hot
240+
whenenver possible.
241+
177242
## Rules
178243

179-
With the established principles and design goals, following rules are imposed:
244+
With the established principles and design goals, the following rules are imposed:
180245

181-
1. In an assignment `o.x = e`, the expression `e` may only point to transitively initialized objects.
246+
1. The field access `e.f` or method call `e.m()` is illegal if `e` is _cold_.
247+
248+
A cold value should not be used.
249+
250+
2. The field access `e.f` is invalid if `e` has the value `ThisRef` and `f` is not initialized.
251+
252+
3. In an assignment `o.x = e`, the expression `e` must be _effectively hot_.
182253

183254
This is how monotonicity is enforced in the system. Note that in an
184-
initialization `val f: T = e`, the expression `e` may point to an object
185-
under initialization. This requires a distinction between mutation and
186-
initialization in order to enforce different rules. Scala
187-
has different syntax for them, it thus is not an issue.
255+
initialization `val f: T = e`, the expression `e` may point to a non-hot
256+
value.
188257

189-
2. Objects under initialization may not be passed as arguments to method calls.
258+
4. Arguments to method calls must be _effectively hot_.
190259

191260
Escape of `this` in the constructor is commonly regarded as an anti-pattern.
192-
However, escape of `this` as constructor arguments are allowed, to support
261+
262+
However, passing non-hot values as argument to another constructor is allowed, to support
193263
creation of cyclic data structures. The checker will ensure that the escaped
194264
non-initialized object is not used, i.e. calling methods or accessing fields
195265
on the escaped object is not allowed.
196266

267+
An exception is for calling synthetic `apply`s of case classes. For example,
268+
the method call `Some.apply(e)` will be interpreted as `new Some(e)`, thus
269+
is valid even if `e` is not hot.
270+
271+
Another exception to this rule is parametric method calls. For example, in
272+
`List.apply(e)`, the argument `e` may be non-hot. If that is the case, the
273+
result value of the parametric method call is taken as _cold_.
274+
275+
5. Method calls on hot values with effectively hot arguments produce hot results.
276+
277+
This rule is assured by local reasoning about initialization.
278+
279+
6. Method calls on `ThisRef` and warm values will be resolved statically and the
280+
corresponding method bodies are checked.
281+
282+
7. In a new expression `new p.C(args)`, if the values of `p` and `args` are
283+
effectively hot, then the result value is also hot.
284+
285+
This is assured by local reasoning about initialization.
286+
287+
8. In a new expression `new p.C(args)`, if any value of `p` and `args` is not
288+
effectively hot, then the result value takes the form `Warm[C] { outer = Vp, args = Vargs }`. The initialization code for the class `C` is checked again to make
289+
sure the non-hot values are used properly.
290+
291+
In the above, `Vp` is the widened value of `p` --- the widening happens if `p`
292+
is a warm value `Warm[D] { outer = V, args }` and we widen it to
293+
`Warm[D] { outer = Cold, args }`.
294+
295+
The variable `Vargs` represents values of `args` with non-hot values widened
296+
to `Cold`.
297+
298+
The motivation for the widening is to finitize the abstract domain and ensure
299+
termination of the initialization check.
300+
301+
9. The scrutinee in a pattern match and the values in return and throw statements must be _effectively hot_.
302+
197303
## Modularity
198304

199305
The analysis takes the primary constructor of concrete classes as entry points.
@@ -206,7 +312,7 @@ tightly coupled. For example, adding a method in the superclass requires
206312
recompiling the child class for checking safe overriding.
207313

208314
Initialization is no exception in this respect. The initialization of an object
209-
essentially invovles close interaction between subclass and superclass. If the
315+
essentially involves close interaction between subclass and superclass. If the
210316
superclass is defined in another project, the crossing of project boundary
211317
cannot be avoided for soundness of the analysis.
212318

@@ -233,4 +339,5 @@ mark some fields as lazy.
233339
## References
234340

235341
1. Fähndrich, M. and Leino, K.R.M., 2003, July. [_Heap monotonic typestates_](https://www.microsoft.com/en-us/research/publication/heap-monotonic-typestate/). In International Workshop on Aliasing, Confinement and Ownership in object-oriented programming (IWACO).
236-
2. Fengyun Liu, Ondřej Lhoták, Aggelos Biboudis, Paolo G. Giarrusso, and Martin Odersky. 2020. [_A type-and-effect system for object initialization_](https://dl.acm.org/doi/10.1145/3428243). OOPSLA, 2020.
342+
2. Fengyun Liu, Ondřej Lhoták, Aggelos Biboudis, Paolo G. Giarrusso, and Martin Odersky. [_A type-and-effect system for object initialization_](https://dl.acm.org/doi/10.1145/3428243). OOPSLA, 2020.
343+
3. Fengyun Liu, Ondřej Lhoták, Enze Xing, Nguyen Cao Pham. [_Safe object initialization, abstractly_](https://dl.acm.org/doi/10.1145/3486610.3486895). Scala 2021.

tests/init/neg/closureLeak.check

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
-- Error: tests/init/neg/closureLeak.scala:11:14 -----------------------------------------------------------------------
22
11 | l.foreach(a => a.addX(this)) // error
33
| ^^^^^^^^^^^^^^^^^
4-
| Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace:
5-
| -> class Outer { [ closureLeak.scala:1 ]
6-
| ^
7-
| -> l.foreach(a => a.addX(this)) // error [ closureLeak.scala:11 ]
8-
| ^^^^^^^^^^^^^^^^^
4+
| Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.
5+
| Found = Fun { this = ThisRef[class Outer], owner = class Outer }. Calling trace:
6+
| -> class Outer { [ closureLeak.scala:1 ]
7+
| ^
8+
| -> l.foreach(a => a.addX(this)) // error [ closureLeak.scala:11 ]
9+
| ^^^^^^^^^^^^^^^^^
910
|
10-
| Promoting the value to fully initialized failed due to the following problem:
11-
| Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.
12-
| Non initialized field(s): value p. Calling trace:
13-
| -> l.foreach(a => a.addX(this)) // error [ closureLeak.scala:11 ]
14-
| ^^^^
11+
| Promoting the value to fully initialized failed due to the following problem:
12+
| Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.
13+
| Found = ThisRef[class Outer].
14+
| Non initialized field(s): value p. Calling trace:
15+
| -> l.foreach(a => a.addX(this)) // error [ closureLeak.scala:11 ]
16+
| ^^^^

tests/init/neg/cycle-structure.check

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
| ^^^^^^^
99
|
1010
| It leads to the following error during object initialization:
11-
| Access field on a value with an unknown initialization status. Calling trace:
11+
| Access field value x on a cold object. Calling trace:
1212
| -> case class B(a: A) { [ cycle-structure.scala:7 ]
1313
| ^
1414
| -> val x1 = a.x [ cycle-structure.scala:8 ]
@@ -23,7 +23,7 @@
2323
| ^^^^^^^
2424
|
2525
| It leads to the following error during object initialization:
26-
| Access field on a value with an unknown initialization status. Calling trace:
26+
| Access field value x on a cold object. Calling trace:
2727
| -> case class A(b: B) { [ cycle-structure.scala:1 ]
2828
| ^
2929
| -> val x1 = b.x [ cycle-structure.scala:2 ]

tests/init/neg/default-this.check

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
9 | compare() // error
33
| ^^^^^^^
44
| Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.
5+
| Found = ThisRef[class B].
56
| Non initialized field(s): value result. Calling trace:
67
| -> class B extends A { [ default-this.scala:6 ]
78
| ^

tests/init/neg/i15363.check

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
| ^^^^^^^^^^^
99
|
1010
| It leads to the following error during object initialization:
11-
| Access field on a value with an unknown initialization status. Calling trace:
11+
| Access field value m on a cold object. Calling trace:
1212
| -> class B(a: A): [ i15363.scala:7 ]
1313
| ^
1414
| -> val x = a.m [ i15363.scala:8 ]

tests/init/neg/i15459.check

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
3 | println(this) // error
33
| ^^^^
44
| Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.
5+
| Found = ThisRef[class Sub].
56
| Non initialized field(s): value b. Calling trace:
67
| -> class Sub extends Sup: [ i15459.scala:5 ]
78
| ^

tests/init/neg/inherit-non-hot.check

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
| ^^^^^^^^^^^^^^^
1313
|
1414
|Promoting the value to fully initialized failed due to the following problem:
15-
|Cannot prove that the field val a is fully initialized.
15+
|Cannot prove that the field value a is fully initialized. Found = Cold.

tests/init/neg/inlined-method.check

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
8 | scala.runtime.Scala3RunTime.assertFailed(message) // error
33
| ^^^^^^^
44
| Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.
5+
| Found = ThisRef[class InlineError].
56
| Non initialized field(s): value v. Calling trace:
67
| -> class InlineError { [ inlined-method.scala:1 ]
78
| ^

tests/init/neg/inner-first.check

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
3 | println(this) // error
33
| ^^^^
44
| Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.
5+
| Found = ThisRef[class B].
56
| Non initialized field(s): value n. Calling trace:
67
| -> class B: [ inner-first.scala:2 ]
78
| ^

tests/init/neg/leak-warm.check

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-- Error: tests/init/neg/leak-warm.scala:19:18 -------------------------------------------------------------------------
22
19 | val l2 = l.map(_.m()) // error
33
| ^^^^^^^^^^^^
4-
| Call method on a value with an unknown initialization. Calling trace:
4+
| Call method method map on a cold object. Calling trace:
55
| -> object leakWarm { [ leak-warm.scala:1 ]
66
| ^
77
| -> val l2 = l.map(_.m()) // error [ leak-warm.scala:19 ]

0 commit comments

Comments
 (0)