Skip to content

Commit 4d79d47

Browse files
aykevldeadprogram
authored andcommitted
compiler: move wasm ABI workaround to transform package
By considering this as a regular transformation, it can be easily tested.
1 parent 91299b6 commit 4d79d47

File tree

6 files changed

+248
-133
lines changed

6 files changed

+248
-133
lines changed

builder/build.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/tinygo-org/tinygo/compiler"
1717
"github.com/tinygo-org/tinygo/goenv"
1818
"github.com/tinygo-org/tinygo/interp"
19+
"github.com/tinygo-org/tinygo/transform"
1920
)
2021

2122
// Build performs a single package to executable Go build. It takes in a package
@@ -61,7 +62,7 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(stri
6162
// stack-allocated values.
6263
// Use -wasm-abi=generic to disable this behaviour.
6364
if config.Options.WasmAbi == "js" && strings.HasPrefix(config.Triple(), "wasm") {
64-
err := c.ExternalInt64AsPtr()
65+
err := transform.ExternalInt64AsPtr(c.Module())
6566
if err != nil {
6667
return err
6768
}

compiler/compiler.go

Lines changed: 0 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -2550,138 +2550,6 @@ func (c *Compiler) NonConstGlobals() {
25502550
}
25512551
}
25522552

2553-
// When -wasm-abi flag set to "js" (default),
2554-
// replace i64 in an external function with a stack-allocated i64*, to work
2555-
// around the lack of 64-bit integers in JavaScript (commonly used together with
2556-
// WebAssembly). Once that's resolved, this pass may be avoided.
2557-
// See also the -wasm-abi= flag
2558-
// https://github.com/WebAssembly/design/issues/1172
2559-
func (c *Compiler) ExternalInt64AsPtr() error {
2560-
int64Type := c.ctx.Int64Type()
2561-
int64PtrType := llvm.PointerType(int64Type, 0)
2562-
for fn := c.mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) {
2563-
if fn.Linkage() != llvm.ExternalLinkage {
2564-
// Only change externally visible functions (exports and imports).
2565-
continue
2566-
}
2567-
if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") {
2568-
// Do not try to modify the signature of internal LLVM functions and
2569-
// assume that runtime functions are only temporarily exported for
2570-
// coroutine lowering.
2571-
continue
2572-
}
2573-
2574-
hasInt64 := false
2575-
paramTypes := []llvm.Type{}
2576-
2577-
// Check return type for 64-bit integer.
2578-
fnType := fn.Type().ElementType()
2579-
returnType := fnType.ReturnType()
2580-
if returnType == int64Type {
2581-
hasInt64 = true
2582-
paramTypes = append(paramTypes, int64PtrType)
2583-
returnType = c.ctx.VoidType()
2584-
}
2585-
2586-
// Check param types for 64-bit integers.
2587-
for param := fn.FirstParam(); !param.IsNil(); param = llvm.NextParam(param) {
2588-
if param.Type() == int64Type {
2589-
hasInt64 = true
2590-
paramTypes = append(paramTypes, int64PtrType)
2591-
} else {
2592-
paramTypes = append(paramTypes, param.Type())
2593-
}
2594-
}
2595-
2596-
if !hasInt64 {
2597-
// No i64 in the paramter list.
2598-
continue
2599-
}
2600-
2601-
// Add $i64wrapper to the real function name as it is only used
2602-
// internally.
2603-
// Add a new function with the correct signature that is exported.
2604-
name := fn.Name()
2605-
fn.SetName(name + "$i64wrap")
2606-
externalFnType := llvm.FunctionType(returnType, paramTypes, fnType.IsFunctionVarArg())
2607-
externalFn := llvm.AddFunction(c.mod, name, externalFnType)
2608-
2609-
if fn.IsDeclaration() {
2610-
// Just a declaration: the definition doesn't exist on the Go side
2611-
// so it cannot be called from external code.
2612-
// Update all users to call the external function.
2613-
// The old $i64wrapper function could be removed, but it may as well
2614-
// be left in place.
2615-
for use := fn.FirstUse(); !use.IsNil(); use = use.NextUse() {
2616-
call := use.User()
2617-
c.builder.SetInsertPointBefore(call)
2618-
callParams := []llvm.Value{}
2619-
var retvalAlloca llvm.Value
2620-
if fnType.ReturnType() == int64Type {
2621-
retvalAlloca = c.builder.CreateAlloca(int64Type, "i64asptr")
2622-
callParams = append(callParams, retvalAlloca)
2623-
}
2624-
for i := 0; i < call.OperandsCount()-1; i++ {
2625-
operand := call.Operand(i)
2626-
if operand.Type() == int64Type {
2627-
// Pass a stack-allocated pointer instead of the value
2628-
// itself.
2629-
alloca := c.builder.CreateAlloca(int64Type, "i64asptr")
2630-
c.builder.CreateStore(operand, alloca)
2631-
callParams = append(callParams, alloca)
2632-
} else {
2633-
// Unchanged parameter.
2634-
callParams = append(callParams, operand)
2635-
}
2636-
}
2637-
if fnType.ReturnType() == int64Type {
2638-
// Pass a stack-allocated pointer as the first parameter
2639-
// where the return value should be stored, instead of using
2640-
// the regular return value.
2641-
c.builder.CreateCall(externalFn, callParams, call.Name())
2642-
returnValue := c.builder.CreateLoad(retvalAlloca, "retval")
2643-
call.ReplaceAllUsesWith(returnValue)
2644-
call.EraseFromParentAsInstruction()
2645-
} else {
2646-
newCall := c.builder.CreateCall(externalFn, callParams, call.Name())
2647-
call.ReplaceAllUsesWith(newCall)
2648-
call.EraseFromParentAsInstruction()
2649-
}
2650-
}
2651-
} else {
2652-
// The function has a definition in Go. This means that it may still
2653-
// be called both Go and from external code.
2654-
// Keep existing calls with the existing convention in place (for
2655-
// better performance), but export a new wrapper function with the
2656-
// correct calling convention.
2657-
fn.SetLinkage(llvm.InternalLinkage)
2658-
fn.SetUnnamedAddr(true)
2659-
entryBlock := c.ctx.AddBasicBlock(externalFn, "entry")
2660-
c.builder.SetInsertPointAtEnd(entryBlock)
2661-
var callParams []llvm.Value
2662-
if fnType.ReturnType() == int64Type {
2663-
return errors.New("not yet implemented: exported function returns i64 with -wasm-abi=js; " +
2664-
"see https://tinygo.org/compiler-internals/calling-convention/")
2665-
}
2666-
for i, origParam := range fn.Params() {
2667-
paramValue := externalFn.Param(i)
2668-
if origParam.Type() == int64Type {
2669-
paramValue = c.builder.CreateLoad(paramValue, "i64")
2670-
}
2671-
callParams = append(callParams, paramValue)
2672-
}
2673-
retval := c.builder.CreateCall(fn, callParams, "")
2674-
if retval.Type().TypeKind() == llvm.VoidTypeKind {
2675-
c.builder.CreateRetVoid()
2676-
} else {
2677-
c.builder.CreateRet(retval)
2678-
}
2679-
}
2680-
}
2681-
2682-
return nil
2683-
}
2684-
26852553
// Emit object file (.o).
26862554
func (c *Compiler) EmitObject(path string) error {
26872555
llvmBuf, err := c.machine.EmitToMemoryBuffer(c.mod, llvm.ObjectFile)

transform/testdata/wasm-abi.ll

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128"
2+
target triple = "wasm32-unknown-unknown-wasm"
3+
4+
declare i64 @externalCall(i8*, i32, i64)
5+
6+
define internal i64 @testCall(i8* %ptr, i32 %len, i64 %foo) {
7+
%val = call i64 @externalCall(i8* %ptr, i32 %len, i64 %foo)
8+
ret i64 %val
9+
}
10+
11+
define internal i64 @testCallNonEntry(i8* %ptr, i32 %len) {
12+
entry:
13+
br label %bb1
14+
15+
bb1:
16+
%val = call i64 @externalCall(i8* %ptr, i32 %len, i64 3)
17+
ret i64 %val
18+
}
19+
20+
define void @exportedFunction(i64 %foo) {
21+
%unused = shl i64 %foo, 1
22+
ret void
23+
}
24+
25+
define internal void @callExportedFunction(i64 %foo) {
26+
call void @exportedFunction(i64 %foo)
27+
ret void
28+
}

transform/testdata/wasm-abi.out.ll

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128"
2+
target triple = "wasm32-unknown-unknown-wasm"
3+
4+
declare i64 @"externalCall$i64wrap"(i8*, i32, i64)
5+
6+
define internal i64 @testCall(i8* %ptr, i32 %len, i64 %foo) {
7+
%i64asptr = alloca i64
8+
%i64asptr1 = alloca i64
9+
store i64 %foo, i64* %i64asptr1
10+
call void @externalCall(i64* %i64asptr, i8* %ptr, i32 %len, i64* %i64asptr1)
11+
%retval = load i64, i64* %i64asptr
12+
ret i64 %retval
13+
}
14+
15+
define internal i64 @testCallNonEntry(i8* %ptr, i32 %len) {
16+
entry:
17+
br label %bb1
18+
19+
bb1: ; preds = %entry
20+
%i64asptr = alloca i64
21+
%i64asptr1 = alloca i64
22+
store i64 3, i64* %i64asptr1
23+
call void @externalCall(i64* %i64asptr, i8* %ptr, i32 %len, i64* %i64asptr1)
24+
%retval = load i64, i64* %i64asptr
25+
ret i64 %retval
26+
}
27+
28+
define internal void @"exportedFunction$i64wrap"(i64 %foo) unnamed_addr {
29+
%unused = shl i64 %foo, 1
30+
ret void
31+
}
32+
33+
define internal void @callExportedFunction(i64 %foo) {
34+
call void @"exportedFunction$i64wrap"(i64 %foo)
35+
ret void
36+
}
37+
38+
declare void @externalCall(i64*, i8*, i32, i64*)
39+
40+
define void @exportedFunction(i64*) {
41+
entry:
42+
%i64 = load i64, i64* %0
43+
call void @"exportedFunction$i64wrap"(i64 %i64)
44+
ret void
45+
}

transform/wasm-abi.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package transform
2+
3+
import (
4+
"errors"
5+
"strings"
6+
7+
"tinygo.org/x/go-llvm"
8+
)
9+
10+
// ExternalInt64AsPtr converts i64 parameters in externally-visible functions to
11+
// values passed by reference (*i64), to work around the lack of 64-bit integers
12+
// in JavaScript (commonly used together with WebAssembly). Once that's
13+
// resolved, this pass may be avoided. For more details:
14+
// https://github.com/WebAssembly/design/issues/1172
15+
//
16+
// This pass can be enabled/disabled with the -wasm-abi flag, and is enabled by
17+
// default as of december 2019.
18+
func ExternalInt64AsPtr(mod llvm.Module) error {
19+
ctx := mod.Context()
20+
builder := ctx.NewBuilder()
21+
defer builder.Dispose()
22+
int64Type := ctx.Int64Type()
23+
int64PtrType := llvm.PointerType(int64Type, 0)
24+
25+
for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) {
26+
if fn.Linkage() != llvm.ExternalLinkage {
27+
// Only change externally visible functions (exports and imports).
28+
continue
29+
}
30+
if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") {
31+
// Do not try to modify the signature of internal LLVM functions and
32+
// assume that runtime functions are only temporarily exported for
33+
// coroutine lowering.
34+
continue
35+
}
36+
37+
hasInt64 := false
38+
paramTypes := []llvm.Type{}
39+
40+
// Check return type for 64-bit integer.
41+
fnType := fn.Type().ElementType()
42+
returnType := fnType.ReturnType()
43+
if returnType == int64Type {
44+
hasInt64 = true
45+
paramTypes = append(paramTypes, int64PtrType)
46+
returnType = ctx.VoidType()
47+
}
48+
49+
// Check param types for 64-bit integers.
50+
for param := fn.FirstParam(); !param.IsNil(); param = llvm.NextParam(param) {
51+
if param.Type() == int64Type {
52+
hasInt64 = true
53+
paramTypes = append(paramTypes, int64PtrType)
54+
} else {
55+
paramTypes = append(paramTypes, param.Type())
56+
}
57+
}
58+
59+
if !hasInt64 {
60+
// No i64 in the paramter list.
61+
continue
62+
}
63+
64+
// Add $i64wrapper to the real function name as it is only used
65+
// internally.
66+
// Add a new function with the correct signature that is exported.
67+
name := fn.Name()
68+
fn.SetName(name + "$i64wrap")
69+
externalFnType := llvm.FunctionType(returnType, paramTypes, fnType.IsFunctionVarArg())
70+
externalFn := llvm.AddFunction(mod, name, externalFnType)
71+
72+
if fn.IsDeclaration() {
73+
// Just a declaration: the definition doesn't exist on the Go side
74+
// so it cannot be called from external code.
75+
// Update all users to call the external function.
76+
// The old $i64wrapper function could be removed, but it may as well
77+
// be left in place.
78+
for use := fn.FirstUse(); !use.IsNil(); use = use.NextUse() {
79+
call := use.User()
80+
builder.SetInsertPointBefore(call)
81+
callParams := []llvm.Value{}
82+
var retvalAlloca llvm.Value
83+
if fnType.ReturnType() == int64Type {
84+
retvalAlloca = builder.CreateAlloca(int64Type, "i64asptr")
85+
callParams = append(callParams, retvalAlloca)
86+
}
87+
for i := 0; i < call.OperandsCount()-1; i++ {
88+
operand := call.Operand(i)
89+
if operand.Type() == int64Type {
90+
// Pass a stack-allocated pointer instead of the value
91+
// itself.
92+
alloca := builder.CreateAlloca(int64Type, "i64asptr")
93+
builder.CreateStore(operand, alloca)
94+
callParams = append(callParams, alloca)
95+
} else {
96+
// Unchanged parameter.
97+
callParams = append(callParams, operand)
98+
}
99+
}
100+
var callName string
101+
if returnType.TypeKind() != llvm.VoidTypeKind {
102+
// Only use the name of the old call instruction if the new
103+
// call is not a void call.
104+
// A call instruction with an i64 return type may have had a
105+
// name, but it cannot have a name after this transform
106+
// because the return type will now be void.
107+
callName = call.Name()
108+
}
109+
if fnType.ReturnType() == int64Type {
110+
// Pass a stack-allocated pointer as the first parameter
111+
// where the return value should be stored, instead of using
112+
// the regular return value.
113+
builder.CreateCall(externalFn, callParams, callName)
114+
returnValue := builder.CreateLoad(retvalAlloca, "retval")
115+
call.ReplaceAllUsesWith(returnValue)
116+
call.EraseFromParentAsInstruction()
117+
} else {
118+
newCall := builder.CreateCall(externalFn, callParams, callName)
119+
call.ReplaceAllUsesWith(newCall)
120+
call.EraseFromParentAsInstruction()
121+
}
122+
}
123+
} else {
124+
// The function has a definition in Go. This means that it may still
125+
// be called both Go and from external code.
126+
// Keep existing calls with the existing convention in place (for
127+
// better performance), but export a new wrapper function with the
128+
// correct calling convention.
129+
fn.SetLinkage(llvm.InternalLinkage)
130+
fn.SetUnnamedAddr(true)
131+
entryBlock := ctx.AddBasicBlock(externalFn, "entry")
132+
builder.SetInsertPointAtEnd(entryBlock)
133+
var callParams []llvm.Value
134+
if fnType.ReturnType() == int64Type {
135+
return errors.New("not yet implemented: exported function returns i64 with -wasm-abi=js; " +
136+
"see https://tinygo.org/compiler-internals/calling-convention/")
137+
}
138+
for i, origParam := range fn.Params() {
139+
paramValue := externalFn.Param(i)
140+
if origParam.Type() == int64Type {
141+
paramValue = builder.CreateLoad(paramValue, "i64")
142+
}
143+
callParams = append(callParams, paramValue)
144+
}
145+
retval := builder.CreateCall(fn, callParams, "")
146+
if retval.Type().TypeKind() == llvm.VoidTypeKind {
147+
builder.CreateRetVoid()
148+
} else {
149+
builder.CreateRet(retval)
150+
}
151+
}
152+
}
153+
154+
return nil
155+
}

0 commit comments

Comments
 (0)