Semantic

class Semantic
Companion
object
class Object
trait Matchable
class Any

Type members

Classlikes

sealed abstract class Addr extends Value
case class ArgInfo(value: Value, source: Tree)

Utility definition used for better error-reporting of argument errors

Utility definition used for better error-reporting of argument errors

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. Which also avoid computing fix-point on the cache, as the cache is immutable.

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: Addr, klass: ClassSymbol, env: Env) extends Value

A function value

A function value

object Heap

Abstract heap stores abstract objects

Abstract heap stores abstract objects

As in the OOPSLA paper, the abstract heap is monotonistic.

case object Hot extends Value

A transitively initialized object

A transitively initialized object

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 Promoted
case class RefSet(refs: List[Fun | Addr]) extends Value

A value which represents a set of addresses

A value which represents a set of addresses

It comes from if expressions.

case class Result(value: Value, errors: Seq[Error])

Result of abstract interpretation

Result of abstract interpretation

case class ThisRef(klass: ClassSymbol) extends Addr

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(C) │ │ │ ▲ │ │ │ │ Warm(D) Fun RefSet │ ▲ ▲ ▲ │ │ │ │ Warm(C) │ │ │ ▲ │ │ │ │ │ │ │ └─────────┴──────┴───────┘ Hot

The most important ordering is the following:

 Hot ⊑ Warm(C) ⊑ ThisRef(C) ⊑ Cold

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 Addr

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.

Types

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.

type Contextual[T] = (Env, Context, Trace, Promoted) => T

The state that threads through the interpreter

The state that threads through the interpreter

type Env = Env
type Heap = Heap
type Trace = Trace

Value members

Concrete methods

def cases(expr: Tree, thisV: Addr, klass: ClassSymbol): () => Result

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: Addr, klass: ClassSymbol, source: Tree): () => Result

Handle semantics of leaf nodes

Handle semantics of leaf nodes

def checkTermUsage(tpt: Tree, thisV: Addr, klass: ClassSymbol): () => List[Error]

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.

def env(using env: Env): Env
def eval(expr: Tree, thisV: Addr, klass: ClassSymbol, cacheResult: Boolean): () => Result

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: Addr, klass: ClassSymbol): () => List[Result]

Evaluate a list of expressions

Evaluate a list of expressions

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

Evaluate arguments of methods

Evaluate arguments of methods

def init(tpl: Template, thisV: Addr, klass: ClassSymbol): () => Result

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: Addr, klass: ClassSymbol, source: Tree): () => Result

Compute the outer value that correspond to tref.prefix

Compute the outer value that correspond to tref.prefix

def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, source: Tree): () => Value

Resolve C.this that appear in klass

Resolve C.this that appear in klass

def trace(using t: Trace): Trace
inline def withEnv[T](env: Env)(op: Env => T): T
inline def withTrace[T](t: Trace)(op: Trace => T): T

Concrete fields

val cache: Cache
val heap: Heap

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 (addr: Addr)
def ensureExists: addr

Ensure the corresponding object exists in the heap

Ensure the corresponding object exists in the heap

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 (thisRef: ThisRef)
extension (value: Value)
def promote(msg: String, source: Tree): () => List[Error]

Promotion of values to hot

Promotion of values to hot

extension (value: Value)
def call(meth: Symbol, args: List[ArgInfo], superType: Type, source: Tree, needResolve: Boolean): () => Result
def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo], source: Tree): () => Result

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, source: Tree, needResolve: Boolean): () => Result
extension (value: Addr)

Can the method call on value be ignored?

Can the method call on value be ignored?

Note: assume overriding resolution has been performed.

extension (values: Seq[Value])
def join: Value
extension (warm: Warm)
def tryPromote(msg: String, source: Tree): () => 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.