Safely Abstracting Memory Layouts

01/23/2019 ∙ by Juliana Franco, et al. ∙ Microsoft Uppsala universitet Imperial College London 0

Modern architectures require applications to make effective use of caches to achieve high performance and hide memory latency. This in turn requires careful consideration of placement of data in memory to exploit spatial locality, leverage hardware prefetching and conserve memory bandwidth. In unmanaged languages like C++, memory optimisations are common, but at the cost of losing object abstraction and memory safety. In managed languages like Java and C#, the abstract view of memory and proliferation of moving compacting garbage collection does not provide enough control over placement and layout. We have proposed SHAPES, a type-driven abstract placement specification that can be integrated with object-oriented languages to enable memory optimisations. SHAPES preserves both memory and object abstraction. In this paper, we formally specify the SHAPES semantics and describe its memory safety model.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1. Introduction

Modern computers use hierarchies of memory caches to hide memory latency (wulf1995hitting, ). Accessing data from the bottom of the hierarchy — from main memory — is often an order of magnitude slower than reading from the top — from the fastest memory cache. Whenever the CPU needs to read from a particular memory location, it first tries to read from the top-most cache. If it succeeds, then the data is delivered at very little cost. Otherwise, a cache miss has occurred, and the CPU tries to obtain the data from the next cache level. In the worst case scenario, data must be fetched from main memory. This commonly causes the data — and adjacent data — to be cached. How much adjacent data is cached and how much data can be cached at what level varies across different hardware. Caches typically have sizes in kilobytes or a few megabytes, meaning that bringing only relevant data into cache is important for proper cache utilisation.

In programs whose performance is memory-bound, reorganising data in memory to reduce the number of cache misses can have enormous performance impact (wulf1995hitting, ; calder-1998, ). Writing cache-friendly code typically amounts to allocating contiguously data that is accessed together and in patterns recognisable to the hardware prefetcher, allowing it to anticipate a program’s data needs ahead-of-time. As a concrete example, in a language like C, one can allocate an array of structures (not pointers to structures), and keep all the objects of the same data structure in that array, in the order in which the objects are most frequently accessed. Thus, when an object is fetched to cache, the next one to be read, is potentially fetched as well. To improve cache utilisation, programmers often split objects across multiple arrays, in order to keep the hot fields — those most often used — of different objects together. This is called transforming an array of structures into a structure of arrays. Both layouts are depicted in Figure 1. As a consequence of this representational change, a complex datum is no longer possible to reference by a single pointer.

(, ) (, ) (, )

Figure 1. Array of structs (left) vs. struct of arrays (right). denotes a struct (object) with fields and . Left, each cell is an object. Right, each column holds the data for one object.

Optimisations like the one sketched above are common in low-level languages such as C or C++, where programmers have control over where and how memory is allocated. Unfortunately, manipulating data layouts in memory is complex, error-prone, and pollutes the logic of the program with layout concerns.

In managed languages like Java and C#, the abstract level at which memory is handled, as well as the presence of moving garbage collectors, make many memory optimisations impossible. To this end, we have proposed a type-driven approach to abstractly express layout concerns, called SHAPES (Onward2017, ), whose aim is to allow the application of layout optimisations in high-level, managed, object-oriented languages. SHAPES protects the object abstraction, allowing an object to be referenced by — and manipulated via – a single pointer, regardless of its physical representation.

Contributions

In previous work, we have introduced the SHAPES idea, and recap the key moving parts in § 2. In this paper, we make the following contributions:

  • We formalise SHAPES (§ 3) through a high-level object-oriented calculus where classes are parameterised by layout specifications. The formalism is an important component of proving soundness of uniform access — allowing programmers to write x.f to access the field f of the object pointed to by x, regardless of how is laid out in memory. We specify the dynamic semantics (§ 3.1) as well as the static semantics (§ 3.2).

  • We define the main invariants of SHAPES in terms of type safety and memory safety (§ 4).

