frontend.go

  1// Package frontend implements the translation of WebAssembly to SSA IR using the ssa package.
  2package frontend
  3
  4import (
  5	"bytes"
  6	"math"
  7
  8	"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
  9	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
 10	"github.com/tetratelabs/wazero/internal/wasm"
 11)
 12
 13// Compiler is in charge of lowering Wasm to SSA IR, and does the optimization
 14// on top of it in architecture-independent way.
 15type Compiler struct {
 16	// Per-module data that is used across all functions.
 17
 18	m      *wasm.Module
 19	offset *wazevoapi.ModuleContextOffsetData
 20	// ssaBuilder is a ssa.Builder used by this frontend.
 21	ssaBuilder             ssa.Builder
 22	signatures             map[*wasm.FunctionType]*ssa.Signature
 23	listenerSignatures     map[*wasm.FunctionType][2]*ssa.Signature
 24	memoryGrowSig          ssa.Signature
 25	memoryWait32Sig        ssa.Signature
 26	memoryWait64Sig        ssa.Signature
 27	memoryNotifySig        ssa.Signature
 28	checkModuleExitCodeSig ssa.Signature
 29	tableGrowSig           ssa.Signature
 30	refFuncSig             ssa.Signature
 31	memmoveSig             ssa.Signature
 32	ensureTermination      bool
 33
 34	// Followings are reset by per function.
 35
 36	// wasmLocalToVariable maps the index (considered as wasm.Index of locals)
 37	// to the corresponding ssa.Variable.
 38	wasmLocalToVariable                   [] /* local index to */ ssa.Variable
 39	wasmLocalFunctionIndex                wasm.Index
 40	wasmFunctionTypeIndex                 wasm.Index
 41	wasmFunctionTyp                       *wasm.FunctionType
 42	wasmFunctionLocalTypes                []wasm.ValueType
 43	wasmFunctionBody                      []byte
 44	wasmFunctionBodyOffsetInCodeSection   uint64
 45	memoryBaseVariable, memoryLenVariable ssa.Variable
 46	needMemory                            bool
 47	memoryShared                          bool
 48	globalVariables                       []ssa.Variable
 49	globalVariablesTypes                  []ssa.Type
 50	mutableGlobalVariablesIndexes         []wasm.Index // index to ^.
 51	needListener                          bool
 52	needSourceOffsetInfo                  bool
 53	// br is reused during lowering.
 54	br            *bytes.Reader
 55	loweringState loweringState
 56
 57	knownSafeBounds    [] /* ssa.ValueID to */ knownSafeBound
 58	knownSafeBoundsSet []ssa.ValueID
 59
 60	knownSafeBoundsAtTheEndOfBlocks   [] /* ssa.BlockID to */ knownSafeBoundsAtTheEndOfBlock
 61	varLengthKnownSafeBoundWithIDPool wazevoapi.VarLengthPool[knownSafeBoundWithID]
 62
 63	execCtxPtrValue, moduleCtxPtrValue ssa.Value
 64
 65	// Following are reused for the known safe bounds analysis.
 66
 67	pointers []int
 68	bounds   [][]knownSafeBoundWithID
 69}
 70
 71type (
 72	// knownSafeBound represents a known safe bound for a value.
 73	knownSafeBound struct {
 74		// bound is a constant upper bound for the value.
 75		bound uint64
 76		// absoluteAddr is the absolute address of the value.
 77		absoluteAddr ssa.Value
 78	}
 79	// knownSafeBoundWithID is a knownSafeBound with the ID of the value.
 80	knownSafeBoundWithID struct {
 81		knownSafeBound
 82		id ssa.ValueID
 83	}
 84	knownSafeBoundsAtTheEndOfBlock = wazevoapi.VarLength[knownSafeBoundWithID]
 85)
 86
 87var knownSafeBoundsAtTheEndOfBlockNil = wazevoapi.NewNilVarLength[knownSafeBoundWithID]()
 88
 89// NewFrontendCompiler returns a frontend Compiler.
 90func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoapi.ModuleContextOffsetData, ensureTermination bool, listenerOn bool, sourceInfo bool) *Compiler {
 91	c := &Compiler{
 92		m:                                 m,
 93		ssaBuilder:                        ssaBuilder,
 94		br:                                bytes.NewReader(nil),
 95		offset:                            offset,
 96		ensureTermination:                 ensureTermination,
 97		needSourceOffsetInfo:              sourceInfo,
 98		varLengthKnownSafeBoundWithIDPool: wazevoapi.NewVarLengthPool[knownSafeBoundWithID](),
 99	}
100	c.declareSignatures(listenerOn)
101	return c
102}
103
104func (c *Compiler) declareSignatures(listenerOn bool) {
105	m := c.m
106	c.signatures = make(map[*wasm.FunctionType]*ssa.Signature, len(m.TypeSection)+2)
107	if listenerOn {
108		c.listenerSignatures = make(map[*wasm.FunctionType][2]*ssa.Signature, len(m.TypeSection))
109	}
110	for i := range m.TypeSection {
111		wasmSig := &m.TypeSection[i]
112		sig := SignatureForWasmFunctionType(wasmSig)
113		sig.ID = ssa.SignatureID(i)
114		c.signatures[wasmSig] = &sig
115		c.ssaBuilder.DeclareSignature(&sig)
116
117		if listenerOn {
118			beforeSig, afterSig := SignatureForListener(wasmSig)
119			beforeSig.ID = ssa.SignatureID(i) + ssa.SignatureID(len(m.TypeSection))
120			afterSig.ID = ssa.SignatureID(i) + ssa.SignatureID(len(m.TypeSection))*2
121			c.listenerSignatures[wasmSig] = [2]*ssa.Signature{beforeSig, afterSig}
122			c.ssaBuilder.DeclareSignature(beforeSig)
123			c.ssaBuilder.DeclareSignature(afterSig)
124		}
125	}
126
127	begin := ssa.SignatureID(len(m.TypeSection))
128	if listenerOn {
129		begin *= 3
130	}
131	c.memoryGrowSig = ssa.Signature{
132		ID: begin,
133		// Takes execution context and the page size to grow.
134		Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32},
135		// Returns the previous page size.
136		Results: []ssa.Type{ssa.TypeI32},
137	}
138	c.ssaBuilder.DeclareSignature(&c.memoryGrowSig)
139
140	c.checkModuleExitCodeSig = ssa.Signature{
141		ID: c.memoryGrowSig.ID + 1,
142		// Only takes execution context.
143		Params: []ssa.Type{ssa.TypeI64},
144	}
145	c.ssaBuilder.DeclareSignature(&c.checkModuleExitCodeSig)
146
147	c.tableGrowSig = ssa.Signature{
148		ID:     c.checkModuleExitCodeSig.ID + 1,
149		Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI32 /* table index */, ssa.TypeI32 /* num */, ssa.TypeI64 /* ref */},
150		// Returns the previous size.
151		Results: []ssa.Type{ssa.TypeI32},
152	}
153	c.ssaBuilder.DeclareSignature(&c.tableGrowSig)
154
155	c.refFuncSig = ssa.Signature{
156		ID:     c.tableGrowSig.ID + 1,
157		Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI32 /* func index */},
158		// Returns the function reference.
159		Results: []ssa.Type{ssa.TypeI64},
160	}
161	c.ssaBuilder.DeclareSignature(&c.refFuncSig)
162
163	c.memmoveSig = ssa.Signature{
164		ID: c.refFuncSig.ID + 1,
165		// dst, src, and the byte count.
166		Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64},
167	}
168
169	c.ssaBuilder.DeclareSignature(&c.memmoveSig)
170
171	c.memoryWait32Sig = ssa.Signature{
172		ID: c.memmoveSig.ID + 1,
173		// exec context, timeout, expected, addr
174		Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI32, ssa.TypeI64},
175		// Returns the status.
176		Results: []ssa.Type{ssa.TypeI32},
177	}
178	c.ssaBuilder.DeclareSignature(&c.memoryWait32Sig)
179
180	c.memoryWait64Sig = ssa.Signature{
181		ID: c.memoryWait32Sig.ID + 1,
182		// exec context, timeout, expected, addr
183		Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64, ssa.TypeI64},
184		// Returns the status.
185		Results: []ssa.Type{ssa.TypeI32},
186	}
187	c.ssaBuilder.DeclareSignature(&c.memoryWait64Sig)
188
189	c.memoryNotifySig = ssa.Signature{
190		ID: c.memoryWait64Sig.ID + 1,
191		// exec context, count, addr
192		Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32, ssa.TypeI64},
193		// Returns the number notified.
194		Results: []ssa.Type{ssa.TypeI32},
195	}
196	c.ssaBuilder.DeclareSignature(&c.memoryNotifySig)
197}
198
199// SignatureForWasmFunctionType returns the ssa.Signature for the given wasm.FunctionType.
200func SignatureForWasmFunctionType(typ *wasm.FunctionType) ssa.Signature {
201	sig := ssa.Signature{
202		// +2 to pass moduleContextPtr and executionContextPtr. See the inline comment LowerToSSA.
203		Params:  make([]ssa.Type, len(typ.Params)+2),
204		Results: make([]ssa.Type, len(typ.Results)),
205	}
206	sig.Params[0] = executionContextPtrTyp
207	sig.Params[1] = moduleContextPtrTyp
208	for j, typ := range typ.Params {
209		sig.Params[j+2] = WasmTypeToSSAType(typ)
210	}
211	for j, typ := range typ.Results {
212		sig.Results[j] = WasmTypeToSSAType(typ)
213	}
214	return sig
215}
216
217// Init initializes the state of frontendCompiler and make it ready for a next function.
218func (c *Compiler) Init(idx, typIndex wasm.Index, typ *wasm.FunctionType, localTypes []wasm.ValueType, body []byte, needListener bool, bodyOffsetInCodeSection uint64) {
219	c.ssaBuilder.Init(c.signatures[typ])
220	c.loweringState.reset()
221
222	c.wasmFunctionTypeIndex = typIndex
223	c.wasmLocalFunctionIndex = idx
224	c.wasmFunctionTyp = typ
225	c.wasmFunctionLocalTypes = localTypes
226	c.wasmFunctionBody = body
227	c.wasmFunctionBodyOffsetInCodeSection = bodyOffsetInCodeSection
228	c.needListener = needListener
229	c.clearSafeBounds()
230	c.varLengthKnownSafeBoundWithIDPool.Reset()
231	c.knownSafeBoundsAtTheEndOfBlocks = c.knownSafeBoundsAtTheEndOfBlocks[:0]
232}
233
234// Note: this assumes 64-bit platform (I believe we won't have 32-bit backend ;)).
235const executionContextPtrTyp, moduleContextPtrTyp = ssa.TypeI64, ssa.TypeI64
236
237// LowerToSSA lowers the current function to SSA function which will be held by ssaBuilder.
238// After calling this, the caller will be able to access the SSA info in *Compiler.ssaBuilder.
239//
240// Note that this only does the naive lowering, and do not do any optimization, instead the caller is expected to do so.
241func (c *Compiler) LowerToSSA() {
242	builder := c.ssaBuilder
243
244	// Set up the entry block.
245	entryBlock := builder.AllocateBasicBlock()
246	builder.SetCurrentBlock(entryBlock)
247
248	// Functions always take two parameters in addition to Wasm-level parameters:
249	//
250	//  1. executionContextPtr: pointer to the *executionContext in wazevo package.
251	//    This will be used to exit the execution in the face of trap, plus used for host function calls.
252	//
253	// 	2. moduleContextPtr: pointer to the *moduleContextOpaque in wazevo package.
254	//	  This will be used to access memory, etc. Also, this will be used during host function calls.
255	//
256	// Note: it's clear that sometimes a function won't need them. For example,
257	//  if the function doesn't trap and doesn't make function call, then
258	// 	we might be able to eliminate the parameter. However, if that function
259	//	can be called via call_indirect, then we cannot eliminate because the
260	//  signature won't match with the expected one.
261	// TODO: maybe there's some way to do this optimization without glitches, but so far I have no clue about the feasibility.
262	//
263	// Note: In Wasmtime or many other runtimes, moduleContextPtr is called "vmContext". Also note that `moduleContextPtr`
264	//  is wazero-specific since other runtimes can naturally use the OS-level signal to do this job thanks to the fact that
265	//  they can use native stack vs wazero cannot use Go-routine stack and have to use Go-runtime allocated []byte as a stack.
266	c.execCtxPtrValue = entryBlock.AddParam(builder, executionContextPtrTyp)
267	c.moduleCtxPtrValue = entryBlock.AddParam(builder, moduleContextPtrTyp)
268	builder.AnnotateValue(c.execCtxPtrValue, "exec_ctx")
269	builder.AnnotateValue(c.moduleCtxPtrValue, "module_ctx")
270
271	for i, typ := range c.wasmFunctionTyp.Params {
272		st := WasmTypeToSSAType(typ)
273		variable := builder.DeclareVariable(st)
274		value := entryBlock.AddParam(builder, st)
275		builder.DefineVariable(variable, value, entryBlock)
276		c.setWasmLocalVariable(wasm.Index(i), variable)
277	}
278	c.declareWasmLocals()
279	c.declareNecessaryVariables()
280
281	c.lowerBody(entryBlock)
282}
283
284// localVariable returns the SSA variable for the given Wasm local index.
285func (c *Compiler) localVariable(index wasm.Index) ssa.Variable {
286	return c.wasmLocalToVariable[index]
287}
288
289func (c *Compiler) setWasmLocalVariable(index wasm.Index, variable ssa.Variable) {
290	idx := int(index)
291	if idx >= len(c.wasmLocalToVariable) {
292		c.wasmLocalToVariable = append(c.wasmLocalToVariable, make([]ssa.Variable, idx+1-len(c.wasmLocalToVariable))...)
293	}
294	c.wasmLocalToVariable[idx] = variable
295}
296
297// declareWasmLocals declares the SSA variables for the Wasm locals.
298func (c *Compiler) declareWasmLocals() {
299	localCount := wasm.Index(len(c.wasmFunctionTyp.Params))
300	for i, typ := range c.wasmFunctionLocalTypes {
301		st := WasmTypeToSSAType(typ)
302		variable := c.ssaBuilder.DeclareVariable(st)
303		c.setWasmLocalVariable(wasm.Index(i)+localCount, variable)
304		c.ssaBuilder.InsertZeroValue(st)
305	}
306}
307
308func (c *Compiler) declareNecessaryVariables() {
309	if c.needMemory = c.m.MemorySection != nil; c.needMemory {
310		c.memoryShared = c.m.MemorySection.IsShared
311	} else if c.needMemory = c.m.ImportMemoryCount > 0; c.needMemory {
312		for _, imp := range c.m.ImportSection {
313			if imp.Type == wasm.ExternTypeMemory {
314				c.memoryShared = imp.DescMem.IsShared
315				break
316			}
317		}
318	}
319
320	if c.needMemory {
321		c.memoryBaseVariable = c.ssaBuilder.DeclareVariable(ssa.TypeI64)
322		c.memoryLenVariable = c.ssaBuilder.DeclareVariable(ssa.TypeI64)
323	}
324
325	c.globalVariables = c.globalVariables[:0]
326	c.mutableGlobalVariablesIndexes = c.mutableGlobalVariablesIndexes[:0]
327	c.globalVariablesTypes = c.globalVariablesTypes[:0]
328	for _, imp := range c.m.ImportSection {
329		if imp.Type == wasm.ExternTypeGlobal {
330			desc := imp.DescGlobal
331			c.declareWasmGlobal(desc.ValType, desc.Mutable)
332		}
333	}
334	for _, g := range c.m.GlobalSection {
335		desc := g.Type
336		c.declareWasmGlobal(desc.ValType, desc.Mutable)
337	}
338
339	// TODO: add tables.
340}
341
342func (c *Compiler) declareWasmGlobal(typ wasm.ValueType, mutable bool) {
343	var st ssa.Type
344	switch typ {
345	case wasm.ValueTypeI32:
346		st = ssa.TypeI32
347	case wasm.ValueTypeI64,
348		// Both externref and funcref are represented as I64 since we only support 64-bit platforms.
349		wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
350		st = ssa.TypeI64
351	case wasm.ValueTypeF32:
352		st = ssa.TypeF32
353	case wasm.ValueTypeF64:
354		st = ssa.TypeF64
355	case wasm.ValueTypeV128:
356		st = ssa.TypeV128
357	default:
358		panic("TODO: " + wasm.ValueTypeName(typ))
359	}
360	v := c.ssaBuilder.DeclareVariable(st)
361	index := wasm.Index(len(c.globalVariables))
362	c.globalVariables = append(c.globalVariables, v)
363	c.globalVariablesTypes = append(c.globalVariablesTypes, st)
364	if mutable {
365		c.mutableGlobalVariablesIndexes = append(c.mutableGlobalVariablesIndexes, index)
366	}
367}
368
369// WasmTypeToSSAType converts wasm.ValueType to ssa.Type.
370func WasmTypeToSSAType(vt wasm.ValueType) ssa.Type {
371	switch vt {
372	case wasm.ValueTypeI32:
373		return ssa.TypeI32
374	case wasm.ValueTypeI64,
375		// Both externref and funcref are represented as I64 since we only support 64-bit platforms.
376		wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
377		return ssa.TypeI64
378	case wasm.ValueTypeF32:
379		return ssa.TypeF32
380	case wasm.ValueTypeF64:
381		return ssa.TypeF64
382	case wasm.ValueTypeV128:
383		return ssa.TypeV128
384	default:
385		panic("TODO: " + wasm.ValueTypeName(vt))
386	}
387}
388
389// addBlockParamsFromWasmTypes adds the block parameters to the given block.
390func (c *Compiler) addBlockParamsFromWasmTypes(tps []wasm.ValueType, blk ssa.BasicBlock) {
391	for _, typ := range tps {
392		st := WasmTypeToSSAType(typ)
393		blk.AddParam(c.ssaBuilder, st)
394	}
395}
396
397// formatBuilder outputs the constructed SSA function as a string with a source information.
398func (c *Compiler) formatBuilder() string {
399	return c.ssaBuilder.Format()
400}
401
402// SignatureForListener returns the signatures for the listener functions.
403func SignatureForListener(wasmSig *wasm.FunctionType) (*ssa.Signature, *ssa.Signature) {
404	beforeSig := &ssa.Signature{}
405	beforeSig.Params = make([]ssa.Type, len(wasmSig.Params)+2)
406	beforeSig.Params[0] = ssa.TypeI64 // Execution context.
407	beforeSig.Params[1] = ssa.TypeI32 // Function index.
408	for i, p := range wasmSig.Params {
409		beforeSig.Params[i+2] = WasmTypeToSSAType(p)
410	}
411	afterSig := &ssa.Signature{}
412	afterSig.Params = make([]ssa.Type, len(wasmSig.Results)+2)
413	afterSig.Params[0] = ssa.TypeI64 // Execution context.
414	afterSig.Params[1] = ssa.TypeI32 // Function index.
415	for i, p := range wasmSig.Results {
416		afterSig.Params[i+2] = WasmTypeToSSAType(p)
417	}
418	return beforeSig, afterSig
419}
420
421// isBoundSafe returns true if the given value is known to be safe to access up to the given bound.
422func (c *Compiler) getKnownSafeBound(v ssa.ValueID) *knownSafeBound {
423	if int(v) >= len(c.knownSafeBounds) {
424		return nil
425	}
426	return &c.knownSafeBounds[v]
427}
428
429// recordKnownSafeBound records the given safe bound for the given value.
430func (c *Compiler) recordKnownSafeBound(v ssa.ValueID, safeBound uint64, absoluteAddr ssa.Value) {
431	if int(v) >= len(c.knownSafeBounds) {
432		c.knownSafeBounds = append(c.knownSafeBounds, make([]knownSafeBound, v+1)...)
433	}
434
435	if exiting := c.knownSafeBounds[v]; exiting.bound == 0 {
436		c.knownSafeBounds[v] = knownSafeBound{
437			bound:        safeBound,
438			absoluteAddr: absoluteAddr,
439		}
440		c.knownSafeBoundsSet = append(c.knownSafeBoundsSet, v)
441	} else if safeBound > exiting.bound {
442		c.knownSafeBounds[v].bound = safeBound
443	}
444}
445
446// clearSafeBounds clears the known safe bounds.
447func (c *Compiler) clearSafeBounds() {
448	for _, v := range c.knownSafeBoundsSet {
449		ptr := &c.knownSafeBounds[v]
450		ptr.bound = 0
451		ptr.absoluteAddr = ssa.ValueInvalid
452	}
453	c.knownSafeBoundsSet = c.knownSafeBoundsSet[:0]
454}
455
456// resetAbsoluteAddressInSafeBounds resets the absolute addresses recorded in the known safe bounds.
457func (c *Compiler) resetAbsoluteAddressInSafeBounds() {
458	for _, v := range c.knownSafeBoundsSet {
459		ptr := &c.knownSafeBounds[v]
460		ptr.absoluteAddr = ssa.ValueInvalid
461	}
462}
463
464func (k *knownSafeBound) valid() bool {
465	return k != nil && k.bound > 0
466}
467
468func (c *Compiler) allocateVarLengthValues(_cap int, vs ...ssa.Value) ssa.Values {
469	builder := c.ssaBuilder
470	pool := builder.VarLengthPool()
471	args := pool.Allocate(_cap)
472	args = args.Append(builder.VarLengthPool(), vs...)
473	return args
474}
475
476func (c *Compiler) finalizeKnownSafeBoundsAtTheEndOfBlock(bID ssa.BasicBlockID) {
477	_bID := int(bID)
478	if l := len(c.knownSafeBoundsAtTheEndOfBlocks); _bID >= l {
479		c.knownSafeBoundsAtTheEndOfBlocks = append(c.knownSafeBoundsAtTheEndOfBlocks,
480			make([]knownSafeBoundsAtTheEndOfBlock, _bID+1-len(c.knownSafeBoundsAtTheEndOfBlocks))...)
481		for i := l; i < len(c.knownSafeBoundsAtTheEndOfBlocks); i++ {
482			c.knownSafeBoundsAtTheEndOfBlocks[i] = knownSafeBoundsAtTheEndOfBlockNil
483		}
484	}
485	p := &c.varLengthKnownSafeBoundWithIDPool
486	size := len(c.knownSafeBoundsSet)
487	allocated := c.varLengthKnownSafeBoundWithIDPool.Allocate(size)
488	// Sort the known safe bounds by the value ID so that we can use the intersection algorithm in initializeCurrentBlockKnownBounds.
489	sortSSAValueIDs(c.knownSafeBoundsSet)
490	for _, vID := range c.knownSafeBoundsSet {
491		kb := c.knownSafeBounds[vID]
492		allocated = allocated.Append(p, knownSafeBoundWithID{
493			knownSafeBound: kb,
494			id:             vID,
495		})
496	}
497	c.knownSafeBoundsAtTheEndOfBlocks[bID] = allocated
498	c.clearSafeBounds()
499}
500
501func (c *Compiler) initializeCurrentBlockKnownBounds() {
502	currentBlk := c.ssaBuilder.CurrentBlock()
503	switch preds := currentBlk.Preds(); preds {
504	case 0:
505	case 1:
506		pred := currentBlk.Pred(0).ID()
507		for _, kb := range c.getKnownSafeBoundsAtTheEndOfBlocks(pred).View() {
508			// Unless the block is sealed, we cannot assume the absolute address is valid:
509			// later we might add another predecessor that has no visibility of that value.
510			addr := ssa.ValueInvalid
511			if currentBlk.Sealed() {
512				addr = kb.absoluteAddr
513			}
514			c.recordKnownSafeBound(kb.id, kb.bound, addr)
515		}
516	default:
517		c.pointers = c.pointers[:0]
518		c.bounds = c.bounds[:0]
519		for i := 0; i < preds; i++ {
520			c.bounds = append(c.bounds, c.getKnownSafeBoundsAtTheEndOfBlocks(currentBlk.Pred(i).ID()).View())
521			c.pointers = append(c.pointers, 0)
522		}
523
524		// If there are multiple predecessors, we need to find the intersection of the known safe bounds.
525
526	outer:
527		for {
528			smallestID := ssa.ValueID(math.MaxUint32)
529			for i, ptr := range c.pointers {
530				if ptr >= len(c.bounds[i]) {
531					break outer
532				}
533				cb := &c.bounds[i][ptr]
534				if id := cb.id; id < smallestID {
535					smallestID = cb.id
536				}
537			}
538
539			// Check if current elements are the same across all lists.
540			same := true
541			minBound := uint64(math.MaxUint64)
542			for i := 0; i < preds; i++ {
543				cb := &c.bounds[i][c.pointers[i]]
544				if cb.id != smallestID {
545					same = false
546				} else {
547					if cb.bound < minBound {
548						minBound = cb.bound
549					}
550					c.pointers[i]++
551				}
552			}
553
554			if same { // All elements are the same.
555				// Absolute address cannot be used in the intersection since the value might be only defined in one of the predecessors.
556				c.recordKnownSafeBound(smallestID, minBound, ssa.ValueInvalid)
557			}
558		}
559	}
560}
561
562func (c *Compiler) getKnownSafeBoundsAtTheEndOfBlocks(id ssa.BasicBlockID) knownSafeBoundsAtTheEndOfBlock {
563	if int(id) >= len(c.knownSafeBoundsAtTheEndOfBlocks) {
564		return knownSafeBoundsAtTheEndOfBlockNil
565	}
566	return c.knownSafeBoundsAtTheEndOfBlocks[id]
567}