Skip to content

Commit 635e57d

Browse files
committed
runtime (gc): convert "list" GC to a precise GC
1 parent 5ffe535 commit 635e57d

File tree

14 files changed

+335
-95
lines changed

14 files changed

+335
-95
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: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package gctype
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"math/big"
7+
8+
"tinygo.org/x/go-llvm"
9+
)
10+
11+
// getPointerBitmap scans the given LLVM type for pointers and sets bits in a
12+
// bigint at the word offset that contains a pointer. This scan is recursive.
13+
func getPointerBitmap(targetData llvm.TargetData, typ llvm.Type, name string) *big.Int {
14+
alignment := targetData.PrefTypeAlignment(llvm.PointerType(typ.Context().Int8Type(), 0))
15+
switch typ.TypeKind() {
16+
case llvm.IntegerTypeKind, llvm.FloatTypeKind, llvm.DoubleTypeKind:
17+
return big.NewInt(0)
18+
case llvm.PointerTypeKind:
19+
return big.NewInt(1)
20+
case llvm.StructTypeKind:
21+
ptrs := big.NewInt(0)
22+
for i, subtyp := range typ.StructElementTypes() {
23+
subptrs := getPointerBitmap(targetData, subtyp, name)
24+
if subptrs.BitLen() == 0 {
25+
continue
26+
}
27+
offset := targetData.ElementOffset(typ, i)
28+
if offset%uint64(alignment) != 0 {
29+
panic("precise GC: global contains unaligned pointer: " + name)
30+
}
31+
subptrs.Lsh(subptrs, uint(offset)/uint(alignment))
32+
ptrs.Or(ptrs, subptrs)
33+
}
34+
return ptrs
35+
case llvm.ArrayTypeKind:
36+
subtyp := typ.ElementType()
37+
subptrs := getPointerBitmap(targetData, subtyp, name)
38+
ptrs := big.NewInt(0)
39+
if subptrs.BitLen() == 0 {
40+
return ptrs
41+
}
42+
elementSize := targetData.TypeAllocSize(subtyp)
43+
for i := 0; i < typ.ArrayLength(); i++ {
44+
ptrs.Lsh(ptrs, uint(elementSize)/uint(alignment))
45+
ptrs.Or(ptrs, subptrs)
46+
}
47+
return ptrs
48+
default:
49+
panic("unknown type kind: " + name)
50+
}
51+
}
52+
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+
ctx: ctx,
60+
mod: mod,
61+
uintptr: ctx.IntType(int(td.TypeSizeInBits(ptr))),
62+
i8: ctx.Int8Type(),
63+
ptrSize: td.TypeAllocSize(ptr),
64+
ptrAlign: uint64(td.ABITypeAlignment(ptr)),
65+
}
66+
}
67+
68+
// Typer creates GC types.
69+
type Typer struct {
70+
// tcache is a cache of GC types by LLVM type.
71+
tcache map[llvm.Type]llvm.Value
72+
73+
// td is the target platform data.
74+
td llvm.TargetData
75+
76+
ctx llvm.Context
77+
78+
mod llvm.Module
79+
80+
uintptr, i8 llvm.Type
81+
82+
ptrSize, ptrAlign uint64
83+
}
84+
85+
// Create a GC type for the given LLVM type.
86+
func (t *Typer) Create(typ llvm.Type) (llvm.Value, error) {
87+
// Check the cache before attempting to create the global.
88+
if g, ok := t.tcache[typ]; ok {
89+
return g, nil
90+
}
91+
92+
// Find the type size.
93+
size := t.td.TypeAllocSize(typ)
94+
95+
// Compute a pointer bitmap.
96+
// TODO: clean this up and maybe use error handling?
97+
b := getPointerBitmap(t.td, typ, "")
98+
if b.Cmp(big.NewInt(0)) == 0 {
99+
// The type has no pointers.
100+
return llvm.ConstNull(llvm.PointerType(t.uintptr, 0)), nil
101+
}
102+
103+
// Use some limited sanity-checking.
104+
align := uint64(t.td.ABITypeAlignment(typ))
105+
switch {
106+
case size < t.ptrSize:
107+
return llvm.Value{}, errors.New("type has pointers but is smaller than a pointer")
108+
case align%t.ptrAlign != 0:
109+
return llvm.Value{}, errors.New("alignment of pointery type is not a multiple of pointer alignment")
110+
case size%align != 0:
111+
return llvm.Value{}, errors.New("type violates the array alignment invariant")
112+
}
113+
114+
// Convert size into increments of pointer-align.
115+
size /= t.ptrAlign
116+
117+
// Create a global for the type.
118+
g := t.createGlobal(size, b.Bytes())
119+
120+
// Save the global to the cache.
121+
t.tcache[typ] = g
122+
123+
return g, nil
124+
}
125+
126+
func (t *Typer) createGlobal(size uint64, layout []byte) llvm.Value {
127+
// TODO: compression?
128+
129+
// Generate the name of the global.
130+
name := fmt.Sprintf("tinygo.gc.type.%d.%x", size, layout)
131+
132+
// Create the global if it does not exist.
133+
g := t.mod.NamedGlobal(name)
134+
if g.IsNil() {
135+
// Convert the encoded layout to a byte array.
136+
bitmapValues := make([]llvm.Value, len(layout))
137+
for i, b := range layout {
138+
bitmapValues[len(layout)-i-1] = llvm.ConstInt(t.i8, uint64(b), false)
139+
}
140+
bitmapArray := llvm.ConstArray(t.i8, bitmapValues)
141+
142+
// Construct a tuple of the size + the array.
143+
tuple := t.ctx.ConstStruct([]llvm.Value{
144+
llvm.ConstInt(t.uintptr, size, false),
145+
bitmapArray,
146+
}, false)
147+
148+
// Create a global constant initialized with the tuple.
149+
g = llvm.AddGlobal(t.mod, tuple.Type(), name)
150+
g.SetInitializer(tuple)
151+
g.SetGlobalConstant(true)
152+
g.SetUnnamedAddr(true)
153+
g.SetLinkage(llvm.InternalLinkage)
154+
}
155+
156+
// Get a pointer to the size component of the global.
157+
// This is used because different globals will end up with different sizes.
158+
g = llvm.ConstBitCast(g, llvm.PointerType(t.uintptr, 0))
159+
160+
return g
161+
}

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/internal/task/task_stack.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (t *Task) Resume() {
5656
// initialize the state and prepare to call the specified function with the specified argument bundle.
5757
func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
5858
// Create a stack.
59-
stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0)))
59+
stack := make([]unsafe.Pointer, stackSize/unsafe.Sizeof(unsafe.Pointer(nil)))
6060

