[experiment] add alternative wasm sqlite3 implementation available via build-tag (#2863)

This allows for building GoToSocial with [SQLite transpiled to WASM](https://github.com/ncruces/go-sqlite3) and accessed through [Wazero](https://wazero.io/).
This commit is contained in:
kim
2024-05-27 15:46:15 +00:00
committed by GitHub
parent cce21c11cb
commit 1e7b32490d
398 changed files with 86174 additions and 684 deletions

View File

@ -0,0 +1,407 @@
package ssa
import (
"fmt"
"strconv"
"strings"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
)
// BasicBlock represents the Basic Block of an SSA function.
// Each BasicBlock always ends with branching instructions (e.g. Branch, Return, etc.),
// and at most two branches are allowed. If there's two branches, these two are placed together at the end of the block.
// In other words, there's no branching instruction in the middle of the block.
//
// Note: we use the "block argument" variant of SSA, instead of PHI functions. See the package level doc comments.
//
// Note: we use "parameter/param" as a placeholder which represents a variant of PHI, and "argument/arg" as an actual
// Value passed to that "parameter/param".
type BasicBlock interface {
// ID returns the unique ID of this block.
ID() BasicBlockID
// Name returns the unique string ID of this block. e.g. blk0, blk1, ...
Name() string
// AddParam adds the parameter to the block whose type specified by `t`.
AddParam(b Builder, t Type) Value
// Params returns the number of parameters to this block.
Params() int
// Param returns (Variable, Value) which corresponds to the i-th parameter of this block.
// The returned Value is the definition of the param in this block.
Param(i int) Value
// InsertInstruction inserts an instruction that implements Value into the tail of this block.
InsertInstruction(raw *Instruction)
// Root returns the root instruction of this block.
Root() *Instruction
// Tail returns the tail instruction of this block.
Tail() *Instruction
// EntryBlock returns true if this block represents the function entry.
EntryBlock() bool
// ReturnBlock returns ture if this block represents the function return.
ReturnBlock() bool
// FormatHeader returns the debug string of this block, not including instruction.
FormatHeader(b Builder) string
// Valid is true if this block is still valid even after optimizations.
Valid() bool
// Sealed is true if this block has been sealed.
Sealed() bool
// BeginPredIterator returns the first predecessor of this block.
BeginPredIterator() BasicBlock
// NextPredIterator returns the next predecessor of this block.
NextPredIterator() BasicBlock
// Preds returns the number of predecessors of this block.
Preds() int
// Pred returns the i-th predecessor of this block.
Pred(i int) BasicBlock
// Succs returns the number of successors of this block.
Succs() int
// Succ returns the i-th successor of this block.
Succ(i int) BasicBlock
// LoopHeader returns true if this block is a loop header.
LoopHeader() bool
// LoopNestingForestChildren returns the children of this block in the loop nesting forest.
LoopNestingForestChildren() []BasicBlock
}
type (
// basicBlock is a basic block in a SSA-transformed function.
basicBlock struct {
id BasicBlockID
rootInstr, currentInstr *Instruction
params []blockParam
predIter int
preds []basicBlockPredecessorInfo
success []*basicBlock
// singlePred is the alias to preds[0] for fast lookup, and only set after Seal is called.
singlePred *basicBlock
// lastDefinitions maps Variable to its last definition in this block.
lastDefinitions map[Variable]Value
// unknownsValues are used in builder.findValue. The usage is well-described in the paper.
unknownValues []unknownValue
// invalid is true if this block is made invalid during optimizations.
invalid bool
// sealed is true if this is sealed (all the predecessors are known).
sealed bool
// loopHeader is true if this block is a loop header:
//
// > A loop header (sometimes called the entry point of the loop) is a dominator that is the target
// > of a loop-forming back edge. The loop header dominates all blocks in the loop body.
// > A block may be a loop header for more than one loop. A loop may have multiple entry points,
// > in which case it has no "loop header".
//
// See https://en.wikipedia.org/wiki/Control-flow_graph for more details.
//
// This is modified during the subPassLoopDetection pass.
loopHeader bool
// loopNestingForestChildren holds the children of this block in the loop nesting forest.
// Non-empty if and only if this block is a loop header (i.e. loopHeader=true)
loopNestingForestChildren []BasicBlock
// reversePostOrder is used to sort all the blocks in the function in reverse post order.
// This is used in builder.LayoutBlocks.
reversePostOrder int
// child and sibling are the ones in the dominator tree.
child, sibling *basicBlock
}
// BasicBlockID is the unique ID of a basicBlock.
BasicBlockID uint32
// blockParam implements Value and represents a parameter to a basicBlock.
blockParam struct {
// value is the Value that corresponds to the parameter in this block,
// and can be considered as an output of PHI instruction in traditional SSA.
value Value
// typ is the type of the parameter.
typ Type
}
unknownValue struct {
// variable is the variable that this unknownValue represents.
variable Variable
// value is the value that this unknownValue represents.
value Value
}
)
const basicBlockIDReturnBlock = 0xffffffff
// Name implements BasicBlock.Name.
func (bb *basicBlock) Name() string {
if bb.id == basicBlockIDReturnBlock {
return "blk_ret"
} else {
return fmt.Sprintf("blk%d", bb.id)
}
}
// String implements fmt.Stringer for debugging.
func (bid BasicBlockID) String() string {
if bid == basicBlockIDReturnBlock {
return "blk_ret"
} else {
return fmt.Sprintf("blk%d", bid)
}
}
// ID implements BasicBlock.ID.
func (bb *basicBlock) ID() BasicBlockID {
return bb.id
}
// basicBlockPredecessorInfo is the information of a predecessor of a basicBlock.
// predecessor is determined by a pair of block and the branch instruction used to jump to the successor.
type basicBlockPredecessorInfo struct {
blk *basicBlock
branch *Instruction
}
// EntryBlock implements BasicBlock.EntryBlock.
func (bb *basicBlock) EntryBlock() bool {
return bb.id == 0
}
// ReturnBlock implements BasicBlock.ReturnBlock.
func (bb *basicBlock) ReturnBlock() bool {
return bb.id == basicBlockIDReturnBlock
}
// AddParam implements BasicBlock.AddParam.
func (bb *basicBlock) AddParam(b Builder, typ Type) Value {
paramValue := b.allocateValue(typ)
bb.params = append(bb.params, blockParam{typ: typ, value: paramValue})
return paramValue
}
// addParamOn adds a parameter to this block whose value is already allocated.
func (bb *basicBlock) addParamOn(typ Type, value Value) {
bb.params = append(bb.params, blockParam{typ: typ, value: value})
}
// Params implements BasicBlock.Params.
func (bb *basicBlock) Params() int {
return len(bb.params)
}
// Param implements BasicBlock.Param.
func (bb *basicBlock) Param(i int) Value {
p := &bb.params[i]
return p.value
}
// Valid implements BasicBlock.Valid.
func (bb *basicBlock) Valid() bool {
return !bb.invalid
}
// Sealed implements BasicBlock.Sealed.
func (bb *basicBlock) Sealed() bool {
return bb.sealed
}
// InsertInstruction implements BasicBlock.InsertInstruction.
func (bb *basicBlock) InsertInstruction(next *Instruction) {
current := bb.currentInstr
if current != nil {
current.next = next
next.prev = current
} else {
bb.rootInstr = next
}
bb.currentInstr = next
switch next.opcode {
case OpcodeJump, OpcodeBrz, OpcodeBrnz:
target := next.blk.(*basicBlock)
target.addPred(bb, next)
case OpcodeBrTable:
for _, _target := range next.targets {
target := _target.(*basicBlock)
target.addPred(bb, next)
}
}
}
// NumPreds implements BasicBlock.NumPreds.
func (bb *basicBlock) NumPreds() int {
return len(bb.preds)
}
// BeginPredIterator implements BasicBlock.BeginPredIterator.
func (bb *basicBlock) BeginPredIterator() BasicBlock {
bb.predIter = 0
return bb.NextPredIterator()
}
// NextPredIterator implements BasicBlock.NextPredIterator.
func (bb *basicBlock) NextPredIterator() BasicBlock {
if bb.predIter >= len(bb.preds) {
return nil
}
pred := bb.preds[bb.predIter].blk
bb.predIter++
return pred
}
// Preds implements BasicBlock.Preds.
func (bb *basicBlock) Preds() int {
return len(bb.preds)
}
// Pred implements BasicBlock.Pred.
func (bb *basicBlock) Pred(i int) BasicBlock {
return bb.preds[i].blk
}
// Succs implements BasicBlock.Succs.
func (bb *basicBlock) Succs() int {
return len(bb.success)
}
// Succ implements BasicBlock.Succ.
func (bb *basicBlock) Succ(i int) BasicBlock {
return bb.success[i]
}
// Root implements BasicBlock.Root.
func (bb *basicBlock) Root() *Instruction {
return bb.rootInstr
}
// Tail implements BasicBlock.Tail.
func (bb *basicBlock) Tail() *Instruction {
return bb.currentInstr
}
// reset resets the basicBlock to its initial state so that it can be reused for another function.
func resetBasicBlock(bb *basicBlock) {
bb.params = bb.params[:0]
bb.rootInstr, bb.currentInstr = nil, nil
bb.preds = bb.preds[:0]
bb.success = bb.success[:0]
bb.invalid, bb.sealed = false, false
bb.singlePred = nil
bb.unknownValues = bb.unknownValues[:0]
bb.lastDefinitions = wazevoapi.ResetMap(bb.lastDefinitions)
bb.reversePostOrder = -1
bb.loopNestingForestChildren = bb.loopNestingForestChildren[:0]
bb.loopHeader = false
bb.sibling = nil
bb.child = nil
}
// addPred adds a predecessor to this block specified by the branch instruction.
func (bb *basicBlock) addPred(blk BasicBlock, branch *Instruction) {
if bb.sealed {
panic("BUG: trying to add predecessor to a sealed block: " + bb.Name())
}
pred := blk.(*basicBlock)
for i := range bb.preds {
existingPred := &bb.preds[i]
if existingPred.blk == pred && existingPred.branch != branch {
// If the target is already added, then this must come from the same BrTable,
// otherwise such redundant branch should be eliminated by the frontend. (which should be simpler).
panic(fmt.Sprintf("BUG: redundant non BrTable jumps in %s whose targes are the same", bb.Name()))
}
}
bb.preds = append(bb.preds, basicBlockPredecessorInfo{
blk: pred,
branch: branch,
})
pred.success = append(pred.success, bb)
}
// FormatHeader implements BasicBlock.FormatHeader.
func (bb *basicBlock) FormatHeader(b Builder) string {
ps := make([]string, len(bb.params))
for i, p := range bb.params {
ps[i] = p.value.formatWithType(b)
}
if len(bb.preds) > 0 {
preds := make([]string, 0, len(bb.preds))
for _, pred := range bb.preds {
if pred.blk.invalid {
continue
}
preds = append(preds, fmt.Sprintf("blk%d", pred.blk.id))
}
return fmt.Sprintf("blk%d: (%s) <-- (%s)",
bb.id, strings.Join(ps, ","), strings.Join(preds, ","))
} else {
return fmt.Sprintf("blk%d: (%s)", bb.id, strings.Join(ps, ", "))
}
}
// validates validates the basicBlock for debugging purpose.
func (bb *basicBlock) validate(b *builder) {
if bb.invalid {
panic("BUG: trying to validate an invalid block: " + bb.Name())
}
if len(bb.preds) > 0 {
for _, pred := range bb.preds {
if pred.branch.opcode != OpcodeBrTable {
if target := pred.branch.blk; target != bb {
panic(fmt.Sprintf("BUG: '%s' is not branch to %s, but to %s",
pred.branch.Format(b), bb.Name(), target.Name()))
}
}
var exp int
if bb.ReturnBlock() {
exp = len(b.currentSignature.Results)
} else {
exp = len(bb.params)
}
if len(pred.branch.vs.View()) != exp {
panic(fmt.Sprintf(
"BUG: len(argument at %s) != len(params at %s): %d != %d: %s",
pred.blk.Name(), bb.Name(),
len(pred.branch.vs.View()), len(bb.params), pred.branch.Format(b),
))
}
}
}
}
// String implements fmt.Stringer for debugging purpose only.
func (bb *basicBlock) String() string {
return strconv.Itoa(int(bb.id))
}
// LoopNestingForestChildren implements BasicBlock.LoopNestingForestChildren.
func (bb *basicBlock) LoopNestingForestChildren() []BasicBlock {
return bb.loopNestingForestChildren
}
// LoopHeader implements BasicBlock.LoopHeader.
func (bb *basicBlock) LoopHeader() bool {
return bb.loopHeader
}

View File

@ -0,0 +1,34 @@
//go:build go1.21
package ssa
import (
"slices"
)
func sortBlocks(blocks []*basicBlock) {
slices.SortFunc(blocks, func(i, j *basicBlock) int {
jIsReturn := j.ReturnBlock()
iIsReturn := i.ReturnBlock()
if iIsReturn && jIsReturn {
return 0
}
if jIsReturn {
return 1
}
if iIsReturn {
return -1
}
iRoot, jRoot := i.rootInstr, j.rootInstr
if iRoot == nil && jRoot == nil { // For testing.
return 0
}
if jRoot == nil {
return 1
}
if iRoot == nil {
return -1
}
return i.rootInstr.id - j.rootInstr.id
})
}

View File

@ -0,0 +1,24 @@
//go:build !go1.21
// TODO: delete after the floor Go version is 1.21
package ssa
import "sort"
func sortBlocks(blocks []*basicBlock) {
sort.SliceStable(blocks, func(i, j int) bool {
iBlk, jBlk := blocks[i], blocks[j]
if jBlk.ReturnBlock() {
return true
}
if iBlk.ReturnBlock() {
return false
}
iRoot, jRoot := iBlk.rootInstr, jBlk.rootInstr
if iRoot == nil || jRoot == nil { // For testing.
return true
}
return iBlk.rootInstr.id < jBlk.rootInstr.id
})
}

View File

@ -0,0 +1,731 @@
package ssa
import (
"fmt"
"sort"
"strings"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
)
// Builder is used to builds SSA consisting of Basic Blocks per function.
type Builder interface {
// Init must be called to reuse this builder for the next function.
Init(typ *Signature)
// Signature returns the Signature of the currently-compiled function.
Signature() *Signature
// BlockIDMax returns the maximum value of BasicBlocksID existing in the currently-compiled function.
BlockIDMax() BasicBlockID
// AllocateBasicBlock creates a basic block in SSA function.
AllocateBasicBlock() BasicBlock
// CurrentBlock returns the currently handled BasicBlock which is set by the latest call to SetCurrentBlock.
CurrentBlock() BasicBlock
// EntryBlock returns the entry BasicBlock of the currently-compiled function.
EntryBlock() BasicBlock
// SetCurrentBlock sets the instruction insertion target to the BasicBlock `b`.
SetCurrentBlock(b BasicBlock)
// DeclareVariable declares a Variable of the given Type.
DeclareVariable(Type) Variable
// DefineVariable defines a variable in the `block` with value.
// The defining instruction will be inserted into the `block`.
DefineVariable(variable Variable, value Value, block BasicBlock)
// DefineVariableInCurrentBB is the same as DefineVariable except the definition is
// inserted into the current BasicBlock. Alias to DefineVariable(x, y, CurrentBlock()).
DefineVariableInCurrentBB(variable Variable, value Value)
// AllocateInstruction returns a new Instruction.
AllocateInstruction() *Instruction
// InsertInstruction executes BasicBlock.InsertInstruction for the currently handled basic block.
InsertInstruction(raw *Instruction)
// allocateValue allocates an unused Value.
allocateValue(typ Type) Value
// MustFindValue searches the latest definition of the given Variable and returns the result.
MustFindValue(variable Variable) Value
// MustFindValueInBlk is the same as MustFindValue except it searches the latest definition from the given BasicBlock.
MustFindValueInBlk(variable Variable, blk BasicBlock) Value
// FindValueInLinearPath tries to find the latest definition of the given Variable in the linear path to the current BasicBlock.
// If it cannot find the definition, or it's not sealed yet, it returns ValueInvalid.
FindValueInLinearPath(variable Variable) Value
// Seal declares that we've known all the predecessors to this block and were added via AddPred.
// After calling this, AddPred will be forbidden.
Seal(blk BasicBlock)
// AnnotateValue is for debugging purpose.
AnnotateValue(value Value, annotation string)
// DeclareSignature appends the *Signature to be referenced by various instructions (e.g. OpcodeCall).
DeclareSignature(signature *Signature)
// Signatures returns the slice of declared Signatures.
Signatures() []*Signature
// ResolveSignature returns the Signature which corresponds to SignatureID.
ResolveSignature(id SignatureID) *Signature
// RunPasses runs various passes on the constructed SSA function.
RunPasses()
// Format returns the debugging string of the SSA function.
Format() string
// BlockIteratorBegin initializes the state to iterate over all the valid BasicBlock(s) compiled.
// Combined with BlockIteratorNext, we can use this like:
//
// for blk := builder.BlockIteratorBegin(); blk != nil; blk = builder.BlockIteratorNext() {
// // ...
// }
//
// The returned blocks are ordered in the order of AllocateBasicBlock being called.
BlockIteratorBegin() BasicBlock
// BlockIteratorNext advances the state for iteration initialized by BlockIteratorBegin.
// Returns nil if there's no unseen BasicBlock.
BlockIteratorNext() BasicBlock
// ValueRefCounts returns the map of ValueID to its reference count.
// The returned slice must not be modified.
ValueRefCounts() []int
// BlockIteratorReversePostOrderBegin is almost the same as BlockIteratorBegin except it returns the BasicBlock in the reverse post-order.
// This is available after RunPasses is run.
BlockIteratorReversePostOrderBegin() BasicBlock
// BlockIteratorReversePostOrderNext is almost the same as BlockIteratorPostOrderNext except it returns the BasicBlock in the reverse post-order.
// This is available after RunPasses is run.
BlockIteratorReversePostOrderNext() BasicBlock
// ReturnBlock returns the BasicBlock which is used to return from the function.
ReturnBlock() BasicBlock
// InsertUndefined inserts an undefined instruction at the current position.
InsertUndefined()
// SetCurrentSourceOffset sets the current source offset. The incoming instruction will be annotated with this offset.
SetCurrentSourceOffset(line SourceOffset)
// LoopNestingForestRoots returns the roots of the loop nesting forest.
LoopNestingForestRoots() []BasicBlock
// LowestCommonAncestor returns the lowest common ancestor in the dominator tree of the given BasicBlock(s).
LowestCommonAncestor(blk1, blk2 BasicBlock) BasicBlock
// Idom returns the immediate dominator of the given BasicBlock.
Idom(blk BasicBlock) BasicBlock
VarLengthPool() *wazevoapi.VarLengthPool[Value]
}
// NewBuilder returns a new Builder implementation.
func NewBuilder() Builder {
return &builder{
instructionsPool: wazevoapi.NewPool[Instruction](resetInstruction),
basicBlocksPool: wazevoapi.NewPool[basicBlock](resetBasicBlock),
varLengthPool: wazevoapi.NewVarLengthPool[Value](),
valueAnnotations: make(map[ValueID]string),
signatures: make(map[SignatureID]*Signature),
blkVisited: make(map[*basicBlock]int),
valueIDAliases: make(map[ValueID]Value),
redundantParameterIndexToValue: make(map[int]Value),
returnBlk: &basicBlock{id: basicBlockIDReturnBlock},
}
}
// builder implements Builder interface.
type builder struct {
basicBlocksPool wazevoapi.Pool[basicBlock]
instructionsPool wazevoapi.Pool[Instruction]
varLengthPool wazevoapi.VarLengthPool[Value]
signatures map[SignatureID]*Signature
currentSignature *Signature
// reversePostOrderedBasicBlocks are the BasicBlock(s) ordered in the reverse post-order after passCalculateImmediateDominators.
reversePostOrderedBasicBlocks []*basicBlock
currentBB *basicBlock
returnBlk *basicBlock
// variables track the types for Variable with the index regarded Variable.
variables []Type
// nextValueID is used by builder.AllocateValue.
nextValueID ValueID
// nextVariable is used by builder.AllocateVariable.
nextVariable Variable
valueIDAliases map[ValueID]Value
valueAnnotations map[ValueID]string
// valueRefCounts is used to lower the SSA in backend, and will be calculated
// by the last SSA-level optimization pass.
valueRefCounts []int
// dominators stores the immediate dominator of each BasicBlock.
// The index is blockID of the BasicBlock.
dominators []*basicBlock
sparseTree dominatorSparseTree
// loopNestingForestRoots are the roots of the loop nesting forest.
loopNestingForestRoots []BasicBlock
// The followings are used for optimization passes/deterministic compilation.
instStack []*Instruction
blkVisited map[*basicBlock]int
valueIDToInstruction []*Instruction
blkStack []*basicBlock
blkStack2 []*basicBlock
ints []int
redundantParameterIndexToValue map[int]Value
// blockIterCur is used to implement blockIteratorBegin and blockIteratorNext.
blockIterCur int
// donePreBlockLayoutPasses is true if all the passes before LayoutBlocks are called.
donePreBlockLayoutPasses bool
// doneBlockLayout is true if LayoutBlocks is called.
doneBlockLayout bool
// donePostBlockLayoutPasses is true if all the passes after LayoutBlocks are called.
donePostBlockLayoutPasses bool
currentSourceOffset SourceOffset
}
func (b *builder) VarLengthPool() *wazevoapi.VarLengthPool[Value] {
return &b.varLengthPool
}
// ReturnBlock implements Builder.ReturnBlock.
func (b *builder) ReturnBlock() BasicBlock {
return b.returnBlk
}
// Init implements Builder.Reset.
func (b *builder) Init(s *Signature) {
b.nextVariable = 0
b.currentSignature = s
resetBasicBlock(b.returnBlk)
b.instructionsPool.Reset()
b.basicBlocksPool.Reset()
b.varLengthPool.Reset()
b.donePreBlockLayoutPasses = false
b.doneBlockLayout = false
b.donePostBlockLayoutPasses = false
for _, sig := range b.signatures {
sig.used = false
}
b.ints = b.ints[:0]
b.blkStack = b.blkStack[:0]
b.blkStack2 = b.blkStack2[:0]
b.dominators = b.dominators[:0]
b.loopNestingForestRoots = b.loopNestingForestRoots[:0]
for i := 0; i < b.basicBlocksPool.Allocated(); i++ {
blk := b.basicBlocksPool.View(i)
delete(b.blkVisited, blk)
}
b.basicBlocksPool.Reset()
for v := ValueID(0); v < b.nextValueID; v++ {
delete(b.valueAnnotations, v)
delete(b.valueIDAliases, v)
b.valueRefCounts[v] = 0
b.valueIDToInstruction[v] = nil
}
b.nextValueID = 0
b.reversePostOrderedBasicBlocks = b.reversePostOrderedBasicBlocks[:0]
b.doneBlockLayout = false
for i := range b.valueRefCounts {
b.valueRefCounts[i] = 0
}
b.currentSourceOffset = sourceOffsetUnknown
}
// Signature implements Builder.Signature.
func (b *builder) Signature() *Signature {
return b.currentSignature
}
// AnnotateValue implements Builder.AnnotateValue.
func (b *builder) AnnotateValue(value Value, a string) {
b.valueAnnotations[value.ID()] = a
}
// AllocateInstruction implements Builder.AllocateInstruction.
func (b *builder) AllocateInstruction() *Instruction {
instr := b.instructionsPool.Allocate()
instr.id = b.instructionsPool.Allocated()
return instr
}
// DeclareSignature implements Builder.AnnotateValue.
func (b *builder) DeclareSignature(s *Signature) {
b.signatures[s.ID] = s
s.used = false
}
// Signatures implements Builder.Signatures.
func (b *builder) Signatures() (ret []*Signature) {
for _, sig := range b.signatures {
ret = append(ret, sig)
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].ID < ret[j].ID
})
return
}
// SetCurrentSourceOffset implements Builder.SetCurrentSourceOffset.
func (b *builder) SetCurrentSourceOffset(l SourceOffset) {
b.currentSourceOffset = l
}
func (b *builder) usedSignatures() (ret []*Signature) {
for _, sig := range b.signatures {
if sig.used {
ret = append(ret, sig)
}
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].ID < ret[j].ID
})
return
}
// ResolveSignature implements Builder.ResolveSignature.
func (b *builder) ResolveSignature(id SignatureID) *Signature {
return b.signatures[id]
}
// AllocateBasicBlock implements Builder.AllocateBasicBlock.
func (b *builder) AllocateBasicBlock() BasicBlock {
return b.allocateBasicBlock()
}
// allocateBasicBlock allocates a new basicBlock.
func (b *builder) allocateBasicBlock() *basicBlock {
id := BasicBlockID(b.basicBlocksPool.Allocated())
blk := b.basicBlocksPool.Allocate()
blk.id = id
return blk
}
// Idom implements Builder.Idom.
func (b *builder) Idom(blk BasicBlock) BasicBlock {
return b.dominators[blk.ID()]
}
// InsertInstruction implements Builder.InsertInstruction.
func (b *builder) InsertInstruction(instr *Instruction) {
b.currentBB.InsertInstruction(instr)
if l := b.currentSourceOffset; l.Valid() {
// Emit the source offset info only when the instruction has side effect because
// these are the only instructions that are accessed by stack unwinding.
// This reduces the significant amount of the offset info in the binary.
if instr.sideEffect() != sideEffectNone {
instr.annotateSourceOffset(l)
}
}
resultTypesFn := instructionReturnTypes[instr.opcode]
if resultTypesFn == nil {
panic("TODO: " + instr.Format(b))
}
t1, ts := resultTypesFn(b, instr)
if t1.invalid() {
return
}
r1 := b.allocateValue(t1)
instr.rValue = r1
tsl := len(ts)
if tsl == 0 {
return
}
rValues := b.varLengthPool.Allocate(tsl)
for i := 0; i < tsl; i++ {
rValues = rValues.Append(&b.varLengthPool, b.allocateValue(ts[i]))
}
instr.rValues = rValues
}
// DefineVariable implements Builder.DefineVariable.
func (b *builder) DefineVariable(variable Variable, value Value, block BasicBlock) {
if b.variables[variable].invalid() {
panic("BUG: trying to define variable " + variable.String() + " but is not declared yet")
}
if b.variables[variable] != value.Type() {
panic(fmt.Sprintf("BUG: inconsistent type for variable %d: expected %s but got %s", variable, b.variables[variable], value.Type()))
}
bb := block.(*basicBlock)
bb.lastDefinitions[variable] = value
}
// DefineVariableInCurrentBB implements Builder.DefineVariableInCurrentBB.
func (b *builder) DefineVariableInCurrentBB(variable Variable, value Value) {
b.DefineVariable(variable, value, b.currentBB)
}
// SetCurrentBlock implements Builder.SetCurrentBlock.
func (b *builder) SetCurrentBlock(bb BasicBlock) {
b.currentBB = bb.(*basicBlock)
}
// CurrentBlock implements Builder.CurrentBlock.
func (b *builder) CurrentBlock() BasicBlock {
return b.currentBB
}
// EntryBlock implements Builder.EntryBlock.
func (b *builder) EntryBlock() BasicBlock {
return b.entryBlk()
}
// DeclareVariable implements Builder.DeclareVariable.
func (b *builder) DeclareVariable(typ Type) Variable {
v := b.allocateVariable()
iv := int(v)
if l := len(b.variables); l <= iv {
b.variables = append(b.variables, make([]Type, 2*(l+1))...)
}
b.variables[v] = typ
return v
}
// allocateVariable allocates a new variable.
func (b *builder) allocateVariable() (ret Variable) {
ret = b.nextVariable
b.nextVariable++
return
}
// allocateValue implements Builder.AllocateValue.
func (b *builder) allocateValue(typ Type) (v Value) {
v = Value(b.nextValueID)
v = v.setType(typ)
b.nextValueID++
return
}
// FindValueInLinearPath implements Builder.FindValueInLinearPath.
func (b *builder) FindValueInLinearPath(variable Variable) Value {
return b.findValueInLinearPath(variable, b.currentBB)
}
func (b *builder) findValueInLinearPath(variable Variable, blk *basicBlock) Value {
if val, ok := blk.lastDefinitions[variable]; ok {
return val
} else if !blk.sealed {
return ValueInvalid
}
if pred := blk.singlePred; pred != nil {
// If this block is sealed and have only one predecessor,
// we can use the value in that block without ambiguity on definition.
return b.findValueInLinearPath(variable, pred)
}
if len(blk.preds) == 1 {
panic("BUG")
}
return ValueInvalid
}
func (b *builder) MustFindValueInBlk(variable Variable, blk BasicBlock) Value {
typ := b.definedVariableType(variable)
return b.findValue(typ, variable, blk.(*basicBlock))
}
// MustFindValue implements Builder.MustFindValue.
func (b *builder) MustFindValue(variable Variable) Value {
typ := b.definedVariableType(variable)
return b.findValue(typ, variable, b.currentBB)
}
// findValue recursively tries to find the latest definition of a `variable`. The algorithm is described in
// the section 2 of the paper https://link.springer.com/content/pdf/10.1007/978-3-642-37051-9_6.pdf.
//
// TODO: reimplement this in iterative, not recursive, to avoid stack overflow.
func (b *builder) findValue(typ Type, variable Variable, blk *basicBlock) Value {
if val, ok := blk.lastDefinitions[variable]; ok {
// The value is already defined in this block!
return val
} else if !blk.sealed { // Incomplete CFG as in the paper.
// If this is not sealed, that means it might have additional unknown predecessor later on.
// So we temporarily define the placeholder value here (not add as a parameter yet!),
// and record it as unknown.
// The unknown values are resolved when we call seal this block via BasicBlock.Seal().
value := b.allocateValue(typ)
if wazevoapi.SSALoggingEnabled {
fmt.Printf("adding unknown value placeholder for %s at %d\n", variable, blk.id)
}
blk.lastDefinitions[variable] = value
blk.unknownValues = append(blk.unknownValues, unknownValue{
variable: variable,
value: value,
})
return value
}
if pred := blk.singlePred; pred != nil {
// If this block is sealed and have only one predecessor,
// we can use the value in that block without ambiguity on definition.
return b.findValue(typ, variable, pred)
} else if len(blk.preds) == 0 {
panic("BUG: value is not defined for " + variable.String())
}
// If this block has multiple predecessors, we have to gather the definitions,
// and treat them as an argument to this block.
//
// The first thing is to define a new parameter to this block which may or may not be redundant, but
// later we eliminate trivial params in an optimization pass. This must be done before finding the
// definitions in the predecessors so that we can break the cycle.
paramValue := blk.AddParam(b, typ)
b.DefineVariable(variable, paramValue, blk)
// After the new param is added, we have to manipulate the original branching instructions
// in predecessors so that they would pass the definition of `variable` as the argument to
// the newly added PHI.
for i := range blk.preds {
pred := &blk.preds[i]
value := b.findValue(typ, variable, pred.blk)
pred.branch.addArgumentBranchInst(b, value)
}
return paramValue
}
// Seal implements Builder.Seal.
func (b *builder) Seal(raw BasicBlock) {
blk := raw.(*basicBlock)
if len(blk.preds) == 1 {
blk.singlePred = blk.preds[0].blk
}
blk.sealed = true
for _, v := range blk.unknownValues {
variable, phiValue := v.variable, v.value
typ := b.definedVariableType(variable)
blk.addParamOn(typ, phiValue)
for i := range blk.preds {
pred := &blk.preds[i]
predValue := b.findValue(typ, variable, pred.blk)
if !predValue.Valid() {
panic("BUG: value is not defined anywhere in the predecessors in the CFG")
}
pred.branch.addArgumentBranchInst(b, predValue)
}
}
}
// definedVariableType returns the type of the given variable. If the variable is not defined yet, it panics.
func (b *builder) definedVariableType(variable Variable) Type {
typ := b.variables[variable]
if typ.invalid() {
panic(fmt.Sprintf("%s is not defined yet", variable))
}
return typ
}
// Format implements Builder.Format.
func (b *builder) Format() string {
str := strings.Builder{}
usedSigs := b.usedSignatures()
if len(usedSigs) > 0 {
str.WriteByte('\n')
str.WriteString("signatures:\n")
for _, sig := range usedSigs {
str.WriteByte('\t')
str.WriteString(sig.String())
str.WriteByte('\n')
}
}
var iterBegin, iterNext func() *basicBlock
if b.doneBlockLayout {
iterBegin, iterNext = b.blockIteratorReversePostOrderBegin, b.blockIteratorReversePostOrderNext
} else {
iterBegin, iterNext = b.blockIteratorBegin, b.blockIteratorNext
}
for bb := iterBegin(); bb != nil; bb = iterNext() {
str.WriteByte('\n')
str.WriteString(bb.FormatHeader(b))
str.WriteByte('\n')
for cur := bb.Root(); cur != nil; cur = cur.Next() {
str.WriteByte('\t')
str.WriteString(cur.Format(b))
str.WriteByte('\n')
}
}
return str.String()
}
// BlockIteratorNext implements Builder.BlockIteratorNext.
func (b *builder) BlockIteratorNext() BasicBlock {
if blk := b.blockIteratorNext(); blk == nil {
return nil // BasicBlock((*basicBlock)(nil)) != BasicBlock(nil)
} else {
return blk
}
}
// BlockIteratorNext implements Builder.BlockIteratorNext.
func (b *builder) blockIteratorNext() *basicBlock {
index := b.blockIterCur
for {
if index == b.basicBlocksPool.Allocated() {
return nil
}
ret := b.basicBlocksPool.View(index)
index++
if !ret.invalid {
b.blockIterCur = index
return ret
}
}
}
// BlockIteratorBegin implements Builder.BlockIteratorBegin.
func (b *builder) BlockIteratorBegin() BasicBlock {
return b.blockIteratorBegin()
}
// BlockIteratorBegin implements Builder.BlockIteratorBegin.
func (b *builder) blockIteratorBegin() *basicBlock {
b.blockIterCur = 0
return b.blockIteratorNext()
}
// BlockIteratorReversePostOrderBegin implements Builder.BlockIteratorReversePostOrderBegin.
func (b *builder) BlockIteratorReversePostOrderBegin() BasicBlock {
return b.blockIteratorReversePostOrderBegin()
}
// BlockIteratorBegin implements Builder.BlockIteratorBegin.
func (b *builder) blockIteratorReversePostOrderBegin() *basicBlock {
b.blockIterCur = 0
return b.blockIteratorReversePostOrderNext()
}
// BlockIteratorReversePostOrderNext implements Builder.BlockIteratorReversePostOrderNext.
func (b *builder) BlockIteratorReversePostOrderNext() BasicBlock {
if blk := b.blockIteratorReversePostOrderNext(); blk == nil {
return nil // BasicBlock((*basicBlock)(nil)) != BasicBlock(nil)
} else {
return blk
}
}
// BlockIteratorNext implements Builder.BlockIteratorNext.
func (b *builder) blockIteratorReversePostOrderNext() *basicBlock {
if b.blockIterCur >= len(b.reversePostOrderedBasicBlocks) {
return nil
} else {
ret := b.reversePostOrderedBasicBlocks[b.blockIterCur]
b.blockIterCur++
return ret
}
}
// ValueRefCounts implements Builder.ValueRefCounts.
func (b *builder) ValueRefCounts() []int {
return b.valueRefCounts
}
// alias records the alias of the given values. The alias(es) will be
// eliminated in the optimization pass via resolveArgumentAlias.
func (b *builder) alias(dst, src Value) {
b.valueIDAliases[dst.ID()] = src
}
// resolveArgumentAlias resolves the alias of the arguments of the given instruction.
func (b *builder) resolveArgumentAlias(instr *Instruction) {
if instr.v.Valid() {
instr.v = b.resolveAlias(instr.v)
}
if instr.v2.Valid() {
instr.v2 = b.resolveAlias(instr.v2)
}
if instr.v3.Valid() {
instr.v3 = b.resolveAlias(instr.v3)
}
view := instr.vs.View()
for i, v := range view {
view[i] = b.resolveAlias(v)
}
}
// resolveAlias resolves the alias of the given value.
func (b *builder) resolveAlias(v Value) Value {
// Some aliases are chained, so we need to resolve them recursively.
for {
if src, ok := b.valueIDAliases[v.ID()]; ok {
v = src
} else {
break
}
}
return v
}
// entryBlk returns the entry block of the function.
func (b *builder) entryBlk() *basicBlock {
return b.basicBlocksPool.View(0)
}
// isDominatedBy returns true if the given block `n` is dominated by the given block `d`.
// Before calling this, the builder must pass by passCalculateImmediateDominators.
func (b *builder) isDominatedBy(n *basicBlock, d *basicBlock) bool {
if len(b.dominators) == 0 {
panic("BUG: passCalculateImmediateDominators must be called before calling isDominatedBy")
}
ent := b.entryBlk()
doms := b.dominators
for n != d && n != ent {
n = doms[n.id]
}
return n == d
}
// BlockIDMax implements Builder.BlockIDMax.
func (b *builder) BlockIDMax() BasicBlockID {
return BasicBlockID(b.basicBlocksPool.Allocated())
}
// InsertUndefined implements Builder.InsertUndefined.
func (b *builder) InsertUndefined() {
instr := b.AllocateInstruction()
instr.opcode = OpcodeUndefined
b.InsertInstruction(instr)
}
// LoopNestingForestRoots implements Builder.LoopNestingForestRoots.
func (b *builder) LoopNestingForestRoots() []BasicBlock {
return b.loopNestingForestRoots
}
// LowestCommonAncestor implements Builder.LowestCommonAncestor.
func (b *builder) LowestCommonAncestor(blk1, blk2 BasicBlock) BasicBlock {
return b.sparseTree.findLCA(blk1.ID(), blk2.ID())
}

