diff --git a/builder/build.go b/builder/build.go index 516b9e64..2e8577e3 100644 --- a/builder/build.go +++ b/builder/build.go @@ -1054,17 +1054,6 @@ func optimizeProgram(mod llvm.Module, config *compileopts.Config) error { return err } - // Browsers cannot handle external functions that have type i64 because it - // cannot be represented exactly in JavaScript (JS only has doubles). To - // keep functions interoperable, pass int64 types as pointers to - // stack-allocated values. - if config.WasmAbi() == "js" { - err := transform.ExternalInt64AsPtr(mod, config) - if err != nil { - return err - } - } - // Optimization levels here are roughly the same as Clang, but probably not // exactly. optLevel, sizeLevel, inlinerThreshold := config.OptLevels() diff --git a/compileopts/config.go b/compileopts/config.go index 9a4bc310..39fc4f2a 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -492,11 +492,6 @@ func (c *Config) RelocationModel() string { return "static" } -// WasmAbi returns the WASM ABI which is specified in the target JSON file. -func (c *Config) WasmAbi() string { - return c.Target.WasmAbi -} - // EmulatorName is a shorthand to get the command for this emulator, something // like qemu-system-arm or simavr. func (c *Config) EmulatorName() string { diff --git a/compileopts/target.go b/compileopts/target.go index 40a1d445..3040f902 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -62,7 +62,6 @@ type TargetSpec struct { JLinkDevice string `json:"jlink-device"` CodeModel string `json:"code-model"` RelocationModel string `json:"relocation-model"` - WasmAbi string `json:"wasm-abi"` } // overrideProperties overrides all properties that are set in child into itself using reflection. diff --git a/targets/wasi.json b/targets/wasi.json index 4c43193f..f55c7c1f 100644 --- a/targets/wasi.json +++ b/targets/wasi.json @@ -21,6 +21,5 @@ "extra-files": [ "src/runtime/asm_tinygowasm.S" ], - "emulator": "wasmtime --mapdir=/tmp::{tmpDir} {}", - "wasm-abi": "generic" + "emulator": "wasmtime --mapdir=/tmp::{tmpDir} {}" } diff --git a/targets/wasm.json b/targets/wasm.json index 26494cc4..37034c6c 100644 --- a/targets/wasm.json +++ b/targets/wasm.json @@ -22,6 +22,5 @@ "extra-files": [ "src/runtime/asm_tinygowasm.S" ], - "emulator": "node {root}/targets/wasm_exec.js {}", - "wasm-abi": "js" + "emulator": "node {root}/targets/wasm_exec.js {}" } diff --git a/targets/wasm_exec.js b/targets/wasm_exec.js index 8021b44e..8e29d626 100644 --- a/targets/wasm_exec.js +++ b/targets/wasm_exec.js @@ -130,6 +130,7 @@ const encoder = new TextEncoder("utf-8"); const decoder = new TextDecoder("utf-8"); + let reinterpretBuf = new DataView(new ArrayBuffer(8)); var logLine = []; global.Go = class { @@ -142,19 +143,9 @@ return new DataView(this._inst.exports.memory.buffer); } - const setInt64 = (addr, v) => { - mem().setUint32(addr + 0, v, true); - mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); - } - - const getInt64 = (addr) => { - const low = mem().getUint32(addr + 0, true); - const high = mem().getInt32(addr + 4, true); - return low + high * 4294967296; - } - - const loadValue = (addr) => { - const f = mem().getFloat64(addr, true); + const unboxValue = (v_ref) => { + reinterpretBuf.setBigInt64(0, v_ref, true); + const f = reinterpretBuf.getFloat64(0, true); if (f === 0) { return undefined; } @@ -162,71 +153,70 @@ return f; } - const id = mem().getUint32(addr, true); + const id = v_ref & 0xffffffffn; return this._values[id]; } - const storeValue = (addr, v) => { - const nanHead = 0x7FF80000; + + const loadValue = (addr) => { + let v_ref = mem().getBigUint64(addr, true); + return unboxValue(v_ref); + } + + const boxValue = (v) => { + const nanHead = 0x7FF80000n; if (typeof v === "number") { if (isNaN(v)) { - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 0, true); - return; + return nanHead << 32n; } if (v === 0) { - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 1, true); - return; + return (nanHead << 32n) | 1n; } - mem().setFloat64(addr, v, true); - return; + reinterpretBuf.setFloat64(0, v, true); + return reinterpretBuf.getBigInt64(0, true); } switch (v) { case undefined: - mem().setFloat64(addr, 0, true); - return; + return 0n; case null: - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 2, true); - return; + return (nanHead << 32n) | 2n; case true: - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 3, true); - return; + return (nanHead << 32n) | 3n; case false: - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 4, true); - return; + return (nanHead << 32n) | 4n; } let id = this._ids.get(v); if (id === undefined) { id = this._idPool.pop(); if (id === undefined) { - id = this._values.length; + id = BigInt(this._values.length); } this._values[id] = v; this._goRefCounts[id] = 0; this._ids.set(v, id); } this._goRefCounts[id]++; - let typeFlag = 1; + let typeFlag = 1n; switch (typeof v) { case "string": - typeFlag = 2; + typeFlag = 2n; break; case "symbol": - typeFlag = 3; + typeFlag = 3n; break; case "function": - typeFlag = 4; + typeFlag = 4n; break; } - mem().setUint32(addr + 4, nanHead | typeFlag, true); - mem().setUint32(addr, id, true); + return id | ((nanHead | typeFlag) << 32n); + } + + const storeValue = (addr, v) => { + let v_ref = boxValue(v); + mem().setBigUint64(addr, v_ref, true); } const loadSlice = (array, len, cap) => { @@ -307,54 +297,54 @@ }, // func finalizeRef(v ref) - "syscall/js.finalizeRef": (sp) => { + "syscall/js.finalizeRef": (v_ref) => { // Note: TinyGo does not support finalizers so this should never be // called. console.error('syscall/js.finalizeRef not implemented'); }, // func stringVal(value string) ref - "syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => { + "syscall/js.stringVal": (value_ptr, value_len) => { const s = loadString(value_ptr, value_len); - storeValue(ret_ptr, s); + return boxValue(s); }, // func valueGet(v ref, p string) ref - "syscall/js.valueGet": (retval, v_addr, p_ptr, p_len) => { + "syscall/js.valueGet": (v_ref, p_ptr, p_len) => { let prop = loadString(p_ptr, p_len); - let value = loadValue(v_addr); - let result = Reflect.get(value, prop); - storeValue(retval, result); + let v = unboxValue(v_ref); + let result = Reflect.get(v, prop); + return boxValue(result); }, // func valueSet(v ref, p string, x ref) - "syscall/js.valueSet": (v_addr, p_ptr, p_len, x_addr) => { - const v = loadValue(v_addr); + "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { + const v = unboxValue(v_ref); const p = loadString(p_ptr, p_len); - const x = loadValue(x_addr); + const x = unboxValue(x_ref); Reflect.set(v, p, x); }, // func valueDelete(v ref, p string) - "syscall/js.valueDelete": (v_addr, p_ptr, p_len) => { - const v = loadValue(v_addr); + "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { + const v = unboxValue(v_ref); const p = loadString(p_ptr, p_len); Reflect.deleteProperty(v, p); }, // func valueIndex(v ref, i int) ref - "syscall/js.valueIndex": (ret_addr, v_addr, i) => { - storeValue(ret_addr, Reflect.get(loadValue(v_addr), i)); + "syscall/js.valueIndex": (v_ref, i) => { + return boxValue(Reflect.get(unboxValue(v_ref), i)); }, // valueSetIndex(v ref, i int, x ref) - "syscall/js.valueSetIndex": (v_addr, i, x_addr) => { - Reflect.set(loadValue(v_addr), i, loadValue(x_addr)); + "syscall/js.valueSetIndex": (v_ref, i, x_ref) => { + Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); }, // func valueCall(v ref, m string, args []ref) (ref, bool) - "syscall/js.valueCall": (ret_addr, v_addr, m_ptr, m_len, args_ptr, args_len, args_cap) => { - const v = loadValue(v_addr); + "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); const name = loadString(m_ptr, m_len); const args = loadSliceOfValues(args_ptr, args_len, args_cap); try { @@ -368,9 +358,9 @@ }, // func valueInvoke(v ref, args []ref) (ref, bool) - "syscall/js.valueInvoke": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { + "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { try { - const v = loadValue(v_addr); + const v = unboxValue(v_ref); const args = loadSliceOfValues(args_ptr, args_len, args_cap); storeValue(ret_addr, Reflect.apply(v, undefined, args)); mem().setUint8(ret_addr + 8, 1); @@ -381,8 +371,8 @@ }, // func valueNew(v ref, args []ref) (ref, bool) - "syscall/js.valueNew": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { - const v = loadValue(v_addr); + "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); const args = loadSliceOfValues(args_ptr, args_len, args_cap); try { storeValue(ret_addr, Reflect.construct(v, args)); @@ -394,62 +384,62 @@ }, // func valueLength(v ref) int - "syscall/js.valueLength": (v_addr) => { - return loadValue(v_addr).length; + "syscall/js.valueLength": (v_ref) => { + return unboxValue(v_ref).length; }, // valuePrepareString(v ref) (ref, int) - "syscall/js.valuePrepareString": (ret_addr, v_addr) => { - const s = String(loadValue(v_addr)); + "syscall/js.valuePrepareString": (ret_addr, v_ref) => { + const s = String(unboxValue(v_ref)); const str = encoder.encode(s); storeValue(ret_addr, str); - setInt64(ret_addr + 8, str.length); + mem().setInt32(ret_addr + 8, str.length, true); }, // valueLoadString(v ref, b []byte) - "syscall/js.valueLoadString": (v_addr, slice_ptr, slice_len, slice_cap) => { - const str = loadValue(v_addr); + "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => { + const str = unboxValue(v_ref); loadSlice(slice_ptr, slice_len, slice_cap).set(str); }, // func valueInstanceOf(v ref, t ref) bool - "syscall/js.valueInstanceOf": (v_addr, t_addr) => { - return loadValue(v_addr) instanceof loadValue(t_addr); + "syscall/js.valueInstanceOf": (v_ref, t_ref) => { + return unboxValue(v_ref) instanceof unboxValue(t_ref); }, // func copyBytesToGo(dst []byte, src ref) (int, bool) - "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, source_addr) => { + "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => { let num_bytes_copied_addr = ret_addr; let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable const dst = loadSlice(dest_addr, dest_len); - const src = loadValue(source_addr); + const src = unboxValue(src_ref); if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { mem().setUint8(returned_status_addr, 0); // Return "not ok" status return; } const toCopy = src.subarray(0, dst.length); dst.set(toCopy); - setInt64(num_bytes_copied_addr, toCopy.length); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); mem().setUint8(returned_status_addr, 1); // Return "ok" status }, // copyBytesToJS(dst ref, src []byte) (int, bool) // Originally copied from upstream Go project, then modified: // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 - "syscall/js.copyBytesToJS": (ret_addr, dest_addr, source_addr, source_len, source_cap) => { + "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => { let num_bytes_copied_addr = ret_addr; let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable - const dst = loadValue(dest_addr); - const src = loadSlice(source_addr, source_len); + const dst = unboxValue(dst_ref); + const src = loadSlice(src_addr, src_len); if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { mem().setUint8(returned_status_addr, 0); // Return "not ok" status return; } const toCopy = src.subarray(0, dst.length); dst.set(toCopy); - setInt64(num_bytes_copied_addr, toCopy.length); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); mem().setUint8(returned_status_addr, 1); // Return "ok" status }, } diff --git a/transform/testdata/wasm-abi.ll b/transform/testdata/wasm-abi.ll deleted file mode 100644 index ade4b5af..00000000 --- a/transform/testdata/wasm-abi.ll +++ /dev/null @@ -1,28 +0,0 @@ -target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" -target triple = "wasm32-unknown-unknown-wasm" - -declare i64 @externalCall(ptr, i32, i64) - -define internal i64 @testCall(ptr %ptr, i32 %len, i64 %foo) { - %val = call i64 @externalCall(ptr %ptr, i32 %len, i64 %foo) - ret i64 %val -} - -define internal i64 @testCallNonEntry(ptr %ptr, i32 %len) { -entry: - br label %bb1 - -bb1: - %val = call i64 @externalCall(ptr %ptr, i32 %len, i64 3) - ret i64 %val -} - -define void @exportedFunction(i64 %foo) { - %unused = shl i64 %foo, 1 - ret void -} - -define internal void @callExportedFunction(i64 %foo) { - call void @exportedFunction(i64 %foo) - ret void -} diff --git a/transform/testdata/wasm-abi.out.ll b/transform/testdata/wasm-abi.out.ll deleted file mode 100644 index a1fc7d6a..00000000 --- a/transform/testdata/wasm-abi.out.ll +++ /dev/null @@ -1,45 +0,0 @@ -target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" -target triple = "wasm32-unknown-unknown-wasm" - -declare i64 @"externalCall$i64wrap"(ptr, i32, i64) - -define internal i64 @testCall(ptr %ptr, i32 %len, i64 %foo) { - %i64asptr = alloca i64, align 8 - %i64asptr1 = alloca i64, align 8 - store i64 %foo, ptr %i64asptr1, align 8 - call void @externalCall(ptr %i64asptr, ptr %ptr, i32 %len, ptr %i64asptr1) - %retval = load i64, ptr %i64asptr, align 8 - ret i64 %retval -} - -define internal i64 @testCallNonEntry(ptr %ptr, i32 %len) { -entry: - %i64asptr = alloca i64, align 8 - %i64asptr1 = alloca i64, align 8 - br label %bb1 - -bb1: ; preds = %entry - store i64 3, ptr %i64asptr1, align 8 - call void @externalCall(ptr %i64asptr, ptr %ptr, i32 %len, ptr %i64asptr1) - %retval = load i64, ptr %i64asptr, align 8 - ret i64 %retval -} - -define internal void @"exportedFunction$i64wrap"(i64 %foo) unnamed_addr { - %unused = shl i64 %foo, 1 - ret void -} - -define internal void @callExportedFunction(i64 %foo) { - call void @"exportedFunction$i64wrap"(i64 %foo) - ret void -} - -declare void @externalCall(ptr, ptr, i32, ptr) - -define void @exportedFunction(ptr %0) { -entry: - %i64 = load i64, ptr %0, align 8 - call void @"exportedFunction$i64wrap"(i64 %i64) - ret void -} diff --git a/transform/wasm-abi.go b/transform/wasm-abi.go deleted file mode 100644 index 081558c9..00000000 --- a/transform/wasm-abi.go +++ /dev/null @@ -1,167 +0,0 @@ -package transform - -import ( - "errors" - "strings" - - "github.com/tinygo-org/tinygo/compileopts" - "tinygo.org/x/go-llvm" -) - -// ExternalInt64AsPtr converts i64 parameters in externally-visible functions to -// values passed by reference (*i64), to work around the lack of 64-bit integers -// in JavaScript (commonly used together with WebAssembly). Once that's -// resolved, this pass may be avoided. For more details: -// https://github.com/WebAssembly/design/issues/1172 -// -// This pass is enabled via the wasm-abi JSON target key. -func ExternalInt64AsPtr(mod llvm.Module, config *compileopts.Config) error { - ctx := mod.Context() - builder := ctx.NewBuilder() - defer builder.Dispose() - int64Type := ctx.Int64Type() - int64PtrType := llvm.PointerType(int64Type, 0) - - // This builder is only used for creating new allocas in the entry block of - // a function, avoiding many SetInsertPoint* calls. - entryBlockBuilder := ctx.NewBuilder() - defer entryBlockBuilder.Dispose() - - for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { - if fn.Linkage() != llvm.ExternalLinkage { - // Only change externally visible functions (exports and imports). - continue - } - if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") { - // Do not try to modify the signature of internal LLVM functions and - // assume that runtime functions are only temporarily exported for - // transforms. - continue - } - if !fn.GetStringAttributeAtIndex(-1, "tinygo-methods").IsNil() { - // These are internal functions (interface method call, interface - // type assert) that will be lowered by the interface lowering pass. - // Don't transform them. - continue - } - - hasInt64 := false - paramTypes := []llvm.Type{} - - // Check return type for 64-bit integer. - fnType := fn.GlobalValueType() - returnType := fnType.ReturnType() - if returnType == int64Type { - hasInt64 = true - paramTypes = append(paramTypes, int64PtrType) - returnType = ctx.VoidType() - } - - // Check param types for 64-bit integers. - for param := fn.FirstParam(); !param.IsNil(); param = llvm.NextParam(param) { - if param.Type() == int64Type { - hasInt64 = true - paramTypes = append(paramTypes, int64PtrType) - } else { - paramTypes = append(paramTypes, param.Type()) - } - } - - if !hasInt64 { - // No i64 in the paramter list. - continue - } - - // Add $i64wrapper to the real function name as it is only used - // internally. - // Add a new function with the correct signature that is exported. - name := fn.Name() - fn.SetName(name + "$i64wrap") - externalFnType := llvm.FunctionType(returnType, paramTypes, fnType.IsFunctionVarArg()) - externalFn := llvm.AddFunction(mod, name, externalFnType) - AddStandardAttributes(fn, config) - - if fn.IsDeclaration() { - // Just a declaration: the definition doesn't exist on the Go side - // so it cannot be called from external code. - // Update all users to call the external function. - // The old $i64wrapper function could be removed, but it may as well - // be left in place. - for _, call := range getUses(fn) { - entryBlockBuilder.SetInsertPointBefore(call.InstructionParent().Parent().EntryBasicBlock().FirstInstruction()) - builder.SetInsertPointBefore(call) - callParams := []llvm.Value{} - var retvalAlloca llvm.Value - if fnType.ReturnType() == int64Type { - retvalAlloca = entryBlockBuilder.CreateAlloca(int64Type, "i64asptr") - callParams = append(callParams, retvalAlloca) - } - for i := 0; i < call.OperandsCount()-1; i++ { - operand := call.Operand(i) - if operand.Type() == int64Type { - // Pass a stack-allocated pointer instead of the value - // itself. - alloca := entryBlockBuilder.CreateAlloca(int64Type, "i64asptr") - builder.CreateStore(operand, alloca) - callParams = append(callParams, alloca) - } else { - // Unchanged parameter. - callParams = append(callParams, operand) - } - } - var callName string - if returnType.TypeKind() != llvm.VoidTypeKind { - // Only use the name of the old call instruction if the new - // call is not a void call. - // A call instruction with an i64 return type may have had a - // name, but it cannot have a name after this transform - // because the return type will now be void. - callName = call.Name() - } - if fnType.ReturnType() == int64Type { - // Pass a stack-allocated pointer as the first parameter - // where the return value should be stored, instead of using - // the regular return value. - builder.CreateCall(externalFnType, externalFn, callParams, callName) - returnValue := builder.CreateLoad(int64Type, retvalAlloca, "retval") - call.ReplaceAllUsesWith(returnValue) - call.EraseFromParentAsInstruction() - } else { - newCall := builder.CreateCall(externalFnType, externalFn, callParams, callName) - call.ReplaceAllUsesWith(newCall) - call.EraseFromParentAsInstruction() - } - } - } else { - // The function has a definition in Go. This means that it may still - // be called both Go and from external code. - // Keep existing calls with the existing convention in place (for - // better performance), but export a new wrapper function with the - // correct calling convention. - fn.SetLinkage(llvm.InternalLinkage) - fn.SetUnnamedAddr(true) - entryBlock := ctx.AddBasicBlock(externalFn, "entry") - builder.SetInsertPointAtEnd(entryBlock) - var callParams []llvm.Value - if fnType.ReturnType() == int64Type { - return errors.New("not yet implemented: exported function returns i64 with the JS wasm-abi; " + - "see https://tinygo.org/compiler-internals/calling-convention/") - } - for i, origParam := range fn.Params() { - paramValue := externalFn.Param(i) - if origParam.Type() == int64Type { - paramValue = builder.CreateLoad(int64Type, paramValue, "i64") - } - callParams = append(callParams, paramValue) - } - retval := builder.CreateCall(fn.GlobalValueType(), fn, callParams, "") - if retval.Type().TypeKind() == llvm.VoidTypeKind { - builder.CreateRetVoid() - } else { - builder.CreateRet(retval) - } - } - } - - return nil -} diff --git a/transform/wasm-abi_test.go b/transform/wasm-abi_test.go deleted file mode 100644 index 374bba13..00000000 --- a/transform/wasm-abi_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package transform_test - -import ( - "testing" - - "github.com/tinygo-org/tinygo/transform" - "tinygo.org/x/go-llvm" -) - -func TestWasmABI(t *testing.T) { - t.Parallel() - testTransform(t, "testdata/wasm-abi", func(mod llvm.Module) { - // Run ABI change pass. - err := transform.ExternalInt64AsPtr(mod, defaultTestConfig) - if err != nil { - t.Errorf("failed to change wasm ABI: %v", err) - } - }) -}