Skip to content

Commit a9d83f3

Browse files
committed
strongly typed memory WIP
1 parent da6d25f commit a9d83f3

File tree

8 files changed

+219
-63
lines changed

8 files changed

+219
-63
lines changed

compiler/compiler.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strings"
1414

1515
"github.com/tinygo-org/tinygo/compileopts"
16+
"github.com/tinygo-org/tinygo/compiler/gctype"
1617
"github.com/tinygo-org/tinygo/compiler/llvmutil"
1718
"github.com/tinygo-org/tinygo/ir"
1819
"github.com/tinygo-org/tinygo/loader"
@@ -51,6 +52,7 @@ type compilerContext struct {
5152
ir *ir.Program
5253
diagnostics []error
5354
astComments map[string]*ast.CommentGroup
55+
typer *gctype.Typer
5456
}
5557

5658
// builder contains all information relevant to build a single function.
@@ -249,6 +251,13 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
249251
c.createFunctionDeclaration(f)
250252
}
251253

254+
allocTyped := c.mod.NamedFunction("runtime.allocTyped")
255+
if !allocTyped.IsNil() {
256+
// The runtime has precise types available.
257+
// Initialize garbage collector type system.
258+
c.typer = gctype.NewTyper(c.ctx, c.mod, c.targetData)
259+
}
260+
252261
// Add definitions to declarations.
253262
var initFuncs []llvm.Value
254263
irbuilder := c.ctx.NewBuilder()
@@ -314,8 +323,14 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
314323

315324
// Tell the optimizer that runtime.alloc is an allocator, meaning that it
316325
// returns values that are never null and never alias to an existing value.
326+
// Do the same for the typed allocator.
327+
alloc := c.mod.NamedFunction("runtime.alloc")
317328
for _, attrName := range []string{"noalias", "nonnull"} {
318-
c.mod.NamedFunction("runtime.alloc").AddAttributeAtIndex(0, getAttr(attrName))
329+
attr := getAttr(attrName)
330+
alloc.AddAttributeAtIndex(0, attr)
331+
if !allocTyped.IsNil() {
332+
allocTyped.AddAttributeAtIndex(0, attr)
333+
}
319334
}
320335

321336
// On *nix systems, the "abort" functuion in libc is used to handle fatal panics.
@@ -1462,7 +1477,18 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) {
14621477
return llvm.Value{}, b.makeError(expr.Pos(), fmt.Sprintf("value is too big (%v bytes)", size))
14631478
}
14641479
sizeValue := llvm.ConstInt(b.uintptrType, size, false)
1465-
buf := b.createRuntimeCall("alloc", []llvm.Value{sizeValue}, expr.Comment)
1480+
var buf llvm.Value
1481+
if b.typer != nil {
1482+
// Allocate a typed value.
1483+
t, err := b.typer.Create(typ)
1484+
if err != nil {
1485+
return llvm.Value{}, b.makeError(expr.Pos(), err.Error())
1486+
}
1487+
buf = b.createRuntimeCall("allocTyped", []llvm.Value{sizeValue, t}, expr.Comment)
1488+
} else {
1489+
// Allocate an untyped value.
1490+
buf = b.createRuntimeCall("alloc", []llvm.Value{sizeValue}, expr.Comment)
1491+
}
14661492
buf = b.CreateBitCast(buf, llvm.PointerType(typ, 0), "")
14671493
return buf, nil
14681494
} else {
@@ -1675,7 +1701,18 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) {
16751701
return llvm.Value{}, err
16761702
}
16771703
sliceSize := b.CreateBinOp(llvm.Mul, elemSizeValue, sliceCapCast, "makeslice.cap")
1678-
slicePtr := b.createRuntimeCall("alloc", []llvm.Value{sliceSize}, "makeslice.buf")
1704+
var slicePtr llvm.Value
1705+
if b.typer != nil {
1706+
// Allocate a typed value.
1707+
t, err := b.typer.Create(llvmElemType)
1708+
if err != nil {
1709+
return llvm.Value{}, b.makeError(expr.Pos(), err.Error())
1710+
}
1711+
slicePtr = b.createRuntimeCall("allocTyped", []llvm.Value{sliceSize, t}, "makeslice.buf")
1712+
} else {
1713+
// Allocate an untyped value.
1714+
slicePtr = b.createRuntimeCall("alloc", []llvm.Value{sliceSize}, "makeslice.buf")
1715+
}
16791716
slicePtr = b.CreateBitCast(slicePtr, llvm.PointerType(llvmElemType, 0), "makeslice.array")
16801717