View File

@ -0,0 +1,107 @@
package ssa
// IntegerCmpCond represents a condition for integer comparison.
type IntegerCmpCond byte
const (
// IntegerCmpCondInvalid represents an invalid condition.
IntegerCmpCondInvalid IntegerCmpCond = iota
// IntegerCmpCondEqual represents "==".
IntegerCmpCondEqual
// IntegerCmpCondNotEqual represents "!=".
IntegerCmpCondNotEqual
// IntegerCmpCondSignedLessThan represents Signed "<".
IntegerCmpCondSignedLessThan
// IntegerCmpCondSignedGreaterThanOrEqual represents Signed ">=".
IntegerCmpCondSignedGreaterThanOrEqual
// IntegerCmpCondSignedGreaterThan represents Signed ">".
IntegerCmpCondSignedGreaterThan
// IntegerCmpCondSignedLessThanOrEqual represents Signed "<=".
IntegerCmpCondSignedLessThanOrEqual
// IntegerCmpCondUnsignedLessThan represents Unsigned "<".
IntegerCmpCondUnsignedLessThan
// IntegerCmpCondUnsignedGreaterThanOrEqual represents Unsigned ">=".
IntegerCmpCondUnsignedGreaterThanOrEqual
// IntegerCmpCondUnsignedGreaterThan represents Unsigned ">".
IntegerCmpCondUnsignedGreaterThan
// IntegerCmpCondUnsignedLessThanOrEqual represents Unsigned "<=".
IntegerCmpCondUnsignedLessThanOrEqual
)
// String implements fmt.Stringer.
func (i IntegerCmpCond) String() string {
switch i {
case IntegerCmpCondEqual:
return "eq"
case IntegerCmpCondNotEqual:
return "neq"
case IntegerCmpCondSignedLessThan:
return "lt_s"
case IntegerCmpCondSignedGreaterThanOrEqual:
return "ge_s"
case IntegerCmpCondSignedGreaterThan:
return "gt_s"
case IntegerCmpCondSignedLessThanOrEqual:
return "le_s"
case IntegerCmpCondUnsignedLessThan:
return "lt_u"
case IntegerCmpCondUnsignedGreaterThanOrEqual:
return "ge_u"
case IntegerCmpCondUnsignedGreaterThan:
return "gt_u"
case IntegerCmpCondUnsignedLessThanOrEqual:
return "le_u"
default:
panic("invalid integer comparison condition")
}
}
// Signed returns true if the condition is signed integer comparison.
func (i IntegerCmpCond) Signed() bool {
switch i {
case IntegerCmpCondSignedLessThan, IntegerCmpCondSignedGreaterThanOrEqual,
IntegerCmpCondSignedGreaterThan, IntegerCmpCondSignedLessThanOrEqual:
return true
default:
return false
}
}
type FloatCmpCond byte
const (
// FloatCmpCondInvalid represents an invalid condition.
FloatCmpCondInvalid FloatCmpCond = iota
// FloatCmpCondEqual represents "==".
FloatCmpCondEqual
// FloatCmpCondNotEqual represents "!=".
FloatCmpCondNotEqual
// FloatCmpCondLessThan represents "<".
FloatCmpCondLessThan
// FloatCmpCondLessThanOrEqual represents "<=".
FloatCmpCondLessThanOrEqual
// FloatCmpCondGreaterThan represents ">".
FloatCmpCondGreaterThan
// FloatCmpCondGreaterThanOrEqual represents ">=".
FloatCmpCondGreaterThanOrEqual
)
// String implements fmt.Stringer.
func (f FloatCmpCond) String() string {
switch f {
case FloatCmpCondEqual:
return "eq"
case FloatCmpCondNotEqual:
return "neq"
case FloatCmpCondLessThan:
return "lt"
case FloatCmpCondLessThanOrEqual:
return "le"
case FloatCmpCondGreaterThan:
return "gt"
case FloatCmpCondGreaterThanOrEqual:
return "ge"
default:
panic("invalid float comparison condition")
}
}