§ 5 discusses related work and § 6 concludes.

2. Getting into Shapes

The SHAPES vision gives the programmer control of how data structures are laid out in memory in object-oriented, managed programming languages. Although programmers have control over how their data structures are allocated, memory is still abstract, and all operations are type and memory safe.

The SHAPES approach makes classes parametric with layouts — abstract regions (pools) which collect their objects together in physical memory, optionally using some form of splitting strategy to keep hot fields together in memory. The design delays the choice of the physical representation of a data structure to instantiation-time, rather than declaration-time. Thus, it is possible to reuse the same data structure declaration with multiple layouts. Reflecting layout choices in types thus serves the dual purpose of separating layout concerns from business logic and guiding compilers’ generation of efficient code for allocating and accessing data. It is also key to a uniform access model (e.g. x.f), regardless of how an object may be laid out in memory. In a typical statically typed OO language, x.f is translated by a compiler into loading a value at the address pointed to by x plus the offset of f in the type of x. Because the same x.f may manipulate x’s that point to objects with different layout, we must take care to propagate the layout information to ensure the correct code is emitted, to ensure memory safety.

2.1. Using Shapes for Improved Cache Utilisation

As a concrete example, Listing 1 is a partial program with a list of Students, with 1 000’s of elements at run-time. (For simplicity, each student holds a pointer to the next student.)

class Student {
  age: int
  supervisor: Professor
  next: Student
  // 
  def getAge(): int {
    return age
  }
}
var list = new Student
Listing 1: A linked list.
1class Student<p> {
2  age: int
3  supervisor: Professor
4  next: Student<p>
5  // 
6  def getAge(): int {
7    return age
8  }
9}
10layout L : [Student] = rec { age, next }, *
11pool P : L
12var list = new Student<P>
Listing 2: Listing 1 using SHAPES.

Iterating over a list of students to calculate the average age in a language like Java involves dereferencing many Student-pointers to get the age and next fields. However, because of how data is loaded into the cache (outlined in § 1), in addition to these relevant fields, the supervisor field and all adjacent data mapped to the same cache line will be loaded as well. If that data is not another student in the list, it is irrelevant to performing this calculation.

For improved cache utilisation, we would like to:

  1. only load the age and next fields into the cache; and

  2. store Student objects from the same list adjacent in memory, with no interleaving from unrelated objects.

Listing 2 shows Listing 1 using SHAPES. We accomplish b) by introducing a pool—a contiguous region of storage—for holding Student objects and use this pool to hold all the objects of our linked list (and nothing else). This effectively stores our students like the left of Figure 1. Listing 2 shows how the Student class is parametrised by the pool parameter (Lines 1 & 4). The pool is created on Line 10 and connected to the list on Line 11.

To accomplish a), we alter the layout of objects in the pool to use a structure of array storage. The layout is declared on Line 9 and used in the pool declaration on Line 10. Our students are now stored like the right of Figure 1. Note how the layout is orthogonal from the Student class.

Finally, if the order of the students in the pool (mostly) matches the order of the list, iterating over the list will result in (mostly) regular load patterns with even strides that will be detected by a prefetcher to bring data into memory ahead-of-time. Unless this order-alignment happens by construction, a pool-aware moving compacting garbage collector can be used to create it. Such a collector will compact respecting pool boundaries, and it is possible to influence moving semantics on a per-pool basis.

2.2. Shapes in a Nutshell

SHAPES adds pools, layout declarations and parameterises classes and types with pools to a Java-like language. The number of pools is not fixed and pools are created at run-time. Objects may be allocated in pools or in a “traditional heap.” Layout specifications specify how objects of a certain class will be laid out in a pool of a specific layout by grouping fields together. Allocation takes place in a pool using the layout specification of that pool. As we do not yet model deallocation or garbage collection, pools can be thought of as growing monotonically in this paper.

