Initial new rt integration

This commit is contained in:
dcode
2019-05-12 13:50:28 +02:00
parent dd2bdd0383
commit ba1a0c2369
52 changed files with 1066 additions and 2985 deletions

View File

@ -8,29 +8,68 @@ It is based on [the TLSF memory manager](./tlsf.ts) and [a pure reference counti
Interface
---------
* **__rt_allocate**(size: `usize`, id: `u32` = 0): `usize`<br />
* **__alloc**(size: `usize`, id: `u32` = 0): `usize`<br />
Dynamically allocates a chunk of memory of at least the specified size and returns its address.
Alignment is guaranteed to be 16 bytes to fit up to v128 values naturally.
* **__rt_reallocate**(ref: `usize`, size: `usize`): `usize`<br />
* **__realloc**(ref: `usize`, size: `usize`): `usize`<br />
Dynamically changes the size of a chunk of memory, possibly moving it to a new address.
* **__rt_free**(ref: `usize`): `void`<br />
* **__free**(ref: `usize`): `void`<br />
Frees a dynamically allocated chunk of memory by its address.
* **__rt_retain**(ref: `usize`): `void`<br />
Retains a reference.
* **__retain**(ref: `usize`): `void`<br />
Retains a reference to an instance of a reference type. The instance doesn't become collected as long as there's at least one retained reference.
* **__rt_release**(ref: `usize`): `void`<br />
Releases a reference.
* **__release**(ref: `usize`): `void`<br />
Releases a reference to an instance of a reference type. The instance is considered for collection once all references to it have been released.
* **__rt_collect**(): `void`<br />
Forces a full garbage collection cycle.
* **__collect**(): `void`<br />
Forces a full garbage collection cycle. By default this means that reference cycles are resolved and possibly collected.
* **__rt_typeinfo**(id: `u32`): `void`<br />
Obtains the runtime type information for objects of the kind represented by the specified id.
* **__visit**(ref: `usize`, cookie: `u32`): `void`<br />
Concrete visitor implementation called during traversal. Cookie can be used to indicate one of multiple operations.
Built-ins
---------
The following functions are generated by the compiler based on compile-time information that wouldn't be available or inefficient to provide otherwise.
* **__info**(id: `u32`): `void`<br />
Obtains the runtime type information for objects with the specified runtime id. Runtime type information is a set of flags indicating whether a reference type is managed, an array or similar, and what the relevant alignments when creating an instance are etc.
* **__visit_globals**(cookie: `u32`)<br />
Calls `__visit` on each global that is of a reference type. Not used anymore (originally provided to support tracing GCs) but still here for possible future use.
* **__visit_members**(ref: `usize`, cookie: `u32`)<br />
Calls `__visit` on each member of the instance pointed to by `ref` that is of a reference type.
Stub
----
The fully functional yet minimal [stub implementation](./stub.ts) provides dynamic memory allocation only but doesn't include sophisticated support to deallocate objects. Useful for prototyping or very short-lived programs with hardly any memory footprint.
A fully functional yet minimal (as in code size) [stub implementation](./index-stub.ts) that provides dynamic memory allocation but no deallocation. Useful for prototyping or very short-lived programs with hardly any memory footprint. The [none implementation](./index-none.ts) is the same as the stub implementation without any runtime exports.
Integration notes
-----------------
Working with the runtime internals within standard library code can be tricky and requires knowledge of where the compiler will insert runtime calls automatically. For example, whenever a value of a reference type is assigned to a local, a global or a field, the compiler *might* insert a `__retain` call, respectively whenever such a value becomes unassigned from one, *might* insert a `__release` call. When a value is handled as an `usize` (i.e. when it comes from `__alloc` or is `changetype<usize>`ed), no such insertion happens (afterwards), but as soon as a `changetype<RefType>`ed (again), the side-effects introduced by automatic insertion must be understood.
A `__retain` call is inserted when a value of a reference type
* is assigned to a local, global or a field **if** the value is not already the exact same value as stored before
* is an argument to a function call, including `this` (i.e. `str.indexOf` retains `str`)
* is returned from a function (i.e. no need to manually `__retain` if explicitly `changetype`d)
A `__release` call is inserted when a value of a reference type
* becomes unassigned from a local, global or a field due to assigning a new value **if** the value is not already the exact same value as stored before
* is popped together with its local from the current scope, i.e. a local declared with `let` in a block, or otherwise at the end of a function
If not taken into account properly
* a memory leak will occur when `__retain`ed more often than intended
* a double-free will occur when `__release`d more often than intended
Also note that a `load<T>(x)` with a reference type acts like a `changetype<T>(load<usize>(x))` and does not `__retain` unless the result is assigned to a local.
Some best practices are:
* Use the fresh `__alloc`ed reference in `usize` form where possible, e.g. when just copying raw bytes is necessary, and `changetype` it once on return.
* When providing such a `usize` reference to a function, if the value isn't needed anymore afterwards, just `changetype` it on the call which will `__retain` and `__release` it automatically, including freeing it if wasn't retained before, or, if still needed afterwards, assign the `changetype`d reference to a local first and provide the local as the argument, hence keeping the reference alive as long as the local or any subsequent target is.
* If it's not avoidable to `changetype` to the actual reference type, do it inline in an expression and avoid assigning to a local.

View File

@ -12,10 +12,22 @@
// @ts-ignore: decorator
@inline export const DEBUG = true;
/** Common block structure. */
@unmanaged export class CommonBlock {
// ╒════════════════ Common block layout (32-bit) ═════════════════╕
// 3 2 1
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 bits
// ├─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┤
// │ MM info │ -16
// ├───────────────────────────────────────────────────────────────┤
// │ GC info │ -12
// ├───────────────────────────────────────────────────────────────┤
// │ runtime id │ -8
// ├───────────────────────────────────────────────────────────────┤
// │ runtime size │ -4
// ╞═══════════════════════════════════════════════════════════════╡
// │ ... │ ref
@unmanaged export class BLOCK {
/** Memory manager info. */
mmInfo: usize; // WASM64 might need adaption
mmInfo: usize; // WASM64 needs adaption
/** Garbage collector info. */
gcInfo: u32;
/** Runtime class id. */
@ -24,16 +36,53 @@
rtSize: u32;
}
/////////////////////////////////// Type information interface ////////////////////////////////////
import { RTTI_BASE } from "../runtime";
import { RTTIData } from "../common/rtti";
// @ts-ignore: decorator
@inline export const BLOCK_OVERHEAD = (offsetof<BLOCK>() + AL_MASK) & ~AL_MASK;
// @ts-ignore: decorator
@global @unsafe
function __rt_typeinfo(id: u32): u32 {
var ptr: usize = RTTI_BASE;
@inline export const BLOCK_MAXSIZE: usize = (1 << 30) - BLOCK_OVERHEAD;
/////////////////////////////////// Type information interface ////////////////////////////////////
import { RTTI_BASE } from "builtins";
import { RTTIData, RTTIFlags } from "common/rtti";
// @ts-ignore: decorator
@unsafe @global
export function __typeinfo(id: u32): RTTIFlags {
var ptr = RTTI_BASE;
return !id || id > load<u32>(ptr)
? unreachable()
: changetype<RTTIData>(ptr + id * offsetof<RTTIData>()).flags;
}
// @ts-ignore: decorator
@unsafe @global
export function __instanceof(ref: usize, superId: u32): bool { // keyword
var id = changetype<BLOCK>(ref - BLOCK_OVERHEAD).rtId;
var ptr = RTTI_BASE;
if (id && id <= load<u32>(ptr)) {
do if (id == superId) return true;
while (id = changetype<RTTIData>(ptr + id * offsetof<RTTIData>()).base);
}
return false;
}
///////////////////////////////////////////// Helpers /////////////////////////////////////////////
import { idof } from "builtins";
import { ArrayBufferView } from "arraybuffer";
// @ts-ignore: decorator
@unsafe @global
export function __allocArray(length: i32, alignLog2: usize, id: u32, data: usize = 0): usize {
var array = __alloc(offsetof<i32[]>(), id);
var bufferSize = <usize>length << alignLog2;
var buffer = __alloc(bufferSize, idof<ArrayBuffer>());
changetype<ArrayBufferView>(array).data = changetype<ArrayBuffer>(buffer); // TODO/RT: retains
changetype<ArrayBufferView>(array).dataStart = buffer;
changetype<ArrayBufferView>(array).dataLength = bufferSize;
store<i32>(changetype<usize>(array), length, offsetof<i32[]>("length_"));
if (data) memory.copy(buffer, data, bufferSize);
return array;
}

View File

@ -0,0 +1,3 @@
export { __alloc, __realloc, __free } from "./tlsf";
export { __retain, __release, __collect } from "./purerc";
export { __instanceof, __typeinfo } from "./common";

View File

@ -0,0 +1 @@
import "rt/index-stub";

View File

@ -1,12 +1,4 @@
import { AL_MASK, CommonBlock } from "./common";
// @ts-ignore: decorator
@inline
const BLOCK_OVERHEAD = (offsetof<CommonBlock>() + AL_MASK) & ~AL_MASK;
// @ts-ignore: decorator
@inline
const BLOCK_MAXSIZE: usize = (1 << 30) - BLOCK_OVERHEAD; // match TLSF
import { AL_MASK, BLOCK, BLOCK_OVERHEAD, BLOCK_MAXSIZE } from "rt/common";
// @ts-ignore: decorator
@lazy
@ -16,11 +8,9 @@ var startOffset: usize = (HEAP_BASE + AL_MASK) & ~AL_MASK;
@lazy
var offset: usize = startOffset;
//////////////////////////////////// Memory manager interface /////////////////////////////////////
// @ts-ignore: decorator
@unsafe @global
export function __rt_allocate(size: usize, id: u32): usize {
export function __alloc(size: usize, id: u32): usize {
if (size > BLOCK_MAXSIZE) unreachable();
var ptr = offset + BLOCK_OVERHEAD;
var newPtr = (ptr + max<usize>(size, 1) + AL_MASK) & ~AL_MASK;
@ -33,7 +23,7 @@ export function __rt_allocate(size: usize, id: u32): usize {
}
}
offset = newPtr;
var block = changetype<CommonBlock>(ptr - BLOCK_OVERHEAD);
var block = changetype<BLOCK>(ptr - BLOCK_OVERHEAD);
block.rtId = id;
block.rtSize = size;
return ptr;
@ -41,11 +31,11 @@ export function __rt_allocate(size: usize, id: u32): usize {
// @ts-ignore: decorator
@unsafe @global
export function __rt_reallocate(ref: usize, size: usize): usize {
var block = changetype<CommonBlock>(ref - BLOCK_OVERHEAD);
export function __realloc(ref: usize, size: usize): usize {
var block = changetype<BLOCK>(ref - BLOCK_OVERHEAD);
var oldSize = <usize>block.rtSize;
if (size > oldSize) {
let newRef = __rt_allocate(size, block.rtId);
let newRef = __alloc(size, block.rtId);
memory.copy(newRef, ref, oldSize);
ref = newRef;
} else {
@ -56,30 +46,40 @@ export function __rt_reallocate(ref: usize, size: usize): usize {
// @ts-ignore: decorator
@unsafe @global
export function __rt_free(ref: usize): void {
export function __free(ref: usize): void {
}
// @ts-ignore: decorator
@unsafe @global
export function __rt_reset(): void { // special
offset = startOffset;
}
/////////////////////////////////// Garbage collector interface ///////////////////////////////////
// @unsafe @global
// export function __reset(): void { // special
// offset = startOffset;
// }
// @ts-ignore: decorator
@global @unsafe
export function __rt_retain(ref: usize): void {
export function __retain(ref: usize): usize {
return ref;
}
// @ts-ignore: decorator
@global @unsafe
export function __rt_release(ref: usize): void {
export function __release(ref: usize): void {
}
// @ts-ignore: decorator
@global @unsafe
export function __rt_collect(): void {
function __visit(ref: usize, cookie: u32): void {
}
export { __rt_typeinfo };
// @ts-ignore: decorator
@global @unsafe
function __retainRelease(ref: usize, oldRef: usize): usize {
return ref;
}
// @ts-ignore: decorator
@global @unsafe
export function __collect(): void {
}
export { __instanceof, __typeinfo } from "rt/common";

13
std/assembly/rt/index.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
declare function __alloc(size: usize, id: u32): usize;
declare function __realloc(ref: usize, size: usize): usize;
declare function __free(ref: usize): void;
declare function __retain(ref: usize): void;
declare function __release(ref: usize): void;
declare function __retainRelease(ref: usize, oldRef: usize): usize;
declare function __collect(): void;
declare function __typeinfo(id: u32): u32;
declare function __instanceof(ref: usize, superId: u32): bool;
declare function __visit(ref: usize, cookie: i32): void;
declare function __visit_globals(cookie: u32): void;
declare function __visit_members(ref: usize, cookie: u32): void;
declare function __allocArray(length: i32, alignLog2: usize, id: u32, data?: usize): usize;

View File

@ -1,58 +0,0 @@
import { AL_MASK, DEBUG } from "./common";
//////////////////////////////////// Memory manager interface /////////////////////////////////////
import { ROOT, Block, BLOCK_OVERHEAD, initializeRoot, allocateBlock, reallocateBlock, freeBlock } from "./tlsf";
// @ts-ignore: decorator
@global @unsafe
export function __rt_allocate(size: usize, id: u32): usize {
var root = ROOT;
if (!root) {
initializeRoot();
root = ROOT;
}
var block = allocateBlock(root, size);
block.rtId = id;
return changetype<usize>(block) + BLOCK_OVERHEAD;
}
// @ts-ignore: decorator
@global @unsafe
export function __rt_reallocate(ref: usize, size: usize): usize {
if (DEBUG) assert(ROOT); // must be initialized
assert(ref != 0 && !(ref & AL_MASK)); // must exist and be aligned
return changetype<usize>(reallocateBlock(ROOT, changetype<Block>(ref - BLOCK_OVERHEAD), size)) + BLOCK_OVERHEAD;
}
// @ts-ignore: decorator
@global @unsafe
export function __rt_free(ref: usize): void {
if (DEBUG) assert(ROOT); // must be initialized
assert(ref != 0 && !(ref & AL_MASK)); // must exist and be aligned
freeBlock(ROOT, changetype<Block>(ref - BLOCK_OVERHEAD));
}
/////////////////////////////////// Garbage collector interface ///////////////////////////////////
import { increment, decrement, collectCycles } from "./pure";
// @ts-ignore: decorator
@global @unsafe
export function __rt_retain(ref: usize): void {
if (ref) increment(changetype<Block>(ref - BLOCK_OVERHEAD));
}
// @ts-ignore: decorator
@global @unsafe
export function __rt_release(ref: usize): void {
if (ref) decrement(changetype<Block>(ref - BLOCK_OVERHEAD));
}
// @ts-ignore: decorator
@global @unsafe
export function __rt_collect(): void {
collectCycles();
}
export { __rt_typeinfo };

View File

@ -1,14 +1,10 @@
import { DEBUG } from "./common";
import { Block, freeBlock, ROOT } from "./tlsf";
import { DEBUG, BLOCK_OVERHEAD, BLOCK } from "rt/common";
import { Block, freeBlock, ROOT } from "rt/tlsf";
import { RTTIFlags } from "common/rtti";
/////////////////////////// A Pure Reference Counting Garbage Collector ///////////////////////////
// see: https://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon03Pure.pdf
// TODO: make visitors eat cookies so we can compile direct calls into a switch
function __rt_visit_members(s: Block, cookie: i32): void { unreachable(); }
function __rt_flags(classId: u32): u32 { return unreachable(); }
const ACYCLIC_FLAG: u32 = 0;
// ╒══════════════════════ GC Info structure ══════════════════════╕
// │ 3 2 1 │
// │1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0│
@ -66,7 +62,9 @@ const ACYCLIC_FLAG: u32 = 0;
// @ts-ignore: decorator
@global
function __rt_visit(s: Block, cookie: i32): void {
function __visit(ref: usize, cookie: i32): void {
if (ref < HEAP_BASE) return;
var s = changetype<Block>(ref - BLOCK_OVERHEAD);
switch (cookie) {
case VISIT_DECREMENT: {
decrement(s);
@ -100,18 +98,18 @@ function __rt_visit(s: Block, cookie: i32): void {
}
/** Increments the reference count of the specified block by one.*/
export function increment(s: Block): void {
function increment(s: Block): void {
var info = s.gcInfo;
assert((info & ~REFCOUNT_MASK) == ((info + 1) & ~REFCOUNT_MASK)); // overflow
s.gcInfo = info + 1;
}
/** Decrements the reference count of the specified block by one, possibly freeing it. */
export function decrement(s: Block): void {
function decrement(s: Block): void {
var info = s.gcInfo;
var rc = info & REFCOUNT_MASK;
if (rc == 1) {
__rt_visit_members(s, VISIT_DECREMENT);
__visit_members(changetype<usize>(s) + BLOCK_OVERHEAD, VISIT_DECREMENT);
if (!(info & BUFFERED_MASK)) {
freeBlock(ROOT, s);
} else {
@ -119,7 +117,7 @@ export function decrement(s: Block): void {
}
} else {
if (DEBUG) assert(rc > 0);
if (!(__rt_flags(s.rtId) & ACYCLIC_FLAG)) {
if (!(__typeinfo(s.rtId) & RTTIFlags.ACYCLIC)) {
s.gcInfo = BUFFERED_MASK | COLOR_PURPLE | (rc - 1);
if (!(info & BUFFERED_MASK)) {
appendRoot(s);
@ -164,7 +162,9 @@ function growRoots(): void {
}
/** Collects cyclic garbage. */
export function collectCycles(): void {
// @ts-ignore: decorator
@global @unsafe
export function __collect(): void {
// markRoots
var roots = ROOTS;
@ -205,7 +205,7 @@ function markGray(s: Block): void {
var info = s.gcInfo;
if ((info & COLOR_MASK) != COLOR_GRAY) {
s.gcInfo = (info & ~COLOR_MASK) | COLOR_GRAY;
__rt_visit_members(s, VISIT_MARKGRAY);
__visit_members(changetype<usize>(s) + BLOCK_OVERHEAD, VISIT_MARKGRAY);
}
}
@ -217,7 +217,7 @@ function scan(s: Block): void {
scanBlack(s);
} else {
s.gcInfo = (info & ~COLOR_MASK) | COLOR_WHITE;
__rt_visit_members(s, VISIT_SCAN);
__visit_members(changetype<usize>(s) + BLOCK_OVERHEAD, VISIT_SCAN);
}
}
}
@ -225,7 +225,7 @@ function scan(s: Block): void {
/** Marks a block as black (in use) if it was found to be reachable during the collection phase. */
function scanBlack(s: Block): void {
s.gcInfo = (s.gcInfo & ~COLOR_MASK) | COLOR_BLACK;
__rt_visit_members(s, VISIT_SCANBLACK);
__visit_members(changetype<usize>(s) + BLOCK_OVERHEAD, VISIT_SCANBLACK);
}
/** Collects all white (member of a garbage cycle) nodes when completing the collection phase. */
@ -233,7 +233,31 @@ function collectWhite(s: Block): void {
var info = s.gcInfo;
if ((info & COLOR_MASK) == COLOR_WHITE && !(info & BUFFERED_MASK)) {
// s.gcInfo = (info & ~COLOR_MASK) | COLOR_BLACK;
__rt_visit_members(s, VISIT_COLLECTWHITE);
__visit_members(changetype<usize>(s) + BLOCK_OVERHEAD, VISIT_COLLECTWHITE);
}
freeBlock(ROOT, s);
}
// @ts-ignore: decorator
@global @unsafe
export function __retain(ref: usize): usize {
if (ref > HEAP_BASE) increment(changetype<Block>(ref - BLOCK_OVERHEAD));
return ref;
}
// @ts-ignore: decorator
@global @unsafe
export function __release(ref: usize): void {
if (ref > HEAP_BASE) decrement(changetype<Block>(ref - BLOCK_OVERHEAD));
}
// @ts-ignore: decorator
@global @unsafe
export function __retainRelease(ref: usize, oldRef: usize): usize {
if (ref != oldRef) {
let heapBase = HEAP_BASE;
if (ref > heapBase) increment(changetype<Block>(ref - BLOCK_OVERHEAD));
if (oldRef > heapBase) decrement(changetype<Block>(oldRef - BLOCK_OVERHEAD));
}
return ref;
}

View File

@ -1,4 +1,4 @@
import { AL_BITS, AL_SIZE, AL_MASK, DEBUG, CommonBlock } from "./common";
import { AL_BITS, AL_MASK, DEBUG, BLOCK, BLOCK_OVERHEAD, BLOCK_MAXSIZE } from "rt/common";
/////////////////////// The TLSF (Two-Level Segregate Fit) memory allocator ///////////////////////
// see: http://www.gii.upv.es/tlsf/
@ -69,7 +69,7 @@ import { AL_BITS, AL_SIZE, AL_MASK, DEBUG, CommonBlock } from "./common";
// │ if free: back ▲ │ ◄─┘
// └───────────────────────────────────────────────────────────────┘ payload ┘ >= MIN SIZE
// F: FREE, L: LEFTFREE
@unmanaged export class Block extends CommonBlock {
@unmanaged export class Block extends BLOCK {
/** Previous free block, if any. Only valid if free, otherwise part of payload. */
prev: Block | null;
@ -79,15 +79,13 @@ import { AL_BITS, AL_SIZE, AL_MASK, DEBUG, CommonBlock } from "./common";
// If the block is free, there is a 'back'reference at its end pointing at its start.
}
// Block constants. Overhead is always present, no matter if free or used. Also, a block must have
// a minimum size of three pointers so it can hold `prev`, `next` and `back` if free.
// Block constants. A block must have a minimum size of three pointers so it can hold `prev`,
// `next` and `back` if free.
// @ts-ignore: decorator
@inline export const BLOCK_OVERHEAD: usize = (offsetof<Block>("prev") + AL_MASK) & ~AL_MASK;
@inline const BLOCK_MINSIZE: usize = (3 * sizeof<usize>() + AL_MASK) & ~AL_MASK; // prev + next + back
// @ts-ignore: decorator
@inline const BLOCK_MINSIZE: usize = (3 * sizeof<usize>() + AL_MASK) & ~AL_MASK;// prev + next + back
// @ts-ignore: decorator
@inline const BLOCK_MAXSIZE: usize = 1 << (FL_BITS + SB_BITS - 1); // exclusive
// @inline const BLOCK_MAXSIZE: usize = 1 << (FL_BITS + SB_BITS - 1); // exclusive, lives in common.ts
/** Gets the left block of a block. Only valid if the left block is free. */
// @ts-ignore: decorator
@ -532,3 +530,32 @@ export function freeBlock(root: Root, block: Block): void {
block.mmInfo = blockInfo | FREE;
insertBlock(root, block);
}
// @ts-ignore: decorator
@global @unsafe
export function __alloc(size: usize, id: u32): usize {
var root = ROOT;
if (!root) {
initializeRoot();
root = ROOT;
}
var block = allocateBlock(root, size);
block.rtId = id;
return changetype<usize>(block) + BLOCK_OVERHEAD;
}
// @ts-ignore: decorator
@global @unsafe
export function __realloc(ref: usize, size: usize): usize {
if (DEBUG) assert(ROOT); // must be initialized
assert(ref != 0 && !(ref & AL_MASK)); // must exist and be aligned
return changetype<usize>(reallocateBlock(ROOT, changetype<Block>(ref - BLOCK_OVERHEAD), size)) + BLOCK_OVERHEAD;
}
// @ts-ignore: decorator
@global @unsafe
export function __free(ref: usize): void {
if (DEBUG) assert(ROOT); // must be initialized
assert(ref != 0 && !(ref & AL_MASK)); // must exist and be aligned
freeBlock(ROOT, changetype<Block>(ref - BLOCK_OVERHEAD));
}