Rename memory instructions; Rework constant handling (#177)

* Rename memory instructions as proposed by the bulk-memory-operations spec.
* Rename memory manager functions to memory.* as well
* Remove automatic inlining of constant globals (Binaryen does this now)
* Improve 'const' enum compatibility
* Improve module-level export generation
* Enable the inline decorator for constant variables
* Add ERROR, WARNING and INFO macros that emit a user-defined diagnostic
* Reintroduce builtin decorator so these can appear anywhere in stdlib again
* Inline isNaN and isFinite by default
* Make an interface around gc.* similar to memory.*
* Emit an error when trying to inline a mutable variable
* Slim down CI stages
* Add a more convenient tracing utility for debugging
* Implement some prequesites for an eventual bundled GC
This commit is contained in:
Daniel Wirtz
2018-07-20 22:53:33 +02:00
committed by GitHub
parent 34e8facfdc
commit 39b489bee2
196 changed files with 28714 additions and 6674 deletions

View File

@ -4,116 +4,18 @@
* @module std/assembly/collector/itcm
*//***/
// Based on the concepts of Bach Le's μgc, see: https://github.com/bullno1/ugc
// Largely based on the Bach Le's μgc, see: https://github.com/bullno1/ugc
const TRACE = false;
import {
AL_MASK,
MAX_SIZE_32
} from "../internal/allocator";
// ╒═══════════════ Managed object 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
// ├─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┼─┴─┴─┤ ┐
// │ next │ F │ ◄─┐ = nextWithFlags
// ├─────────────────────────────────────────────────────────┴─────┤ │ usize
// │ prev │ ◄─┘
// ╞═══════════════════════════════════════════════════════════════╡ SIZE ┘
// │ ... data ... │
// └───────────────────────────────────────────────────────────────┘
// F: flags
/** Managed object flags. */
namespace Flags {
/** Object is unreachable (so far). */
export var WHITE = 0;
/** Object is reachable. */
export var BLACK = 1;
/** Object is reachable but its children have not yet been scanned. */
export const GRAY = 2;
/** Mask to obtain just the flag bits. */
export const MASK = AL_MASK;
}
/** Represents a managed object in memory, consisting of a header followed by the object's data. */
@unmanaged
class ManagedObject {
/** Pointer to the next object with additional flags stored in the alignment bits. */
nextWithFlags: usize;
/** Pointer to the previous object. */
prev: ManagedObject;
/** Visitor function called with the data pointer (excl. header). */
visitFn: (obj: usize) => void;
/** Size of a managed object after alignment. */
static readonly SIZE: usize = (offsetof<ManagedObject>() + AL_MASK) & ~AL_MASK;
/** Gets the pointer to the next object in the list. */
get next(): ManagedObject {
return changetype<ManagedObject>(this.nextWithFlags & ~Flags.MASK);
}
/** Sets the pointer to the next object in the list. */
set next(obj: ManagedObject) {
this.nextWithFlags = changetype<usize>(obj) | (this.nextWithFlags & Flags.MASK);
}
/** Inserts an object to this list. */
insert(obj: ManagedObject): void {
var prev = this.prev;
obj.next = this;
obj.prev = prev;
prev.next = obj;
this.prev = obj;
}
/** Removes this object from its list. */
remove(): void {
var next = this.next;
var prev = this.prev;
next.prev = prev;
prev.next = next;
}
/** Tests if this object is white, that is unreachable (so far). */
get isWhite(): bool {
return (this.nextWithFlags & Flags.MASK) == Flags.WHITE;
}
/** Marks this object as white, that is unreachable (so far). */
makeWhite(): void {
this.nextWithFlags = (this.nextWithFlags & ~Flags.MASK) | Flags.WHITE;
}
/** Tests if this object is black, that is reachable. Root objects are always reachable. */
get isBlack(): bool {
return (this.nextWithFlags & Flags.MASK) == Flags.BLACK;
}
/** Marks this object as black, that is reachable. */
makeBlack(): void {
this.nextWithFlags = (this.nextWithFlags & ~Flags.MASK) | Flags.BLACK;
}
/** Tests if this object is gray, that is reachable with unscanned children. */
get isGray(): bool {
return (this.nextWithFlags & Flags.MASK) == Flags.GRAY;
}
/** Marks this object as gray, that is reachable with unscanned children. */
makeGray(): void {
if (this != iter) {
this.remove();
to.insert(this);
} else {
iter = iter.prev;
}
this.nextWithFlags = (this.nextWithFlags & ~Flags.MASK) | Flags.GRAY;
}
}
import {
iterateRoots
} from "../gc";
/** Collector states. */
const enum State {
@ -129,125 +31,213 @@ const enum State {
/** Current collector state. */
var state = State.INIT;
/** Current white color value. */
var white = 0;
// From and to spaces
var from: ManagedObject;
var to: ManagedObject;
var from: ManagedObjectSet;
var to: ManagedObjectSet;
var iter: ManagedObject;
// ╒═══════════════ Managed object 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
// ├─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┼─┴─┴─┤ ┐
// │ next │ F │ ◄─┐ = nextWithFlags
// ├─────────────────────────────────────────────────────────┴─────┤ │ usize
// │ prev │ ◄─┘
// ╞═══════════════════════════════════════════════════════════════╡ SIZE ┘
// │ ... data ... │
// └───────────────────────────────────────────────────────────────┘
// F: flags
/** Represents a managed object in memory, consisting of a header followed by the object's data. */
@unmanaged
class ManagedObject {
/** Pointer to the next object with color flags stored in the alignment bits. */
nextWithColor: usize;
/** Pointer to the previous object. */
prev: ManagedObject;
/** Visitor function called with the payload reference. */
visitFn: (ref: usize) => void;
/** Size of a managed object after alignment. */
static readonly SIZE: usize = (offsetof<ManagedObject>() + AL_MASK) & ~AL_MASK;
/** Gets the pointer to the next object in the list. */
get next(): ManagedObject {
return changetype<ManagedObject>(this.nextWithColor & ~3);
}
/** Sets the pointer to the next object in the list. */
set next(obj: ManagedObject) {
this.nextWithColor = changetype<usize>(obj) | (this.nextWithColor & 3);
}
/** Gets this object's color. */
get color(): i32 {
return this.nextWithColor & 3;
}
/** Sets this object's color. */
set color(color: i32) {
this.nextWithColor = (this.nextWithColor & ~3) | color;
}
/** Unlinks this object from its list. */
unlink(): void {
var next = this.next;
var prev = this.prev;
if (TRACE) trace(" unlink", 3, objToRef(prev), objToRef(this), objToRef(next));
next.prev = prev;
prev.next = next;
}
/** Marks this object as gray, that is reachable with unscanned children. */
makeGray(): void {
if (TRACE) trace(" makeGray", 1, objToRef(this));
const gray = 2;
if (this == iter) iter = this.prev;
this.unlink();
to.push(this);
this.nextWithColor = (this.nextWithColor & ~3) | gray;
}
}
/** A set of managed objects. Used for the from and to spaces. */
@unmanaged
class ManagedObjectSet extends ManagedObject {
/** Inserts an object. */
push(obj: ManagedObject): void {
var prev = this.prev;
if (TRACE) trace(" push", 3, objToRef(prev), objToRef(obj), objToRef(this));
obj.next = this;
obj.prev = prev;
prev.next = obj;
this.prev = obj;
}
/** Clears this list. */
clear(): void {
if (TRACE) trace(" clear", 1, objToRef(this));
this.nextWithColor = changetype<usize>(this);
this.prev = this;
}
}
/** Performs a single step according to the current state. */
function gc_step(): void {
function step(): void {
var obj: ManagedObject;
switch (state) {
case State.INIT: {
from = changetype<ManagedObject>(allocate_memory(ManagedObject.SIZE));
from.nextWithFlags = changetype<usize>(from);
from.prev = from;
to = changetype<ManagedObject>(allocate_memory(ManagedObject.SIZE));
to.nextWithFlags = changetype<usize>(to);
to.prev = to;
if (TRACE) trace("gc~step/INIT");
from = changetype<ManagedObjectSet>(memory.allocate(ManagedObject.SIZE));
from.visitFn = changetype<(ref: usize) => void>(<u32>-1); // would error
from.clear();
to = changetype<ManagedObjectSet>(memory.allocate(ManagedObject.SIZE));
to.visitFn = changetype<(ref: usize) => void>(<u32>-1); // would error
to.clear();
iter = to;
state = State.IDLE;
if (TRACE) trace("gc~state = IDLE");
// fall-through
}
case State.IDLE: {
if (TRACE) trace("gc~step/IDLE");
iterateRoots(__gc_mark);
state = State.MARK;
if (TRACE) trace("gc~state = MARK");
break;
}
case State.MARK: {
obj = iter.next;
if (obj != to) {
if (obj !== to) {
if (TRACE) trace("gc~step/MARK iterate", 1, objToRef(obj));
iter = obj;
obj.makeBlack();
obj.visitFn(changetype<usize>(obj) + ManagedObject.SIZE);
obj.color = <i32>!white;
obj.visitFn(objToRef(obj));
} else {
if (TRACE) trace("gc~step/MARK finish");
iterateRoots(__gc_mark);
obj = iter.next;
if (obj == to) {
let temp = from;
if (obj === to) {
let prevFrom = from;
from = to;
to = temp;
Flags.WHITE ^= 1;
Flags.BLACK ^= 1;
iter = from.next;
to = prevFrom;
white = <i32>!white;
iter = prevFrom.next;
state = State.SWEEP;
if (TRACE) trace("gc~state = SWEEP");
}
}
break;
}
case State.SWEEP: {
obj = iter;
if (obj != to) {
if (obj !== to) {
if (TRACE) trace("gc~step/SWEEP free", 1, objToRef(obj));
iter = obj.next;
free_memory(changetype<usize>(obj));
memory.free(changetype<usize>(obj));
} else {
to.nextWithFlags = changetype<usize>(to);
to.prev = to;
if (TRACE) trace("gc~step/SWEEP finish");
to.clear();
state = State.IDLE;
if (TRACE) trace("gc~state = IDLE");
}
break;
}
}
}
/** Allocates a managed object. */
@global
export function gc_allocate(
size: usize,
visitFn: (obj: usize) => void
): usize {
assert(size <= MAX_SIZE_32 - ManagedObject.SIZE);
var obj = changetype<ManagedObject>(allocate_memory(ManagedObject.SIZE + size));
obj.makeWhite();
obj.visitFn = visitFn;
from.insert(obj);
@inline function refToObj(ref: usize): ManagedObject {
return changetype<ManagedObject>(ref - ManagedObject.SIZE);
}
@inline function objToRef(obj: ManagedObject): usize {
return changetype<usize>(obj) + ManagedObject.SIZE;
}
/** Visits a reachable object. Called from the visitFn functions. */
@global
export function gc_visit(obj: ManagedObject): void {
if (state == State.SWEEP) return;
if (obj.isWhite) obj.makeGray();
// Garbage collector interface
@global export function __gc_allocate(
size: usize,
visitFn: (ref: usize) => void
): usize {
if (TRACE) trace("gc.allocate", 1, size);
if (size > MAX_SIZE_32 - ManagedObject.SIZE) unreachable();
step(); // also makes sure it's initialized
var obj = changetype<ManagedObject>(memory.allocate(ManagedObject.SIZE + size));
obj.visitFn = visitFn;
obj.color = white;
from.push(obj);
return objToRef(obj);
}
/** Registers a managed child object with its parent object. */
@global
export function gc_register(parent: ManagedObject, child: ManagedObject): void {
if (parent.isBlack && child.isWhite) parent.makeGray();
@global export function __gc_link(parentRef: usize, childRef: usize): void {
if (TRACE) trace("gc.link", 2, parentRef, childRef);
var parent = refToObj(parentRef);
if (parent.color == <i32>!white && refToObj(childRef).color == white) parent.makeGray();
}
/** Iterates the root set. Provided by the compiler according to the program. */
@global
export declare function gc_roots(): void;
@global export function __gc_mark(ref: usize): void {
if (TRACE) trace("gc.mark", 1, ref);
if (ref) {
let obj = refToObj(ref);
if (obj.color == white) obj.makeGray();
}
}
/** Performs a full garbage collection cycle. */
@global
export function gc_collect(): void {
@global export function __gc_collect(): void {
if (TRACE) trace("gc.collect");
// begin collecting if not yet collecting
switch (state) {
case State.INIT:
case State.IDLE: gc_step();
case State.IDLE: step();
}
// finish the cycle
while (state != State.IDLE) gc_step();
while (state != State.IDLE) step();
}
declare function allocate_memory(size: usize): usize;
declare function free_memory(ptr: usize): void;
// Considerations
//
// - An API that consists mostly of just replacing `allocate_memory` would be ideal, possibly taking
// any additional number of parameters that are necessary, like the parent and the visitor.
//
// - Not having to generate a helper function for iterating globals but instead marking specific
// nodes as roots could simplify the embedding, but whether this is feasible or not depends on its
// performance characteristics and the possibility of tracking root status accross assignments.
// For example, root status could be implemented as some sort of referenced-by-globals counting
// and a dedicated list of root objects.
//
// - In 32-bit specifically, there is some free space in TLSF object headers due to alignment that
// could be repurposed to store some GC information, like a class id. Certainly, this somewhat
// depends on the efficiency of the used mechanism to detect this at compile time, including when
// a different allocator is used.
//
// - Think about generations.