The first parameter in a class declaration indicates the pool the object will be allocated into. The remaining parameters can be used in the type declarations of fields, parameter types and return types of methods and as local variables inside them. Similarly, types are annotated with pool arguments. In that respect, SHAPES is similar to parametric polymorphism in C++ (cpp11standard, ) and Java (java8spec, ) and to Ownership Types (OwnershipSurvey, ).

The type system for shapes follows our desire for pools to be homogeneous. A pool of objects of a class is homogeneous iff for all objects and for all non-primitive fields , and always point to objects in the same pool (or null).

To prevent the occurrence of heterogeneous pools, we require that the types of objects in the same pool share the same set of parameters. This falls out of the (upcoming) rules for well-formed types. We favour homogeneous pools for the following reasons:

  • Smaller memory footprint. Pointers to pools consist of a pool address and an index. By using homogeneous pools, the pool address becomes redundant, as all objects of a pool will point to objects allocated inside a specific pool for a specific field .

  • Better cache utilisation. A direct result of the smaller footprint.

  • No need for run-time support. Heterogeneous pools allow objects allocated inside them to point to objects allocated in different pools. These pools may have different layouts, forcing a run-time check on each field access at run-time to obtain the correct address to load. This issue is eliminated for homogeneous pools.

Shapes and Performance

In a nutshell, the SHAPES design currently targets programs that perform iteration over subsets of pooled objects, roughly in the order of access (pool allocation creating prefetcher-friendly access patterns). If objects in data structures are spread over multiple pools, programmers must manually align the objects in the pools to be cache-friendly (although we have ideas on how to automate this in future work). When objects are split into clusters, the cost of one-off accesses to objects increase because objects are spread over multiple locations that must be loaded separately. However, iterations become more efficient by loading only relevant data into cache. Note that performance means both execution time and power-efficiency stemming from improved cache-utilisation.

3. Formalising Shapes

The syntax of SHAPES is shown in Figure 2. To simplify the formalism, we do not support programmer conveniences including ones used in Listing 2: the * notation used to group remaining fields (Line 9) and defaulting to store professors in the none pool (Line 3).

Figure 2. Syntax of SHAPES where , , , , and .

Notation and Implicit requirements.

We append to names to indicate sequences; for instance, is a sequence of -s, while is a sequence of sequences of -s.

We assume that layout and class identifiers are unique within the same program, and field and method identifiers are unique within the same class.

Lookup Functions

SHAPES rely on the following set of lookup functions. Their exact definitions are in Appendix A.

Fun. Used to Lookup
The class declaration for a given class identifier.
All the class pool parameters of a given class.
The bound of a given parameter in a given class.
The return type, the parameter type, local variables, and expression of a given method.
The type of a given field in a given class.
All the field identifiers declared in a given class.
The layout declaration of a given layout identifier.
The offset(s) (i.e., where it is located) of a given field, within an object, or within a pool.

3.1. Dynamic Semantics

SHAPES’s run-time entities are defined in Figure 3.

Figure 3. Dynamic Entities of SHAPES.

A heap () maps addresses to objects () and pools (). Objects consist of a class identifier (determining its type), a sequence of pool addresses and a record (). A record is a sequence of values representing the values in the fields of an object.

Pools consist of a layout identifier, a sequence of pool addresses and a sequence of clusters (). The layout identifier determines how the objects inside the pool are laid out. Pools can only store instances of the class indicated by the layout identifier. Furthermore, the layout type determines the type of the pool and the type of the objects stored inside it. SHAPES also defines a global pool called none that permits objects of any type and no splitting is performed. This is similar to the default heap representation of e.g. the JVM.

The pool addresses of both objects and pools are ghost state intended to be used for proofs; we will demonstrate in future work that they are superfluous and serve no purpose at run-time.

