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
3 changes: 2 additions & 1 deletion internal/wasmresume/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const (
resumeEntryPrefix = "__llgo_wasm_resume."
startEntryPrefix = "__llgo_wasm_start."
descriptorPrefix = "__llgo_wasm_resume_desc."
frameCloseName = "__llgo_wasm_resume_close"
compatEnterName = "__llgo_wasm_resume_compat_enter"
compatLeaveName = "__llgo_wasm_resume_compat_leave"
actionContinue = 0
actionReturn = 1
actionSuspend = 2
Expand Down
233 changes: 233 additions & 0 deletions internal/wasmresume/arena.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package wasmresume

import "github.com/xgo-dev/llvm"

// Fast paths know only the pointer-sized frameBlock prefix shared with the
// runtime. Block creation, retained-block selection, and final reclamation stay
// behind the runtime ABI.
const (
frameAllocFastName = "__llgo_wasm_resume_alloc.fast"
frameDynamicAllocFastName = "__llgo_wasm_resume_alloc_dynamic.fast"
frameFreeFastName = "__llgo_wasm_resume_free.fast"
)

func declareFrameAllocator(mod llvm.Module, abi resumeABI) llvm.Value {
return defineFastFrameAllocator(mod, abi, frameAllocFastName, frameAllocName)
}

func declareDynamicAllocator(mod llvm.Module, abi resumeABI) llvm.Value {
return defineFastFrameAllocator(
mod, abi, frameDynamicAllocFastName, frameDynamicAllocName,
)
}

func declareFrameFree(mod llvm.Module, abi resumeABI) llvm.Value {
fn := mod.NamedFunction(frameFreeFastName)
if !fn.IsNil() {
return fn
}

ctx := mod.Context()
fnType := llvm.FunctionType(ctx.VoidType(), []llvm.Type{abi.ptr, abi.ptr}, false)
fn = llvm.AddFunction(mod, frameFreeFastName, fnType)
fn.SetLinkage(llvm.InternalLinkage)
fn.AddFunctionAttr(ctx.CreateEnumAttribute(llvm.AttributeKindID("noinline"), 0))

slow := mod.NamedFunction(frameFreeName)
if slow.IsNil() {
slow = llvm.AddFunction(mod, frameFreeName, fnType)
}
blockType := frameBlockType(abi)
entry := ctx.AddBasicBlock(fn, "entry")
bounds := ctx.AddBasicBlock(fn, "bounds")
header := ctx.AddBasicBlock(fn, "header")
fast := ctx.AddBasicBlock(fn, "fast")
slowPath := ctx.AddBasicBlock(fn, "slow")

builder := ctx.NewBuilder()
defer builder.Dispose()
builder.SetInsertPointAtEnd(entry)
currentField := builder.CreateStructGEP(abi.contextType, fn.Param(0), 2, "")
block := builder.CreateLoad(abi.ptr, currentField, "frame.block")
builder.CreateCondBr(
builder.CreateICmp(llvm.IntNE, block, llvm.ConstNull(abi.ptr), ""),
bounds,
slowPath,
)

builder.SetInsertPointAtEnd(bounds)
begin := builder.CreateLoad(
abi.uintptrType, builder.CreateStructGEP(blockType, block, 2, ""), "frame.begin",
)
stackPointerField := builder.CreateStructGEP(blockType, block, 4, "")
stackPointer := builder.CreateLoad(abi.uintptrType, stackPointerField, "frame.sp")
frameAddress := builder.CreatePtrToInt(fn.Param(1), abi.uintptrType, "frame.address")
pointerSize := llvm.ConstInt(abi.uintptrType, uint64(abi.uintptrType.IntTypeWidth()/8), false)
minimum := builder.CreateAdd(begin, pointerSize, "frame.minimum")
inBounds := builder.CreateAnd(
builder.CreateICmp(llvm.IntUGE, frameAddress, minimum, ""),
builder.CreateICmp(llvm.IntULT, frameAddress, stackPointer, ""),
"frame.in.bounds",
)
builder.CreateCondBr(inBounds, header, slowPath)

builder.SetInsertPointAtEnd(header)
headerAddress := builder.CreateSub(frameAddress, pointerSize, "frame.header")
previous := builder.CreateLoad(
abi.uintptrType,
builder.CreateIntToPtr(headerAddress, abi.ptr, "frame.header.ptr"),
"frame.previous",
)
previousBlock := builder.CreateLoad(
abi.ptr, builder.CreateStructGEP(blockType, block, 0, ""), "frame.previous.block",
)
validHeader := builder.CreateAnd(
builder.CreateICmp(llvm.IntUGE, previous, begin, ""),
builder.CreateICmp(llvm.IntULT, previous, frameAddress, ""),
"frame.valid.header",
)
needsBlockPop := builder.CreateAnd(
builder.CreateICmp(llvm.IntEQ, previous, begin, ""),
builder.CreateICmp(llvm.IntNE, previousBlock, llvm.ConstNull(abi.ptr), ""),
"frame.needs.block.pop",
)
builder.CreateCondBr(
builder.CreateAnd(validHeader, builder.CreateNot(needsBlockPop, ""), "frame.fast"),
fast,
slowPath,
)

builder.SetInsertPointAtEnd(fast)
builder.CreateIntrinsic(ctx.VoidType(), llvm.LookupIntrinsicID("llvm.memset"), []llvm.Value{
builder.CreateIntToPtr(previous, abi.ptr, "frame.clear.begin"),
llvm.ConstInt(ctx.Int8Type(), 0, false),
builder.CreateSub(stackPointer, previous, "frame.clear.size"),
llvm.ConstInt(ctx.Int1Type(), 0, false),
}, "")
builder.CreateStore(previous, stackPointerField)
builder.CreateRetVoid()

builder.SetInsertPointAtEnd(slowPath)
builder.CreateCall(slow.GlobalValueType(), slow, []llvm.Value{fn.Param(0), fn.Param(1)}, "")
builder.CreateRetVoid()
return fn
}

func defineFastFrameAllocator(
mod llvm.Module,
abi resumeABI,
fastName, slowName string,
) llvm.Value {
fn := mod.NamedFunction(fastName)
if !fn.IsNil() {
return fn
}

ctx := mod.Context()
fnType := llvm.FunctionType(
abi.ptr, []llvm.Type{abi.ptr, abi.uintptrType, abi.uintptrType}, false,
)
fn = llvm.AddFunction(mod, fastName, fnType)
fn.SetLinkage(llvm.InternalLinkage)
fn.AddFunctionAttr(ctx.CreateEnumAttribute(llvm.AttributeKindID("noinline"), 0))

slow := mod.NamedFunction(slowName)
if slow.IsNil() {
slow = llvm.AddFunction(mod, slowName, fnType)
}
blockType := frameBlockType(abi)
entry := ctx.AddBasicBlock(fn, "entry")
check := ctx.AddBasicBlock(fn, "check")
fast := ctx.AddBasicBlock(fn, "fast")
slowPath := ctx.AddBasicBlock(fn, "slow")

builder := ctx.NewBuilder()
defer builder.Dispose()
builder.SetInsertPointAtEnd(entry)
currentField := builder.CreateStructGEP(abi.contextType, fn.Param(0), 2, "")
block := builder.CreateLoad(abi.ptr, currentField, "frame.block")
builder.CreateCondBr(
builder.CreateICmp(llvm.IntNE, block, llvm.ConstNull(abi.ptr), ""),
check,
slowPath,
)

builder.SetInsertPointAtEnd(check)
stackPointerField := builder.CreateStructGEP(blockType, block, 4, "")
stackPointer := builder.CreateLoad(abi.uintptrType, stackPointerField, "frame.sp")
end := builder.CreateLoad(
abi.uintptrType, builder.CreateStructGEP(blockType, block, 3, ""), "frame.end",
)
pointerSize := llvm.ConstInt(abi.uintptrType, uint64(abi.uintptrType.IntTypeWidth()/8), false)
header := builder.CreateAdd(stackPointer, pointerSize, "frame.header")
alignMask := builder.CreateSub(
fn.Param(2), llvm.ConstInt(abi.uintptrType, 1, false), "frame.align.mask",
)
padded := builder.CreateAdd(header, alignMask, "frame.padded")
negativeAlign := builder.CreateSub(
llvm.ConstNull(abi.uintptrType), fn.Param(2), "frame.negative.align",
)
frameAddress := builder.CreateAnd(padded, negativeAlign, "frame.address")
next := builder.CreateAdd(frameAddress, fn.Param(1), "frame.next")
fits := builder.CreateAnd(
builder.CreateAnd(
builder.CreateICmp(llvm.IntUGE, header, stackPointer, ""),
builder.CreateICmp(llvm.IntUGE, padded, header, ""),
"frame.header.valid",
),
builder.CreateAnd(
builder.CreateICmp(llvm.IntUGE, frameAddress, header, ""),
builder.CreateAnd(
builder.CreateICmp(llvm.IntUGE, next, frameAddress, ""),
builder.CreateICmp(llvm.IntULE, next, end, ""),
"frame.end.valid",
),
"frame.bounds.valid",
),
"frame.fits",
)
builder.CreateCondBr(fits, fast, slowPath)

builder.SetInsertPointAtEnd(fast)
headerAddress := builder.CreateSub(frameAddress, pointerSize, "frame.header.address")
builder.CreateStore(
stackPointer, builder.CreateIntToPtr(headerAddress, abi.ptr, "frame.header.ptr"),
)
builder.CreateStore(next, stackPointerField)
builder.CreateRet(builder.CreateIntToPtr(frameAddress, abi.ptr, "frame.ptr"))

builder.SetInsertPointAtEnd(slowPath)
allocated := builder.CreateCall(
slow.GlobalValueType(), slow,
[]llvm.Value{fn.Param(0), fn.Param(1), fn.Param(2)},
"frame.slow",
)
builder.CreateRet(allocated)
return fn
}

func frameBlockType(abi resumeABI) llvm.Type {
return abi.ctx.StructType([]llvm.Type{
abi.ptr,
abi.ptr,
abi.uintptrType,
abi.uintptrType,
abi.uintptrType,
}, false)
}
78 changes: 78 additions & 0 deletions internal/wasmresume/arena_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package wasmresume

import (
"testing"

"github.com/xgo-dev/llvm"
)

func TestFrameArenaABILayout(t *testing.T) {
for _, layout := range []string{
"e-m:e-p:32:32-i64:64-n32:64-S128",
"e-m:e-p:64:64-i64:64-n32:64-S128",
} {
t.Run(layout, func(t *testing.T) {
ctx := llvm.NewContext()
defer ctx.Dispose()
targetData := llvm.NewTargetData(layout)
defer targetData.Dispose()
abi := newResumeABI(ctx, targetData)
pointerSize := uint64(targetData.PointerSize())

if got, want := targetData.TypeAllocSize(abi.contextType), 3*pointerSize; got != want {
t.Fatalf("Context size = %d, want %d", got, want)
}
if got, want := targetData.ElementOffset(abi.contextType, 2), 2*pointerSize; got != want {
t.Fatalf("Context storage offset = %d, want %d", got, want)
}

block := frameBlockType(abi)
if got, want := targetData.TypeAllocSize(block), 5*pointerSize; got != want {
t.Fatalf("frameBlock size = %d, want %d", got, want)
}
for field := range 5 {
if got, want := targetData.ElementOffset(block, field), uint64(field)*pointerSize; got != want {
t.Fatalf("frameBlock field %d offset = %d, want %d", field, got, want)
}
}
})
}
}

func TestFrameArenaFastPathsAreNotInlined(t *testing.T) {
ctx := llvm.NewContext()
defer ctx.Dispose()
mod := ctx.NewModule("frame-arena-fast-paths")
defer mod.Dispose()
targetData := llvm.NewTargetData("e-m:e-p:32:32-i64:64-n32:64-S128")
defer targetData.Dispose()
abi := newResumeABI(ctx, targetData)

functions := []llvm.Value{
declareFrameAllocator(mod, abi),
declareDynamicAllocator(mod, abi),
declareFrameFree(mod, abi),
}
kind := llvm.AttributeKindID("noinline")
for _, fn := range functions {
if fn.GetEnumFunctionAttribute(kind).IsNil() {
t.Errorf("%s is missing the noinline attribute", fn.Name())
}
}
}
6 changes: 4 additions & 2 deletions internal/wasmresume/boundary.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const (
runtimeFrameAlloc = "__llgo_wasm_resume_alloc"
runtimeDynamicAlloc = "__llgo_wasm_resume_alloc_dynamic"
runtimeFrameFree = "__llgo_wasm_resume_free"
runtimeFrameClose = "__llgo_wasm_resume_close"
runtimeCompatEnter = "__llgo_wasm_resume_compat_enter"
runtimeCompatLeave = "__llgo_wasm_resume_compat_leave"
)

// IsRuntimeABIImplementation reports functions which implement the resumable
Expand Down Expand Up @@ -71,5 +72,6 @@ func IsNonSuspendingBoundary(name string) bool {
name == runtimeFrameAlloc ||
name == runtimeDynamicAlloc ||
name == runtimeFrameFree ||
name == runtimeFrameClose
name == runtimeCompatEnter ||
name == runtimeCompatLeave
}
3 changes: 2 additions & 1 deletion internal/wasmresume/boundary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ func TestRuntimeBoundaries(t *testing.T) {
runtimeFrameAlloc,
runtimeDynamicAlloc,
runtimeFrameFree,
runtimeFrameClose,
runtimeCompatEnter,
runtimeCompatLeave,
runtimeRunWasmResumeContext,
"github.com/goplus/llgo/runtime/internal/runtime.AllocU",
"github.com/goplus/llgo/runtime/internal/runtime.AllocZ",
Expand Down
12 changes: 9 additions & 3 deletions internal/wasmresume/compat.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ func emitCompatibilityWrapper(
llvm.ConstInt(abi.uintptrType, targetData.TypeAllocSize(abi.contextType), false),
llvm.ConstInt(ctx.Int1Type(), 0, false),
}, "")
owner := builder.CreateCall(
declareCompatEnter(mod, abi).GlobalValueType(),
declareCompatEnter(mod, abi),
[]llvm.Value{context},
"resume.arena.owner",
)
builder.CreateIntrinsic(ctx.VoidType(), llvm.LookupIntrinsicID("llvm.memset"), []llvm.Value{
root,
llvm.ConstInt(ctx.Int8Type(), 0, false),
Expand Down Expand Up @@ -123,9 +129,9 @@ func emitCompatibilityWrapper(

builder.SetInsertPointAtEnd(finished)
builder.CreateCall(
declareFrameClose(mod, abi).GlobalValueType(),
declareFrameClose(mod, abi),
[]llvm.Value{context},
declareCompatLeave(mod, abi).GlobalValueType(),
declareCompatLeave(mod, abi),
[]llvm.Value{context, owner},
"",
)
if lowered.layout.plan.resultSlot == 0 {
Expand Down
10 changes: 0 additions & 10 deletions internal/wasmresume/dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,3 @@ func isCallToIntrinsic(value llvm.Value, name string) bool {
return !callee.IsAFunction().IsNil() &&
(callee.Name() == name || strings.HasPrefix(callee.Name(), name+"."))
}

func declareDynamicAllocator(mod llvm.Module, abi resumeABI) llvm.Value {
fn := mod.NamedFunction(frameDynamicAllocName)
if fn.IsNil() {
fn = llvm.AddFunction(mod, frameDynamicAllocName, llvm.FunctionType(
abi.ptr, []llvm.Type{abi.ptr, abi.uintptrType, abi.uintptrType}, false,
))
}
return fn
}
Loading
Loading