Skip to content

Commit 1c5d9bd

Browse files
committed
compiler: refactor and add tests
This commit finally introduces unit tests for the compiler, to check whether input Go code is converted to the expected output IR. To make this necessary, a few refactors were needed. Hopefully these refactors (to compile a program package by package instead of all at once) will eventually become standard, so that packages can all be compiled separate from each other and be cached between compiles.
1 parent f2e789f commit 1c5d9bd

File tree

12 files changed

+610
-100
lines changed

12 files changed

+610
-100
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ commands:
114114
key: wasi-libc-sysroot-systemclang-v2
115115
paths:
116116
- lib/wasi-libc/sysroot
117-
- run: go test -v -tags=llvm<<parameters.llvm>> ./cgo ./compileopts ./interp ./transform .
117+
- run: go test -v -tags=llvm<<parameters.llvm>> ./cgo ./compileopts ./compiler ./interp ./transform .
118118
- run: make gen-device -j4
119119
- run: make smoketest XTENSA=0
120120
- run: make tinygo-test

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ tinygo:
163163
CGO_CPPFLAGS="$(CGO_CPPFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GO) build -buildmode exe -o build/tinygo$(EXE) -tags byollvm -ldflags="-X main.gitSha1=`git rev-parse --short HEAD`" .
164164

165165
test: wasi-libc
166-
CGO_CPPFLAGS="$(CGO_CPPFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GO) test -v -buildmode exe -tags byollvm ./cgo ./compileopts ./interp ./transform .
166+
CGO_CPPFLAGS="$(CGO_CPPFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GO) test -v -buildmode exe -tags byollvm ./cgo ./compileopts ./compiler ./interp ./transform .
167167

168168
# Test known-working standard library packages.
169169
# TODO: do this in one command, parallelize, and only show failing tests (no

