From 39a105dab9a558a63475b86af8bd1c46095e5c9b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 14 Jul 2026 06:22:02 -0700 Subject: [PATCH] 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. --- compiler/channel.go | 48 ++++++++---------------- compiler/compiler.go | 87 +++++++++++++++++++++++++++++++++++-------- compiler/goroutine.go | 6 ++- compiler/map.go | 37 ++++++------------ 4 files changed, 104 insertions(+), 74 deletions(-) diff --git a/compiler/channel.go b/compiler/channel.go index 82139de8b..444c1f2b1 100644 --- a/compiler/channel.go +++ b/compiler/channel.go @@ -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 +} diff --git a/compiler/compiler.go b/compiler/compiler.go index f51a9871c..90776b892 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -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 diff --git a/compiler/goroutine.go b/compiler/goroutine.go index b3e02c200..3d2dac49b 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -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. diff --git a/compiler/map.go b/compiler/map.go index 4f9ec66ea..b2478b19c 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -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"