Semantic

object Semantic
class Object
trait Matchable
class Any

Type members

Classlikes

case class ArgInfo(value: Value, trace: Trace)

Utility definition used for better error-reporting of argument errors

Utility definition used for better error-reporting of argument errors

case class ByNameArg(tree: Tree)
object Cache

Cache used to terminate the analysis

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 Call
case object Cold extends Value

An object with unknown initialization status

An object with unknown initialization status

case class Config(thisV: Value, expr: Tree)

Interpreter configuration

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.

object Env

The environment for method parameters

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.

case class Fun(expr: Tree, thisV: Ref, klass: ClassSymbol, env: Env) extends Value

A function value

A function value

case object Hot extends Value

A transitively initialized object

A transitively initialized object

object NewExpr
case class Objekt(klass: ClassSymbol, fields: Map[Symbol, Value], outers: Map[ClassSymbol, Value])

The abstract object which stores value about its fields and immediate outers.

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.

object PolyFun
object Promoted
sealed abstract class Ref extends Value
case class RefSet(refs: List[Fun | Ref]) extends Value

A value which represents a set of addresses

A value which represents a set of addresses

It comes from if expressions.

object Reporter
Companion:
class
trait Reporter

Error reporting

Error reporting

Companion:
object
case class Task(value: ThisRef)
case class ThisRef(klass: ClassSymbol) extends Ref

A reference to the object under initialization pointed by this

A reference to the object under initialization pointed by this

object Trace
sealed abstract class Value

Abstract values

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
case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref

An object with all fields initialized but reaches objects under initialization

An object with all fields initialized but reaches objects under initialization

We need to restrict nesting levels of outer to finitize the domain.

class WorkList

Types

opaque type Arg

The state that threads through the interpreter

The state that threads through the interpreter

type Env = Env
type Trace = Trace

Value members

Concrete methods

def addTask(thisRef: ThisRef)(using WorkList): Unit

Add a checking task to the work list

Add a checking task to the work list

inline def cache(using c: Cache): Cache
def cases(expr: Tree, thisV: Ref, klass: ClassSymbol): () ?=> Value

Handles the evaluation of different expressions

Handles the evaluation of different expressions

Note: Recursive call should go to eval instead of cases.

def cases(tp: Type, thisV: Ref, klass: ClassSymbol): () ?=> Value

Handle semantics of leaf nodes

Handle semantics of leaf nodes

def check()(using Cache, WorkList, Context): Unit

Perform check on the work list until it becomes empty

Perform check on the work list until it becomes empty

Should only be called once from the checker.

def checkTermUsage(tpt: Tree, thisV: Ref, klass: ClassSymbol): () ?=> Unit

Check that path in path-dependent types are initialized

Check that path in path-dependent types are initialized

This is intended to avoid type soundness issues in Dotty.

inline def env(using env: Env): Env
def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean): () ?=> Value

Evaluate an expression with the given value for this in a given class klass

Evaluate an expression with the given value for this in a given class klass

Note that klass might be a super class of the object referred by thisV. The parameter klass is needed for this resolution. Consider the following code:

class A { A.this class B extends A { A.this } }

As can be seen above, the meaning of the expression A.this depends on where it is located.

This method only handles cache logic and delegates the work to cases.

def eval(exprs: List[Tree], thisV: Ref, klass: ClassSymbol): () ?=> List[Value]

Evaluate a list of expressions

Evaluate a list of expressions

def evalArgs(args: List[Arg], thisV: Ref, klass: ClassSymbol): () ?=> List[ArgInfo]

Evaluate arguments of methods

Evaluate arguments of methods

inline def extendTrace[T](node: Tree)(using t: Trace)(op: Trace ?=> T): T
def init(tpl: Template, thisV: Ref, klass: ClassSymbol): () ?=> Value

Initialize part of an abstract object in klass of the inheritance chain

Initialize part of an abstract object in klass of the inheritance chain

def outerValue(tref: TypeRef, thisV: Ref, klass: ClassSymbol): () ?=> Value

Compute the outer value that correspond to tref.prefix

Compute the outer value that correspond to tref.prefix

inline def reporter(using r: Reporter): Reporter
def resolve(cls: ClassSymbol, sym: Symbol)(using Context): Symbol
def resolveOuterSelect(target: ClassSymbol, thisV: Value, hops: Int): () ?=> Value

Resolve outer select introduced during inlining.

Resolve outer select introduced during inlining.

See tpd.outerSelect and ElimOuterSelect.

def resolveSuper(cls: ClassSymbol, superType: Type, sym: Symbol)(using Context): Symbol
def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol): () ?=> Value

Resolve C.this that appear in klass

Resolve C.this that appear in klass

def trace(using t: Trace): Trace
def typeRefOf(tp: Type)(using Context): TypeRef
inline def withEnv[T](env: Env)(op: Env ?=> T): T
def withInitialState[T](work: (Cache, WorkList) ?=> T): T

Perform actions with initial checking state.

Perform actions with initial checking state.

Semantic.withInitialState {
   Semantic.addTask(...)
   ...
   Semantic.check()
}
inline def withTrace[T](t: Trace)(op: Trace ?=> T): T
inline def workList(using wl: WorkList): WorkList

Extensions

Extensions

extension (a: Value)
def join(b: Value): Value

Conservatively approximate the value with Cold or Hot

Conservatively approximate the value with Cold or Hot

extension (arg: Arg)
def tree: Tree
extension (ref: Ref)
def ensureFresh()(using Cache): ref.type
def ensureObjectExists()(using Cache): ref.type
def objekt: () ?=> Objekt
def updateField(field: Symbol, value: Value): () ?=> Unit

Update field value of the abstract object

Update field value of the abstract object

Invariant: fields are immutable and only set once

def updateOuter(klass: ClassSymbol, value: Value): () ?=> Unit

Update the immediate outer of the given klass of the abstract object

Update the immediate outer of the given klass of the abstract object

Invariant: outers are immutable and only set once

extension (ref: Ref)

Whether the object is fully assigned

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.

extension (ref: Ref)
def accessLocal(tmref: TermRef, klass: ClassSymbol): () ?=> Value
extension (symbol: Symbol)
extension (thisRef: ThisRef)
extension (value: Value)
def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean): () ?=> Value
def callConstructor(ctor: Symbol, args: List[ArgInfo]): () ?=> Value
def ensureHot(msg: String): () ?=> Value
def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): () ?=> Value

Handle a new expression new p.C where p is abstracted by value

Handle a new expression new p.C where p is abstracted by value

def select(field: Symbol, needResolve: Boolean): () ?=> Value
extension (value: Ref)

Can the method call on value be ignored?

Can the method call on value be ignored?

Note: assume overriding resolution has been performed.

extension (value: Value)
def promote(msg: String): () ?=> Unit

Promotion of values to hot

Promotion of values to hot

extension (values: Seq[Value])
def join: Value
extension (warm: Warm)
def tryPromote(msg: String): () ?=> List[Error]

Try early promotion of warm objects

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.