Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ jobs:
GOOS=js GOARCH=wasm llgo build -o "$RUNNER_TEMP/runtime-js.wasm" ./internal/build/testdata/wasm-runtime
GOOS=wasip1 GOARCH=wasm llgo build -o "$RUNNER_TEMP/runtime-wasip1.wasm" ./internal/build/testdata/wasm-runtime
LLGO_WASI_THREADS=1 GOOS=wasip1 GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/runtime-wasip1-threads.wasm" ./internal/build/testdata/wasm-runtime
-o "$RUNNER_TEMP/runtime-wasip1-threads.wasm" ./internal/build/testdata/wasm-blocking
test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/runtime-wasip1.wasm" 2>&1)" = "wasip1"
GOOS=js GOARCH=wasm llgo build -o "$RUNNER_TEMP/wasm-scheduler-go.mjs" ./internal/build/testdata/wasm-scheduler
run_wasm_scheduler "$RUNNER_TEMP/wasm-scheduler-go.mjs"
Expand Down
33 changes: 33 additions & 0 deletions internal/build/source_patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,39 @@ func boolToUint8(bool) uint8
}
}

func TestBuildSourcePatchOverlayForGo124HashTrieMap(t *testing.T) {
goroot := t.TempDir()
syncDir := filepath.Join(goroot, "src", "internal", "sync")
hashTrieMap := filepath.Join(syncDir, "hashtriemap.go")
mustWriteFile(t, hashTrieMap, `package sync

type HashTrieMap[K comparable, V any] struct{}

func (ht *HashTrieMap[K, V]) CompareAndSwap(key K, old, new V) bool {
return false
}
`)

overlay, _, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), goroot, sourcePatchBuildContext{
goos: runtime.GOOS,
goarch: runtime.GOARCH,
goversion: "go1.24.2",
})
if err != nil {
t.Fatal(err)
}

patch := filepath.Join(syncDir, "z_llgo_patch_hashtriemap.go")
if src, ok := overlay[patch]; !ok {
t.Fatalf("missing source patch file %s", patch)
} else if !strings.Contains(string(src), "type HashTrieMap") {
t.Fatalf("source patch file %s does not contain HashTrieMap replacement", patch)
}
if stdSrc := string(overlay[hashTrieMap]); strings.Contains(stdSrc, "type HashTrieMap") {
t.Fatalf("stub overlay for internal/sync still contains HashTrieMap: %s", stdSrc)
}
}

func TestGo126PayloadsUseSourcePatchInsteadOfAltPkg(t *testing.T) {
for _, pkgPath := range []string{"internal/sync", "crypto/internal/constanttime"} {
if !llruntime.HasSourcePatchPkg(pkgPath) {
Expand Down
63 changes: 63 additions & 0 deletions internal/build/testdata/wasm-blocking/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"sync"
"sync/atomic"
)

func main() {
values := make(chan int)
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(1)
go func() {
mu.Lock()
values <- 42
mu.Unlock()
wg.Done()
}()
if value := <-values; value != 42 {
panic("channel value mismatch")
}
wg.Wait()

var rw sync.RWMutex
rw.RLock()
rw.RUnlock()
rw.Lock()
rw.Unlock()

cond := sync.NewCond(&mu)
started := make(chan struct{})
ready := false
wg.Add(1)
go func() {
mu.Lock()
close(started)
for !ready {
cond.Wait()
}
mu.Unlock()
wg.Done()
}()
<-started
mu.Lock()
ready = true
cond.Signal()
mu.Unlock()
wg.Wait()

var once sync.Once
once.Do(func() {})
var pool sync.Pool
pool.Put("value")
if pool.Get() != "value" {
panic("sync.Pool value mismatch")
}
var atomicValue atomic.Value
atomicValue.Store("value")
if atomicValue.Load() != "value" {
panic("atomic.Value mismatch")
}
println("wasm blocking primitives ok")
}
245 changes: 245 additions & 0 deletions internal/build/testdata/wasm-scheduler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"runtime"
"sync"
"sync/atomic"
"unsafe"
)

Expand Down Expand Up @@ -137,9 +139,252 @@ func main() {
panic("not all goroutines ran")
}
testGoroutineLifecycle()
testBlockingPrimitives()
println("wasm scheduler ok")
}

func testBlockingPrimitives() {
testChannelsAndSelect()
testWaitGroup()
testMutexes()
testCond()
testSyncHelpers()
testSyncMap()
}