16811718
// Extend or truncate if necessary. This is safe as we've already done

compiler/gctype/gctype.go

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package gctype
22

33
import (
4+
"errors"
5+
"fmt"
46
"math/big"
57

68
"tinygo.org/x/go-llvm"
@@ -48,56 +50,109 @@ func getPointerBitmap(targetData llvm.TargetData, typ llvm.Type, name string) *b
4850
}
4951
}
5052

51-
func bitmapToValue(b *big.Int, size uintptr, byte llvm.Type) llvm.Value {
52-
// Create a bitmap (a new global) that stores for each word in the globals
53-
// bundle whether it contains a pointer. This allows globals to be scanned
54-
// precisely: no non-pointers will be considered pointers if the bit pattern
55-
// looks like one.
56-
// This code assumes that pointers are self-aligned. For example, that a
57-
// 32-bit (4-byte) pointer is also aligned to 4 bytes.
58-
bitmapBytes := b.Bytes()
59-
bitmapValues := make([]llvm.Value, len(bitmapBytes))
60-
for i, b := range bitmapBytes {
61-
bitmapValues[len(bitmapBytes)-i-1] = llvm.ConstInt(byte, uint64(b), false)
53+
// NewTyper creates a Typer.
54+
func NewTyper(ctx llvm.Context, mod llvm.Module, td llvm.TargetData) *Typer {
55+
ptr := llvm.PointerType(ctx.Int8Type(), 0)
56+
return &Typer{
57+
tcache: make(map[llvm.Type]llvm.Value),
58+
td: td,
59+
mod: mod,
60+
uintptr: ctx.IntType(int(td.TypeSizeInBits(ptr))),
61+
i8: ctx.Int8Type(),
62+
ptrSize: td.TypeAllocSize(ptr),
63+
ptrAlign: uint64(td.ABITypeAlignment(ptr)),
6264
}
63-
return llvm.ConstArray(byte, bitmapValues)
6465
}
6566

6667
// Typer creates GC types.
6768
type Typer struct {
6869
// tcache is a cache of GC types by LLVM type.
6970
tcache map[llvm.Type]llvm.Value
7071

71-
// bcache is a cache of GC types by size and GC bitmap.
72-
bcache map[uint64]map[string]llvm.Value
73-
7472
// td is the target platform data.
7573
td llvm.TargetData
7674

77-
// idIntrinsic is used on AVR to map a type bitmap to a uintptr ID.
78-
idIntrinsic llvm.Value
75+
mod llvm.Module
7976

8077
uintptr, i8 llvm.Type
78+
79+
ptrSize, ptrAlign uint64
80+
}
81+
82+
// Create a GC type for the given LLVM type.
83+
func (t *Typer) Create(typ llvm.Type) (llvm.Value, error) {
84+
// Check the cache before attempting to create the global.
85+
if g, ok := t.tcache[typ]; ok {
86+
return g, nil
87+
}
88+
89+
// Find the type size.
90+
size := t.td.TypeAllocSize(typ)
91+
92+
// Compute a pointer bitmap.
93+
// TODO: clean this up and maybe use error handling?
94+
b := getPointerBitmap(t.td, typ, "")
95+
if b.Cmp(big.NewInt(0)) == 0 {
96+
// The type has no pointers.
97+
return llvm.ConstNull(llvm.PointerType(t.uintptr, 0)), nil
98+
}
99+
100+
// Use some limited sanity-checking.
101+
align := uint64(t.td.ABITypeAlignment(typ))
102+
switch {
103+
case size < t.ptrSize:
104+
return llvm.Value{}, errors.New("type has pointers but is smaller than a pointer")
105+
case align%t.ptrAlign != 0:
106+
return llvm.Value{}, errors.New("alignment of pointery type is not a multiple of pointer alignment")
107+
case size%align != 0:
108+
return llvm.Value{}, errors.New("type violates the array alignment invariant")
109+
}
110+
111+
// Convert size into increments of pointer-align.
112+
size /= t.ptrAlign
113+
114+
// Create a global for the type.
115+
g := t.createGlobal(size, b.Bytes())
116+
117+
// Save the global to the cache.
118+
t.tcache[typ] = g
119+
120+
return g, nil
81121
}
82122

83123
func (t *Typer) createGlobal(size uint64, layout []byte) llvm.Value {
84124
// TODO: compression?
85125

86-
// Check the cache before attempting to create the global.
87-
if g, ok := t.bcache[size][string(layout)]; ok {
88-
return g
89-
}
126+
// Generate the name of the global.
127+
name := fmt.Sprintf("tinygo.gc.type.%d.%x", size, layout)
128+
129+
// Create the global if it does not exist.
130+
g := t.mod.NamedGlobal(name)
131+
if g.IsNil() {
132+
// Convert the encoded layout to a byte array.
133+
bitmapValues := make([]llvm.Value, len(layout))
134+
for i, b := range layout {
135+
bitmapValues[len(layout)-i-1] = llvm.ConstInt(t.i8, uint64(b), false)
136+
}
137+
bitmapArray := llvm.ConstArray(t.i8, bitmapValues)
90138

91-
// Convert the encoded layout to a byte array.
92-
bitmapValues := make([]llvm.Value, len(layout))
93-
for i, b := range layout {
94-
bitmapValues[len(layout)-i-1] = llvm.ConstInt(t.i8, uint64(b), false)
139+
// Construct a tuple of the size + the array.
140+
tuple := llvm.ConstStruct([]llvm.Value{
141+
llvm.ConstInt(t.uintptr, size, false),
142+
bitmapArray,
143+
}, false)
144+
145+
// Create a global constant initialized with the tuple.
146+
g = llvm.AddGlobal(t.mod, tuple.Type(), name)
147+
g.SetInitializer(tuple)
148+
g.SetGlobalConstant(true)
149+
g.SetUnnamedAddr(true)
150+
g.SetLinkage(llvm.InternalLinkage)
95151
}
96-
bitmapArray := llvm.ConstArray(t.i8, bitmapValues)
97152

98-
// Construct a tuple of the size + the array.
99-
tuple := llvm.ConstStruct([]llvm.Value{
100-
llvm.ConstInt(t.uintptr, size, false),
101-
bitmapArray,
102-
}, false)
153+
// Get a pointer to the size component of the global.
154+
// This is used because different globals will end up with different sizes.
155+
g = llvm.ConstBitCast(g, llvm.PointerType(t.uintptr, 0))
156+
157+
return g
103158
}

interp/frame.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re
177177
operand := inst.Operand(0)
178178
if !operand.IsACallInst().IsNil() {
179179
fn := operand.CalledValue()
180-
if !fn.IsAFunction().IsNil() && fn.Name() == "runtime.alloc" {
180+
if !fn.IsAFunction().IsNil() && (fn.Name() == "runtime.alloc" || fn.Name() == "runtime.allocTyped") {
181181
continue // special case: bitcast of alloc
182182
}
183183
}
@@ -245,7 +245,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re
245245
case !inst.IsACallInst().IsNil():
246246
callee := inst.CalledValue()
247247
switch {
248-
case callee.Name() == "runtime.alloc":
248+
case callee.Name() == "runtime.alloc" || callee.Name() == "runtime.allocTyped":
249249
// heap allocation
250250
users := getUses(inst)
251251
var resultInst = inst

src/runtime/gc_list.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,28 @@ func isInHeap(addr uintptr) bool {
2121
return addr >= heapStart && addr < heapEnd
2222
}
2323

24+
// gcType is a chunk of metadata used to semi-precisely scan an allocation.
25+
// The compiler will produce constant globals which can be used in this form.
26+
type gcType struct {
27+
// size is the element size, measured in increments of pointer alignment.
28+
size uintptr
29+
30+
// data is a bitmap following the size value.
31+
// It is organized as a []byte, and set bits indicate places where a pointer may be present.
32+
data struct{}
33+
}
34+
35+
// untypedType is a placeholder type used for now to deal with untyped allocations.
36+
var untypedType = struct {
37+
t gcType
38+
data [1]byte
39+
}{
40+
t: gcType{
41+
size: 1,
42+
},
43+
data: [1]byte{1},
44+
}
45+
2446
// allocNode is a node in the allocations list.
2547
// It is prepended to every allocation, resulting in an overhead of 4 bytes per allocation.
2648
type allocNode struct {
@@ -30,6 +52,9 @@ type allocNode struct {
3052
// len is the length of the body of this node in bytes.
3153
len uintptr
3254

55+
// typ is the memory type of this node.
56+
typ *gcType
57+
3358
// base is the start of the body of this node.
3459
base struct{}
3560
}
@@ -46,7 +71,7 @@ func markRoot(addr, root uintptr) {
4671
}
4772

4873
func markRoots(start, end uintptr) {
49-
markMem(start, end)
74+
untypedType.t.markMem(start, end)
5075
}
5176

5277
// markAddr marks the allocation containing the specified address.
@@ -69,6 +94,7 @@ func markAddr(addr uintptr) {
6994
if addr < endAddr {
7095
// The address is included in this allocation.
7196
// Move the allocation to the scan stack.
97+
println(" mark", addr)
7298
*prev = node.next
7399
scanList, node.next = node, scanList
74100
return
@@ -79,7 +105,9 @@ func markAddr(addr uintptr) {
79105
// GC runs a garbage collection cycle.
80106
func GC() {
81107
// Mark phase: mark all reachable objects, recursively.
108+
println("scan globals")
82109
markGlobals()
110+
println("scan stack")
83111
markStack()
84112
var markedTaskQueue task.Queue
85113
runqueueScan:
@@ -98,16 +126,19 @@ runqueueScan:
98126
}
99127
}
100128

129+
println("scan work list")
101130
var keep *allocNode
102131
for scanList != nil {
103132
// Pop a node off of the scan list.
104133
node := scanList
105134
scanList = node.next
106135

136+
println(" scan node", node, node.typ)
137+
107138
// Scan the node.
108139
baseAddr := uintptr(unsafe.Pointer(&node.base))
109140
endAddr := baseAddr + node.len
110-
markMem(baseAddr, endAddr)
141+
node.typ.markMem(baseAddr, endAddr)
111142

112143
// Insert the node into the output heap.
113144
var prev *allocNode
@@ -205,10 +236,16 @@ searchLoop:
205236
return mem
206237
}
207238

208-
// alloc tries to find some free space on the heap, possibly doing a garbage
239+
// alloc a chunk of untyped memory.
240+
//go:inline
241+
func alloc(size uintptr) unsafe.Pointer {
242+
return allocTyped(size, &untypedType.t.size)
243+
}
244+
245+
// allocTyped tries to find some free space on the heap, possibly doing a garbage
209246
// collection cycle if needed. If no space is free, it panics.
210247
//go:noinline
211-
func alloc(size uintptr) unsafe.Pointer {
248+
func allocTyped(size uintptr, typ *uintptr) unsafe.Pointer {
212249
var ranGC bool
213250
tryAlloc:
214251
// Search for available memory.
@@ -227,9 +264,13 @@ tryAlloc:
227264
goto tryAlloc
228265
}
229266

230-
// Zero the allocation and return it.
267+
// Zero the allocation.
231268
ptr := unsafe.Pointer(&node.base)
232269
memzero(ptr, size)
270+
271+
// Apply the type to the allocation.
272+
node.typ = (*gcType)(unsafe.Pointer(typ))
273+
233274
return ptr
234275
}
235276

src/runtime/gc_list_avr.go

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)