6161
// Invoke architecture-specific initialization.
6262
s.archInit(stack, fn, args)

src/internal/task/task_stack_avr.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ var startTask [0]uint8
3737
// archInit runs architecture-specific setup for the goroutine startup.
3838
// Note: adding //go:noinline to work around an AVR backend bug.
3939
//go:noinline
40-
func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) {
40+
func (s *state) archInit(stack []unsafe.Pointer, fn uintptr, args unsafe.Pointer) {
4141
// Set up the stack canary, a random number that should be checked when
4242
// switching from the task back to the scheduler. The stack canary pointer
4343
// points to the first word of the stack. If it has changed between now and
4444
// the next stack switch, there was a stack overflow.
45-
s.canaryPtr = &stack[0]
45+
s.canaryPtr = (*uintptr)(unsafe.Pointer(&stack[0]))
4646
*s.canaryPtr = stackCanary
4747

4848
// Store the initial sp for the startTask function (implemented in assembly).

src/internal/task/task_stack_cortexm.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ func (s *state) registers() *calleeSavedRegs {
3232
var startTask [0]uint8
3333

3434
// archInit runs architecture-specific setup for the goroutine startup.
35-
func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) {
35+
func (s *state) archInit(stack []unsafe.Pointer, fn uintptr, args unsafe.Pointer) {
3636
// Set up the stack canary, a random number that should be checked when
3737
// switching from the task back to the scheduler. The stack canary pointer
3838
// points to the first word of the stack. If it has changed between now and
3939
// the next stack switch, there was a stack overflow.
40-
s.canaryPtr = &stack[0]
40+
s.canaryPtr = (*uintptr)(unsafe.Pointer(&stack[0]))
4141
*s.canaryPtr = stackCanary
4242

4343
// Store the initial sp for the startTask function (implemented in assembly).

0 commit comments

Comments
 (0)