func testChannelsAndSelect() {
values := make(chan int)
ack := make(chan struct{})
go func() {
values <- 41
close(ack)
}()
if value := <-values; value != 41 {
panic("unexpected channel value")
}
<-ack

buffered := make(chan int, 2)
buffered <- 1
buffered <- 2
if len(buffered) != 2 || cap(buffered) != 2 {
panic("buffered channel size mismatch")
}
if <-buffered != 1 || <-buffered != 2 {
panic("buffered channel order mismatch")
}

left := make(chan int)
right := make(chan int)
go func() {
right <- 42
}()
select {
case <-left:
panic("select chose a blocked channel")
case value := <-right:
if value != 42 {
panic("unexpected select value")
}
}

selected := make(chan int)
go func() {
select {
case value := <-left:
selected <- value
case value := <-right:
selected <- value
}
}()
runtime.Gosched()
left <- 43
if value := <-selected; value != 43 {
panic("blocked select chose the wrong channel")
}

select {
case <-right:
panic("non-blocking select chose a blocked channel")
default:
}

close(right)
if value, ok := <-right; value != 0 || ok {
panic("closed channel receive mismatch")
}
}

func testWaitGroup() {
var wg sync.WaitGroup
count := 0
wg.Add(2)
go func() {
count++
wg.Done()
}()
go func() {
runtime.Gosched()
count++
wg.Done()
}()
wg.Wait()
if count != 2 {
panic("WaitGroup returned too early")
}

wg.Add(1)
go wg.Done()
wg.Wait()
}

func testMutexes() {
var mu sync.Mutex
started := make(chan struct{})
finished := make(chan struct{})
value := 0
mu.Lock()
go func() {
close(started)
mu.Lock()
value = 1
mu.Unlock()
close(finished)
}()
<-started
mu.Unlock()
<-finished
if value != 1 {
panic("Mutex waiter did not run")
}
if !mu.TryLock() {
panic("Mutex.TryLock failed")
}
mu.Unlock()

var rw sync.RWMutex
rw.RLock()
finished = make(chan struct{})
go func() {
rw.Lock()
value = 2
rw.Unlock()
close(finished)
}()
runtime.Gosched()
rw.RUnlock()
<-finished
if value != 2 {
panic("RWMutex waiter did not run")
}
}

func testCond() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
arrived := make(chan struct{}, 2)
done := make(chan struct{}, 2)
ready := false
for i := 0; i < 2; i++ {
go func() {
mu.Lock()
arrived <- struct{}{}
for !ready {
cond.Wait()
}
mu.Unlock()
done <- struct{}{}
}()
}
<-arrived
<-arrived

mu.Lock()
ready = true
cond.Signal()
mu.Unlock()
<-done
select {
case <-done:
panic("Cond.Signal woke more than one waiter")
default:
}

mu.Lock()
cond.Broadcast()
mu.Unlock()
<-done
}

func testSyncHelpers() {
var once sync.Once
var wg sync.WaitGroup
count := 0
wg.Add(2)
for i := 0; i < 2; i++ {
go func() {
once.Do(func() {
count++
})
wg.Done()
}()
}
wg.Wait()
if count != 1 {
panic("sync.Once ran more than once")
}

var pool sync.Pool
pool.Put("pooled")
if value := pool.Get(); value != "pooled" {
panic("sync.Pool value mismatch")
}

var value atomic.Value
value.Store("before")
if old := value.Swap("after"); old != "before" || value.Load() != "after" {
panic("atomic.Value swap mismatch")
}
}

func testSyncMap() {
var m sync.Map
if _, loaded := m.LoadOrStore("key", 1); loaded {
panic("sync.Map unexpectedly loaded a missing key")
}
if value, loaded := m.Load("key"); !loaded || value != 1 {
panic("sync.Map load mismatch")
}
if !m.CompareAndSwap("key", 1, 2) {
panic("sync.Map compare-and-swap failed")
}
if previous, loaded := m.Swap("key", 3); !loaded || previous != 2 {
panic("sync.Map swap mismatch")
}
count := 0
m.Range(func(key, value any) bool {
if key != "key" || value != 3 {
panic("sync.Map range mismatch")
}
count++
return true
})
if count != 1 {
panic("sync.Map range count mismatch")
}
if !m.CompareAndDelete("key", 3) {
panic("sync.Map compare-and-delete failed")
}
if _, loaded := m.LoadAndDelete("key"); loaded {
panic("sync.Map retained a deleted key")
}
m.Store("clear", 4)
m.Clear()
if _, loaded := m.Load("clear"); loaded {
panic("sync.Map clear failed")
}
}

func testGoroutineLifecycle() {
const count = 5000
for i := 1; i <= count; i++ {
Expand Down
Loading
Loading