compiler: centralize loads and stores of SSA values

Map, channel, index, and goroutine lowering each create allocas, store
SSA values into them, load results, and emit lifetime intrinsics.

Add helpers for these operations and use runtimeValueResult for runtime
calls that write a value and an optional comma-ok result. The generated
LLVM IR is unchanged.
This commit is contained in:
Jake Bailey
2026-07-14 06:22:02 -07:00
committed by Damian Gryski
parent aa914bea5c
commit 39a105dab9
4 changed files with 104 additions and 74 deletions
+15 -33
View File
@@ -30,7 +30,6 @@ func (b *builder) createMakeChan(expr *ssa.MakeChan) llvm.Value {
// actual channel send operation during goroutine lowering.
func (b *builder) createChanSend(instr *ssa.Send) {
ch := b.getValue(instr.Chan, getPos(instr))
chanValue := b.getValue(instr.X, getPos(instr))
// store value-to-send
valueType := b.getLLVMType(instr.X.Type())
@@ -39,8 +38,7 @@ func (b *builder) createChanSend(instr *ssa.Send) {
if isZeroSize {
valueAlloca = llvm.ConstNull(b.dataPtrType)
} else {
valueAlloca, valueAllocaSize = b.createTemporaryAlloca(valueType, "chan.value")
b.CreateStore(chanValue, valueAlloca)
valueAlloca, valueAllocaSize = b.getValueStorage(instr.X, "chan.value")
}
// Allocate buffer for the channel operation.
@@ -65,38 +63,17 @@ func (b *builder) createChanRecv(unop *ssa.UnOp) llvm.Value {
valueType := b.getLLVMType(unop.X.Type().Underlying().(*types.Chan).Elem())
ch := b.getValue(unop.X, getPos(unop))
// Allocate memory to receive into.
isZeroSize := b.targetData.TypeAllocSize(valueType) == 0
var valueAlloca, valueAllocaSize llvm.Value
if isZeroSize {
valueAlloca = llvm.ConstNull(b.dataPtrType)
} else {
valueAlloca, valueAllocaSize = b.createTemporaryAlloca(valueType, "chan.value")
}
result := b.createRuntimeValueResult(valueType, unop.CommaOk, true, "chan")
// Allocate buffer for the channel operation.
channelOp := b.getLLVMRuntimeType("channelOp")
channelOpAlloca, channelOpAllocaSize := b.createTemporaryAlloca(channelOp, "chan.op")
// Do the receive.
commaOk := b.createRuntimeCall("chanRecv", []llvm.Value{ch, valueAlloca, channelOpAlloca}, "")
var received llvm.Value
if isZeroSize {
received = llvm.ConstNull(valueType)
} else {
received = b.CreateLoad(valueType, valueAlloca, "chan.received")
b.emitLifetimeEnd(valueAlloca, valueAllocaSize)
}
commaOk := b.createRuntimeCall("chanRecv", []llvm.Value{ch, result.valuePtr, channelOpAlloca}, "")
received := result.finish(b, commaOk, "chan.received")
b.emitLifetimeEnd(channelOpAlloca, channelOpAllocaSize)
if unop.CommaOk {
tuple := llvm.Undef(b.ctx.StructType([]llvm.Type{valueType, b.ctx.Int1Type()}, false))
tuple = b.CreateInsertValue(tuple, received, 0, "")
tuple = b.CreateInsertValue(tuple, commaOk, 1, "")
return tuple
} else {
return received
}
return received
}
// createChanClose closes the given channel.
@@ -170,9 +147,7 @@ func (b *builder) createSelect(expr *ssa.Select) llvm.Value {
case types.SendOnly:
// Store this value in an alloca and put a pointer to this alloca
// in the send state.
sendValue := b.getValue(state.Send, state.Pos)
alloca := llvmutil.CreateEntryBlockAlloca(b.Builder, sendValue.Type(), "select.send.value")
b.CreateStore(sendValue, alloca)
alloca := b.getSelectSendStorage(state.Send)
selectState = b.CreateInsertValue(selectState, alloca, 1, "")
default:
panic("unreachable")
@@ -280,7 +255,14 @@ func (b *builder) getChanSelectResult(expr *ssa.Extract) llvm.Value {
// receive can proceed at a time) so we'll get that alloca, bitcast
// it to the correct type, and dereference it.
recvbuf := b.selectRecvBuf[expr.Tuple.(*ssa.Select)]
typ := b.getLLVMType(expr.Type())
return b.CreateLoad(typ, recvbuf, "")
return b.loadFromStorage(recvbuf, expr.Type(), "select.received")
}
}
func (b *builder) getSelectSendStorage(value ssa.Value) llvm.Value {
typ := b.getLLVMType(value.Type())
llvmValue := b.getValue(value, getPos(value))
ptr := llvmutil.CreateEntryBlockAlloca(b.Builder, typ, "select.send.value")
b.CreateStore(llvmValue, ptr)
return ptr
}
+72 -15
View File
@@ -1546,10 +1546,8 @@ func (b *builder) createInstruction(instr ssa.Instruction) {
b.CreateBr(blockJump)
case *ssa.MapUpdate:
m := b.getValue(instr.Map, getPos(instr))
key := b.getValue(instr.Key, getPos(instr))
value := b.getValue(instr.Value, getPos(instr))
mapType := instr.Map.Type().Underlying().(*types.Map)
b.createMapUpdate(mapType.Key(), m, key, value, instr.Pos())
b.createMapUpdate(mapType.Key(), m, instr.Key, instr.Value, instr.Pos())
case *ssa.Panic:
value := b.getValue(instr.X, getPos(instr))
b.createRuntimeInvoke("_panic", []llvm.Value{value}, "")
@@ -1584,18 +1582,80 @@ func (b *builder) createInstruction(instr ssa.Instruction) {
b.createChanSend(instr)
case *ssa.Store:
llvmAddr := b.getValue(instr.Addr, getPos(instr))
llvmVal := b.getValue(instr.Val, getPos(instr))
b.createNilCheck(instr.Addr, llvmAddr, "store")
if b.targetData.TypeAllocSize(llvmVal.Type()) == 0 {
llvmType := b.getLLVMType(instr.Val.Type())
if b.targetData.TypeAllocSize(llvmType) == 0 {
// nothing to store
return
}
b.CreateStore(llvmVal, llvmAddr)
b.storeValue(llvmAddr, instr.Val)
default:
b.addError(instr.Pos(), "unknown instruction: "+instr.String())
}
}
func (b *builder) storeValue(dst llvm.Value, value ssa.Value) {
b.CreateStore(b.getValue(value, getPos(value)), dst)
}
func (b *builder) loadFromStorage(ptr llvm.Value, typ types.Type, name string) llvm.Value {
return b.CreateLoad(b.getLLVMType(typ), ptr, name)
}
func (b *builder) getValueStorage(value ssa.Value, name string) (ptr, size llvm.Value) {
typ := b.getLLVMType(value.Type())
ptr, size = b.createTemporaryAlloca(typ, name)
b.storeValue(ptr, value)
return ptr, size
}
type runtimeValueResult struct {
valueType llvm.Type
resultType llvm.Type
valuePtr llvm.Value
valueSize llvm.Value
temporary bool
zero bool
commaOk bool
}
func (b *builder) createRuntimeValueResult(valueType llvm.Type, commaOk, zeroAsNull bool, name string) runtimeValueResult {
result := runtimeValueResult{
valueType: valueType,
resultType: valueType,
commaOk: commaOk,
}
if commaOk {
result.resultType = b.ctx.StructType([]llvm.Type{valueType, b.ctx.Int1Type()}, false)
}
if zeroAsNull && b.targetData.TypeAllocSize(valueType) == 0 {
result.valuePtr = llvm.ConstNull(b.dataPtrType)
result.zero = true
return result
}
result.valuePtr, result.valueSize = b.createTemporaryAlloca(valueType, name+".value")
result.temporary = true
return result
}
func (r runtimeValueResult) finish(b *builder, commaOk llvm.Value, name string) llvm.Value {
var value llvm.Value
if r.zero {
value = llvm.ConstNull(r.valueType)
} else {
value = b.CreateLoad(r.valueType, r.valuePtr, name)
}
if r.temporary {
b.emitLifetimeEnd(r.valuePtr, r.valueSize)
}
if !r.commaOk {
return value
}
result := llvm.Undef(r.resultType)
result = b.CreateInsertValue(result, value, 0, "")
return b.CreateInsertValue(result, commaOk, 1, "")
}
// createBuiltin lowers a builtin Go function (append, close, delete, etc.) to
// LLVM IR. It uses runtime calls for some builtins.
func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, callName string, pos token.Pos) (llvm.Value, error) {
@@ -2270,11 +2330,11 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) {
case *ssa.Global:
panic("global is not an expression")
case *ssa.Index:
collection := b.getValue(expr.X, getPos(expr))
index := b.getValue(expr.Index, getPos(expr))
switch xType := expr.X.Type().Underlying().(type) {
case *types.Basic: // extract byte from string
collection := b.getValue(expr.X, getPos(expr))
// Value type must be a string, which is a basic type.
if xType.Info()&types.IsString == 0 {
panic("lookup on non-string?")
@@ -2308,12 +2368,11 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) {
// Can't load directly from array (as index is non-constant), so
// have to do it using an alloca+gep+load.
arrayType := collection.Type()
alloca, allocaSize := b.createTemporaryAlloca(arrayType, "index.alloca")
b.CreateStore(collection, alloca)
arrayType := b.getLLVMType(expr.X.Type())
alloca, allocaSize := b.getValueStorage(expr.X, "index.alloca")
zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false)
ptr := b.CreateInBoundsGEP(arrayType, alloca, []llvm.Value{zero, index}, "index.gep")
result := b.CreateLoad(arrayType.ElementType(), ptr, "index.load")
result := b.loadFromStorage(ptr, expr.Type(), "index.load")
b.emitLifetimeEnd(alloca, allocaSize)
return result, nil
default:
@@ -2373,12 +2432,11 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) {
}
case *ssa.Lookup: // map lookup
value := b.getValue(expr.X, getPos(expr))
index := b.getValue(expr.Index, getPos(expr))
valueType := expr.Type()
if expr.CommaOk {
valueType = valueType.(*types.Tuple).At(0).Type()
}
return b.createMapLookup(expr.X.Type().Underlying().(*types.Map).Key(), valueType, value, index, expr.CommaOk, expr.Pos())
return b.createMapLookup(expr.X.Type().Underlying().(*types.Map).Key(), valueType, value, expr.Index, expr.CommaOk, expr.Pos())
case *ssa.MakeChan:
return b.createMakeChan(expr), nil
case *ssa.MakeClosure:
@@ -3453,8 +3511,7 @@ func (b *builder) createUnOp(unop *ssa.UnOp) (llvm.Value, error) {
return fn, nil
} else {
b.createNilCheck(unop.X, x, "deref")
load := b.CreateLoad(valueType, x, "")
return load, nil
return b.loadFromStorage(x, unop.Type(), ""), nil
}
case token.XOR: // ^x, toggle all bits in integer
return b.CreateXor(x, llvm.ConstInt(x.Type(), ^uint64(0), false), ""), nil
+5 -1
View File
@@ -50,7 +50,7 @@ func (b *builder) createGo(instr *ssa.Go) {
// Get all function parameters to pass to the goroutine.
var params []llvm.Value
for _, param := range instr.Call.Args {
params = append(params, b.expandFormalParam(b.getValue(param, getPos(instr)))...)
params = append(params, b.getGoroutineCallArgument(param)...)
}
var prefix string
@@ -122,6 +122,10 @@ func (b *builder) createGo(instr *ssa.Go) {
b.createCall(fnType, start, []llvm.Value{callee, paramBundle, stackSize, llvm.Undef(b.dataPtrType)}, "")
}
func (b *builder) getGoroutineCallArgument(value ssa.Value) []llvm.Value {
return b.expandFormalParam(b.getValue(value, getPos(value)))
}
// Create an exported wrapper function for functions with the //go:wasmexport
// pragma. This wrapper function is quite complex when the scheduler is enabled:
// it needs to start a new goroutine each time the exported function is called.
+12 -25
View File
@@ -72,19 +72,20 @@ func (b *builder) getRuntimeFunctionValue(name string, sig *types.Signature) llv
// createMapLookup returns the value in a map. It calls a runtime function
// depending on the map key type to load the map value and its comma-ok value.
func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Value, commaOk bool, pos token.Pos) (llvm.Value, error) {
func (b *builder) createMapLookup(keyType, valueType types.Type, m llvm.Value, key ssa.Value, commaOk bool, pos token.Pos) (llvm.Value, error) {
llvmValueType := b.getLLVMType(valueType)
// Allocate the memory for the resulting type. Do not zero this memory: it
// will be zeroed by the hashmap get implementation if the key is not
// present in the map.
mapValueAlloca, mapValueAllocaSize := b.createTemporaryAlloca(llvmValueType, "hashmap.value")
result := b.createRuntimeValueResult(llvmValueType, commaOk, false, "hashmap")
mapValueAlloca := result.valuePtr
// We need the map size (with type uintptr) to pass to the hashmap*Get
// functions. This is necessary because those *Get functions are valid on
// nil maps, and they'll need to zero the value pointer by that number of
// bytes.
mapValueSize := mapValueAllocaSize
mapValueSize := result.valueSize
if mapValueSize.Type().IntTypeWidth() > b.uintptrType.IntTypeWidth() {
mapValueSize = llvm.ConstTrunc(mapValueSize, b.uintptrType)
}
@@ -94,13 +95,12 @@ func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Val
keyType = keyType.Underlying()
if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 {
// key is a string
params := []llvm.Value{m, key, mapValueAlloca, mapValueSize}
params := []llvm.Value{m, b.getValue(key, getPos(key)), mapValueAlloca, mapValueSize}
commaOkValue = b.createRuntimeCall("hashmapStringGet", params, "")
} else {
// Key stored at actual type: either binary-comparable or with
// compiler-generated hash/equal.
mapKeyAlloca, mapKeySize := b.createTemporaryAlloca(key.Type(), "hashmap.key")
b.CreateStore(key, mapKeyAlloca)
mapKeyAlloca, mapKeySize := b.getValueStorage(key, "hashmap.key")
params := []llvm.Value{m, mapKeyAlloca, mapValueAlloca, mapValueSize}
fnName := "hashmapBinaryGet"
if !hashmapIsBinaryKey(keyType) {
@@ -110,35 +110,22 @@ func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Val
b.emitLifetimeEnd(mapKeyAlloca, mapKeySize)
}
// Load the resulting value from the hashmap. The value is set to the zero
// value if the key doesn't exist in the hashmap.
mapValue := b.CreateLoad(llvmValueType, mapValueAlloca, "")
b.emitLifetimeEnd(mapValueAlloca, mapValueAllocaSize)
if commaOk {
tuple := llvm.Undef(b.ctx.StructType([]llvm.Type{llvmValueType, b.ctx.Int1Type()}, false))
tuple = b.CreateInsertValue(tuple, mapValue, 0, "")
tuple = b.CreateInsertValue(tuple, commaOkValue, 1, "")
return tuple, nil
} else {
return mapValue, nil
}
// The value is set to the zero value if the key doesn't exist.
return result.finish(b, commaOkValue, ""), nil
}
// createMapUpdate updates a map key to a given value, by creating an
// appropriate runtime call.
func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, pos token.Pos) {
valueAlloca, valueSize := b.createTemporaryAlloca(value.Type(), "hashmap.value")
b.CreateStore(value, valueAlloca)
func (b *builder) createMapUpdate(keyType types.Type, m llvm.Value, key, value ssa.Value, pos token.Pos) {
valueAlloca, valueSize := b.getValueStorage(value, "hashmap.value")
keyType = keyType.Underlying()
if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 {
// key is a string
params := []llvm.Value{m, key, valueAlloca}
params := []llvm.Value{m, b.getValue(key, getPos(key)), valueAlloca}
b.createRuntimeInvoke("hashmapStringSet", params, "")
} else {
// Key stored at actual type.
keyAlloca, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key")
b.CreateStore(key, keyAlloca)
keyAlloca, keySize := b.getValueStorage(key, "hashmap.key")
fnName := "hashmapBinarySet"
if !hashmapIsBinaryKey(keyType) {
fnName = "hashmapGenericSet"