src/Heap.h
author David Anderson <dvander@alliedmods.net>
Sat Nov 17 18:41:01 2012 -0800 (2012-11-17)
changeset 195 c1ba166f3fc4
parent 122 0fc1d94166f5
permissions -rw-r--r--
Massive SemA/BC refactoring to support better type systems. Read on for more.

The previous compiler had a simple pipeline, best outlined as:
(1) Parsing and name binding -> AST
(2) Storage allocation -> AST
(3) Semantic Analysis (SemA) -> AST
(4) Bytecode Compilation (BC) -> Code

This pipeline, unfortunately, had two problems. First, name binding cannot be performed in one pass if we wish to have multi-file support in the form of modules. Name binding must be two-pass.

Second, because SemA was only capable of annotating AST nodes with a single type, most coercion work had to be duplicated in the BC phase, as coercion was not explicit in the SemA output. This made introducing new type rules extremely difficult, and all but precluded concepts like operator overloading (or overloading at all).

This patch rewrites most of Keima's backend. Of interest are the following changes:
(1) CompileContext has been refactored around future multi-file support.
(2) Parsing no longer performs any name binding.
(3) The grammar for type-and-name has been changed from:
(Label | Identifier)? Identifier
to:
Type? Identifier
where:
Type ::= OldType | NewType
OldType ::= Label
NewType ::= Identifier (. NewType)*


Although we do not implement the full production for Type yet, this
distinction is important. A type identifier is now affixed to the AST
as an Expression (right now, always a NameProxy), so it can fully
participate in name binding.

(4) Immediately after parsing, the AST goes through a NamePopulation phase.
This phase creates Scope objects for every scope which declares a name,
and if those names are declaring types or functions, a Symbol is created
and entered into the scope. Symbols entirely replace the old BoundName
classes.

(5) After NamePopulation comes NameBinding. This phase performs three steps:
(a) Links Scope objects together, to form a tree.
(b) Binds any free name to an existing name in the scope chain.
(c) Creates and registers any Symbols for names which have not yet been
added to the scope (for example, local variables).

In accordance, all "allocation" and "binding" concepts have been removed
from Scopes, which are now lightweight container classes.

(6) SemA now generates bytecode for each statement in the AST. Expression
compilation is performed via a separate mechanism called HIR (High-level
Intermediate Representation). To analyze an expression, SemA will walk
the AST, and produce a HIR object for each node. HIR is typed, and SemA
may insert coercion nodes, or even expand an AST node into multiple HIR
nodes. The result of evaluating an expression in SemA is therefore the
the root of a HIR tree, which SemA then sends to the HIRTranslator, which
performs bytecode generation.

As before, SemA still performs full semantic analysis. However, not it
also produces each function's bytecode.

This split allows us to decompose potentially complex semantics into
a more fine-grained, AST-like structure, which can have very simple
code-generation logic. For example, (1.0 + 5) might look like:

HAdd(HFloat(1.0), HConvert(HInteger(5), <Float>))

As part of this decomposition, many opcodes have been removed. Rather than
using typed jumps, we now expand jumps into longer tests. For example:
jge.f <label>
becomes:
ge.f
cvt.f2b
jt <label>

This simplifies the pipeline, and JITs should be able to melt the added
work away.

HIR is not used at a statement level, and HIR does not have any concept
of control-flow.

(7) The old BytecodeCompiler has been removed, as its work is now split
between SemA and HIRTranslator.


In addition, some minor refactorings have taken place:

(1) Opcodes.tbl no longer hardcodes numbers (aaah).
(2) Type has been split into smaller, typed structs.
(3) BytecodeEmitter no longer relies on Pools or RootScopes.
(4) SemA is now responsible for variable/storage allocation.
(5) Native table are now global, rather than per-module. As such, native
declarations now result in a Native object (which still requires a
CALLNATIVE opcode), which is an index into the global table. Natives
must be bound globally. This is in preparation for module support.
(6) Publics are no longer tracked, but rather registered via a global
callback upon module load. This callback can be set by embedders. This
is in preparation for multi-file support.
(7) The BytecodeEmitter now performs Symbol allocations itself, and it
also generates Code objects itself, which removes a good deal of
complexity.