The values () corresponding to the fields of objects that are allocated in pools are stored in clusters. A layout declaration designates splits; each split contains a subset of the class’ fields. Each cluster, therefore, contains the values that correspond to the respective split’s fields for all objects allocated in that pool. Addresses of objects inside pools require a pool address and an index . The index indicates which record in each cluster contains the values that the fields of the instance correspond to.

A frame () maps variables to values. SHAPES designates three kinds of local variables:

Local object variables:

The this variable, and function parameters. These behave exactly like local variables in object-oriented languages.

Local pool variables:

These correspond to the pools that are constructed upon entering a method body and are initially empty. The reason local pool variables are defined altogether is because we allow reference cycles between objects that are allocated inside different pools.

Class pool parameters:

The class’ pool parameters can be used inside method bodies as local variables for type declarations, object instantiations, etc.

For convenience in our definitions, we define .

SHAPES semantics rules are of the form . They take a heap, a stack frame and an expression and reduce to a new heap, a new stack frame, and a new value, in a big-step manner.

Figure 4. Operational semantics for pool-agnostic operations.

Operations on Pool-Agnostic Expressions

The operational semantics for these rules are given in Figure 4. They are unsurprising, with the exception of New Object and Method Call. New Object deal with creation of unpooled objects (i.e., stored in the none pool). It stores pool parameters in the objects’ run-time types. As pool parameters are initialised in methods, and the implicit passing of the current object’s pool parameters, Method Call is not pool-agnostic.

Pool-dependent operations

The operational semantic is given in Figure 5. New Pooled Object allocates objects inside an existing pool , by appending a new record of values (all initialised to null) for each cluster in the pool. The notation denotes the number of fields in cluster and a sequence of null values.

By Pooled Object Read, accessing a field of an object (at location ) in a pool () requires looking up the :th value of the :th cluster of the :th object, where is the the cluster containing , and the offset into that cluster according to the layout (by way of helper function ). For brevity, we conflate the latter into a single 3-ary lookup: .

As shown by Pooled Object Write, modifying a field is isomorphic. We use a shorthand for updating, , similar to lookup.

Note that the syntax for constructing objects and accessing/mutating their members is the same regardless of layout and whether the object is allocated in a pool or not.

Figure 5. Operational semantics for pool dependent operations.

Method Call

The operational semantics for method invocation are presented in Figure 6 as two separate rules for readability. The second rule, Variable/Pool Declaration is only used from inside the first, Method Call. By Method Call, a method call proceeds by constructing a new stack frame for the method , populating it with the current this, the method parameter and the this’ pool parameters. In a big-step way, it then proceeds to evaluate the method’s body in the context of the new stack frame using Variable/Pool Declaration and returning the resulting value .

Variable/Pool declaration initialises the method’s local variables. For simplicity, we require all local variables to be declared upfront and initiaise local object variables to null. For pool variables , new (empty) pools are constructed in a two-step manner: The pools are first reserved on the heap and then they are actually constructed, along with the stack frame. This enable cycles among pools.

Where:

Figure 6. Operational semantics for method call.

3.2. Type System

Our type system, in addition to ensuring well-typedness of run-time objects, ensures that objects are allocated with the correct layout in the correct pool. This is essential to ensure that there can be no accesses to undefined memory.

Figure 7. Expression type checking.

Expression Types

The type rules are presented in Figure 7, and have the form . maps variables to types:

We distinguish three kinds of types:

Object Types:

where is a class and are pool parameters which correspond to the class pool parameters of .

Pool Types:

where is a layout. If is a layout that stores objects of class , then are pool parameters which correspond to the class pool parameters of .

Pool Bounds:

where is a class and are pool parameters corresponding to the class pool parameters of .

Pool types characterise pool values, i.e. pools that have been allocated dynamically (that is, when a method has been called) and are organised according to a layout. Pool bounds characterise class pool parameters, which have not yet been initialised and, therefore, do not have a layout. However, when a method is called, the class pool parameters will have pool values assigned to them.