View File

@ -0,0 +1,12 @@
package ssa
import "fmt"
// FuncRef is a unique identifier for a function of the frontend,
// and is used to reference the function in function call.
type FuncRef uint32
// String implements fmt.Stringer.
func (r FuncRef) String() string {
return fmt.Sprintf("f%d", r)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,417 @@
package ssa
import (
"fmt"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
)
// RunPasses implements Builder.RunPasses.
//
// The order here matters; some pass depends on the previous ones.
//
// Note that passes suffixed with "Opt" are the optimization passes, meaning that they edit the instructions and blocks
// while the other passes are not, like passEstimateBranchProbabilities does not edit them, but only calculates the additional information.
func (b *builder) RunPasses() {
b.runPreBlockLayoutPasses()
b.runBlockLayoutPass()
b.runPostBlockLayoutPasses()
b.runFinalizingPasses()
}
func (b *builder) runPreBlockLayoutPasses() {
passSortSuccessors(b)
passDeadBlockEliminationOpt(b)
passRedundantPhiEliminationOpt(b)
// The result of passCalculateImmediateDominators will be used by various passes below.
passCalculateImmediateDominators(b)
passNopInstElimination(b)
// TODO: implement either conversion of irreducible CFG into reducible one, or irreducible CFG detection where we panic.
// WebAssembly program shouldn't result in irreducible CFG, but we should handle it properly in just in case.
// See FixIrreducible pass in LLVM: https://llvm.org/doxygen/FixIrreducible_8cpp_source.html
// TODO: implement more optimization passes like:
// block coalescing.
// Copy-propagation.
// Constant folding.
// Common subexpression elimination.
// Arithmetic simplifications.
// and more!
// passDeadCodeEliminationOpt could be more accurate if we do this after other optimizations.
passDeadCodeEliminationOpt(b)
b.donePreBlockLayoutPasses = true
}
func (b *builder) runBlockLayoutPass() {
if !b.donePreBlockLayoutPasses {
panic("runBlockLayoutPass must be called after all pre passes are done")
}
passLayoutBlocks(b)
b.doneBlockLayout = true
}
// runPostBlockLayoutPasses runs the post block layout passes. After this point, CFG is somewhat stable,
// but still can be modified before finalizing passes. At this point, critical edges are split by passLayoutBlocks.
func (b *builder) runPostBlockLayoutPasses() {
if !b.doneBlockLayout {
panic("runPostBlockLayoutPasses must be called after block layout pass is done")
}
// TODO: Do more. e.g. tail duplication, loop unrolling, etc.
b.donePostBlockLayoutPasses = true
}
// runFinalizingPasses runs the finalizing passes. After this point, CFG should not be modified.
func (b *builder) runFinalizingPasses() {
if !b.donePostBlockLayoutPasses {
panic("runFinalizingPasses must be called after post block layout passes are done")
}
// Critical edges are split, so we fix the loop nesting forest.
passBuildLoopNestingForest(b)
passBuildDominatorTree(b)
// Now that we know the final placement of the blocks, we can explicitly mark the fallthrough jumps.
b.markFallthroughJumps()
}
// passDeadBlockEliminationOpt searches the unreachable blocks, and sets the basicBlock.invalid flag true if so.
func passDeadBlockEliminationOpt(b *builder) {
entryBlk := b.entryBlk()
b.clearBlkVisited()
b.blkStack = append(b.blkStack, entryBlk)
for len(b.blkStack) > 0 {
reachableBlk := b.blkStack[len(b.blkStack)-1]
b.blkStack = b.blkStack[:len(b.blkStack)-1]
b.blkVisited[reachableBlk] = 0 // the value won't be used in this pass.
if !reachableBlk.sealed && !reachableBlk.ReturnBlock() {
panic(fmt.Sprintf("%s is not sealed", reachableBlk))
}
if wazevoapi.SSAValidationEnabled {
reachableBlk.validate(b)
}
for _, succ := range reachableBlk.success {
if _, ok := b.blkVisited[succ]; ok {
continue
}
b.blkStack = append(b.blkStack, succ)
}
}
for blk := b.blockIteratorBegin(); blk != nil; blk = b.blockIteratorNext() {
if _, ok := b.blkVisited[blk]; !ok {
blk.invalid = true
}
}
}
// passRedundantPhiEliminationOpt eliminates the redundant PHIs (in our terminology, parameters of a block).
func passRedundantPhiEliminationOpt(b *builder) {
redundantParameterIndexes := b.ints[:0] // reuse the slice from previous iterations.
// TODO: this might be costly for large programs, but at least, as far as I did the experiment, it's almost the
// same as the single iteration version in terms of the overall compilation time. That *might be* mostly thanks to the fact
// that removing many PHIs results in the reduction of the total instructions, not because of this indefinite iteration is
// relatively small. For example, sqlite speedtest binary results in the large number of redundant PHIs,
// the maximum number of iteration was 22, which seems to be acceptable but not that small either since the
// complexity here is O(BlockNum * Iterations) at the worst case where BlockNum might be the order of thousands.
for {
changed := false
_ = b.blockIteratorBegin() // skip entry block!
// Below, we intentionally use the named iteration variable name, as this comes with inevitable nested for loops!
for blk := b.blockIteratorNext(); blk != nil; blk = b.blockIteratorNext() {
paramNum := len(blk.params)
for paramIndex := 0; paramIndex < paramNum; paramIndex++ {
phiValue := blk.params[paramIndex].value
redundant := true
nonSelfReferencingValue := ValueInvalid
for predIndex := range blk.preds {
br := blk.preds[predIndex].branch
// Resolve the alias in the arguments so that we could use the previous iteration's result.
b.resolveArgumentAlias(br)
pred := br.vs.View()[paramIndex]
if pred == phiValue {
// This is self-referencing: PHI from the same PHI.
continue
}
if !nonSelfReferencingValue.Valid() {
nonSelfReferencingValue = pred
continue
}
if nonSelfReferencingValue != pred {
redundant = false
break
}
}
if !nonSelfReferencingValue.Valid() {
// This shouldn't happen, and must be a bug in builder.go.
panic("BUG: params added but only self-referencing")
}
if redundant {
b.redundantParameterIndexToValue[paramIndex] = nonSelfReferencingValue
redundantParameterIndexes = append(redundantParameterIndexes, paramIndex)
}
}
if len(b.redundantParameterIndexToValue) == 0 {
continue
}
changed = true
// Remove the redundant PHIs from the argument list of branching instructions.
for predIndex := range blk.preds {
var cur int
predBlk := blk.preds[predIndex]
branchInst := predBlk.branch
view := branchInst.vs.View()
for argIndex, value := range view {
if _, ok := b.redundantParameterIndexToValue[argIndex]; !ok {
view[cur] = value
cur++
}
}
branchInst.vs.Cut(cur)
}
// Still need to have the definition of the value of the PHI (previously as the parameter).
for _, redundantParamIndex := range redundantParameterIndexes {
phiValue := blk.params[redundantParamIndex].value
onlyValue := b.redundantParameterIndexToValue[redundantParamIndex]
// Create an alias in this block from the only phi argument to the phi value.
b.alias(phiValue, onlyValue)
}
// Finally, Remove the param from the blk.
var cur int
for paramIndex := 0; paramIndex < paramNum; paramIndex++ {
param := blk.params[paramIndex]
if _, ok := b.redundantParameterIndexToValue[paramIndex]; !ok {
blk.params[cur] = param
cur++
}
}
blk.params = blk.params[:cur]
// Clears the map for the next iteration.
for _, paramIndex := range redundantParameterIndexes {
delete(b.redundantParameterIndexToValue, paramIndex)
}
redundantParameterIndexes = redundantParameterIndexes[:0]
}
if !changed {
break
}
}
// Reuse the slice for the future passes.
b.ints = redundantParameterIndexes
}
// passDeadCodeEliminationOpt traverses all the instructions, and calculates the reference count of each Value, and
// eliminates all the unnecessary instructions whose ref count is zero.
// The results are stored at builder.valueRefCounts. This also assigns a InstructionGroupID to each Instruction
// during the process. This is the last SSA-level optimization pass and after this,
// the SSA function is ready to be used by backends.
//
// TODO: the algorithm here might not be efficient. Get back to this later.
func passDeadCodeEliminationOpt(b *builder) {
nvid := int(b.nextValueID)
if nvid >= len(b.valueRefCounts) {
b.valueRefCounts = append(b.valueRefCounts, make([]int, b.nextValueID)...)
}
if nvid >= len(b.valueIDToInstruction) {
b.valueIDToInstruction = append(b.valueIDToInstruction, make([]*Instruction, b.nextValueID)...)
}
// First, we gather all the instructions with side effects.
liveInstructions := b.instStack[:0]
// During the process, we will assign InstructionGroupID to each instruction, which is not
// relevant to dead code elimination, but we need in the backend.
var gid InstructionGroupID
for blk := b.blockIteratorBegin(); blk != nil; blk = b.blockIteratorNext() {
for cur := blk.rootInstr; cur != nil; cur = cur.next {
cur.gid = gid
switch cur.sideEffect() {
case sideEffectTraps:
// The trappable should always be alive.
liveInstructions = append(liveInstructions, cur)
case sideEffectStrict:
liveInstructions = append(liveInstructions, cur)
// The strict side effect should create different instruction groups.
gid++
}
r1, rs := cur.Returns()
if r1.Valid() {
b.valueIDToInstruction[r1.ID()] = cur
}
for _, r := range rs {
b.valueIDToInstruction[r.ID()] = cur
}
}
}
// Find all the instructions referenced by live instructions transitively.
for len(liveInstructions) > 0 {
tail := len(liveInstructions) - 1
live := liveInstructions[tail]
liveInstructions = liveInstructions[:tail]
if live.live {
// If it's already marked alive, this is referenced multiple times,
// so we can skip it.
continue
}
live.live = true
// Before we walk, we need to resolve the alias first.
b.resolveArgumentAlias(live)
v1, v2, v3, vs := live.Args()
if v1.Valid() {
producingInst := b.valueIDToInstruction[v1.ID()]
if producingInst != nil {
liveInstructions = append(liveInstructions, producingInst)
}
}
if v2.Valid() {
producingInst := b.valueIDToInstruction[v2.ID()]
if producingInst != nil {
liveInstructions = append(liveInstructions, producingInst)
}
}
if v3.Valid() {
producingInst := b.valueIDToInstruction[v3.ID()]
if producingInst != nil {
liveInstructions = append(liveInstructions, producingInst)
}
}
for _, v := range vs {
producingInst := b.valueIDToInstruction[v.ID()]
if producingInst != nil {
liveInstructions = append(liveInstructions, producingInst)
}
}
}
// Now that all the live instructions are flagged as live=true, we eliminate all dead instructions.
for blk := b.blockIteratorBegin(); blk != nil; blk = b.blockIteratorNext() {
for cur := blk.rootInstr; cur != nil; cur = cur.next {
if !cur.live {
// Remove the instruction from the list.
if prev := cur.prev; prev != nil {
prev.next = cur.next
} else {
blk.rootInstr = cur.next
}
if next := cur.next; next != nil {
next.prev = cur.prev
}
continue
}
// If the value alive, we can be sure that arguments are used definitely.
// Hence, we can increment the value reference counts.
v1, v2, v3, vs := cur.Args()
if v1.Valid() {
b.incRefCount(v1.ID(), cur)
}
if v2.Valid() {
b.incRefCount(v2.ID(), cur)
}
if v3.Valid() {
b.incRefCount(v3.ID(), cur)
}
for _, v := range vs {
b.incRefCount(v.ID(), cur)
}
}
}
b.instStack = liveInstructions // we reuse the stack for the next iteration.
}
func (b *builder) incRefCount(id ValueID, from *Instruction) {
if wazevoapi.SSALoggingEnabled {
fmt.Printf("v%d referenced from %v\n", id, from.Format(b))
}
b.valueRefCounts[id]++
}
// clearBlkVisited clears the b.blkVisited map so that we can reuse it for multiple places.
func (b *builder) clearBlkVisited() {
b.blkStack2 = b.blkStack2[:0]
for key := range b.blkVisited {
b.blkStack2 = append(b.blkStack2, key)
}
for _, blk := range b.blkStack2 {
delete(b.blkVisited, blk)
}
b.blkStack2 = b.blkStack2[:0]
}
// passNopInstElimination eliminates the instructions which is essentially a no-op.
func passNopInstElimination(b *builder) {
if int(b.nextValueID) >= len(b.valueIDToInstruction) {
b.valueIDToInstruction = append(b.valueIDToInstruction, make([]*Instruction, b.nextValueID)...)
}
for blk := b.blockIteratorBegin(); blk != nil; blk = b.blockIteratorNext() {
for cur := blk.rootInstr; cur != nil; cur = cur.next {
r1, rs := cur.Returns()
if r1.Valid() {
b.valueIDToInstruction[r1.ID()] = cur
}
for _, r := range rs {
b.valueIDToInstruction[r.ID()] = cur
}
}
}
for blk := b.blockIteratorBegin(); blk != nil; blk = b.blockIteratorNext() {
for cur := blk.rootInstr; cur != nil; cur = cur.next {
switch cur.Opcode() {
// TODO: add more logics here.
case OpcodeIshl, OpcodeSshr, OpcodeUshr:
x, amount := cur.Arg2()
definingInst := b.valueIDToInstruction[amount.ID()]
if definingInst == nil {
// If there's no defining instruction, that means the amount is coming from the parameter.
continue
}
if definingInst.Constant() {
v := definingInst.ConstantVal()
if x.Type().Bits() == 64 {
v = v % 64
} else {
v = v % 32
}
if v == 0 {
b.alias(cur.Return(), x)
}
}
}
}
}
}
// passSortSuccessors sorts the successors of each block in the natural program order.
func passSortSuccessors(b *builder) {
for i := 0; i < b.basicBlocksPool.Allocated(); i++ {
blk := b.basicBlocksPool.View(i)
sortBlocks(blk.success)
}
}

View File

@ -0,0 +1,335 @@
package ssa
import (
"fmt"
"strings"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
)
// passLayoutBlocks implements Builder.LayoutBlocks. This re-organizes builder.reversePostOrderedBasicBlocks.
//
// TODO: there are tons of room for improvement here. e.g. LLVM has BlockPlacementPass using BlockFrequencyInfo,
// BranchProbabilityInfo, and LoopInfo to do a much better job. Also, if we have the profiling instrumentation
// like ball-larus algorithm, then we could do profile-guided optimization. Basically all of them are trying
// to maximize the fall-through opportunities which is most efficient.
//
// Here, fallthrough happens when a block ends with jump instruction whose target is the right next block in the
// builder.reversePostOrderedBasicBlocks.
//
// Currently, we just place blocks using the DFS reverse post-order of the dominator tree with the heuristics:
// 1. a split edge trampoline towards a loop header will be placed as a fallthrough.
// 2. we invert the brz and brnz if it makes the fallthrough more likely.
//
// This heuristic is done in maybeInvertBranches function.
func passLayoutBlocks(b *builder) {
b.clearBlkVisited()
// We might end up splitting critical edges which adds more basic blocks,
// so we store the currently existing basic blocks in nonSplitBlocks temporarily.
// That way we can iterate over the original basic blocks while appending new ones into reversePostOrderedBasicBlocks.
nonSplitBlocks := b.blkStack[:0]
for i, blk := range b.reversePostOrderedBasicBlocks {
if !blk.Valid() {
continue
}
nonSplitBlocks = append(nonSplitBlocks, blk)
if i != len(b.reversePostOrderedBasicBlocks)-1 {
_ = maybeInvertBranches(blk, b.reversePostOrderedBasicBlocks[i+1])
}
}
var trampolines []*basicBlock
// Reset the order slice since we update on the fly by splitting critical edges.
b.reversePostOrderedBasicBlocks = b.reversePostOrderedBasicBlocks[:0]
uninsertedTrampolines := b.blkStack2[:0]
for _, blk := range nonSplitBlocks {
for i := range blk.preds {
pred := blk.preds[i].blk
if _, ok := b.blkVisited[pred]; ok || !pred.Valid() {
continue
} else if pred.reversePostOrder < blk.reversePostOrder {
// This means the edge is critical, and this pred is the trampoline and yet to be inserted.
// Split edge trampolines must come before the destination in reverse post-order.
b.reversePostOrderedBasicBlocks = append(b.reversePostOrderedBasicBlocks, pred)
b.blkVisited[pred] = 0 // mark as inserted, the value is not used.
}
}
// Now that we've already added all the potential trampoline blocks incoming to this block,
// we can add this block itself.
b.reversePostOrderedBasicBlocks = append(b.reversePostOrderedBasicBlocks, blk)
b.blkVisited[blk] = 0 // mark as inserted, the value is not used.
if len(blk.success) < 2 {
// There won't be critical edge originating from this block.
continue
} else if blk.currentInstr.opcode == OpcodeBrTable {
// We don't split critical edges here, because at the construction site of BrTable, we already split the edges.
continue
}
for sidx, succ := range blk.success {
if !succ.ReturnBlock() && // If the successor is a return block, we need to split the edge any way because we need "epilogue" to be inserted.
// Plus if there's no multiple incoming edges to this successor, (pred, succ) is not critical.
len(succ.preds) < 2 {
continue
}
// Otherwise, we are sure this is a critical edge. To modify the CFG, we need to find the predecessor info
// from the successor.
var predInfo *basicBlockPredecessorInfo
for i := range succ.preds { // This linear search should not be a problem since the number of predecessors should almost always small.
pred := &succ.preds[i]
if pred.blk == blk {
predInfo = pred
break
}
}
if predInfo == nil {
// This must be a bug in somewhere around branch manipulation.
panic("BUG: predecessor info not found while the successor exists in successors list")
}
if wazevoapi.SSALoggingEnabled {
fmt.Printf("trying to split edge from %d->%d at %s\n",
blk.ID(), succ.ID(), predInfo.branch.Format(b))
}
trampoline := b.splitCriticalEdge(blk, succ, predInfo)
// Update the successors slice because the target is no longer the original `succ`.
blk.success[sidx] = trampoline
if wazevoapi.SSAValidationEnabled {
trampolines = append(trampolines, trampoline)
}
if wazevoapi.SSALoggingEnabled {
fmt.Printf("edge split from %d->%d at %s as %d->%d->%d \n",
blk.ID(), succ.ID(), predInfo.branch.Format(b),
blk.ID(), trampoline.ID(), succ.ID())
}
fallthroughBranch := blk.currentInstr
if fallthroughBranch.opcode == OpcodeJump && fallthroughBranch.blk == trampoline {
// This can be lowered as fallthrough at the end of the block.
b.reversePostOrderedBasicBlocks = append(b.reversePostOrderedBasicBlocks, trampoline)
b.blkVisited[trampoline] = 0 // mark as inserted, the value is not used.
} else {
uninsertedTrampolines = append(uninsertedTrampolines, trampoline)
}
}
for _, trampoline := range uninsertedTrampolines {
if trampoline.success[0].reversePostOrder <= trampoline.reversePostOrder { // "<=", not "<" because the target might be itself.
// This means the critical edge was backward, so we insert after the current block immediately.
b.reversePostOrderedBasicBlocks = append(b.reversePostOrderedBasicBlocks, trampoline)
b.blkVisited[trampoline] = 0 // mark as inserted, the value is not used.
} // If the target is forward, we can wait to insert until the target is inserted.
}
uninsertedTrampolines = uninsertedTrampolines[:0] // Reuse the stack for the next block.
}
if wazevoapi.SSALoggingEnabled {
var bs []string
for _, blk := range b.reversePostOrderedBasicBlocks {
bs = append(bs, blk.Name())
}
fmt.Println("ordered blocks: ", strings.Join(bs, ", "))
}
if wazevoapi.SSAValidationEnabled {
for _, trampoline := range trampolines {
if _, ok := b.blkVisited[trampoline]; !ok {
panic("BUG: trampoline block not inserted: " + trampoline.FormatHeader(b))
}
trampoline.validate(b)
}
}
// Reuse the stack for the next iteration.
b.blkStack2 = uninsertedTrampolines[:0]
}
// markFallthroughJumps finds the fallthrough jumps and marks them as such.
func (b *builder) markFallthroughJumps() {
l := len(b.reversePostOrderedBasicBlocks) - 1
for i, blk := range b.reversePostOrderedBasicBlocks {
if i < l {
cur := blk.currentInstr
if cur.opcode == OpcodeJump && cur.blk == b.reversePostOrderedBasicBlocks[i+1] {
cur.AsFallthroughJump()
}
}
}
}
// maybeInvertBranches inverts the branch instructions if it is likely possible to the fallthrough more likely with simple heuristics.
// nextInRPO is the next block in the reverse post-order.
//
// Returns true if the branch is inverted for testing purpose.
func maybeInvertBranches(now *basicBlock, nextInRPO *basicBlock) bool {
fallthroughBranch := now.currentInstr
if fallthroughBranch.opcode == OpcodeBrTable {
return false
}
condBranch := fallthroughBranch.prev
if condBranch == nil || (condBranch.opcode != OpcodeBrnz && condBranch.opcode != OpcodeBrz) {
return false
}
if len(fallthroughBranch.vs.View()) != 0 || len(condBranch.vs.View()) != 0 {
// If either one of them has arguments, we don't invert the branches.
return false
}
// So this block has two branches (a conditional branch followed by an unconditional branch) at the end.
// We can invert the condition of the branch if it makes the fallthrough more likely.
fallthroughTarget, condTarget := fallthroughBranch.blk.(*basicBlock), condBranch.blk.(*basicBlock)
if fallthroughTarget.loopHeader {
// First, if the tail's target is loopHeader, we don't need to do anything here,
// because the edge is likely to be critical edge for complex loops (e.g. loop with branches inside it).
// That means, we will split the edge in the end of LayoutBlocks function, and insert the trampoline block
// right after this block, which will be fallthrough in any way.
return false
} else if condTarget.loopHeader {
// On the other hand, if the condBranch's target is loopHeader, we invert the condition of the branch
// so that we could get the fallthrough to the trampoline block.
goto invert
}
if fallthroughTarget == nextInRPO {
// Also, if the tail's target is the next block in the reverse post-order, we don't need to do anything here,
// because if this is not critical edge, we would end up placing these two blocks adjacent to each other.
// Even if it is the critical edge, we place the trampoline block right after this block, which will be fallthrough in any way.
return false
} else if condTarget == nextInRPO {
// If the condBranch's target is the next block in the reverse post-order, we invert the condition of the branch
// so that we could get the fallthrough to the block.
goto invert
} else {
return false
}
invert:
for i := range fallthroughTarget.preds {
pred := &fallthroughTarget.preds[i]
if pred.branch == fallthroughBranch {
pred.branch = condBranch
break
}
}
for i := range condTarget.preds {
pred := &condTarget.preds[i]
if pred.branch == condBranch {
pred.branch = fallthroughBranch
break
}
}
condBranch.InvertBrx()
condBranch.blk = fallthroughTarget
fallthroughBranch.blk = condTarget
if wazevoapi.SSALoggingEnabled {
fmt.Printf("inverting branches at %d->%d and %d->%d\n",
now.ID(), fallthroughTarget.ID(), now.ID(), condTarget.ID())
}
return true
}
// splitCriticalEdge splits the critical edge between the given predecessor (`pred`) and successor (owning `predInfo`).
//
// - `pred` is the source of the critical edge,
// - `succ` is the destination of the critical edge,
// - `predInfo` is the predecessor info in the succ.preds slice which represents the critical edge.
//
// Why splitting critical edges is important? See following links:
//
// - https://en.wikipedia.org/wiki/Control-flow_graph
// - https://nickdesaulniers.github.io/blog/2023/01/27/critical-edge-splitting/
//
// The returned basic block is the trampoline block which is inserted to split the critical edge.
func (b *builder) splitCriticalEdge(pred, succ *basicBlock, predInfo *basicBlockPredecessorInfo) *basicBlock {
// In the following, we convert the following CFG:
//
// pred --(originalBranch)--> succ
//
// to the following CFG:
//
// pred --(newBranch)--> trampoline --(originalBranch)-> succ
//
// where trampoline is a new basic block which is created to split the critical edge.
trampoline := b.allocateBasicBlock()
if int(trampoline.id) >= len(b.dominators) {
b.dominators = append(b.dominators, make([]*basicBlock, trampoline.id+1)...)
}
b.dominators[trampoline.id] = pred
originalBranch := predInfo.branch
// Replace originalBranch with the newBranch.
newBranch := b.AllocateInstruction()
newBranch.opcode = originalBranch.opcode
newBranch.blk = trampoline
switch originalBranch.opcode {
case OpcodeJump:
case OpcodeBrz, OpcodeBrnz:
originalBranch.opcode = OpcodeJump // Trampoline consists of one unconditional branch.
newBranch.v = originalBranch.v
originalBranch.v = ValueInvalid
default:
panic("BUG: critical edge shouldn't be originated from br_table")
}
swapInstruction(pred, originalBranch, newBranch)
// Replace the original branch with the new branch.
trampoline.rootInstr = originalBranch
trampoline.currentInstr = originalBranch
trampoline.success = append(trampoline.success, succ) // Do not use []*basicBlock{pred} because we might have already allocated the slice.
trampoline.preds = append(trampoline.preds, // same as ^.
basicBlockPredecessorInfo{blk: pred, branch: newBranch})
b.Seal(trampoline)
// Update the original branch to point to the trampoline.
predInfo.blk = trampoline
predInfo.branch = originalBranch
if wazevoapi.SSAValidationEnabled {
trampoline.validate(b)
}
if len(trampoline.params) > 0 {
panic("trampoline should not have params")
}
// Assign the same order as the original block so that this will be placed before the actual destination.
trampoline.reversePostOrder = pred.reversePostOrder
return trampoline
}
// swapInstruction replaces `old` in the block `blk` with `New`.
func swapInstruction(blk *basicBlock, old, New *Instruction) {
if blk.rootInstr == old {
blk.rootInstr = New
next := old.next
New.next = next
next.prev = New
} else {
if blk.currentInstr == old {
blk.currentInstr = New
}
prev := old.prev
prev.next, New.prev = New, prev
if next := old.next; next != nil {
New.next, next.prev = next, New
}
}
old.prev, old.next = nil, nil
}

View File

@ -0,0 +1,312 @@
package ssa
import (
"fmt"
"math"
"strings"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
)
// passCalculateImmediateDominators calculates immediate dominators for each basic block.
// The result is stored in b.dominators. This make it possible for the following passes to
// use builder.isDominatedBy to check if a block is dominated by another block.
//
// At the last of pass, this function also does the loop detection and sets the basicBlock.loop flag.
func passCalculateImmediateDominators(b *builder) {
reversePostOrder := b.reversePostOrderedBasicBlocks[:0]
exploreStack := b.blkStack[:0]
b.clearBlkVisited()
entryBlk := b.entryBlk()
// Store the reverse postorder from the entrypoint into reversePostOrder slice.
// This calculation of reverse postorder is not described in the paper,
// so we use heuristic to calculate it so that we could potentially handle arbitrary
// complex CFGs under the assumption that success is sorted in program's natural order.
// That means blk.success[i] always appears before blk.success[i+1] in the source program,
// which is a reasonable assumption as long as SSA Builder is properly used.
//
// First we push blocks in postorder iteratively visit successors of the entry block.
exploreStack = append(exploreStack, entryBlk)
const visitStateUnseen, visitStateSeen, visitStateDone = 0, 1, 2
b.blkVisited[entryBlk] = visitStateSeen
for len(exploreStack) > 0 {
tail := len(exploreStack) - 1
blk := exploreStack[tail]
exploreStack = exploreStack[:tail]
switch b.blkVisited[blk] {
case visitStateUnseen:
// This is likely a bug in the frontend.
panic("BUG: unsupported CFG")
case visitStateSeen:
// This is the first time to pop this block, and we have to see the successors first.
// So push this block again to the stack.
exploreStack = append(exploreStack, blk)
// And push the successors to the stack if necessary.
for _, succ := range blk.success {
if succ.ReturnBlock() || succ.invalid {
continue
}
if b.blkVisited[succ] == visitStateUnseen {
b.blkVisited[succ] = visitStateSeen
exploreStack = append(exploreStack, succ)
}
}
// Finally, we could pop this block once we pop all of its successors.
b.blkVisited[blk] = visitStateDone
case visitStateDone:
// Note: at this point we push blk in postorder despite its name.
reversePostOrder = append(reversePostOrder, blk)
}
}
// At this point, reversePostOrder has postorder actually, so we reverse it.
for i := len(reversePostOrder)/2 - 1; i >= 0; i-- {
j := len(reversePostOrder) - 1 - i
reversePostOrder[i], reversePostOrder[j] = reversePostOrder[j], reversePostOrder[i]
}
for i, blk := range reversePostOrder {
blk.reversePostOrder = i
}
// Reuse the dominators slice if possible from the previous computation of function.
b.dominators = b.dominators[:cap(b.dominators)]
if len(b.dominators) < b.basicBlocksPool.Allocated() {
// Generously reserve space in the slice because the slice will be reused future allocation.
b.dominators = append(b.dominators, make([]*basicBlock, b.basicBlocksPool.Allocated())...)
}
calculateDominators(reversePostOrder, b.dominators)
// Reuse the slices for the future use.
b.blkStack = exploreStack
// For the following passes.
b.reversePostOrderedBasicBlocks = reversePostOrder
// Ready to detect loops!
subPassLoopDetection(b)
}
// calculateDominators calculates the immediate dominator of each node in the CFG, and store the result in `doms`.
// The algorithm is based on the one described in the paper "A Simple, Fast Dominance Algorithm"
// https://www.cs.rice.edu/~keith/EMBED/dom.pdf which is a faster/simple alternative to the well known Lengauer-Tarjan algorithm.
//
// The following code almost matches the pseudocode in the paper with one exception (see the code comment below).
//
// The result slice `doms` must be pre-allocated with the size larger than the size of dfsBlocks.
func calculateDominators(reversePostOrderedBlks []*basicBlock, doms []*basicBlock) {
entry, reversePostOrderedBlks := reversePostOrderedBlks[0], reversePostOrderedBlks[1: /* skips entry point */]
for _, blk := range reversePostOrderedBlks {
doms[blk.id] = nil
}
doms[entry.id] = entry
changed := true
for changed {
changed = false
for _, blk := range reversePostOrderedBlks {
var u *basicBlock
for i := range blk.preds {
pred := blk.preds[i].blk
// Skip if this pred is not reachable yet. Note that this is not described in the paper,
// but it is necessary to handle nested loops etc.
if doms[pred.id] == nil {
continue
}
if u == nil {
u = pred
continue
} else {
u = intersect(doms, u, pred)
}
}
if doms[blk.id] != u {
doms[blk.id] = u
changed = true
}
}
}
}
// intersect returns the common dominator of blk1 and blk2.
//
// This is the `intersect` function in the paper.
func intersect(doms []*basicBlock, blk1 *basicBlock, blk2 *basicBlock) *basicBlock {
finger1, finger2 := blk1, blk2
for finger1 != finger2 {
// Move the 'finger1' upwards to its immediate dominator.
for finger1.reversePostOrder > finger2.reversePostOrder {
finger1 = doms[finger1.id]
}
// Move the 'finger2' upwards to its immediate dominator.
for finger2.reversePostOrder > finger1.reversePostOrder {
finger2 = doms[finger2.id]
}
}
return finger1
}
// subPassLoopDetection detects loops in the function using the immediate dominators.
//
// This is run at the last of passCalculateImmediateDominators.
func subPassLoopDetection(b *builder) {
for blk := b.blockIteratorBegin(); blk != nil; blk = b.blockIteratorNext() {
for i := range blk.preds {
pred := blk.preds[i].blk
if pred.invalid {
continue
}
if b.isDominatedBy(pred, blk) {
blk.loopHeader = true
}
}
}
}
// buildLoopNestingForest builds the loop nesting forest for the function.
// This must be called after branch splitting since it relies on the CFG.
func passBuildLoopNestingForest(b *builder) {
ent := b.entryBlk()
doms := b.dominators
for _, blk := range b.reversePostOrderedBasicBlocks {
n := doms[blk.id]
for !n.loopHeader && n != ent {
n = doms[n.id]
}
if n == ent && blk.loopHeader {
b.loopNestingForestRoots = append(b.loopNestingForestRoots, blk)
} else if n == ent {
} else if n.loopHeader {
n.loopNestingForestChildren = append(n.loopNestingForestChildren, blk)
}
}
if wazevoapi.SSALoggingEnabled {
for _, root := range b.loopNestingForestRoots {
printLoopNestingForest(root.(*basicBlock), 0)
}
}
}
func printLoopNestingForest(root *basicBlock, depth int) {
fmt.Println(strings.Repeat("\t", depth), "loop nesting forest root:", root.ID())
for _, child := range root.loopNestingForestChildren {
fmt.Println(strings.Repeat("\t", depth+1), "child:", child.ID())
if child.LoopHeader() {
printLoopNestingForest(child.(*basicBlock), depth+2)
}
}
}
type dominatorSparseTree struct {
time int
euler []*basicBlock
first, depth []int
table [][]int
}
// passBuildDominatorTree builds the dominator tree for the function, and constructs builder.sparseTree.
func passBuildDominatorTree(b *builder) {
// First we materialize the children of each node in the dominator tree.
idoms := b.dominators
for _, blk := range b.reversePostOrderedBasicBlocks {
parent := idoms[blk.id]
if parent == nil {
panic("BUG")
} else if parent == blk {
// This is the entry block.
continue
}
if prev := parent.child; prev == nil {
parent.child = blk
} else {
parent.child = blk
blk.sibling = prev
}
}
// Reset the state from the previous computation.
n := b.basicBlocksPool.Allocated()
st := &b.sparseTree
st.euler = append(st.euler[:0], make([]*basicBlock, 2*n-1)...)
st.first = append(st.first[:0], make([]int, n)...)
for i := range st.first {
st.first[i] = -1
}
st.depth = append(st.depth[:0], make([]int, 2*n-1)...)
st.time = 0
// Start building the sparse tree.
st.eulerTour(b.entryBlk(), 0)
st.buildSparseTable()
}
func (dt *dominatorSparseTree) eulerTour(node *basicBlock, height int) {
if wazevoapi.SSALoggingEnabled {
fmt.Println(strings.Repeat("\t", height), "euler tour:", node.ID())
}
dt.euler[dt.time] = node
dt.depth[dt.time] = height
if dt.first[node.id] == -1 {
dt.first[node.id] = dt.time
}
dt.time++
for child := node.child; child != nil; child = child.sibling {
dt.eulerTour(child, height+1)
dt.euler[dt.time] = node // add the current node again after visiting a child
dt.depth[dt.time] = height
dt.time++
}
}
// buildSparseTable builds a sparse table for RMQ queries.
func (dt *dominatorSparseTree) buildSparseTable() {
n := len(dt.depth)
k := int(math.Log2(float64(n))) + 1
table := dt.table
if n >= len(table) {
table = append(table, make([][]int, n+1)...)
}
for i := range table {
if len(table[i]) < k {
table[i] = append(table[i], make([]int, k)...)
}
table[i][0] = i
}
for j := 1; 1<<j <= n; j++ {
for i := 0; i+(1<<j)-1 < n; i++ {
if dt.depth[table[i][j-1]] < dt.depth[table[i+(1<<(j-1))][j-1]] {
table[i][j] = table[i][j-1]
} else {
table[i][j] = table[i+(1<<(j-1))][j-1]
}
}
}
dt.table = table
}
// rmq performs a range minimum query on the sparse table.
func (dt *dominatorSparseTree) rmq(l, r int) int {
table := dt.table
depth := dt.depth
j := int(math.Log2(float64(r - l + 1)))
if depth[table[l][j]] <= depth[table[r-(1<<j)+1][j]] {
return table[l][j]
}
return table[r-(1<<j)+1][j]
}
// findLCA finds the LCA using the Euler tour and RMQ.
func (dt *dominatorSparseTree) findLCA(u, v BasicBlockID) *basicBlock {
first := dt.first
if first[u] > first[v] {
u, v = v, u
}
return dt.euler[dt.rmq(first[u], first[v])]
}

View File

@ -0,0 +1,49 @@
package ssa
import (
"fmt"
"strings"
)
// Signature is a function prototype.
type Signature struct {
// ID is a unique identifier for this signature used to lookup.
ID SignatureID
// Params and Results are the types of the parameters and results of the function.
Params, Results []Type
// used is true if this is used by the currently-compiled function.
// Debugging only.
used bool
}
// String implements fmt.Stringer.
func (s *Signature) String() string {
str := strings.Builder{}
str.WriteString(s.ID.String())
str.WriteString(": ")
if len(s.Params) > 0 {
for _, typ := range s.Params {
str.WriteString(typ.String())
}
} else {
str.WriteByte('v')
}
str.WriteByte('_')
if len(s.Results) > 0 {
for _, typ := range s.Results {
str.WriteString(typ.String())
}
} else {
str.WriteByte('v')
}
return str.String()
}
// SignatureID is an unique identifier used to lookup.
type SignatureID int
// String implements fmt.Stringer.
func (s SignatureID) String() string {
return fmt.Sprintf("sig%d", s)
}

View File

@ -0,0 +1,14 @@
// Package ssa is used to construct SSA function. By nature this is free of Wasm specific thing
// and ISA.
//
// We use the "block argument" variant of SSA: https://en.wikipedia.org/wiki/Static_single-assignment_form#Block_arguments
// which is equivalent to the traditional PHI function based one, but more convenient during optimizations.
// However, in this package's source code comment, we might use PHI whenever it seems necessary in order to be aligned with
// existing literatures, e.g. SSA level optimization algorithms are often described using PHI nodes.
//
// The rationale doc for the choice of "block argument" by MLIR of LLVM is worth a read:
// https://mlir.llvm.org/docs/Rationale/Rationale/#block-arguments-vs-phi-nodes
//
// The algorithm to resolve variable definitions used here is based on the paper
// "Simple and Efficient Construction of Static Single Assignment Form": https://link.springer.com/content/pdf/10.1007/978-3-642-37051-9_6.pdf.
package ssa

View File

@ -0,0 +1,112 @@
package ssa
type Type byte
const (
typeInvalid Type = iota
// TODO: add 8, 16 bit types when it's needed for optimizations.
// TypeI32 represents an integer type with 32 bits.
TypeI32
// TypeI64 represents an integer type with 64 bits.
TypeI64
// TypeF32 represents 32-bit floats in the IEEE 754.
TypeF32
// TypeF64 represents 64-bit floats in the IEEE 754.
TypeF64
// TypeV128 represents 128-bit SIMD vectors.
TypeV128
)
// String implements fmt.Stringer.
func (t Type) String() (ret string) {
switch t {
case typeInvalid:
return "invalid"
case TypeI32:
return "i32"
case TypeI64:
return "i64"
case TypeF32:
return "f32"
case TypeF64:
return "f64"
case TypeV128:
return "v128"
default:
panic(int(t))
}
}
// IsInt returns true if the type is an integer type.
func (t Type) IsInt() bool {
return t == TypeI32 || t == TypeI64
}
// IsFloat returns true if the type is a floating point type.
func (t Type) IsFloat() bool {
return t == TypeF32 || t == TypeF64
}
// Bits returns the number of bits required to represent the type.
func (t Type) Bits() byte {
switch t {
case TypeI32, TypeF32:
return 32
case TypeI64, TypeF64:
return 64
case TypeV128:
return 128
default:
panic(int(t))
}
}
// Size returns the number of bytes required to represent the type.
func (t Type) Size() byte {
return t.Bits() / 8
}
func (t Type) invalid() bool {
return t == typeInvalid
}
// VecLane represents a lane in a SIMD vector.
type VecLane byte
const (
VecLaneInvalid VecLane = 1 + iota
VecLaneI8x16
VecLaneI16x8
VecLaneI32x4
VecLaneI64x2
VecLaneF32x4
VecLaneF64x2
)
// String implements fmt.Stringer.
func (vl VecLane) String() (ret string) {
switch vl {
case VecLaneInvalid:
return "invalid"
case VecLaneI8x16:
return "i8x16"
case VecLaneI16x8:
return "i16x8"
case VecLaneI32x4:
return "i32x4"
case VecLaneI64x2:
return "i64x2"
case VecLaneF32x4:
return "f32x4"
case VecLaneF64x2:
return "f64x2"
default:
panic(int(vl))
}
}

View File

@ -0,0 +1,87 @@
package ssa
import (
"fmt"
"math"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
)
// Variable is a unique identifier for a source program's variable and will correspond to
// multiple ssa Value(s).
//
// For example, `Local 1` is a Variable in WebAssembly, and Value(s) will be created for it
// whenever it executes `local.set 1`.
//
// Variable is useful to track the SSA Values of a variable in the source program, and
// can be used to find the corresponding latest SSA Value via Builder.FindValue.
type Variable uint32
// String implements fmt.Stringer.
func (v Variable) String() string {
return fmt.Sprintf("var%d", v)
}
// Value represents an SSA value with a type information. The relationship with Variable is 1: N (including 0),
// that means there might be multiple Variable(s) for a Value.
//
// Higher 32-bit is used to store Type for this value.
type Value uint64
// ValueID is the lower 32bit of Value, which is the pure identifier of Value without type info.
type ValueID uint32
const (
valueIDInvalid ValueID = math.MaxUint32
ValueInvalid Value = Value(valueIDInvalid)
)
// Format creates a debug string for this Value using the data stored in Builder.
func (v Value) Format(b Builder) string {
if annotation, ok := b.(*builder).valueAnnotations[v.ID()]; ok {
return annotation
}
return fmt.Sprintf("v%d", v.ID())
}
func (v Value) formatWithType(b Builder) (ret string) {
if annotation, ok := b.(*builder).valueAnnotations[v.ID()]; ok {
ret = annotation + ":" + v.Type().String()
} else {
ret = fmt.Sprintf("v%d:%s", v.ID(), v.Type())
}
if wazevoapi.SSALoggingEnabled { // This is useful to check live value analysis bugs.
if bd := b.(*builder); bd.donePostBlockLayoutPasses {
id := v.ID()
ret += fmt.Sprintf("(ref=%d)", bd.valueRefCounts[id])
}
}
return ret
}
// Valid returns true if this value is valid.
func (v Value) Valid() bool {
return v.ID() != valueIDInvalid
}
// Type returns the Type of this value.
func (v Value) Type() Type {
return Type(v >> 32)
}
// ID returns the valueID of this value.
func (v Value) ID() ValueID {
return ValueID(v)
}
// setType sets a type to this Value and returns the updated Value.
func (v Value) setType(typ Type) Value {
return v | Value(typ)<<32
}
// Values is a slice of Value. Use this instead of []Value to reuse the underlying memory.
type Values = wazevoapi.VarLength[Value]
// ValuesNil is a nil Values.
var ValuesNil = wazevoapi.NewNilVarLength[Value]()