builder/build.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/binary"
99
"errors"
1010
"fmt"
11+
"go/types"
1112
"io/ioutil"
1213
"os"
1314
"path/filepath"
@@ -19,6 +20,7 @@ import (
1920
"github.com/tinygo-org/tinygo/compiler"
2021
"github.com/tinygo-org/tinygo/goenv"
2122
"github.com/tinygo-org/tinygo/interp"
23+
"github.com/tinygo-org/tinygo/loader"
2224
"github.com/tinygo-org/tinygo/stacksize"
2325
"github.com/tinygo-org/tinygo/transform"
2426
"tinygo.org/x/go-llvm"
@@ -43,16 +45,31 @@ type BuildResult struct {
4345
// The error value may be of type *MultiError. Callers will likely want to check
4446
// for this case and print such errors individually.
4547
func Build(pkgName, outpath string, config *compileopts.Config, action func(BuildResult) error) error {
46-
// Compile Go code to IR.
48+
// Load the target machine, which is the LLVM object that contains all
49+
// details of a target (alignment restrictions, pointer size, default
50+
// address spaces, etc).
4751
machine, err := compiler.NewTargetMachine(config)
4852
if err != nil {
4953
return err
5054
}
51-
buildOutput, errs := compiler.Compile(pkgName, machine, config)
55+
56+
// Load entire program AST into memory.
57+
lprogram, err := loader.Load(config, []string{pkgName}, config.ClangHeaders, types.Config{
58+
Sizes: compiler.Sizes(machine),
59+
})
60+
if err != nil {
61+
return err
62+
}
63+
err = lprogram.Parse()
64+
if err != nil {
65+
return err
66+
}
67+
68+
// Compile AST to IR.
69+
mod, errs := compiler.CompileProgram(pkgName, lprogram, machine, config)
5270
if errs != nil {
5371
return newMultiError(errs)
5472
}
55-
mod := buildOutput.Mod
5673

5774
if config.Options.PrintIR {
5875
fmt.Println("; Generated LLVM IR:")
@@ -208,17 +225,21 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
208225
}
209226

210227
// Compile C files in packages.
211-
for i, file := range buildOutput.ExtraFiles {
212-
outpath := filepath.Join(dir, "pkg"+strconv.Itoa(i)+"-"+filepath.Base(file)+".o")
213-
err := runCCompiler(config.Target.Compiler, append(config.CFlags(), "-c", "-o", outpath, file)...)
214-
if err != nil {
215-
return &commandError{"failed to build", file, err}
228+
// Gather the list of (C) file paths that should be included in the build.
229+
for i, pkg := range lprogram.Sorted() {
230+
for j, filename := range pkg.CFiles {
231+
file := filepath.Join(pkg.Dir, filename)
232+
outpath := filepath.Join(dir, "pkg"+strconv.Itoa(i)+"."+strconv.Itoa(j)+"-"+filepath.Base(file)+".o")
233+
err := runCCompiler(config.Target.Compiler, append(config.CFlags(), "-c", "-o", outpath, file)...)
234+
if err != nil {
235+
return &commandError{"failed to build", file, err}
236+
}
237+
ldflags = append(ldflags, outpath)
216238
}
217-
ldflags = append(ldflags, outpath)
218239
}
219240

220-
if len(buildOutput.ExtraLDFlags) > 0 {
221-
ldflags = append(ldflags, buildOutput.ExtraLDFlags...)
241+
if len(lprogram.LDFlags) > 0 {
242+
ldflags = append(ldflags, lprogram.LDFlags...)
222243
}
223244

224245
// Link the object files together.
@@ -304,7 +325,7 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
304325
}
305326
return action(BuildResult{
306327
Binary: tmppath,
307-
MainDir: buildOutput.MainDir,
328+
MainDir: lprogram.MainPkg().Dir,
308329
})
309330
}
310331
}

compiler/compiler.go

Lines changed: 133 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"go/token"
1010
"go/types"
1111
"path/filepath"
12+
"sort"
1213
"strconv"
1314
"strings"
1415

@@ -53,6 +54,45 @@ type compilerContext struct {
5354
astComments map[string]*ast.CommentGroup
5455
}
5556

57+
// newCompilerContext returns a new compiler context ready for use, most
58+
// importantly with a newly created LLVM context and module.
59+
func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *compileopts.Config) *compilerContext {
60+
c := &compilerContext{
61+
Config: config,
62+
difiles: make(map[string]llvm.Metadata),
63+
ditypes: make(map[types.Type]llvm.Metadata),
64+
machine: machine,
65+
targetData: machine.CreateTargetData(),
66+
}
67+
68+
c.ctx = llvm.NewContext()
69+
c.mod = c.ctx.NewModule(moduleName)
70+
c.mod.SetTarget(config.Triple())
71+
c.mod.SetDataLayout(c.targetData.String())
72+
if c.Debug() {
73+
c.dibuilder = llvm.NewDIBuilder(c.mod)
74+
}
75+
76+
c.uintptrType = c.ctx.IntType(c.targetData.PointerSize() * 8)
77+
if c.targetData.PointerSize() <= 4 {
78+
// 8, 16, 32 bits targets
79+
c.intType = c.ctx.Int32Type()
80+
} else if c.targetData.PointerSize() == 8 {
81+
// 64 bits target
82+
c.intType = c.ctx.Int64Type()
83+
} else {
84+
panic("unknown pointer size")
85+
}
86+
c.i8ptrType = llvm.PointerType(c.ctx.Int8Type(), 0)
87+
88+
dummyFuncType := llvm.FunctionType(c.ctx.VoidType(), nil, false)
89+
dummyFunc := llvm.AddFunction(c.mod, "tinygo.dummy", dummyFuncType)
90+
c.funcPtrAddrSpace = dummyFunc.Type().PointerAddressSpace()
91+
dummyFunc.EraseFromParentAsFunction()
92+
93+
return c
94+
}
95+
5696
// builder contains all information relevant to build a single function.
5797
type builder struct {
5898
*compilerContext
@@ -76,6 +116,18 @@ type builder struct {
76116
deferBuiltinFuncs map[ssa.Value]deferBuiltin
77117
}
78118

119+
func newBuilder(c *compilerContext, irbuilder llvm.Builder, f *ir.Function) *builder {
120+
return &builder{
121+
compilerContext: c,
122+
Builder: irbuilder,
123+
fn: f,
124+
locals: make(map[ssa.Value]llvm.Value),
125+
dilocals: make(map[*types.Var]llvm.Metadata),
126+
blockEntries: make(map[*ssa.BasicBlock]llvm.BasicBlock),
127+
blockExits: make(map[*ssa.BasicBlock]llvm.BasicBlock),
128+
}
129+
}
130+
79131
type deferBuiltin struct {
80132
funcName string
81133
callback int
@@ -127,94 +179,47 @@ func NewTargetMachine(config *compileopts.Config) (llvm.TargetMachine, error) {
127179
return machine, nil
128180
}
129181

130-
// CompilerOutput is returned from the Compile() call. It contains the compile
131-
// output and information necessary to continue to compile and link the program.
132-
type CompilerOutput struct {
133-
// The LLVM module that contains the compiled but not optimized LLVM module
134-
// for all the Go code in the program.
135-
Mod llvm.Module
136-
137-
// ExtraFiles is a list of C source files included in packages that should
138-
// be built and linked together with the main executable to form one
139-
// program. They can be used from CGo, for example.
140-
ExtraFiles []string
141-
142-
// ExtraLDFlags are linker flags obtained during CGo processing. These flags
143-
// must be passed to the linker which links the entire executable.
144-
ExtraLDFlags []string
145-
146-
// MainDir is the absolute directory path to the directory of the main
147-
// package. This is useful for testing: tests must be run in the package
148-
// directory that is being tested.
149-
MainDir string
150-
}
182+
// Sizes returns a types.Sizes appropriate for the given target machine. It
183+
// includes the correct int size and aligment as is necessary for the Go
184+
// typechecker.
185+
func Sizes(machine llvm.TargetMachine) types.Sizes {
186+
targetData := machine.CreateTargetData()
187+
defer targetData.Dispose()
151188

152-
// Compile the given package path or .go file path. Return an error when this
153-
// fails (in any stage). If successful it returns the LLVM module and a list of
154-
// extra C files to be compiled. If not, one or more errors will be returned.
155-
//
156-
// The fact that it returns a list of filenames to compile is a layering
157-
// violation. Eventually, this Compile function should only compile a single
158-
// package and not the whole program, and loading of the program (including CGo
159-
// processing) should be moved outside the compiler package.
160-
func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Config) (output CompilerOutput, errors []error) {
161-
c := &compilerContext{
162-
Config: config,
163-
difiles: make(map[string]llvm.Metadata),
164-
ditypes: make(map[types.Type]llvm.Metadata),
165-
machine: machine,
166-
targetData: machine.CreateTargetData(),
189+
intPtrType := targetData.IntPtrType()
190+
if intPtrType.IntTypeWidth()/8 <= 32 {
167191
}
168192

169-
c.ctx = llvm.NewContext()
170-
c.mod = c.ctx.NewModule(pkgName)
171-
c.mod.SetTarget(config.Triple())
172-
c.mod.SetDataLayout(c.targetData.String())
173-
if c.Debug() {
174-
c.dibuilder = llvm.NewDIBuilder(c.mod)
175-
}
176-
output.Mod = c.mod
177-
178-
c.uintptrType = c.ctx.IntType(c.targetData.PointerSize() * 8)
179-
if c.targetData.PointerSize() <= 4 {
193+
var intWidth int
194+
if targetData.PointerSize() <= 4 {
180195
// 8, 16, 32 bits targets
181-
c.intType = c.ctx.Int32Type()
182-
} else if c.targetData.PointerSize() == 8 {
196+
intWidth = 32
197+
} else if targetData.PointerSize() == 8 {
183198
// 64 bits target
184-
c.intType = c.ctx.Int64Type()
199+
intWidth = 64
185200
} else {
186201
panic("unknown pointer size")
187202
}
188-
c.i8ptrType = llvm.PointerType(c.ctx.Int8Type(), 0)
189203

190-
dummyFuncType := llvm.FunctionType(c.ctx.VoidType(), nil, false)
191-
dummyFunc := llvm.AddFunction(c.mod, "tinygo.dummy", dummyFuncType)
192-
c.funcPtrAddrSpace = dummyFunc.Type().PointerAddressSpace()
193-
dummyFunc.EraseFromParentAsFunction()
194-
195-
lprogram, err := loader.Load(c.Config, []string{pkgName}, c.ClangHeaders, types.Config{
196-
Sizes: &stdSizes{
197-
IntSize: int64(c.targetData.TypeAllocSize(c.intType)),
198-
PtrSize: int64(c.targetData.PointerSize()),
199-
MaxAlign: int64(c.targetData.PrefTypeAlignment(c.i8ptrType)),
200-
}})
201-
if err != nil {
202-
return output, []error{err}
204+
return &stdSizes{
205+
IntSize: int64(intWidth / 8),
206+
PtrSize: int64(targetData.PointerSize()),
207+
MaxAlign: int64(targetData.PrefTypeAlignment(intPtrType)),
203208
}
209+
}
204210

205-
err = lprogram.Parse()
206-
if err != nil {
207-
return output, []error{err}
208-
}
209-
output.ExtraLDFlags = lprogram.LDFlags
210-
output.MainDir = lprogram.MainPkg().Dir
211+
// CompileProgram compiles the given package path or .go file path. Return an
212+
// error when this fails (in any stage). If successful it returns the LLVM
213+
// module. If not, one or more errors will be returned.
214+
func CompileProgram(pkgName string, lprogram *loader.Program, machine llvm.TargetMachine, config *compileopts.Config) (llvm.Module, []error) {
215+
c := newCompilerContext(pkgName, machine, config)
211216

212217
c.ir = ir.NewProgram(lprogram)
213218

214219
// Run a simple dead code elimination pass.
215-
err = c.ir.SimpleDCE()
220+
err := c.ir.SimpleDCE()
216221
if err != nil {
217-
return output, []error{err}
222+
return llvm.Module{}, []error{err}
218223
}
219224

220225
// Initialize debug information.
@@ -265,15 +270,7 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
265270
}
266271

267272
// Create the function definition.
268-
b := builder{
269-
compilerContext: c,
270-
Builder: irbuilder,
271-
fn: f,
272-
locals: make(map[ssa.Value]llvm.Value),
273-
dilocals: make(map[*types.Var]llvm.Metadata),
274-
blockEntries: make(map[*ssa.BasicBlock]llvm.BasicBlock),
275-
blockExits: make(map[*ssa.BasicBlock]llvm.BasicBlock),
276-
}
273+
b := newBuilder(c, irbuilder, f)
277274
b.createFunctionDefinition()
278275
}
279276

@@ -352,14 +349,64 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
352349
c.dibuilder.Finalize()
353350
}
354351

355-
// Gather the list of (C) file paths that should be included in the build.
356-
for _, pkg := range c.ir.LoaderProgram.Sorted() {
357-
for _, filename := range pkg.CFiles {
358-
output.ExtraFiles = append(output.ExtraFiles, filepath.Join(pkg.Dir, filename))
352+
return c.mod, c.diagnostics
353+
}
354+
355+
// CompilePackage compiles a single package to a LLVM module.
356+
func CompilePackage(moduleName string, pkg *loader.Package, machine llvm.TargetMachine, config *compileopts.Config) (llvm.Module, []error) {
357+
c := newCompilerContext(moduleName, machine, config)
358+
359+
// Build SSA from AST.
360+
ssaPkg := pkg.LoadSSA()
361+
ssaPkg.Build()
362+
363+
// Sort by position, so that the order of the functions in the IR matches
364+
// the order of functions in the source file. This is useful for testing,
365+
// for example.
366+
var members []string
367+
for name := range ssaPkg.Members {
368+
members = append(members, name)
369+
}
370+
sort.Slice(members, func(i, j int) bool {
371+
iPos := ssaPkg.Members[members[i]].Pos()
372+
jPos := ssaPkg.Members[members[j]].Pos()
373+
if i == j {
374+
// Cannot sort by pos, so do it by name.
375+
return members[i] < members[j]
376+
}
377+
return iPos < jPos
378+
})
379+
380+
// Create *ir.Functions objects.
381+
var functions []*ir.Function
382+
for _, name := range members {
383+
member := ssaPkg.Members[name]
384+
switch member := member.(type) {
385+
case *ssa.Function:
386+
functions = append(functions, &ir.Function{
387+
Function: member,
388+
})
359389
}
360390
}
361391

362-
return output, c.diagnostics
392+
// Declare all functions.
393+
for _, fn := range functions {
394+
c.createFunctionDeclaration(fn)
395+
}
396+
397+
// Add definitions to declarations.
398+
irbuilder := c.ctx.NewBuilder()
399+
defer irbuilder.Dispose()
400+
for _, f := range functions {
401+
if f.Blocks == nil {
402+
continue // external function
403+
}
404+
// Create the function definition.
405+
b := newBuilder(c, irbuilder, f)
406+
b.createFunctionDefinition()
407+
}
408+
409+
return c.mod, nil
363410
}
364411

365412
// getLLVMRuntimeType obtains a named type from the runtime package and returns

0 commit comments

Comments
 (0)