The rationale for bounds is that a method may be invoked on an object that could be allocated on the none pool or a pool that adheres to a specific layout. Thanks to bounds, we can write code that is agnostic on the kind of pool the object is allocated.

By Value, null can have any well-formed object type. By Variable, the type of a variable is looked-up from . By Assignment, assignment to a local variable must match types. While we do not model inheritance or subtyping, these extensions are possible and straightforward. By New Object, we can create objects from any well-formed type. By Field Read, looking up a field from a receiver of type requires that is in . Furthermore, we must translate the pool parameters names internal to , used in its typing of , to the arguments to which these parameters are bound where the field lookup takes place. We use the helper function to extract the names of the formal pool parameters of the class . Both Field Write and Method Call must apply similar substitutions to translate between the internal names of the formal parameters and the arguments used at the call-site. Otherwise, these rules are standard.

Roles for pool variables are straightforward. By Pool Variable, the type of a pool variable is looked-up from . By None Bound, any well-formed pool type is a bound on the none pool. By Pool Bound, the bound of a pool is derived from its pool type.

Figure 8. Type bound well-formedness.

Figure 8 describes well-formedness of types. They are similar to Featherweight Java (Igarashi2001, ). By Bound Well-Formedness, a pool bound with object type is well-formed if has all its formal pool parameters bound to arguments of the correct type, modulo a substitution from the names of the formal parameters to the actual arguments of . By Type Well-Formedness, and Pool Well-Formedness, well-formed object types and pool types can be derived from well-formed pool bounds.

3.3. Type Checking Examples

In this section we illustrate how our type rules reject programs that violate our designs and would therefore suffer performance penalties (and add complexity to our implementation).

Pool Monomorphism

With the exception of the none pool, pools in SHAPES are monomorphic, i.e. only store objects of a single type.

class Student<<ps: [Student<<ps, pp>>], pp: [Professor<<pp, ps>>] >> {
    supervisor: Professor<<pp, ps>>;
    def generate() {
        pools stuPool: StudentSplit<<stuPool, profPool>>
              profPool: ProfessorSplit<<profPool, stuPool>>
        locals stu: Student<<stuPool, profPool>>
               prof: Professor<<profPool, stuPool>> ;
        stu = new Student<<stuPool, profPool>>;    // OK
        prof = new Professor<<stuPool, profPool>>; // Error!
    }
}
layout StudentSplit: [Student] = …;
layout ProfessorSplit: [Professor] = …;

The above program is rejected because of the attempt to construct a new Professor object inside a pool of students on Line 10.

Pool Homogeneity

Pools in SHAPES are homogeneous. This means that two objects in a pool cannot have equi-named fields with different types. Consequently, all objects in a pool can share the same code for dereferencing a field.

class Student<<ps: [Student<<ps, pp>>], pp: [Professor<<pp>>] >> {
    supervisor: Professor<<pp>>;
    def generate() {
        pools stuPool: StudentSplit<<stuPool, profPool1>>
              profPool1: ProfessorSplit<<profPool1>>
              profPool2: ProfessorSplit<<profPool2>>
        locals stu: Student<<stuPool, profPool1>>
               prof1: Professor<<profPool1>>
               prof2: Professor<<profPool2>> ;
        stu1 = new Student<<stuPool, profPool1>>;
        stu2 = new Student<<stuPool, profPool2>>;
        stu1.supervisor = new Professor<<profPool1>>; // OK
        stu2.supervisor = new Professor<<profPool2>>; // Error!
    }
}
layout StudentSplit: [Student] = …;
layout ProfessorSplit: [Professor] = …;

The above program is rejected as it attempts to assign two students in the same pool to professors in different pools (Line 12 & 15). If this was legal, emitting code for student.supervisor.name, where student could refer to either stu1 or stu2, would be forced to branch on the layout of the supervisor at run-time.

4. Well-formedness and Type Safety