Finally, a test harness has been added. This harness is a python script which finds *.test files, recursively, in the tests folder. For each file it runs the corresponding .sp. The contents of the .test file must match stdout+stderr.
     1 /* vim: set ts=4 sw=4 tw=99 et:
     2  *
     3  * Copyright (C) 2012 David Anderson
     4  *
     5  * This file is part of SourcePawn.
     6  *
     7  * SourcePawn is free software: you can redistribute it and/or modify it under
     8  * the terms of the GNU General Public License as published by the Free
     9  * Software Foundation, either version 3 of the License, or (at your option)
    10  * any later version.
    11  * 
    12  * SourcePawn is distributed in the hope that it will be useful, but WITHOUT ANY
    13  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    14  * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
    15  *
    16  * You should have received a copy of the GNU General Public License along with
    17  * SourcePawn. If not, see http://www.gnu.org/licenses/.
    18  */
    19 #ifndef _include_jitcraft_heap_h_
    20 #define _include_jitcraft_heap_h_
    21 
    22 #include "Utility.h"
    23 #include "Spaces.h"
    24 #include "Barriers.h"
    25 #include "Objects.h"
    26 #include "Handles.h"
    27 
    28 namespace ke {
    29 
    30 class Zone;
    31 class MarkStack;
    32 
    33 // Heap-allocated objects go here. Heaps are thread-local. The heap contains
    34 // four spaces:
    35 //
    36 //    LIFO Space:  Used for objects that may be stack-allocated.
    37 //    Old Space:   Used for objects that are expected to be long-lived, or that
    38 //                 survive one collection.
    39 //    Large Space: Used for objects that are so large they should never be
    40 //                 moved between spaces.
    41 //
    42 // ALLOCATING AN OBJECT DOES NOT ZERO ITS MEMORY.
    43 // ALLOCATING AN OBJECT DOES NOT ZERO ITS MEMORY!
    44 //
    45 // ---------------------------------------------------------------------------
    46 //
    47 // The following is a high-level overview of how garbage collection works in
    48 // Keima. Detailed notes are present in Heap.cpp.
    49 //
    50 // THE TRI-COLOR INVARIANT: Via an object's mark bits, we maintain the
    51 //   property that an object that has been fully marked may never reference a
    52 //   dead object. Objects exist in three states during collection: unmarked
    53 //   (white), scheduled for marking (grey), and fully marked (black).
    54 //
    55 //   When initiating collection, all objects are white. Objects in the root
    56 //   set are made grey. The marking process takes grey objects and marks that
    57 //   each child object is live: if the child is white, it becomes grey. Once
    58 //   all children are grey or black, the parent grey object can become black,
    59 //   as it has been fully traversed.
    60 //
    61 //   The result of this process is that when there are no more grey objects,
    62 //   then we are guaranteed that all black objects are live, and all white
    63 //   objects are dead.
    64 //
    65 // MAJOR COLLECTIONS are performed periodically, when either a given memory
    66 //   threshold has been reached, or when requested by the host application.
    67 //   They are also performed if no memory can be allocated.
    68 //
    69 //   Major collections are extremely slow; they require full heap traversal,
    70 //   which can take more than a second on very large heaps. However, they
    71 //   have the greatest chance at reclaiming all syntactically dead objects.
    72 //
    73 // SWEEPING is performed after major collections. The free space in between
    74 //   live objects is added to a free list. Tenured allocations are then
    75 //   performed in the free space, if possible.
    76 //
    77 class Heap
    78 {
    79     Zone *zone_;
    80     HeapStack lifoSpace_;
    81     OldSpace mapSpace_;
    82     OldSpace oldSpace_;
    83     LargeObjectSpace largeSpace_;
    84 
    85     Map *metaMap_;
    86 
    87     // This is a big hunk o' memory used for marking iteratively. Eventually,
    88     // this will be replaced by the inactive semispace in the nursery.
    89     void *markStack_;
    90 
    91   public:
    92     enum Tenure {
    93         // Default, objects go into the nursery.
    94         Tenure_Default,
    95         
    96         // If objects are proven to not escape the stack, they can go into
    97         // a LIFO arena. Allocation on the LIFO arena is not guaranteed to
    98         // succeed; thus, objects may still be allocated in other arenas.
    99         Tenure_Stack,
   100 
   101         // Objects that should not go into the nursery (i.e. are expected
   102         // to be long-living) go here.
   103         Tenure_Old
   104     };
   105 
   106   private:
   107     inline Address newLargeObject(size_t bytes);
   108     inline Address newOldObject(size_t bytes);
   109     inline Address newStackObject(size_t bytes);
   110     inline Address newObject(size_t bytes, Tenure pretenure);
   111 
   112   private:
   113     void findGreyObjects(MarkStack &stack);
   114     void mark();
   115     void sweep();
   116 
   117     void reportOutOfMemory();
   118 
   119   public:
   120     Heap(Zone *zone);
   121     ~Heap();
   122     bool initialize();
   123 
   124     inline Object *allocate(Handle<Map> map, size_t byes, Tenure pretenure);
   125     Map *allocateMap(MapKind kind, size_t bytes);
   126 
   127     Map *metaMap() {
   128         return metaMap_;
   129     }
   130     inline Address lifoPosition() const {
   131         return lifoSpace_.position();
   132     }
   133     inline void unwindLifo(Address to) {
   134         lifoSpace_.unwind(to);
   135     }
   136 
   137     // Perform a full garbage collection.
   138     void reclaim();
   139 };
   140 
   141 }
   142 
   143 #endif //_include_jitcraft_heap_h_