diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 998be388fd..a0876654d1 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -671,6 +671,33 @@ jobs: --env LLGO_WASM_BLOCKED_G=1000 \ "$RUNNER_TEMP/wasm-resume-hardening-wasip1.wasm" resume-hardening-p1 2>&1) grep -Fxq "wasm hardening ok" <<<"$output" + LLGO_WASM_RESUME=1 GOOS=js GOARCH=wasm llgo build -p=1 \ + -o "$RUNNER_TEMP/wasm-resume-acceptance-go.mjs" ./internal/build/testdata/wasm-resume-acceptance + run_wasm_workers "$RUNNER_TEMP/wasm-resume-acceptance-go.mjs" "wasm resume acceptance ok" + LLGO_WASM_RESUME=1 llgo build -p=1 -target wasm \ + -o "$RUNNER_TEMP/wasm-resume-acceptance.mjs" ./internal/build/testdata/wasm-resume-acceptance + run_wasm_workers "$RUNNER_TEMP/wasm-resume-acceptance.mjs" "wasm resume acceptance ok" + LLGO_WASM_RESUME=1 GOOS=wasip1 GOARCH=wasm llgo build -p=1 \ + -o "$RUNNER_TEMP/wasm-resume-acceptance-wasip1.wasm" ./internal/build/testdata/wasm-resume-acceptance + wasm-tools validate --features all "$RUNNER_TEMP/wasm-resume-acceptance-wasip1.wasm" + test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/wasm-resume-acceptance-wasip1.wasm" 2>&1)" = \ + "wasm resume acceptance ok" + LLGO_WASM_RESUME=1 GOOS=js GOARCH=wasm llgo build -p=1 -O0 -ldflags=-w=false \ + -o "$RUNNER_TEMP/wasm-resume-debug-go.mjs" ./internal/build/testdata/wasm-resume-debug + run_wasm_workers "$RUNNER_TEMP/wasm-resume-debug-go.mjs" "wasm resume debug ok" + llvm-dwarfdump --verify "$RUNNER_TEMP/wasm-resume-debug-go.wasm" + LLGO_WASM_RESUME=1 llgo build -p=1 -O0 -ldflags=-w=false -target wasm \ + -o "$RUNNER_TEMP/wasm-resume-debug.mjs" ./internal/build/testdata/wasm-resume-debug + run_wasm_workers "$RUNNER_TEMP/wasm-resume-debug.mjs" "wasm resume debug ok" + llvm-dwarfdump --verify "$RUNNER_TEMP/wasm-resume-debug.wasm" + LLGO_WASM_RESUME=1 GOOS=wasip1 GOARCH=wasm llgo build -p=1 -O0 -ldflags=-w=false \ + -o "$RUNNER_TEMP/wasm-resume-debug-wasip1.wasm" ./internal/build/testdata/wasm-resume-debug + wasm-tools validate --features all "$RUNNER_TEMP/wasm-resume-debug-wasip1.wasm" + test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/wasm-resume-debug-wasip1.wasm" 2>&1)" = \ + "wasm resume debug ok" + wasm_sections=$(llvm-readobj --sections "$RUNNER_TEMP/wasm-resume-debug-wasip1.wasm") + grep -Fq '.debug_info' <<<"$wasm_sections" + grep -Fq '.debug_line' <<<"$wasm_sections" file "$RUNNER_TEMP/runtime-js.wasm" \ "$RUNNER_TEMP/runtime-wasip1.wasm" \ "$RUNNER_TEMP/runtime-wasip1-threads.wasm" \ diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index a5022e81f5..3d7a98fe0f 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -413,12 +413,14 @@ func TestCallerFrameTrackingEligibility(t *testing.T) { track bool targetName string goarch string + wasmResume bool want bool }{ {name: "enabled user package", pkgPath: "example.com/foo", track: true, want: true}, {name: "disabled flag", pkgPath: "example.com/foo", want: false}, {name: "named target", pkgPath: "example.com/foo", track: true, targetName: "esp32", want: false}, {name: "wasm", pkgPath: "example.com/foo", track: true, goarch: "wasm", want: false}, + {name: "resumable wasm", pkgPath: "example.com/foo", track: true, goarch: "wasm", wasmResume: true, want: true}, {name: "stdlib", pkgPath: "fmt", track: true, want: true}, {name: "runtime", pkgPath: "runtime", track: true, want: false}, {name: "llgo runtime", pkgPath: llssa.PkgRuntime, track: true, want: false}, @@ -438,6 +440,7 @@ func f() { runtime.Caller(0) } if tt.goarch != "" { prog.Target().GOARCH = tt.goarch } + prog.EnableWasmResumeABI(tt.wasmResume) pkg := prog.NewPackage("foo", tt.pkgPath) fn := pkg.NewFunc("f", llssa.NoArgsNoRet, llssa.InGo) goFn := ssapkg.Func("f") @@ -824,6 +827,41 @@ func f() { _ = runtime.FuncForPC(0) } } } +func TestCompileRuntimeCallerFrameInstrumentationForWasm(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func f() { + runtime.Caller(0) +} +`) + for _, tt := range []struct { + name string + wasmResume bool + wantTracking bool + }{ + {name: "default"}, + {name: "resumable", wasmResume: true, wantTracking: true}, + } { + t.Run(tt.name, func(t *testing.T) { + prog := newLLSSAProg(t) + prog.Target().GOOS = "js" + prog.Target().GOARCH = "wasm" + prog.EnableWasmResumeABI(tt.wasmResume) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, symbol := range []string{"PushCallerLocationFrame", "RecordCallerLocation", "PopCallerLocationFrame"} { + if got := strings.Contains(ir, symbol); got != tt.wantTracking { + t.Fatalf("wasm runtime.Caller tracking %s = %v, want %v:\n%s", symbol, got, tt.wantTracking, ir) + } + } + }) + } +} + func TestCompileRuntimeCallerLocationOnlyForRuntimePaths(t *testing.T) { old := emitShadowStackInstrumentation emitShadowStackInstrumentation = true diff --git a/cl/debug_compile_test.go b/cl/debug_compile_test.go index 2f177005b2..b369c35d28 100644 --- a/cl/debug_compile_test.go +++ b/cl/debug_compile_test.go @@ -34,6 +34,11 @@ func inspect(items [2]item, seed int) int { return items[0].value + local[0].value } +func inspectDefer(seed int) (result int) { + defer func() { result++ }() + return seed +} + var anonymous = func(seed int) int { value := seed + 1 return value diff --git a/cl/gcroot.go b/cl/gcroot.go index eec2c51377..9aeecfafe7 100644 --- a/cl/gcroot.go +++ b/cl/gcroot.go @@ -37,8 +37,7 @@ func (p *context) prepareGCRoots(fn *ssa.Function, hasClosureContext bool) { case *ssa.FreeVar: return false } - typ := p.type_(value.Type(), llssa.InGo) - return p.prog.GCRootCount(typ) != 0 + return p.gcRootCount(value) != 0 }, p.isGCSafepoint) if p.safepointEntry { for _, param := range fn.Params { @@ -54,8 +53,7 @@ func (p *context) prepareGCRoots(fn *ssa.Function, hasClosureContext bool) { if _, ok := planned[value]; !ok { return } - typ := p.type_(value.Type(), llssa.InGo) - if n := p.prog.GCRootCount(typ); n != 0 { + if n := p.gcRootCount(value); n != 0 { counts[value] = n total += n } @@ -99,6 +97,31 @@ func (p *context) prepareGCRoots(fn *ssa.Function, hasClosureContext bool) { } } +func (p *context) gcRootCount(value ssa.Value) int { + if call, ok := value.(*ssa.Call); ok { + if fn, ok := call.Call.Value.(*ssa.Function); ok { + _, name, kind := p.funcName(fn) + if kind == llgoInstr && llgoInstrs[name] == llgoFuncAddr { + return 0 + } + } + } + if next, ok := value.(*ssa.Next); ok { + if next.IsString { + return 0 + } + if iter, ok := next.Iter.(*ssa.Range); ok { + if typ, ok := types.Unalias(iter.X.Type()).Underlying().(*types.Map); ok { + key := p.type_(typ.Key(), llssa.InGo) + elem := p.type_(typ.Elem(), llssa.InGo) + return p.prog.GCRootCount(key) + p.prog.GCRootCount(elem) + } + } + } + typ := p.type_(value.Type(), llssa.InGo) + return p.prog.GCRootCount(typ) +} + func (p *context) initGCRoots(b llssa.Builder, fn *ssa.Function) { if len(p.gcRoots) == 0 && p.gcClosureRoot.IsNil() { return diff --git a/cl/gcroot_internal_test.go b/cl/gcroot_internal_test.go index fda4d0dd83..92ade6026b 100644 --- a/cl/gcroot_internal_test.go +++ b/cl/gcroot_internal_test.go @@ -8,8 +8,10 @@ import ( "go/parser" "go/token" "go/types" + "runtime" "testing" + llssa "github.com/goplus/llgo/ssa" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" ) @@ -132,6 +134,73 @@ func classify(p *int) *int { return p } } } +func TestGCRootCountUsesConcreteRangeNextType(t *testing.T) { + fn := buildGCRootSSAFunction(t, `package p +func classify(values map[int]*int, text string, pointer *int) { + for range values {} + for range text {} +}`) + prog := llssa.NewProgram(nil) + defer prog.Dispose() + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + ctx := context{prog: prog} + if got := ctx.gcRootCount(fn.Params[2]); got != 1 { + t.Fatalf("pointer parameter root count = %d, want 1", got) + } + + seenMap, seenString := false, false + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + next, ok := instr.(*ssa.Next) + if !ok { + continue + } + got := ctx.gcRootCount(next) + if next.IsString { + seenString = true + if got != 0 { + t.Fatalf("string range Next root count = %d, want 0", got) + } + } else { + seenMap = true + if got != 1 { + t.Fatalf("map range Next root count = %d, want 1", got) + } + } + } + } + if !seenMap || !seenString { + t.Fatalf("range Next instructions: map=%v string=%v", seenMap, seenString) + } +} + +func TestGCRootCountExcludesFunctionAddressIntrinsic(t *testing.T) { + fn := buildGCRootSSAFunction(t, `package p +import "unsafe" +func intrinsic(any) unsafe.Pointer +func classify(fn func()) unsafe.Pointer { return intrinsic(fn) } +`) + prog := llssa.NewProgram(nil) + defer prog.Dispose() + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + prog.SetLinkname("gcroot.intrinsic", "llgo.funcAddr") + ctx := context{prog: prog, goTyps: fn.Pkg.Pkg} + + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + call, ok := instr.(*ssa.Call) + if !ok { + continue + } + if got := ctx.gcRootCount(call); got != 0 { + t.Fatalf("function address root count = %d, want 0", got) + } + return + } + } + t.Fatal("function address call not found") +} + func buildGCRootSSAFunction(t *testing.T, src string) *ssa.Function { t.Helper() fset := token.NewFileSet() diff --git a/cl/instr.go b/cl/instr.go index 678c4afc6d..303e5673c8 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -869,7 +869,9 @@ func (p *context) shouldTrackCallerFrames() bool { return false } if target := p.prog.Target(); target != nil && (target.Target != "" || target.GOARCH == "wasm") { - return false + if !p.prog.WasmResumeABIEnabled() { + return false + } } return canTrackCallerFramesForPackage(p.pkg.Path()) } @@ -1485,12 +1487,19 @@ func (p *context) runtimeCallerFrameName() string { // (PushCallerLocationFrame / RecordCallerLocation / RecordPanicLocation). // The FP-chain unwinder supersedes them: physical pcs resolve through the // prebuilt ftab and pcline labels, so tracked functions keep only noinline, -// no-tail-call and the label records. The emitters stay for one release as -// an escape hatch (LLGO_SHADOW_STACK=1). +// no-tail-call and the label records. The resumable wasm ABI has no FP +// unwinder, so packages that use runtime stack APIs retain this fallback. var emitShadowStackInstrumentation = os.Getenv("LLGO_SHADOW_STACK") == "1" +func (p *context) shouldEmitShadowStackInstrumentation() bool { + if emitShadowStackInstrumentation { + return true + } + return p.prog.WasmResumeABIEnabled() +} + func (p *context) pushCallerLocationFrame(b llssa.Builder, fn *ssa.Function) { - if !emitShadowStackInstrumentation { + if !p.shouldEmitShadowStackInstrumentation() { return } if fn == nil { @@ -1516,7 +1525,7 @@ func (p *context) recordPanicLocation(b llssa.Builder, pos token.Pos) { } func (p *context) recordRuntimeLocation(b llssa.Builder, pos token.Pos, fn string) { - if !emitShadowStackInstrumentation || !p.shouldTrackCallerFrames() { + if !p.shouldEmitShadowStackInstrumentation() || !p.shouldTrackCallerFrames() { return } position := p.fset.Position(pos) diff --git a/internal/build/source_patch_test.go b/internal/build/source_patch_test.go index 9aa0b9c1b4..0c35178649 100644 --- a/internal/build/source_patch_test.go +++ b/internal/build/source_patch_test.go @@ -84,8 +84,8 @@ func logPackageErrors(t *testing.T, pkg *packages.Package, seen map[string]bool) } } -func TestWasmBytealgSourcePatchReplacesAsm(t *testing.T) { - for _, pkgPath := range []string{"internal/bytealg", "internal/chacha8rand", "internal/runtime/atomic"} { +func TestWasmSourcePatchReplacesAsm(t *testing.T) { + for _, pkgPath := range []string{"internal/bytealg", "internal/chacha8rand", "internal/runtime/atomic", "math"} { if !llruntime.HasSourcePatchPkg(pkgPath) { t.Fatalf("%s should be registered as a source patch package", pkgPath) } @@ -111,6 +111,7 @@ func TestWasmBytealgSourcePatchReplacesAsm(t *testing.T) { "internal/bytealg/indexbyte_wasm.s", "internal/chacha8rand/chacha8_stub.s", "internal/runtime/atomic/atomic_wasm.s", + "math/floor_wasm.s", } { path := filepath.Join(runtime.GOROOT(), "src", filepath.FromSlash(file)) if got := string(overlay[path]); got != "// replaced by LLGo source patch\n" { diff --git a/internal/build/testdata/wasm-resume-acceptance/abi.c b/internal/build/testdata/wasm-resume-acceptance/abi.c new file mode 100644 index 0000000000..7c1b8e1219 --- /dev/null +++ b/internal/build/testdata/wasm-resume-acceptance/abi.c @@ -0,0 +1,7 @@ +#include + +extern int32_t wasm_acceptance_export(int32_t value); + +int32_t llgo_call_wasm_acceptance_export(int32_t value) { + return wasm_acceptance_export(value); +} diff --git a/internal/build/testdata/wasm-resume-acceptance/abi.go b/internal/build/testdata/wasm-resume-acceptance/abi.go new file mode 100644 index 0000000000..b97fbc3363 --- /dev/null +++ b/internal/build/testdata/wasm-resume-acceptance/abi.go @@ -0,0 +1,8 @@ +package main + +import _ "unsafe" + +const LLGoFiles = "abi.c" + +//go:linkname callWasmAcceptanceExport C.llgo_call_wasm_acceptance_export +func callWasmAcceptanceExport(int32) int32 diff --git a/internal/build/testdata/wasm-resume-acceptance/main.go b/internal/build/testdata/wasm-resume-acceptance/main.go new file mode 100644 index 0000000000..1541c9644c --- /dev/null +++ b/internal/build/testdata/wasm-resume-acceptance/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "bytes" + "encoding/base64" + "math" + "reflect" + "regexp" + "runtime" + "strings" +) + +type payload struct { + Value int +} + +type reflectedValue struct { + base int +} + +func (value reflectedValue) Add(delta int) int { + runtime.Gosched() + return value.base + delta +} + +//go:noinline +func variadicSuspend(ready chan<- struct{}, resume <-chan struct{}, base int, values ...int) int { + live := &payload{Value: base} + ready <- struct{}{} + <-resume + runtime.GC() + result := live.Value + for _, value := range values { + result += value + } + return result +} + +func testVariadicIndirectCall() { + call := variadicSuspend + ready := make(chan struct{}) + resume := make(chan struct{}) + done := make(chan int) + go func() { + done <- call(ready, resume, 10, 1, 2, 3) + }() + <-ready + runtime.GC() + close(resume) + if got := <-done; got != 16 { + panic("variadic indirect call lost state") + } +} + +func testReflectionCalls() { + value := reflect.ValueOf(reflectedValue{base: 40}) + method := value.MethodByName("Add") + add, ok := method.Interface().(func(int) int) + if !ok || add(2) != 42 { + panic("reflected method returned the wrong value") + } + + typ := value.Type() + field, ok := typ.FieldByName("base") + methodType, hasMethod := typ.MethodByName("Add") + if !ok || field.Type.Kind() != reflect.Int || !hasMethod || methodType.Type.NumIn() != 2 { + panic("reflection metadata is incomplete") + } + methodExpr, ok := methodType.Func.Interface().(func(reflectedValue, int) int) + if !ok || methodExpr(reflectedValue{base: 41}, 1) != 42 { + panic("reflected method expression returned the wrong value") + } + + items := reflect.MakeMapWithSize(reflect.TypeOf(map[string]int{}), 1) + items.SetMapIndex(reflect.ValueOf("answer"), reflect.ValueOf(42)) + if got := items.MapIndex(reflect.ValueOf("answer")); !got.IsValid() || got.Int() != 42 { + panic("reflection map workload failed") + } +} + +//export wasm_acceptance_export +func wasm_acceptance_export(value int32) int32 { + live := &payload{Value: int(value)} + runtime.GC() + return int32(live.Value + 1) +} + +func testCExportRootEntry() { + if got := callWasmAcceptanceExport(41); got != 42 { + panic("C export root entry returned the wrong value") + } +} + +//go:noinline +func pclnProbe() bool { + runtime.Gosched() + var pcs [32]uintptr + frames := runtime.CallersFrames(pcs[:runtime.Callers(0, pcs[:])]) + for { + frame, more := frames.Next() + if strings.HasSuffix(frame.Function, ".pclnProbe") && frame.File != "" && frame.Line > 0 { + return true + } + if !more { + return false + } + } +} + +func testPCLNAfterResume() { + if !pclnProbe() { + panic("pclntab did not report the resumed Go frame") + } +} + +func testStandardLibraryWorkloads() { + input := bytes.Repeat([]byte("llgo-wasm-"), 32) + encoded := base64.StdEncoding.EncodeToString(input) + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil || !bytes.Equal(decoded, input) { + panic("encoding/base64 round trip failed") + } + matched, err := regexp.MatchString(`^llgo-(wasm|native)-[0-9]+$`, "llgo-wasm-42") + if err != nil || !matched { + panic("regexp workload failed") + } + if math.Floor(1.75) != 1 || math.Ceil(-1.75) != -1 || math.Trunc(-1.75) != -1 { + panic("math rounding workload failed") + } +} + +func main() { + testVariadicIndirectCall() + testReflectionCalls() + testCExportRootEntry() + testPCLNAfterResume() + testStandardLibraryWorkloads() + println("wasm resume acceptance ok") +} diff --git a/internal/build/testdata/wasm-resume-debug/main.go b/internal/build/testdata/wasm-resume-debug/main.go new file mode 100644 index 0000000000..49f1e98cb1 --- /dev/null +++ b/internal/build/testdata/wasm-resume-debug/main.go @@ -0,0 +1,22 @@ +package main + +import "runtime" + +type debugPayload struct { + value int +} + +//go:noinline +func resumeDebug(value int) int { + live := &debugPayload{value: value} + runtime.Gosched() + runtime.GC() + return live.value + 1 +} + +func main() { + if resumeDebug(41) != 42 { + panic("resumable debug state was not preserved") + } + println("wasm resume debug ok") +} diff --git a/internal/wasmresume/boundary.go b/internal/wasmresume/boundary.go index a22525aa6b..dd16271ab0 100644 --- a/internal/wasmresume/boundary.go +++ b/internal/wasmresume/boundary.go @@ -64,12 +64,28 @@ func IsNonSuspendingBoundary(name string) bool { switch name { case "github.com/goplus/llgo/runtime/internal/runtime.AllocU", "github.com/goplus/llgo/runtime/internal/runtime.AllocZ", + "github.com/goplus/llgo/runtime/internal/runtime.AssertNilDeref", + "github.com/goplus/llgo/runtime/internal/runtime.AssertNilDerefPtr", "github.com/goplus/llgo/runtime/internal/runtime.ClearThreadDefer", "github.com/goplus/llgo/runtime/internal/runtime.FreeDeferNode", "github.com/goplus/llgo/runtime/internal/runtime.GetThreadDefer", "github.com/goplus/llgo/runtime/internal/runtime.Goexit", + "github.com/goplus/llgo/runtime/internal/runtime.MakeMap", + "github.com/goplus/llgo/runtime/internal/runtime.MakeSmallMap", + "github.com/goplus/llgo/runtime/internal/runtime.MapAccess1", + "github.com/goplus/llgo/runtime/internal/runtime.MapAccess2", + "github.com/goplus/llgo/runtime/internal/runtime.MapAssign", + "github.com/goplus/llgo/runtime/internal/runtime.MapClear", + "github.com/goplus/llgo/runtime/internal/runtime.MapDelete", + "github.com/goplus/llgo/runtime/internal/runtime.MapIterNext", + "github.com/goplus/llgo/runtime/internal/runtime.MapLen", + "github.com/goplus/llgo/runtime/internal/runtime.NewMapIter", "github.com/goplus/llgo/runtime/internal/runtime.Panic", + "github.com/goplus/llgo/runtime/internal/runtime.PopCallerLocationFrame", + "github.com/goplus/llgo/runtime/internal/runtime.PushCallerLocationFrame", "github.com/goplus/llgo/runtime/internal/runtime.Recover", + "github.com/goplus/llgo/runtime/internal/runtime.RecordCallerLocation", + "github.com/goplus/llgo/runtime/internal/runtime.RecordPanicLocation", "github.com/goplus/llgo/runtime/internal/runtime.Rethrow", "github.com/goplus/llgo/runtime/internal/runtime.captureWasmResumeGCRoot", "github.com/goplus/llgo/runtime/internal/runtime.restoreWasmResumeGCRoot", diff --git a/internal/wasmresume/boundary_test.go b/internal/wasmresume/boundary_test.go index 66fea4fbec..e15a580fe3 100644 --- a/internal/wasmresume/boundary_test.go +++ b/internal/wasmresume/boundary_test.go @@ -42,11 +42,27 @@ func TestRuntimeBoundaries(t *testing.T) { runtimeWasmSyncUnlock, "github.com/goplus/llgo/runtime/internal/runtime.AllocU", "github.com/goplus/llgo/runtime/internal/runtime.AllocZ", + "github.com/goplus/llgo/runtime/internal/runtime.AssertNilDeref", + "github.com/goplus/llgo/runtime/internal/runtime.AssertNilDerefPtr", "github.com/goplus/llgo/runtime/internal/runtime.GetThreadDefer", + "github.com/goplus/llgo/runtime/internal/runtime.MakeMap", + "github.com/goplus/llgo/runtime/internal/runtime.MakeSmallMap", + "github.com/goplus/llgo/runtime/internal/runtime.MapAccess1", + "github.com/goplus/llgo/runtime/internal/runtime.MapAccess2", + "github.com/goplus/llgo/runtime/internal/runtime.MapAssign", + "github.com/goplus/llgo/runtime/internal/runtime.MapClear", + "github.com/goplus/llgo/runtime/internal/runtime.MapDelete", + "github.com/goplus/llgo/runtime/internal/runtime.MapIterNext", + "github.com/goplus/llgo/runtime/internal/runtime.MapLen", + "github.com/goplus/llgo/runtime/internal/runtime.NewMapIter", "github.com/goplus/llgo/runtime/internal/runtime.SetDeferGCRoot", "github.com/goplus/llgo/runtime/internal/runtime.SetThreadDefer", "github.com/goplus/llgo/runtime/internal/runtime.Panic", + "github.com/goplus/llgo/runtime/internal/runtime.PopCallerLocationFrame", + "github.com/goplus/llgo/runtime/internal/runtime.PushCallerLocationFrame", "github.com/goplus/llgo/runtime/internal/runtime.Recover", + "github.com/goplus/llgo/runtime/internal/runtime.RecordCallerLocation", + "github.com/goplus/llgo/runtime/internal/runtime.RecordPanicLocation", "github.com/goplus/llgo/runtime/internal/runtime.Rethrow", "github.com/goplus/llgo/runtime/internal/runtime.captureWasmResumeGCRoot", "github.com/goplus/llgo/runtime/internal/runtime.restoreWasmResumeGCRoot", diff --git a/internal/wasmresume/state.go b/internal/wasmresume/state.go index 8e09578fb2..8cbf1d80de 100644 --- a/internal/wasmresume/state.go +++ b/internal/wasmresume/state.go @@ -129,6 +129,10 @@ func lowerStateMachine( } originalEntry := blocks[0] blockAddresses := collectMovedBlockAddresses(fn, blocks) + if subprogram := fn.Subprogram(); !subprogram.IsNil() { + lowered.entry.SetSubprogram(subprogram) + fn.SetSubprogram(llvm.Metadata{}) + } dispatch := ctx.AddBasicBlock(lowered.entry, "dispatch") for _, block := range blocks { @@ -152,6 +156,11 @@ func lowerStateMachine( if slot.kind == slotUnwind { continue } + if slot.kind == slotParameter { + loaded := builder.CreateLoad(slot.typ, fields[slot.id], slot.value.Name()+".reload") + slot.value.ReplaceAllUsesWith(loaded) + continue + } if isStackSave(slot.value) { if err := lowerPersistentStackSave(slot.value); err != nil { return fmt.Errorf("%s: %w", fn.Name(), err) diff --git a/internal/wasmresume/state_test.go b/internal/wasmresume/state_test.go index 5135121359..9729a599f1 100644 --- a/internal/wasmresume/state_test.go +++ b/internal/wasmresume/state_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + "github.com/goplus/llgo/internal/debuginfo" "github.com/xgo-dev/llvm" ) @@ -183,6 +184,75 @@ func TestLowerPrototypeBuildsDirectCallStateMachine(t *testing.T) { } } +func TestLowerPrototypeRemapsParameterDebugValue(t *testing.T) { + ctx := llvm.NewContext() + defer ctx.Dispose() + mod := ctx.NewModule("state-debug") + defer mod.Dispose() + mod.SetTarget("wasm32-unknown-unknown") + mod.SetDataLayout("e-m:e-p:32:32-i64:64-n32:64-S128") + targetData := llvm.NewTargetData(mod.DataLayout()) + defer targetData.Dispose() + + i32 := ctx.Int32Type() + sig := llvm.FunctionType(i32, []llvm.Type{i32}, false) + callee := llvm.AddFunction(mod, "callee", sig) + markFunction(ctx, callee) + calleeBlock := ctx.AddBasicBlock(callee, "entry") + irBuilder := ctx.NewBuilder() + defer irBuilder.Dispose() + irBuilder.SetInsertPointAtEnd(calleeBlock) + irBuilder.CreateRet(callee.Param(0)) + + caller := llvm.AddFunction(mod, "caller", sig) + markFunction(ctx, caller) + caller.Param(0).SetName("input") + callerBlock := ctx.AddBasicBlock(caller, "entry") + + di := debuginfo.New(mod, debuginfo.Config{Producer: "LLGo"}) + cu := di.CompileUnit("debug.go", "/src") + file := di.File("/src/debug.go") + intType := di.CreateBasicType(llvm.DIBasicType{ + Name: "int32", SizeInBits: 32, Encoding: llvm.DW_ATE_signed, + }) + subroutine := di.CreateSubroutineType(llvm.DISubroutineType{ + File: file, Parameters: []llvm.Metadata{intType, intType}, + }) + subprogram := di.CreateFunction(cu, llvm.DIFunction{ + Name: "caller", LinkageName: "caller", File: file, Line: 1, + ScopeLine: 1, Type: subroutine, IsDefinition: true, + }) + caller.SetSubprogram(subprogram) + parameter := di.CreateParameterVariable(subprogram, llvm.DIParameterVariable{ + Name: "input", File: file, Line: 1, Type: intType, + AlwaysPreserve: true, ArgNo: 1, + }) + di.InsertValueAtEnd( + caller.Param(0), parameter, di.CreateExpression(nil), + llvm.DebugLoc{Line: 1, Scope: subprogram}, callerBlock, + ) + irBuilder.SetInsertPointAtEnd(callerBlock) + call := irBuilder.CreateCall(sig, callee, []llvm.Value{caller.Param(0)}, "called") + markCall(ctx, call) + irBuilder.CreateRet(call) + di.Finalize() + + lowered, err := lowerPrototype(mod, targetData) + if err != nil { + t.Fatal(err) + } + if len(lowered) != 1 || lowered[0].entry.Subprogram().IsNil() || !caller.Subprogram().IsNil() { + t.Fatal("source subprogram was not transferred to the resumable entry") + } + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify debug state machine: %v\n%s", err, mod.String()) + } + ir := mod.String() + if !strings.Contains(ir, "#dbg_value(i32 %input.reload") { + t.Fatalf("parameter debug value was not remapped to its frame reload:\n%s", ir) + } +} + func TestLowerEmitsWasmObject(t *testing.T) { llvm.InitializeAllTargetInfos() llvm.InitializeAllTargets() diff --git a/runtime/_patch/math/floor_wasm.go b/runtime/_patch/math/floor_wasm.go new file mode 100644 index 0000000000..aa3f242f2e --- /dev/null +++ b/runtime/_patch/math/floor_wasm.go @@ -0,0 +1,25 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build wasm + +package math + +const haveArchFloor = false + +func archFloor(x float64) float64 { + panic("not implemented") +} + +const haveArchCeil = false + +func archCeil(x float64) float64 { + panic("not implemented") +} + +const haveArchTrunc = false + +func archTrunc(x float64) float64 { + panic("not implemented") +} diff --git a/runtime/build.go b/runtime/build.go index 179e2d7323..4efbc8a948 100644 --- a/runtime/build.go +++ b/runtime/build.go @@ -94,6 +94,7 @@ var sourcePatchPkgs = map[string]struct{}{ "internal/runtime/atomic": {}, "internal/sync": {}, "iter": {}, + "math": {}, "runtime": {}, "runtime/metrics": {}, } @@ -102,4 +103,5 @@ var sourcePatchAsmPkgs = map[string]map[string]struct{}{ "internal/bytealg": {"wasm": {}}, "internal/chacha8rand": {"wasm": {}}, "internal/runtime/atomic": {"wasm": {}}, + "math": {"wasm": {}}, } diff --git a/runtime/internal/clite/ffi/_wrap/libffi_wasm_stub.c b/runtime/internal/clite/ffi/_wrap/libffi_wasm_stub.c new file mode 100644 index 0000000000..1427e90116 --- /dev/null +++ b/runtime/internal/clite/ffi/_wrap/libffi_wasm_stub.c @@ -0,0 +1,31 @@ +#include + +enum { LLGO_FFI_BAD_ABI = 2 }; + +unsigned int ffi_prep_cif(void *cif, unsigned int abi, unsigned int nargs, + void *rtype, void *atypes) { + return LLGO_FFI_BAD_ABI; +} + +unsigned int ffi_prep_cif_var(void *cif, unsigned int abi, + unsigned int nfixedargs, + unsigned int ntotalargs, void *rtype, + void *atypes) { + return LLGO_FFI_BAD_ABI; +} + +void ffi_call(void *cif, void (*fn)(void), void *rvalue, void *avalue) { + __builtin_trap(); +} + +void *llgo_ffi_closure_alloc(void **code) { + *code = NULL; + return NULL; +} + +void ffi_closure_free(void *closure) {} + +unsigned int ffi_prep_closure_loc(void *closure, void *cif, void *fn, + void *userdata, void *codeloc) { + return LLGO_FFI_BAD_ABI; +} diff --git a/runtime/internal/clite/ffi/ffi_link.go b/runtime/internal/clite/ffi/ffi_link.go index b3a908641d..045a239841 100644 --- a/runtime/internal/clite/ffi/ffi_link.go +++ b/runtime/internal/clite/ffi/ffi_link.go @@ -6,11 +6,6 @@ import ( c "github.com/goplus/llgo/runtime/internal/clite" ) -const ( - LLGoPackage = "link: $(pkg-config --libs libffi); -lffi" - LLGoFiles = "$(pkg-config --cflags libffi): _wrap/libffi.c" -) - /* ffi_status ffi_prep_cif(ffi_cif *cif, diff --git a/runtime/internal/clite/ffi/link_flags.go b/runtime/internal/clite/ffi/link_flags.go new file mode 100644 index 0000000000..8151581131 --- /dev/null +++ b/runtime/internal/clite/ffi/link_flags.go @@ -0,0 +1,8 @@ +//go:build !wasm + +package ffi + +const ( + LLGoPackage = "link: $(pkg-config --libs libffi); -lffi" + LLGoFiles = "$(pkg-config --cflags libffi): _wrap/libffi.c" +) diff --git a/runtime/internal/clite/ffi/link_flags_wasm.go b/runtime/internal/clite/ffi/link_flags_wasm.go new file mode 100644 index 0000000000..685c0d62a9 --- /dev/null +++ b/runtime/internal/clite/ffi/link_flags_wasm.go @@ -0,0 +1,5 @@ +//go:build wasm + +package ffi + +const LLGoFiles = "_wrap/libffi_wasm_stub.c" diff --git a/ssa/di.go b/ssa/di.go index f046d8601a..641500566d 100644 --- a/ssa/di.go +++ b/ssa/di.go @@ -798,6 +798,15 @@ func (b Builder) DISetCurrentDebugLocation(diScope DIScope, pos token.Position) ) } +func (b Builder) ensureCallDebugLocation(call llvm.Value) { + if b.Func.diFunc == nil || !call.InstructionDebugLoc().IsNil() { + return + } + b.impl.SetCurrentDebugLocation(0, 0, b.Func.diFunc.ll, llvm.Metadata{}) + b.impl.SetInstDebugLocation(call) + b.impl.SetCurrentDebugLocation(0, 0, llvm.Metadata{}, llvm.Metadata{}) +} + func (b Builder) DebugFunction(f Function, funcScope *types.Scope, pos token.Position, bodyPos token.Position) { b.diFuncScope = funcScope p := f diff --git a/ssa/di_debug_test.go b/ssa/di_debug_test.go index 5732096564..ec88b121d2 100644 --- a/ssa/di_debug_test.go +++ b/ssa/di_debug_test.go @@ -171,6 +171,35 @@ func TestDIGlobalIgnoresStorageLessFrontendVariable(t *testing.T) { builder.DIGlobal(pyVarExpr(Nil, "attribute"), "module.attribute", token.Position{}) } +func TestCallGetsFunctionScopeDebugFallback(t *testing.T) { + fset := token.NewFileSet() + file := fset.AddFile("fallback.go", -1, 32) + file.SetLines([]int{0, 8}) + + prog := NewProgram(&Target{OptLevel: optlevel.O0}) + defer prog.Dispose() + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + pkg := prog.NewPackage("p", "example.com/p") + pkg.InitDebug("p", "example.com/p", fset) + callee := pkg.NewFunc("example.com/p.callee", NoArgsNoRet, InGo) + caller := pkg.NewFunc("example.com/p.caller", NoArgsNoRet, InGo) + builder := caller.MakeBody(1) + defer builder.Dispose() + pos := fset.Position(file.Pos(1)) + builder.DebugFunction(caller, nil, pos, pos) + builder.impl.SetCurrentDebugLocation(0, 0, llvm.Metadata{}, llvm.Metadata{}) + call := builder.Call(callee.Expr) + builder.Return() + pkg.FinalizeDebug() + + if call.impl.InstructionDebugLoc().IsNil() { + t.Fatalf("call has no function-scope debug fallback:\n%s", pkg.Module().String()) + } + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("debug fallback module is invalid: %v\n%s", err, pkg.Module().String()) + } +} + func newDebugRuntimePackage() *types.Package { pkg := types.NewPackage(PkgRuntime, "runtime") unsafePointer := types.Typ[types.UnsafePointer] diff --git a/ssa/expr.go b/ssa/expr.go index c906346469..5e8ceab967 100644 --- a/ssa/expr.go +++ b/ssa/expr.go @@ -1253,6 +1253,7 @@ func (b Builder) Call(fn Expr, args ...Expr) (ret Expr) { } ll = b.Prog.FuncDecl(sigCtx, InC).ll ret.impl = llvm.CreateCall(b.impl, ll, fn.impl, llvmParamsEx(data, args, sigCtx.Params(), b)) + b.ensureCallDebugLocation(ret.impl) b.markWasmResumeCall(ret.impl, InGo) return ret case vkFuncPtr: @@ -1273,6 +1274,7 @@ func (b Builder) Call(fn Expr, args ...Expr) (ret Expr) { } ret.Type = b.Prog.retType(sig) ret.impl = llvm.CreateCall(b.impl, ll, fn.impl, llvmParamsEx(data, args, sig.Params(), b)) + b.ensureCallDebugLocation(ret.impl) b.markWasmResumeCall(ret.impl, b.directCallBackground(fn)) if reflectCheck.Kind&ReflectMethodByName != 0 && reflectCheck.Name == "" { nameArgIndex := len(args) - 1