The main meta-theoretic result of this paper is the type and memory safety of field accesses, invariant of layout changes. Going back to Figure 1, a programmer cannot directly reference in the right-most representation. To access “”, we must find the th in the array. To extract the object , we must take care not to accidentally return , which would be an object created out of thin air. This is a consequence of the broken object abstraction.

Therefore, it is necessary for a type safety definition of SHAPES to show that given two aliasing references to a pooled object, the values yielded from accessing the respective fields must be always equal. This must also take into consideration that the types of the variables/fields that store these references may have different conceptual types (for instance, during a method call, the pool parameters of a variable may be renamed).

In the earlier sections, we have treated the program as implicitly given. Here too, we assume its existence and we assume its well-formedness. A program is well-formed if all of its class and layout declarations are well-formed. The definition of the former is quite standard and the definition of the latter checks that the layout declaration for a given class considers all the fields of that class and that no field appears repeated in different clusters. Formal definitions can be found in Appendix B.

Because of the above aliasing requirement, we need to define well-formedness in such a way that the requirement holds. We also want to show that the well-formedness definition allows us to perform a few optimising transformations, while showing that the compiled code still preserves the layout requirements of all pools and returns appropriate values (we leave the definition of such a compilation for future work). Two of these optimisations are the removal of pool parameters from objects and pools and the simplification of pool addresses from a pool and an index to simply an index ().

Therefore, we define a pool-aware and layout-aware definition of well-formed configurations that is stronger than a typical definition of well-formed configurations in object-oriented languages. Well-formed configurations in SHAPES require, among other things, that all objects in a pool have the same class (the one required in the pool’s layout type), and that all the fields of an object point to objects which have classes and are in pools as described in the object’s type. We formally define well-formed configuration in § C.

We state type safety for SHAPES in Theorem 4.1, in the sense that if a well-formed configuration takes a reduction step, then the resulting configuration is well-formed too.

Theorem 4.1 (Type Safety).

For a well-formed program with heap , stack frame , corresponding typing context , and expression ,

If

then

Proof sketch.

By structural induction over the derivation . ∎

5. Related Work

For an extended coverage of related work, see Franco et al. (Onward2017, )

Memory layouts

The idea of data placement to reduce cache misses was first introduced by Calder et al. (calder-1998, ), applying profiling techniques to find temporal relationships among objects.

This work was then followed up by Lattner et al. (lattner-2003, ; lattner-2005a, ) where rather than relying on profiling, static analysis of C and C++ programs finds what layout to use. Huang et al. (huang-2004, ) explore pool allocation in the context of Java.

Ureche et al. (Ureche2015, ) present a Scala extension that allows automatic changes to the data layout. The developer defines transformations and the compiler applies the transformation during code generation.

Heap partitioning

Deterministic Parallel Java also provides means to split data in the heap: Java code is annotated with regions information used to calculate the effects of reading and writing to data (BocchinoETAL2009DPJ, ). Loci (loci, ) split the heap into per-thread subheaps plus a shared space. Note that these languages only divide the heap conceptually, and do not aim to affect representation in memory.

Jaber et al. (Jaber2017, ) present a heap partitioning scheme that works by inferring ownership properties between objects.

In the context of NUMA systems, Franco and Drossopoulou use annotations to describe in which NUMA nodes the objects should be placed (franco2015behavioural, ),with the aim to improve program performance by reducing memory accesses to remote nodes, ignoring any possible in-cache data accesses.

Formalisation

The SHAPES type system is influenced by Ownership types (clarke_potter_noble:ownership_types, ) but uses pools rather than ownership contexts, and without enforcing a hierarchical decomposition of the object graph (that is, we allow and handle cycles between pools).

As mentioned above, the concept of bounds and well-formed types is drawn from Featherweight Java (Igarashi2001, ), with the exception that our formalism does not have any concepts of polymorphism.

