aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.circleci/config.yml94
-rw-r--r--.github/workflows/windows.yml15
-rw-r--r--.gitignore1
-rw-r--r--.gitmodules3
-rw-r--r--Dockerfile2
-rw-r--r--Makefile15
-rw-r--r--build/.gitignore2
-rw-r--r--builder/build.go33
-rw-r--r--compileopts/config.go2
-rw-r--r--compileopts/options.go2
-rw-r--r--compileopts/options_test.go2
-rw-r--r--compileopts/target.go5
-rw-r--r--compiler/compiler_test.go48
-rw-r--r--compiler/goroutine.go34
-rw-r--r--compiler/testdata/channel.ll3
-rw-r--r--compiler/testdata/func-coroutines.ll (renamed from compiler/testdata/func.ll)0
-rw-r--r--compiler/testdata/gc.ll7
-rw-r--r--compiler/testdata/goroutine-cortex-m-qemu-tasks.ll (renamed from compiler/testdata/goroutine-cortex-m-qemu.ll)0
-rw-r--r--compiler/testdata/goroutine-wasm-asyncify.ll210
-rw-r--r--compiler/testdata/goroutine-wasm-coroutines.ll (renamed from compiler/testdata/goroutine-wasm.ll)0
-rw-r--r--goenv/goenv.go90
m---------lib/binaryen0
-rw-r--r--src/internal/task/task_asyncify.go127
-rw-r--r--src/internal/task/task_asyncify_wasm.S99
-rw-r--r--src/runtime/gc_conservative.go1
-rw-r--r--src/runtime/runtime_wasm_js.go22
-rw-r--r--src/runtime/scheduler.go18
-rw-r--r--targets/wasi.json2
-rw-r--r--targets/wasm.json2
-rw-r--r--tests/wasm/chan_test.go3
-rw-r--r--transform/optimizer.go4
31 files changed, 784 insertions, 62 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index de75739b7..f3534edde 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -25,7 +25,9 @@ commands:
qemu-system-arm \
qemu-user \
gcc-avr \
- avr-libc
+ avr-libc \
+ cmake \
+ ninja-build
install-node:
steps:
- run:
@@ -49,6 +51,13 @@ commands:
command: |
curl https://wasmtime.dev/install.sh -sSf | bash
sudo ln -s ~/.wasmtime/bin/wasmtime /usr/local/bin/wasmtime
+ install-cmake:
+ steps:
+ - run:
+ name: "Install CMake"
+ command: |
+ wget https://github.com/Kitware/CMake/releases/download/v3.21.4/cmake-3.21.4-linux-x86_64.tar.gz
+ sudo tar --strip-components=1 -C /usr/local -xf cmake-3.21.4-linux-x86_64.tar.gz
install-xtensa-toolchain:
parameters:
variant:
@@ -76,6 +85,39 @@ commands:
- llvm-project/clang/include
- llvm-project/lld/include
- llvm-project/llvm/include
+ hack-ninja-jobs:
+ steps:
+ - run:
+ name: "Hack Ninja to use less jobs"
+ command: |
+ echo -e '#!/bin/sh\n/usr/bin/ninja -j3 "$@"' > /go/bin/ninja
+ chmod +x /go/bin/ninja
+ build-binaryen-linux:
+ steps:
+ - restore_cache:
+ keys:
+ - binaryen-linux-v1
+ - run:
+ name: "Build Binaryen"
+ command: |
+ make binaryen
+ - save_cache:
+ key: binaryen-linux-v1
+ paths:
+ - build/wasm-opt
+ build-binaryen-linux-stretch:
+ steps:
+ - restore_cache:
+ keys:
+ - binaryen-linux-stretch-v1
+ - run:
+ name: "Build Binaryen"
+ command: |
+ CC=$PWD/llvm-build/bin/clang make binaryen
+ - save_cache:
+ key: binaryen-linux-stretch-v1
+ paths:
+ - build/wasm-opt
build-wasi-libc:
steps:
- restore_cache:
@@ -100,6 +142,8 @@ commands:
- install-node
- install-chrome
- install-wasmtime
+ - hack-ninja-jobs
+ - build-binaryen-linux
- restore_cache:
keys:
- go-cache-v2-{{ checksum "go.mod" }}-{{ .Environment.CIRCLE_PREVIOUS_BUILD_NUM }}
@@ -141,9 +185,13 @@ commands:
qemu-system-arm \
qemu-user \
gcc-avr \
- avr-libc
+ avr-libc \
+ ninja-build \
+ python3
- install-node
- install-wasmtime
+ - install-cmake
+ - hack-ninja-jobs
- install-xtensa-toolchain:
variant: "linux-amd64"
- restore_cache:
@@ -159,14 +207,9 @@ commands:
command: |
if [ ! -f llvm-build/lib/liblldELF.a ]
then
- # fetch LLVM source
+ # fetch LLVM source (may only have headers right now)
rm -rf llvm-project
make llvm-source
- # install dependencies
- sudo apt-get install cmake ninja-build
- # hack ninja to use less jobs
- echo -e '#!/bin/sh\n/usr/bin/ninja -j3 "$@"' > /go/bin/ninja
- chmod +x /go/bin/ninja
# build!
make ASSERT=1 llvm-build
find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \;
@@ -175,6 +218,7 @@ commands:
key: llvm-build-11-linux-v4-assert
paths:
llvm-build
+ - build-binaryen-linux-stretch
- run: make ASSERT=1
- build-wasi-libc
- run:
@@ -206,9 +250,13 @@ commands:
qemu-system-arm \
qemu-user \
gcc-avr \
- avr-libc
+ avr-libc \
+ ninja-build \
+ python3
- install-node
- install-wasmtime
+ - install-cmake
+ - hack-ninja-jobs
- install-xtensa-toolchain:
variant: "linux-amd64"
- restore_cache:
@@ -224,14 +272,9 @@ commands:
command: |
if [ ! -f llvm-build/lib/liblldELF.a ]
then
- # fetch LLVM source
+ # fetch LLVM source (may only have headers right now)
rm -rf llvm-project
make llvm-source
- # install dependencies
- sudo apt-get install cmake ninja-build
- # hack ninja to use less jobs
- echo -e '#!/bin/sh\n/usr/bin/ninja -j3 "$@"' > /go/bin/ninja
- chmod +x /go/bin/ninja
# build!
make llvm-build
find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \;
@@ -240,6 +283,7 @@ commands:
key: llvm-build-11-linux-v4-noassert
paths:
llvm-build
+ - build-binaryen-linux-stretch
- build-wasi-libc
- run:
name: "Test TinyGo"
@@ -283,7 +327,7 @@ commands:
curl https://dl.google.com/go/go1.17.darwin-amd64.tar.gz -o go1.17.darwin-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.17.darwin-amd64.tar.gz
ln -s /usr/local/go/bin/go /usr/local/bin/go
- HOMEBREW_NO_AUTO_UPDATE=1 brew install qemu
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install qemu cmake ninja
- install-xtensa-toolchain:
variant: "macos"
- restore_cache:
@@ -311,11 +355,9 @@ commands:
command: |
if [ ! -f llvm-build/lib/liblldELF.a ]
then
- # fetch LLVM source
+ # fetch LLVM source (may only have headers right now)
rm -rf llvm-project
make llvm-source
- # install dependencies
- HOMEBREW_NO_AUTO_UPDATE=1 brew install cmake ninja
# build!
make llvm-build
find llvm-build -name CMakeFiles -prune -exec rm -r '{}' \;
@@ -326,6 +368,20 @@ commands:
llvm-build
- restore_cache:
keys:
+ - binaryen-macos-v1
+ - run:
+ name: "Build Binaryen"
+ command: |
+ if [ ! -f build/wasm-opt ]
+ then
+ make binaryen
+ fi
+ - save_cache:
+ key: binaryen-macos-v1
+ paths:
+ - build/wasm-opt
+ - restore_cache:
+ keys:
- wasi-libc-sysroot-macos-v4
- run:
name: "Build wasi-libc"
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index bdb27dca0..4c1f8dd9d 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -20,6 +20,10 @@ jobs:
run: |
choco install qemu --version=2020.06.12
echo "C:\Program Files\QEMU" >> $GITHUB_PATH
+ - name: Install Ninja
+ shell: bash
+ run: |
+ choco install ninja
- name: Checkout
uses: actions/checkout@v2
with:
@@ -50,8 +54,6 @@ jobs:
# fetch LLVM source
rm -rf llvm-project
make llvm-source
- # install dependencies
- choco install ninja
# build!
make llvm-build
# Remove unnecessary object files (to reduce cache size).
@@ -65,6 +67,15 @@ jobs:
- name: Build wasi-libc
if: steps.cache-wasi-libc.outputs.cache-hit != 'true'
run: make wasi-libc
+ - name: Cache Binaryen
+ uses: actions/cache@v2
+ id: cache-binaryen
+ with:
+ key: binaryen-v1
+ path: build/binaryen
+ - name: Build Binaryen
+ if: steps.cache-binaryen.outputs.cache-hit != 'true'
+ run: make binaryen
- name: Test TinyGo
shell: bash
run: make test
diff --git a/.gitignore b/.gitignore
index 483c789c7..a5603ec7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
-build
docs/_build
src/device/avr/*.go
src/device/avr/*.ld
diff --git a/.gitmodules b/.gitmodules
index 4df14b5ef..c126bd4b9 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -26,3 +26,6 @@
[submodule "lib/musl"]
path = lib/musl
url = git://git.musl-libc.org/musl
+[submodule "lib/binaryen"]
+ path = lib/binaryen
+ url = https://github.com/WebAssembly/binaryen.git
diff --git a/Dockerfile b/Dockerfile
index f1b04d2fc..f6c721229 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -30,7 +30,7 @@ COPY --from=tinygo-base /tinygo/targets /tinygo/targets
RUN cd /tinygo/ && \
apt-get update && \
apt-get install -y make clang-11 libllvm11 lld-11 && \
- make wasi-libc
+ make wasi-libc binaryen
# tinygo-avr stage installs the needed dependencies to compile TinyGo programs for AVR microcontrollers.
FROM tinygo-base AS tinygo-avr
diff --git a/Makefile b/Makefile
index 68b66c10a..aaabc2e86 100644
--- a/Makefile
+++ b/Makefile
@@ -54,6 +54,8 @@ ifeq ($(OS),Windows_NT)
CGO_LDFLAGS += -static -static-libgcc -static-libstdc++
CGO_LDFLAGS_EXTRA += -lversion
+ BINARYEN_OPTION += -DCMAKE_EXE_LINKER_FLAGS='-static-libgcc -static-libstdc++'
+
LIBCLANG_NAME = libclang
else ifeq ($(shell uname -s),Darwin)
@@ -163,12 +165,18 @@ llvm-source: $(LLVM_PROJECTDIR)/llvm
# Configure LLVM.
TINYGO_SOURCE_DIR=$(shell pwd)
$(LLVM_BUILDDIR)/build.ninja: llvm-source
- mkdir -p $(LLVM_BUILDDIR); cd $(LLVM_BUILDDIR); cmake -G Ninja $(TINYGO_SOURCE_DIR)/$(LLVM_PROJECTDIR)/llvm "-DLLVM_TARGETS_TO_BUILD=X86;ARM;AArch64;RISCV;WebAssembly" "-DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=AVR;Xtensa" -DCMAKE_BUILD_TYPE=Release -DLIBCLANG_BUILD_STATIC=ON -DLLVM_ENABLE_TERMINFO=OFF -DLLVM_ENABLE_ZLIB=OFF -DLLVM_ENABLE_LIBEDIT=OFF -DLLVM_ENABLE_Z3_SOLVER=OFF -DLLVM_ENABLE_OCAMLDOC=OFF -DLLVM_ENABLE_LIBXML2=OFF -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TOOL_CLANG_TOOLS_EXTRA_BUILD=OFF -DCLANG_ENABLE_STATIC_ANALYZER=OFF -DCLANG_ENABLE_ARCMT=OFF $(LLVM_OPTION)
+ mkdir -p $(LLVM_BUILDDIR) && cd $(LLVM_BUILDDIR) && cmake -G Ninja $(TINYGO_SOURCE_DIR)/$(LLVM_PROJECTDIR)/llvm "-DLLVM_TARGETS_TO_BUILD=X86;ARM;AArch64;RISCV;WebAssembly" "-DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=AVR;Xtensa" -DCMAKE_BUILD_TYPE=Release -DLIBCLANG_BUILD_STATIC=ON -DLLVM_ENABLE_TERMINFO=OFF -DLLVM_ENABLE_ZLIB=OFF -DLLVM_ENABLE_LIBEDIT=OFF -DLLVM_ENABLE_Z3_SOLVER=OFF -DLLVM_ENABLE_OCAMLDOC=OFF -DLLVM_ENABLE_LIBXML2=OFF -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TOOL_CLANG_TOOLS_EXTRA_BUILD=OFF -DCLANG_ENABLE_STATIC_ANALYZER=OFF -DCLANG_ENABLE_ARCMT=OFF $(LLVM_OPTION)
# Build LLVM.
$(LLVM_BUILDDIR): $(LLVM_BUILDDIR)/build.ninja
- cd $(LLVM_BUILDDIR); ninja $(NINJA_BUILD_TARGETS)
+ cd $(LLVM_BUILDDIR) && ninja $(NINJA_BUILD_TARGETS)
+# Build Binaryen
+.PHONY: binaryen
+binaryen: build/wasm-opt
+build/wasm-opt:
+ cd lib/binaryen && cmake -G Ninja . -DBUILD_STATIC_LIB=ON $(BINARYEN_OPTION) && ninja
+ cp lib/binaryen/bin/wasm-opt build/wasm-opt
# Build wasi-libc sysroot
.PHONY: wasi-libc
@@ -476,7 +484,7 @@ endif
wasmtest:
$(GO) test ./tests/wasm
-build/release: tinygo gen-device wasi-libc
+build/release: tinygo gen-device wasi-libc binaryen
@mkdir -p build/release/tinygo/bin
@mkdir -p build/release/tinygo/lib/clang/include
@mkdir -p build/release/tinygo/lib/CMSIS/CMSIS
@@ -493,6 +501,7 @@ build/release: tinygo gen-device wasi-libc
@mkdir -p build/release/tinygo/pkg/armv7em-unknown-unknown-eabi
@echo copying source files
@cp -p build/tinygo$(EXE) build/release/tinygo/bin
+ @cp -p build/wasm-opt$(EXE) build/release/tinygo/bin
@cp -p $(abspath $(CLANG_SRC))/lib/Headers/*.h build/release/tinygo/lib/clang/include
@cp -rp lib/CMSIS/CMSIS/Include build/release/tinygo/lib/CMSIS/CMSIS
@cp -rp lib/CMSIS/README.md build/release/tinygo/lib/CMSIS
diff --git a/build/.gitignore b/build/.gitignore
new file mode 100644
index 000000000..d6b7ef32c
--- /dev/null
+++ b/build/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/builder/build.go b/builder/build.go
index 5c3150c11..252714964 100644
--- a/builder/build.go
+++ b/builder/build.go
@@ -16,9 +16,11 @@ import (
"io/ioutil"
"math/bits"
"os"
+ "os/exec"
"path/filepath"
"runtime"
"sort"
+ "strconv"
"strings"
"github.com/tinygo-org/tinygo/cgo"
@@ -658,6 +660,37 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
}
}
+ // Run wasm-opt if necessary.
+ if config.Scheduler() == "asyncify" {
+ var optLevel, shrinkLevel int
+ switch config.Options.Opt {
+ case "none", "0":
+ case "1":
+ optLevel = 1
+ case "2":
+ optLevel = 2
+ case "s":
+ optLevel = 2
+ shrinkLevel = 1
+ case "z":
+ optLevel = 2
+ shrinkLevel = 2
+ default:
+ return fmt.Errorf("unknown opt level: %q", config.Options.Opt)
+ }
+ cmd := exec.Command(goenv.Get("WASMOPT"), "--asyncify", "-g",
+ "--optimize-level", strconv.Itoa(optLevel),
+ "--shrink-level", strconv.Itoa(shrinkLevel),
+ executable, "--output", executable)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ err := cmd.Run()
+ if err != nil {
+ return fmt.Errorf("wasm-opt failed: %w", err)
+ }
+ }
+
// Print code size if requested.
if config.Options.PrintSizes == "short" || config.Options.PrintSizes == "full" {
packagePathMap := make(map[string]string, len(lprogram.Packages))
diff --git a/compileopts/config.go b/compileopts/config.go
index 0ee4d1d07..2d68b60b2 100644
--- a/compileopts/config.go
+++ b/compileopts/config.go
@@ -157,7 +157,7 @@ func (c *Config) OptLevels() (optLevel, sizeLevel int, inlinerThreshold uint) {
// target.
func (c *Config) FuncImplementation() string {
switch c.Scheduler() {
- case "tasks":
+ case "tasks", "asyncify":
// A func value is implemented as a pair of pointers:
// {context, function pointer}
// where the context may be a pointer to a heap-allocated struct
diff --git a/compileopts/options.go b/compileopts/options.go
index fd0416459..065eb97a3 100644
--- a/compileopts/options.go
+++ b/compileopts/options.go
@@ -8,7 +8,7 @@ import (
var (
validGCOptions = []string{"none", "leaking", "extalloc", "conservative"}
- validSchedulerOptions = []string{"none", "tasks", "coroutines"}
+ validSchedulerOptions = []string{"none", "tasks", "coroutines", "asyncify"}
validSerialOptions = []string{"none", "uart", "usb"}
validPrintSizeOptions = []string{"none", "short", "full"}
validPanicStrategyOptions = []string{"print", "trap"}
diff --git a/compileopts/options_test.go b/compileopts/options_test.go
index 1ff532cc4..646558215 100644
--- a/compileopts/options_test.go
+++ b/compileopts/options_test.go
@@ -10,7 +10,7 @@ import (
func TestVerifyOptions(t *testing.T) {
expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, extalloc, conservative`)
- expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, coroutines`)
+ expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, coroutines, asyncify`)
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full`)
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)
diff --git a/compileopts/target.go b/compileopts/target.go
index f16df32cd..0c8ce1d93 100644
--- a/compileopts/target.go
+++ b/compileopts/target.go
@@ -208,6 +208,11 @@ func LoadTarget(options *Options) (*TargetSpec, error) {
if err != nil {
return nil, err
}
+
+ if spec.Scheduler == "asyncify" {
+ spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_asyncify_wasm.S")
+ }
+
return spec, nil
}
diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go
index f1a0e00c8..cbf89fe20 100644
--- a/compiler/compiler_test.go
+++ b/compiler/compiler_test.go
@@ -18,8 +18,9 @@ import (
var flagUpdate = flag.Bool("update", false, "update tests based on test output")
type testCase struct {
- file string
- target string
+ file string
+ target string
+ scheduler string
}
// Basic tests for the compiler. Build some Go files and compare the output with
@@ -41,20 +42,21 @@ func TestCompiler(t *testing.T) {
}
tests := []testCase{
- {"basic.go", ""},
- {"pointer.go", ""},
- {"slice.go", ""},
- {"string.go", ""},
- {"float.go", ""},
- {"interface.go", ""},
- {"func.go", ""},
- {"pragma.go", ""},
- {"goroutine.go", "wasm"},
- {"goroutine.go", "cortex-m-qemu"},
- {"channel.go", ""},
- {"intrinsics.go", "cortex-m-qemu"},
- {"intrinsics.go", "wasm"},
- {"gc.go", ""},
+ {"basic.go", "", ""},
+ {"pointer.go", "", ""},
+ {"slice.go", "", ""},
+ {"string.go", "", ""},
+ {"float.go", "", ""},
+ {"interface.go", "", ""},
+ {"func.go", "", "coroutines"},
+ {"pragma.go", "", ""},
+ {"goroutine.go", "wasm", "asyncify"},
+ {"goroutine.go", "wasm", "coroutines"},
+ {"goroutine.go", "cortex-m-qemu", "tasks"},
+ {"channel.go", "", ""},
+ {"intrinsics.go", "cortex-m-qemu", ""},
+ {"intrinsics.go", "wasm", ""},
+ {"gc.go", "", ""},
}
_, minor, err := goenv.GetGorootVersion(goenv.Get("GOROOT"))
@@ -62,7 +64,7 @@ func TestCompiler(t *testing.T) {
t.Fatal("could not read Go version:", err)
}
if minor >= 17 {
- tests = append(tests, testCase{"go1.17.go", ""})
+ tests = append(tests, testCase{"go1.17.go", "", ""})
}
for _, tc := range tests {
@@ -70,7 +72,10 @@ func TestCompiler(t *testing.T) {
targetString := "wasm"
if tc.target != "" {
targetString = tc.target
- name = tc.file + "-" + tc.target
+ name += "-" + tc.target
+ }
+ if tc.scheduler != "" {
+ name += "-" + tc.scheduler
}
t.Run(name, func(t *testing.T) {
@@ -81,6 +86,9 @@ func TestCompiler(t *testing.T) {
if err != nil {
t.Fatal("failed to load target:", err)
}
+ if tc.scheduler != "" {
+ options.Scheduler = tc.scheduler
+ }
config := &compileopts.Config{
Options: options,
Target: target,
@@ -94,6 +102,7 @@ func TestCompiler(t *testing.T) {
Scheduler: config.Scheduler(),
FuncImplementation: config.FuncImplementation(),
AutomaticStackSize: config.AutomaticStackSize(),
+ DefaultStackSize: config.Target.DefaultStackSize,
}
machine, err := NewTargetMachine(compilerConfig)
if err != nil {
@@ -142,6 +151,9 @@ func TestCompiler(t *testing.T) {
if tc.target != "" {
outFilePrefix += "-" + tc.target
}
+ if tc.scheduler != "" {
+ outFilePrefix += "-" + tc.scheduler
+ }
outPath := "./testdata/" + outFilePrefix + ".ll"
// Update test if needed. Do not check the result.
diff --git a/compiler/goroutine.go b/compiler/goroutine.go
index b58481928..a77a9b861 100644
--- a/compiler/goroutine.go
+++ b/compiler/goroutine.go
@@ -100,7 +100,7 @@ func (b *builder) createGo(instr *ssa.Go) {
switch b.Scheduler {
case "none", "coroutines":
// There are no additional parameters needed for the goroutine start operation.
- case "tasks":
+ case "tasks", "asyncify":
// Add the function pointer as a parameter to start the goroutine.
params = append(params, funcPtr)
default:
@@ -112,7 +112,7 @@ func (b *builder) createGo(instr *ssa.Go) {
paramBundle := b.emitPointerPack(params)
var callee, stackSize llvm.Value
switch b.Scheduler {
- case "none", "tasks":
+ case "none", "tasks", "asyncify":
callee = b.createGoroutineStartWrapper(funcPtr, prefix, hasContext, instr.Pos())
if b.AutomaticStackSize {
// The stack size is not known until after linking. Call a dummy
@@ -124,7 +124,7 @@ func (b *builder) createGo(instr *ssa.Go) {
} else {
// The stack size is fixed at compile time. By emitting it here as a
// constant, it can be optimized.
- if b.Scheduler == "tasks" && b.DefaultStackSize == 0 {
+ if (b.Scheduler == "tasks" || b.Scheduler == "asyncify") && b.DefaultStackSize == 0 {
b.addError(instr.Pos(), "default stack size for goroutines is not set")
}
stackSize = llvm.ConstInt(b.uintptrType, b.DefaultStackSize, false)
@@ -170,6 +170,11 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri
builder := c.ctx.NewBuilder()
defer builder.Dispose()
+ var deadlock llvm.Value
+ if c.Scheduler == "asyncify" {
+ deadlock = c.getFunction(c.program.ImportedPackage("runtime").Members["deadlock"].(*ssa.Function))
+ }
+
if !fn.IsAFunction().IsNil() {
// See whether this wrapper has already been created. If so, return it.
name := fn.Name()
@@ -225,6 +230,12 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri
// Create the call.
builder.CreateCall(fn, params, "")
+ if c.Scheduler == "asyncify" {
+ builder.CreateCall(deadlock, []llvm.Value{
+ llvm.Undef(c.i8ptrType), llvm.Undef(c.i8ptrType),
+ }, "")
+ }
+
} else {
// For a function pointer like this:
//
@@ -292,11 +303,22 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri
// Create the call.
builder.CreateCall(fnPtr, params, "")
+
+ if c.Scheduler == "asyncify" {
+ builder.CreateCall(deadlock, []llvm.Value{
+ llvm.Undef(c.i8ptrType), llvm.Undef(c.i8ptrType),
+ }, "")
+ }
}
- // Finish the function. Every basic block must end in a terminator, and
- // because goroutines never return a value we can simply return void.
- builder.CreateRetVoid()
+ if c.Scheduler == "asyncify" {
+ // The goroutine was terminated via deadlock.
+ builder.CreateUnreachable()
+ } else {
+ // Finish the function. Every basic block must end in a terminator, and
+ // because goroutines never return a value we can simply return void.
+ builder.CreateRetVoid()
+ }
// Return a ptrtoint of the wrapper, not the function itself.
return builder.CreatePtrToInt(wrapper, c.uintptrType, "")
diff --git a/compiler/testdata/channel.ll b/compiler/testdata/channel.ll
index 2d4f1475b..04bfa4af1 100644
--- a/compiler/testdata/channel.ll
+++ b/compiler/testdata/channel.ll
@@ -6,7 +6,8 @@ target triple = "wasm32-unknown-wasi"
%runtime.channel = type { i32, i32, i8, %runtime.channelBlockedList*, i32, i32, i32, i8* }
%runtime.channelBlockedList = type { %runtime.channelBlockedList*, %"internal/task.Task"*, %runtime.chanSelectState*, { %runtime.channelBlockedList*, i32, i32 } }
%"internal/task.Task" = type { %"internal/task.Task"*, i8*, i64, %"internal/task.state" }
-%"internal/task.state" = type { i8* }
+%"internal/task.state" = type { i32, i8*, %"internal/task.stackState", i1 }
+%"internal/task.stackState" = type { i32, i32 }
%runtime.chanSelectState = type { %runtime.channel*, i8* }
declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*, i8*)
diff --git a/compiler/testdata/func.ll b/compiler/testdata/func-coroutines.ll
index eeefa43cf..eeefa43cf 100644
--- a/compiler/testdata/func.ll
+++ b/compiler/testdata/func-coroutines.ll
diff --git a/compiler/testdata/gc.ll b/compiler/testdata/gc.ll
index 7ac0aa1ba..375763f3c 100644
--- a/compiler/testdata/gc.ll
+++ b/compiler/testdata/gc.ll
@@ -5,7 +5,6 @@ target triple = "wasm32-unknown-wasi"
%runtime.typecodeID = type { %runtime.typecodeID*, i32, %runtime.interfaceMethodInfo*, %runtime.typecodeID*, i32 }
%runtime.interfaceMethodInfo = type { i8*, i32 }
-%runtime.funcValue = type { i8*, i32 }
%runtime._interface = type { i32, i8* }
@main.scalar1 = hidden global i8* null, align 4
@@ -72,11 +71,11 @@ entry:
}
; Function Attrs: nounwind
-define hidden %runtime.funcValue* @main.newFuncValue(i8* %context, i8* %parentHandle) unnamed_addr #0 {
+define hidden { i8*, void ()* }* @main.newFuncValue(i8* %context, i8* %parentHandle) unnamed_addr #0 {
entry:
%new = call i8* @runtime.alloc(i32 8, i8* nonnull inttoptr (i32 197 to i8*), i8* undef, i8* null) #0
- %0 = bitcast i8* %new to %runtime.funcValue*
- ret %runtime.funcValue* %0
+ %0 = bitcast i8* %new to { i8*, void ()* }*
+ ret { i8*, void ()* }* %0
}
; Function Attrs: nounwind
diff --git a/compiler/testdata/goroutine-cortex-m-qemu.ll b/compiler/testdata/goroutine-cortex-m-qemu-tasks.ll
index d34a1eb43..d34a1eb43 100644
--- a/compiler/testdata/goroutine-cortex-m-qemu.ll
+++ b/compiler/testdata/goroutine-cortex-m-qemu-tasks.ll
diff --git a/compiler/testdata/goroutine-wasm-asyncify.ll b/compiler/testdata/goroutine-wasm-asyncify.ll
new file mode 100644
index 000000000..1ecf0c796
--- /dev/null
+++ b/compiler/testdata/goroutine-wasm-asyncify.ll
@@ -0,0 +1,210 @@
+; ModuleID = 'goroutine.go'
+source_filename = "goroutine.go"
+target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128"
+target triple = "wasm32-unknown-wasi"
+
+%runtime.channel = type { i32, i32, i8, %runtime.channelBlockedList*, i32, i32, i32, i8* }
+%runtime.channelBlockedList = type { %runtime.channelBlockedList*, %"internal/task.Task"*, %runtime.chanSelectState*, { %runtime.channelBlockedList*, i32, i32 } }
+%"internal/task.Task" = type { %"internal/task.Task"*, i8*, i64, %"internal/task.state" }
+%"internal/task.state" = type { i32, i8*, %"internal/task.stackState", i1 }
+%"internal/task.stackState" = type { i32, i32 }
+%runtime.chanSelectState = type { %runtime.channel*, i8* }
+
+@"main$string" = internal unnamed_addr constant [4 x i8] c"test", align 1
+
+declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*, i8*)
+
+; Function Attrs: nounwind
+define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ ret void
+}
+
+; Function Attrs: nounwind
+define hidden void @main.regularFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.regularFunction$gowrapper" to i32), i8* nonnull inttoptr (i32 5 to i8*), i32 8192, i8* undef, i8* null) #0
+ ret void
+}
+
+declare void @main.regularFunction(i32, i8*, i8*)
+
+declare void @runtime.deadlock(i8*, i8*)
+
+; Function Attrs: nounwind
+define linkonce_odr void @"main.regularFunction$gowrapper"(i8* %0) unnamed_addr #1 {
+entry:
+ %unpack.int = ptrtoint i8* %0 to i32
+ call void @main.regularFunction(i32 %unpack.int, i8* undef, i8* undef) #0
+ call void @runtime.deadlock(i8* undef, i8* undef) #0
+ unreachable
+}
+
+declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*)
+
+; Function Attrs: nounwind
+define hidden void @main.inlineFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.inlineFunctionGoroutine$1$gowrapper" to i32), i8* nonnull inttoptr (i32 5 to i8*), i32 8192, i8* undef, i8* null) #0
+ ret void
+}
+
+; Function Attrs: nounwind
+define hidden void @"main.inlineFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ ret void
+}
+
+; Function Attrs: nounwind
+define linkonce_odr void @"main.inlineFunctionGoroutine$1$gowrapper"(i8* %0) unnamed_addr #2 {
+entry:
+ %unpack.int = ptrtoint i8* %0 to i32
+ call void @"main.inlineFunctionGoroutine$1"(i32 %unpack.int, i8* undef, i8* undef)
+ call void @runtime.deadlock(i8* undef, i8* undef) #0
+ unreachable
+}
+
+; Function Attrs: nounwind
+define hidden void @main.closureFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ %n = call i8* @runtime.alloc(i32 4, i8* nonnull inttoptr (i32 3 to i8*), i8* undef, i8* null) #0
+ %0 = bitcast i8* %n to i32*
+ store i32 3, i32* %0, align 4
+ %1 = call i8* @runtime.alloc(i32 8, i8* null, i8* undef, i8* null) #0
+ %2 = bitcast i8* %1 to i32*
+ store i32 5, i32* %2, align 4
+ %3 = getelementptr inbounds i8, i8* %1, i32 4
+ %4 = bitcast i8* %3 to i8**
+ store i8* %n, i8** %4, align 4
+ call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.closureFunctionGoroutine$1$gowrapper" to i32), i8* nonnull %1, i32 8192, i8* undef, i8* null) #0
+ %5 = load i32, i32* %0, align 4
+ call void @runtime.printint32(i32 %5, i8* undef, i8* null) #0
+ ret void
+}
+
+; Function Attrs: nounwind
+define hidden void @"main.closureFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ %unpack.ptr = bitcast i8* %context to i32*
+ store i32 7, i32* %unpack.ptr, align 4
+ ret void
+}
+
+; Function Attrs: nounwind
+define linkonce_odr void @"main.closureFunctionGoroutine$1$gowrapper"(i8* %0) unnamed_addr #3 {
+entry:
+ %1 = bitcast i8* %0 to i32*
+ %2 = load i32, i32* %1, align 4
+ %3 = getelementptr inbounds i8, i8* %0, i32 4
+ %4 = bitcast i8* %3 to i8**
+ %5 = load i8*, i8** %4, align 4
+ call void @"main.closureFunctionGoroutine$1"(i32 %2, i8* %5, i8* undef)
+ call void @runtime.deadlock(i8* undef, i8* undef) #0
+ unreachable
+}
+
+declare void @runtime.printint32(i32, i8*, i8*)
+
+; Function Attrs: nounwind
+define hidden void @main.funcGoroutine(i8* %fn.context, void ()* %fn.funcptr, i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ %0 = call i8* @runtime.alloc(i32 12, i8* null, i8* undef, i8* null) #0
+ %1 = bitcast i8* %0 to i32*
+ store i32 5, i32* %1, align 4
+ %2 = getelementptr inbounds i8, i8* %0, i32 4
+ %3 = bitcast i8* %2 to i8**
+ store i8* %fn.context, i8** %3, align 4
+ %4 = getelementptr inbounds i8, i8* %0, i32 8
+ %5 = bitcast i8* %4 to void ()**
+ store void ()* %fn.funcptr, void ()** %5, align 4
+ call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @main.funcGoroutine.gowrapper to i32), i8* nonnull %0, i32 8192, i8* undef, i8* null) #0
+ ret void
+}
+
+; Function Attrs: nounwind
+define linkonce_odr void @main.funcGoroutine.gowrapper(i8* %0) unnamed_addr #4 {
+entry:
+ %1 = bitcast i8* %0 to i32*
+ %2 = load i32, i32* %1, align 4
+ %3 = getelementptr inbounds i8, i8* %0, i32 4
+ %4 = bitcast i8* %3 to i8**
+ %5 = load i8*, i8** %4, align 4
+ %6 = getelementptr inbounds i8, i8* %0, i32 8
+ %7 = bitcast i8* %6 to void (i32, i8*, i8*)**
+ %8 = load void (i32, i8*, i8*)*, void (i32, i8*, i8*)** %7, align 4
+ call void %8(i32 %2, i8* %5, i8* undef) #0
+ call void @runtime.deadlock(i8* undef, i8* undef) #0
+ unreachable
+}
+
+; Function Attrs: nounwind
+define hidden void @main.recoverBuiltinGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ ret void
+}
+
+; Function Attrs: nounwind
+define hidden void @main.copyBuiltinGoroutine(i8* %dst.data, i32 %dst.len, i32 %dst.cap, i8* %src.data, i32 %src.len, i32 %src.cap, i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ %copy.n = call i32 @runtime.sliceCopy(i8* %dst.data, i8* %src.data, i32 %dst.len, i32 %src.len, i32 1, i8* undef, i8* null) #0
+ ret void
+}
+
+declare i32 @runtime.sliceCopy(i8* nocapture writeonly, i8* nocapture readonly, i32, i32, i32, i8*, i8*)
+
+; Function Attrs: nounwind
+define hidden void @main.closeBuiltinGoroutine(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ call void @runtime.chanClose(%runtime.channel* %ch, i8* undef, i8* null) #0
+ ret void
+}
+
+declare void @runtime.chanClose(%runtime.channel* dereferenceable_or_null(32), i8*, i8*)
+
+; Function Attrs: nounwind
+define hidden void @main.startInterfaceMethod(i32 %itf.typecode, i8* %itf.value, i8* %context, i8* %parentHandle) unnamed_addr #0 {
+entry:
+ %0 = call i8* @runtime.alloc(i32 16, i8* null, i8* undef, i8* null) #0
+ %1 = bitcast i8* %0 to i8**
+ store i8* %itf.value, i8** %1, align 4
+ %2 = getelementptr inbounds i8, i8* %0, i32 4
+ %.repack = bitcast i8* %2 to i8**
+ store i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"main$string", i32 0, i32 0), i8** %.repack, align 4
+ %.repack1 = getelementptr inbounds i8, i8* %0, i32 8
+ %3 = bitcast i8* %.repack1 to i32*
+ store i32 4, i32* %3, align 4
+ %4 = getelementptr inbounds i8, i8* %0, i32 12
+ %5 = bitcast i8* %4 to i32*
+ store i32 %itf.typecode, i32* %5, align 4
+ call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"interface:{Print:func:{basic:string}{}}.Print$invoke$gowrapper" to i32), i8* nonnull %0, i32 8192, i8* undef, i8* null) #0
+ ret void
+}
+
+declare void @"interface:{Print:func:{basic:string}{}}.Print$invoke"(i8*, i8*, i32, i32, i8*, i8*) #5
+
+; Function Attrs: nounwind
+define linkonce_odr void @"interface:{Print:func:{basic:string}{}}.Print$invoke$gowrapper"(i8* %0) unnamed_addr #6 {
+entry:
+ %1 = bitcast i8* %0 to i8**
+ %2 = load i8*, i8** %1, align 4
+ %3 = getelementptr inbounds i8, i8* %0, i32 4
+ %4 = bitcast i8* %3 to i8**
+ %5 = load i8*, i8** %4, align 4
+ %6 = getelementptr inbounds i8, i8* %0, i32 8
+ %7 = bitcast i8* %6 to i32*
+ %8 = load i32, i32* %7, align 4
+ %9 = getelementptr inbounds i8, i8* %0, i32 12
+ %10 = bitcast i8* %9 to i32*
+ %11 = load i32, i32* %10, align 4
+ call void @"interface:{Print:func:{basic:string}{}}.Print$invoke"(i8* %2, i8* %5, i32 %8, i32 %11, i8* undef, i8* undef) #0
+ call void @runtime.deadlock(i8* undef, i8* undef) #0
+ unreachable
+}
+
+attributes #0 = { nounwind }
+attributes #1 = { nounwind "tinygo-gowrapper"="main.regularFunction" }
+attributes #2 = { nounwind "tinygo-gowrapper"="main.inlineFunctionGoroutine$1" }
+attributes #3 = { nounwind "tinygo-gowrapper"="main.closureFunctionGoroutine$1" }
+attributes #4 = { nounwind "tinygo-gowrapper" }
+attributes #5 = { "tinygo-invoke"="reflect/methods.Print(string)" "tinygo-methods"="reflect/methods.Print(string)" }
+attributes #6 = { nounwind "tinygo-gowrapper"="interface:{Print:func:{basic:string}{}}.Print$invoke" }
diff --git a/compiler/testdata/goroutine-wasm.ll b/compiler/testdata/goroutine-wasm-coroutines.ll
index 6f3082471..6f3082471 100644
--- a/compiler/testdata/goroutine-wasm.ll
+++ b/compiler/testdata/goroutine-wasm-coroutines.ll
diff --git a/goenv/goenv.go b/goenv/goenv.go
index 3f1352a78..0ef18ba50 100644
--- a/goenv/goenv.go
+++ b/goenv/goenv.go
@@ -3,12 +3,15 @@
package goenv
import (
+ "bytes"
+ "errors"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
+ "strings"
)
// Keys is a slice of all available environment variable keys.
@@ -67,11 +70,98 @@ func Get(name string) string {
return "1"
case "TINYGOROOT":
return sourceDir()
+ case "WASMOPT":
+ if path := os.Getenv("WASMOPT"); path != "" {
+ err := wasmOptCheckVersion(path)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "cannot use %q as wasm-opt (from WASMOPT environment variable): %s", path, err.Error())
+ os.Exit(1)
+ }
+
+ return path
+ }
+
+ return findWasmOpt()
default:
return ""
}
}
+// Find wasm-opt, or exit with an error.
+func findWasmOpt() string {
+ tinygoroot := sourceDir()
+ searchPaths := []string{
+ tinygoroot + "/bin/wasm-opt",
+ tinygoroot + "/build/wasm-opt",
+ }
+
+ var paths []string
+ for _, path := range searchPaths {
+ if runtime.GOOS == "windows" {
+ path += ".exe"
+ }
+
+ _, err := os.Stat(path)
+ if err != nil && os.IsNotExist(err) {
+ continue
+ }
+
+ paths = append(paths, path)
+ }
+
+ if path, err := exec.LookPath("wasm-opt"); err == nil {
+ paths = append(paths, path)
+ }
+
+ if len(paths) == 0 {
+ fmt.Fprintln(os.Stderr, "error: could not find wasm-opt, set the WASMOPT environment variable to override")
+ os.Exit(1)
+ }
+
+ errs := make([]error, len(paths))
+ for i, path := range paths {
+ err := wasmOptCheckVersion(path)
+ if err == nil {
+ return path
+ }
+
+ errs[i] = err
+ }
+ fmt.Fprintln(os.Stderr, "no usable wasm-opt found, update or run \"make binaryen\"")
+ for i, path := range paths {
+ fmt.Fprintf(os.Stderr, "\t%s: %s\n", path, errs[i].Error())
+ }
+ os.Exit(1)
+ panic("unreachable")
+}
+
+// wasmOptCheckVersion checks if a copy of wasm-opt is usable.
+func wasmOptCheckVersion(path string) error {
+ cmd := exec.Command(path, "--version")
+ var buf bytes.Buffer
+ cmd.Stdout = &buf
+ cmd.Stderr = os.Stderr
+ err := cmd.Run()
+ if err != nil {
+ return err
+ }
+
+ str := buf.String()
+ if strings.Contains(str, "(") {
+ // The git tag may be placed in parentheses after the main version string.
+ str = strings.Split(str, "(")[0]
+ }
+
+ str = strings.TrimSpace(str)
+ var ver uint
+ _, err = fmt.Sscanf(str, "wasm-opt version %d", &ver)
+ if err != nil || ver < 102 {
+ return errors.New("incompatible wasm-opt (need 102 or newer)")
+ }
+
+ return nil
+}
+
// Return the TINYGOROOT, or exit with an error.
func sourceDir() string {
// Use $TINYGOROOT as root, if available.
diff --git a/lib/binaryen b/lib/binaryen
new file mode 160000
+Subproject 96f7acf09aae1ec6e8bc573dfa8f309c4f892a4
diff --git a/src/internal/task/task_asyncify.go b/src/internal/task/task_asyncify.go
new file mode 100644
index 000000000..d67f0e1ca
--- /dev/null
+++ b/src/internal/task/task_asyncify.go
@@ -0,0 +1,127 @@
+//go:build scheduler.asyncify
+// +build scheduler.asyncify
+
+package task
+
+import (
+ "unsafe"
+)
+
+// Stack canary, to detect a stack overflow. The number is a random number
+// generated by random.org. The bit fiddling dance is necessary because
+// otherwise Go wouldn't allow the cast to a smaller integer size.
+const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0)))
+
+//go:linkname runtimePanic runtime.runtimePanic
+func runtimePanic(str string)
+
+// state is a structure which holds a reference to the state of the task.
+// When the task is suspended, the stack pointers are saved here.
+type state struct {
+ // entry is the entry function of the task.
+ // This is needed every time the function is invoked so that asyncify knows what to rewind.
+ entry uintptr
+
+ // args are a pointer to a struct holding the arguments of the function.
+ args unsafe.Pointer
+
+ // stackState is the state of the stack while unwound.
+ stackState
+
+ launched bool
+}
+
+// stackState is the saved state of a stack while unwound.
+// The stack is arranged with asyncify at the bottom, C stack at the top, and a gap of available stack space between the two.
+type stackState struct {
+ // asyncify is the stack pointer of the asyncify stack.
+ // This starts from the bottom and grows upwards.
+ asyncifysp uintptr
+
+ // asyncify is stack pointer of the C stack.
+ // This starts from the top and grows downwards.
+ csp uintptr
+}
+
+// start creates and starts a new goroutine with the given function and arguments.
+// The new goroutine is immediately started.
+func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
+ t := &Task{}
+ t.state.initialize(fn, args, stackSize)
+ runqueuePushBack(t)
+}
+
+//export tinygo_launch
+func (*state) launch()
+
+//go:linkname align runtime.align
+func align(p uintptr) uintptr
+
+// initialize the state and prepare to call the specified function with the specified argument bundle.
+func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
+ // Save the entry call.
+ s.entry = fn
+ s.args = args
+
+ // Create a stack.
+ stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0)))
+
+ // Calculate stack base addresses.
+ s.asyncifysp = uintptr(unsafe.Pointer(&stack[0]))
+ s.csp = uintptr(unsafe.Pointer(&stack[0])) + uintptr(len(stack))*unsafe.Sizeof(uintptr(0))
+ stack[0] = stackCanary
+}
+
+//go:linkname runqueuePushBack runtime.runqueuePushBack
+func runqueuePushBack(*Task)
+
+// currentTask is the current running task, or nil if currently in the scheduler.
+var currentTask *Task
+
+// Current returns the current active task.
+func Current() *Task {
+ return currentTask
+}
+
+// Pause suspends the current task and returns to the scheduler.
+// This function may only be called when running on a goroutine stack, not when running on the system stack.
+func Pause() {
+ // This is mildly unsafe but this is also the only place we can do this.
+ if *(*uintptr)(unsafe.Pointer(currentTask.state.asyncifysp)) != stackCanary {
+ runtimePanic("stack overflow")
+ }
+
+ currentTask.state.unwind()
+
+ *(*uintptr)(unsafe.Pointer(currentTask.state.asyncifysp)) = stackCanary
+}
+
+//export tinygo_unwind
+func (*stackState) unwind()
+
+// Resume the task until it pauses or completes.
+// This may only be called from the scheduler.
+func (t *Task) Resume() {
+ // The current task must be saved and restored because this can nest on WASM with JS.
+ prevTask := currentTask
+ currentTask = t
+ if !t.state.launched {
+ t.state.launch()
+ t.state.launched = true
+ } else {
+ t.state.rewind()
+ }
+ currentTask = prevTask
+ if t.state.asyncifysp > t.state.csp {
+ runtimePanic("stack overflow")
+ }
+}
+
+//export tinygo_rewind
+func (*state) rewind()
+
+// OnSystemStack returns whether the caller is running on the system stack.
+func OnSystemStack() bool {
+ // If there is not an active goroutine, then this must be running on the system stack.
+ return Current() == nil
+}
diff --git a/src/internal/task/task_asyncify_wasm.S b/src/internal/task/task_asyncify_wasm.S
new file mode 100644
index 000000000..3d146b4e8
--- /dev/null
+++ b/src/internal/task/task_asyncify_wasm.S
@@ -0,0 +1,99 @@
+.globaltype __stack_pointer, i32
+
+.global tinygo_unwind
+.type tinygo_unwind,@function
+tinygo_unwind: // func (state *stackState) unwind()
+ .functype tinygo_unwind (i32) -> ()
+ // Check if we are rewinding.
+ i32.const 0
+ i32.load8_u tinygo_rewinding
+ if // if tinygo_rewinding {
+ // Stop rewinding.
+ call stop_rewind
+ i32.const 0
+ i32.const 0
+ i32.store8 tinygo_rewinding // tinygo_rewinding = false;
+ else
+ // Save the C stack pointer (destination structure pointer is in local 0).
+ local.get 0
+ global.get __stack_pointer
+ i32.store 4 // state.csp = getCurrentStackPointer()
+ // Ask asyncify to unwind.
+ // When resuming, asyncify will return this function with tinygo_rewinding set to true.
+ local.get 0
+ call start_unwind // asyncify.start_unwind(state)
+ end_if
+ return
+ end_function
+
+.global tinygo_launch
+.type tinygo_launch,@function
+tinygo_launch: // func (state *state) launch()
+ .functype tinygo_launch (i32) -> ()
+ // Switch to the goroutine's C stack.
+ global.get __stack_pointer // prev := getCurrentStackPointer()
+ local.get 0
+ i32.load 12
+ global.set __stack_pointer // setStackPointer(state.csp)
+ // Get the argument pack and entry pointer.
+ local.get 0
+ i32.load 4 // args := state.args
+ local.get 0
+ i32.load 0 // fn := state.entry
+ // Launch the entry function.
+ call_indirect (i32) -> () // fn(args)
+ // Stop unwinding.
+ call stop_unwind
+ // Restore the C stack.
+ global.set __stack_pointer // setStackPointer(prev)
+ return
+ end_function
+
+.global tinygo_rewind
+.type tinygo_rewind,@function
+tinygo_rewind: // func (state *state) rewind()
+ .functype tinygo_rewind (i32) -> ()
+ // Switch to the goroutine's C stack.
+ global.get __stack_pointer // prev := getCurrentStackPointer()
+ local.get 0
+ i32.load 12
+ global.set __stack_pointer // setStackPointer(state.csp)
+ // Get the argument pack and entry pointer.
+ local.get 0
+ i32.load 4 // args := state.args
+ local.get 0
+ i32.load 0 // fn := state.entry
+ // Prepare to rewind.
+ i32.const 0
+ i32.const 1
+ i32.store8 tinygo_rewinding // tinygo_rewinding = true;
+ local.get 0
+ i32.const 8
+ i32.add
+ call start_rewind // asyncify.start_rewind(&state.stackState)
+ // Launch the entry function.
+ // This will actually rewind the call stack.
+ call_indirect (i32) -> () // fn(args)
+ // Stop unwinding.
+ call stop_unwind
+ // Restore the C stack.
+ global.set __stack_pointer // setStackPointer(prev)
+ return
+ end_function
+
+.functype start_unwind (i32) -> ()
+.import_module start_unwind, asyncify
+.functype stop_unwind () -> ()
+.import_module stop_unwind, asyncify
+.functype start_rewind (i32) -> ()
+.import_module start_rewind, asyncify
+.functype stop_rewind () -> ()
+.import_module stop_rewind, asyncify
+
+ .hidden tinygo_rewinding # @tinygo_rewinding
+ .type tinygo_rewinding,@object
+ .section .bss.tinygo_rewinding,"",@
+ .globl tinygo_rewinding
+tinygo_rewinding:
+ .int8 0 # 0x0
+ .size tinygo_rewinding, 1
diff --git a/src/runtime/gc_conservative.go b/src/runtime/gc_conservative.go
index ea142be93..cabd41427 100644
--- a/src/runtime/gc_conservative.go
+++ b/src/runtime/gc_conservative.go
@@ -1,3 +1,4 @@
+//go:build gc.conservative
// +build gc.conservative
package runtime
diff --git a/src/runtime/runtime_wasm_js.go b/src/runtime/runtime_wasm_js.go
index f4335e121..a5f2505d3 100644
--- a/src/runtime/runtime_wasm_js.go
+++ b/src/runtime/runtime_wasm_js.go
@@ -1,3 +1,4 @@
+//go:build wasm && !wasi
// +build wasm,!wasi
package runtime
@@ -6,13 +7,19 @@ import "unsafe"
type timeUnit float64 // time in milliseconds, just like Date.now() in JavaScript
+// wasmNested is used to detect scheduler nesting (WASM calls into JS calls back into WASM).
+// When this happens, we need to use a reduced version of the scheduler.
+var wasmNested bool
+
//export _start
func _start() {
// These need to be initialized early so that the heap can be initialized.
heapStart = uintptr(unsafe.Pointer(&heapStartSymbol))
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
+ wasmNested = true
run()
+ wasmNested = false
}
var handleEvent func()
@@ -27,12 +34,27 @@ func resume() {
go func() {
handleEvent()
}()
+
+ if wasmNested {
+ minSched()
+ return
+ }
+
+ wasmNested = true
scheduler()
+ wasmNested = false
}
//export go_scheduler
func go_scheduler() {
+ if wasmNested {
+ minSched()
+ return
+ }
+
+ wasmNested = true
scheduler()
+ wasmNested = false
}
func ticksToNanoseconds(ticks timeUnit) int64 {
diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go
index 44b07f75d..618b6638d 100644
--- a/src/runtime/scheduler.go
+++ b/src/runtime/scheduler.go
@@ -172,6 +172,24 @@ func scheduler() {
}
}
+// This horrible hack exists to make WASM work properly.
+// When a WASM program calls into JS which calls back into WASM, the event with which we called back in needs to be handled before returning.
+// Thus there are two copies of the scheduler running at once.
+// This is a reduced version of the scheduler which does not deal with the timer queue (that is a problem for the outer scheduler).
+func minSched() {
+ scheduleLog("start nested scheduler")
+ for !schedulerDone {
+ t := runqueue.Pop()
+ if t == nil {
+ break
+ }
+
+ scheduleLogTask(" run:", t)
+ t.Resume()
+ }
+ scheduleLog("stop nested scheduler")
+}
+
func Gosched() {
runqueue.Push(task.Current())
task.Pause()
diff --git a/targets/wasi.json b/targets/wasi.json
index b7852ad46..b056740b0 100644
--- a/targets/wasi.json
+++ b/targets/wasi.json
@@ -6,6 +6,8 @@
"goarch": "arm",
"linker": "wasm-ld",
"libc": "wasi-libc",
+ "scheduler": "asyncify",
+ "default-stack-size": 8192,
"ldflags": [
"--allow-undefined",
"--stack-first",
diff --git a/targets/wasm.json b/targets/wasm.json
index 047dd8c38..8a033fa95 100644
--- a/targets/wasm.json
+++ b/targets/wasm.json
@@ -6,6 +6,8 @@
"goarch": "wasm",
"linker": "wasm-ld",
"libc": "wasi-libc",
+ "scheduler": "asyncify",
+ "default-stack-size": 8192,
"ldflags": [
"--allow-undefined",
"--stack-first",
diff --git a/tests/wasm/chan_test.go b/tests/wasm/chan_test.go
index 793bf1de3..e44d7ebe4 100644
--- a/tests/wasm/chan_test.go
+++ b/tests/wasm/chan_test.go
@@ -24,13 +24,12 @@ func TestChan(t *testing.T) {
err = chromedp.Run(ctx,
chromedp.Navigate(server.URL+"/run?file=chan.wasm"),
waitLog(`1
-2
4
+2
3
true`),
)
if err != nil {
t.Fatal(err)
}
-
}
diff --git a/transform/optimizer.go b/transform/optimizer.go
index bd6d21687..64c3d0b54 100644
--- a/transform/optimizer.go
+++ b/transform/optimizer.go
@@ -125,7 +125,7 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i
if err != nil {
return []error{err}
}
- case "tasks":
+ case "tasks", "asyncify":
// No transformations necessary.
case "none":
// Check for any goroutine starts.
@@ -219,7 +219,7 @@ func getFunctionsUsedInTransforms(config *compileopts.Config) []string {
case "none":
case "coroutines":
fnused = append(append([]string{}, fnused...), coroFunctionsUsedInTransforms...)
- case "tasks":
+ case "tasks", "asyncify":
fnused = append(append([]string{}, fnused...), taskFunctionsUsedInTransforms...)
default:
panic(fmt.Errorf("invalid scheduler %q", config.Scheduler()))