Formalisms for automatic data transformations with regards to functional languages also exist. Leroy (Leroy1992, ) presents a formalised transformation of ML programs that allows them to make use of unboxed representations. Thiemann (Thiemann1995, ) extends on this work and Shao (Shao1997, ) generalises it.

Petersen et al (Petersen2003, ) describe a model that uses ordered type theory to define how constructs in high-level languages are laid out in memory. This allows the runtime to achieve optimisations such as the coalescing of multiple calls to the allocator.

6. Final Remarks

We have presented a formal model (operational semantics and type system) of SHAPES, an object-oriented programming language that provides first class support for object splitting and pooling. We have also provided the definition for a well-formed runtime configuration of SHAPES and justified our design decisions.

We include an extended discussion of future work in (Onward2017, ). Our next step is showing that the well-formedness of a configuration is preserved during execution. We will also provide a translation to a low-level language that will be designed in such a way so as to achieve reasonable performance and preserve the operational semantics of SHAPES code.

7. Acknowledgements

We would like to thank Christabel Neo and the FTfJP reviewers for their feedback, and in particular, the very useful suggestions on how to make our explanations and notation better and easier to understand.

References

Appendix A Lookup functions

Appendix B Well-formed Programs

Definition B.1 (Well-formed context).

We use the notation to declare that a context is well-formed. We define:

Given definitions B.3 and B.4, we define well-formed programs as follows:

Definition B.2 (Well-formed program).

A program is well-formed if all its layout and all its class declarations are well-formed.

Definition B.3 (Well-formed class declaration).

A class is well-formed if:

  • Their first pool parameter has to be annotated with a bound that is of the same class and its parameters are the same as in the class declaration (and in the same order). That is, if the class pool parameters of the class are , then .

  • The parameter list of all pool types must only contain parameters from the class’ pool parameter list (i.e. ). This means that the none keyword is disallowed as a pool parameter name.

  • The fields must have class types that are well-formed against the typing context where the class’ formal pool parameters have their corresponding bounds as types. Moreover, is well-formed.

  • All the methods have a parameter and return type that is well-formed against the context . Moreover, for each method, the corresponding method body is typeable against a context which is an augmentation of and contains the types of this variable, local pool, and object variables of the method. Moreover is well-formed.

We now define well-formedness of layout declarations:

Definition B.4 (Well-formed layout declaration).

A layout declaration for instances of a class is well-formed iff the disjoint union of its clusters’ fields is the set of the fields declared in .

This definition excludes repeated or missing fields. For example, if a class Video has 3 fields with names id, likes, views, then both of these layout declarations are ill-formed:

// repeated field
layout Ill_formed_Layout1: [Video] = rec {id, likes} + rec {likes, views}
// missing field
layout Ill_formed_Layout2: [Video] = rec {id} + rec {views}

Appendix C Well-formed Configurations

As usual, an object or pool adheres to a type or , respectively, if the class of the object or pool is , the pool parameters are and all of its fields or clusters adhere to their type. To avoid the circularities in the definition of agreement, we break it down into weak agreement, which only ensures that the run-time type of the object is the one specified by its class and pool parameters, and strong agreement, which also considers the contents of the fields or clusters. In a nutshell, strong agreement checks the vertices of the object graph, while weak agreement checks the edges.

Although the type system uses local variables in its types, we cannot use them in the well-formedness definition, because local pool names may change across function calls. Thus, we use run-time types instead, where the pool parameters are substituted with pool addresses.

Definition C.1 (Run-Time types).

A run-time type is a defined as follows:

We now define the well-formedness of a run-time configuration:

Definition C.2 (Well-formed high-level configurations).

The definition of well-formedness of a heap is as follows:

An address can be either an address to heap-allocated object, to a pool, or to a pool-allocated object, therefore we split the definition of well-formed address as follows:

The definition of weak agreement for objects is as follows:

The definition of weak agreement for pools and bounds is as follows:

Finally, the definition of well-formedness of a stack frame and a heap against a context is as follows: