// Package frontend implements the translation of WebAssembly to SSA IR using the ssa package.
package frontend import ( ) // Compiler is in charge of lowering Wasm to SSA IR, and does the optimization // on top of it in architecture-independent way. type Compiler struct { // Per-module data that is used across all functions. m *wasm.Module offset *wazevoapi.ModuleContextOffsetData // ssaBuilder is a ssa.Builder used by this frontend. ssaBuilder ssa.Builder signatures map[*wasm.FunctionType]*ssa.Signature listenerSignatures map[*wasm.FunctionType][2]*ssa.Signature memoryGrowSig ssa.Signature memoryWait32Sig ssa.Signature memoryWait64Sig ssa.Signature memoryNotifySig ssa.Signature checkModuleExitCodeSig ssa.Signature tableGrowSig ssa.Signature refFuncSig ssa.Signature memmoveSig ssa.Signature ensureTermination bool // Followings are reset by per function. // wasmLocalToVariable maps the index (considered as wasm.Index of locals) // to the corresponding ssa.Variable. wasmLocalToVariable [] /* local index to */ ssa.Variable wasmLocalFunctionIndex wasm.Index wasmFunctionTypeIndex wasm.Index wasmFunctionTyp *wasm.FunctionType wasmFunctionLocalTypes []wasm.ValueType wasmFunctionBody []byte wasmFunctionBodyOffsetInCodeSection uint64 memoryBaseVariable, memoryLenVariable ssa.Variable needMemory bool memoryShared bool globalVariables []ssa.Variable globalVariablesTypes []ssa.Type mutableGlobalVariablesIndexes []wasm.Index // index to ^. needListener bool needSourceOffsetInfo bool // br is reused during lowering. br *bytes.Reader loweringState loweringState knownSafeBounds [] /* ssa.ValueID to */ knownSafeBound knownSafeBoundsSet []ssa.ValueID knownSafeBoundsAtTheEndOfBlocks [] /* ssa.BlockID to */ knownSafeBoundsAtTheEndOfBlock varLengthKnownSafeBoundWithIDPool wazevoapi.VarLengthPool[knownSafeBoundWithID] execCtxPtrValue, moduleCtxPtrValue ssa.Value // Following are reused for the known safe bounds analysis. pointers []int bounds [][]knownSafeBoundWithID } type ( // knownSafeBound represents a known safe bound for a value. knownSafeBound struct { // bound is a constant upper bound for the value. bound uint64 // absoluteAddr is the absolute address of the value. absoluteAddr ssa.Value } // knownSafeBoundWithID is a knownSafeBound with the ID of the value. knownSafeBoundWithID struct { knownSafeBound id ssa.ValueID } knownSafeBoundsAtTheEndOfBlock = wazevoapi.VarLength[knownSafeBoundWithID] ) var knownSafeBoundsAtTheEndOfBlockNil = wazevoapi.NewNilVarLength[knownSafeBoundWithID]() // NewFrontendCompiler returns a frontend Compiler. func ( *wasm.Module, ssa.Builder, *wazevoapi.ModuleContextOffsetData, bool, bool, bool) *Compiler { := &Compiler{ m: , ssaBuilder: , br: bytes.NewReader(nil), offset: , ensureTermination: , needSourceOffsetInfo: , varLengthKnownSafeBoundWithIDPool: wazevoapi.NewVarLengthPool[knownSafeBoundWithID](), } .declareSignatures() return } func ( *Compiler) ( bool) { := .m .signatures = make(map[*wasm.FunctionType]*ssa.Signature, len(.TypeSection)+2) if { .listenerSignatures = make(map[*wasm.FunctionType][2]*ssa.Signature, len(.TypeSection)) } for := range .TypeSection { := &.TypeSection[] := SignatureForWasmFunctionType() .ID = ssa.SignatureID() .signatures[] = & .ssaBuilder.DeclareSignature(&) if { , := SignatureForListener() .ID = ssa.SignatureID() + ssa.SignatureID(len(.TypeSection)) .ID = ssa.SignatureID() + ssa.SignatureID(len(.TypeSection))*2 .listenerSignatures[] = [2]*ssa.Signature{, } .ssaBuilder.DeclareSignature() .ssaBuilder.DeclareSignature() } } := ssa.SignatureID(len(.TypeSection)) if { *= 3 } .memoryGrowSig = ssa.Signature{ ID: , // Takes execution context and the page size to grow. Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}, // Returns the previous page size. Results: []ssa.Type{ssa.TypeI32}, } .ssaBuilder.DeclareSignature(&.memoryGrowSig) .checkModuleExitCodeSig = ssa.Signature{ ID: .memoryGrowSig.ID + 1, // Only takes execution context. Params: []ssa.Type{ssa.TypeI64}, } .ssaBuilder.DeclareSignature(&.checkModuleExitCodeSig) .tableGrowSig = ssa.Signature{ ID: .checkModuleExitCodeSig.ID + 1, Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI32 /* table index */, ssa.TypeI32 /* num */, ssa.TypeI64 /* ref */}, // Returns the previous size. Results: []ssa.Type{ssa.TypeI32}, } .ssaBuilder.DeclareSignature(&.tableGrowSig) .refFuncSig = ssa.Signature{ ID: .tableGrowSig.ID + 1, Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI32 /* func index */}, // Returns the function reference. Results: []ssa.Type{ssa.TypeI64}, } .ssaBuilder.DeclareSignature(&.refFuncSig) .memmoveSig = ssa.Signature{ ID: .refFuncSig.ID + 1, // dst, src, and the byte count. Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64}, } .ssaBuilder.DeclareSignature(&.memmoveSig) .memoryWait32Sig = ssa.Signature{ ID: .memmoveSig.ID + 1, // exec context, timeout, expected, addr Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, // Returns the status. Results: []ssa.Type{ssa.TypeI32}, } .ssaBuilder.DeclareSignature(&.memoryWait32Sig) .memoryWait64Sig = ssa.Signature{ ID: .memoryWait32Sig.ID + 1, // exec context, timeout, expected, addr Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64, ssa.TypeI64}, // Returns the status. Results: []ssa.Type{ssa.TypeI32}, } .ssaBuilder.DeclareSignature(&.memoryWait64Sig) .memoryNotifySig = ssa.Signature{ ID: .memoryWait64Sig.ID + 1, // exec context, count, addr Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, // Returns the number notified. Results: []ssa.Type{ssa.TypeI32}, } .ssaBuilder.DeclareSignature(&.memoryNotifySig) } // SignatureForWasmFunctionType returns the ssa.Signature for the given wasm.FunctionType. func ( *wasm.FunctionType) ssa.Signature { := ssa.Signature{ // +2 to pass moduleContextPtr and executionContextPtr. See the inline comment LowerToSSA. Params: make([]ssa.Type, len(.Params)+2), Results: make([]ssa.Type, len(.Results)), } .Params[0] = executionContextPtrTyp .Params[1] = moduleContextPtrTyp for , := range .Params { .Params[+2] = WasmTypeToSSAType() } for , := range .Results { .Results[] = WasmTypeToSSAType() } return } // Init initializes the state of frontendCompiler and make it ready for a next function. func ( *Compiler) (, wasm.Index, *wasm.FunctionType, []wasm.ValueType, []byte, bool, uint64) { .ssaBuilder.Init(.signatures[]) .loweringState.reset() .wasmFunctionTypeIndex = .wasmLocalFunctionIndex = .wasmFunctionTyp = .wasmFunctionLocalTypes = .wasmFunctionBody = .wasmFunctionBodyOffsetInCodeSection = .needListener = .clearSafeBounds() .varLengthKnownSafeBoundWithIDPool.Reset() .knownSafeBoundsAtTheEndOfBlocks = .knownSafeBoundsAtTheEndOfBlocks[:0] } // Note: this assumes 64-bit platform (I believe we won't have 32-bit backend ;)). const executionContextPtrTyp, moduleContextPtrTyp = ssa.TypeI64, ssa.TypeI64 // LowerToSSA lowers the current function to SSA function which will be held by ssaBuilder. // After calling this, the caller will be able to access the SSA info in *Compiler.ssaBuilder. // // Note that this only does the naive lowering, and do not do any optimization, instead the caller is expected to do so. func ( *Compiler) () { := .ssaBuilder // Set up the entry block. := .AllocateBasicBlock() .SetCurrentBlock() // Functions always take two parameters in addition to Wasm-level parameters: // // 1. executionContextPtr: pointer to the *executionContext in wazevo package. // This will be used to exit the execution in the face of trap, plus used for host function calls. // // 2. moduleContextPtr: pointer to the *moduleContextOpaque in wazevo package. // This will be used to access memory, etc. Also, this will be used during host function calls. // // Note: it's clear that sometimes a function won't need them. For example, // if the function doesn't trap and doesn't make function call, then // we might be able to eliminate the parameter. However, if that function // can be called via call_indirect, then we cannot eliminate because the // signature won't match with the expected one. // TODO: maybe there's some way to do this optimization without glitches, but so far I have no clue about the feasibility. // // Note: In Wasmtime or many other runtimes, moduleContextPtr is called "vmContext". Also note that `moduleContextPtr` // is wazero-specific since other runtimes can naturally use the OS-level signal to do this job thanks to the fact that // they can use native stack vs wazero cannot use Go-routine stack and have to use Go-runtime allocated []byte as a stack. .execCtxPtrValue = .AddParam(, executionContextPtrTyp) .moduleCtxPtrValue = .AddParam(, moduleContextPtrTyp) .AnnotateValue(.execCtxPtrValue, "exec_ctx") .AnnotateValue(.moduleCtxPtrValue, "module_ctx") for , := range .wasmFunctionTyp.Params { := WasmTypeToSSAType() := .DeclareVariable() := .AddParam(, ) .DefineVariable(, , ) .setWasmLocalVariable(wasm.Index(), ) } .declareWasmLocals() .declareNecessaryVariables() .lowerBody() } // localVariable returns the SSA variable for the given Wasm local index. func ( *Compiler) ( wasm.Index) ssa.Variable { return .wasmLocalToVariable[] } func ( *Compiler) ( wasm.Index, ssa.Variable) { := int() if >= len(.wasmLocalToVariable) { .wasmLocalToVariable = append(.wasmLocalToVariable, make([]ssa.Variable, +1-len(.wasmLocalToVariable))...) } .wasmLocalToVariable[] = } // declareWasmLocals declares the SSA variables for the Wasm locals. func ( *Compiler) () { := wasm.Index(len(.wasmFunctionTyp.Params)) for , := range .wasmFunctionLocalTypes { := WasmTypeToSSAType() := .ssaBuilder.DeclareVariable() .setWasmLocalVariable(wasm.Index()+, ) .ssaBuilder.InsertZeroValue() } } func ( *Compiler) () { if .needMemory = .m.MemorySection != nil; .needMemory { .memoryShared = .m.MemorySection.IsShared } else if .needMemory = .m.ImportMemoryCount > 0; .needMemory { for , := range .m.ImportSection { if .Type == wasm.ExternTypeMemory { .memoryShared = .DescMem.IsShared break } } } if .needMemory { .memoryBaseVariable = .ssaBuilder.DeclareVariable(ssa.TypeI64) .memoryLenVariable = .ssaBuilder.DeclareVariable(ssa.TypeI64) } .globalVariables = .globalVariables[:0] .mutableGlobalVariablesIndexes = .mutableGlobalVariablesIndexes[:0] .globalVariablesTypes = .globalVariablesTypes[:0] for , := range .m.ImportSection { if .Type == wasm.ExternTypeGlobal { := .DescGlobal .declareWasmGlobal(.ValType, .Mutable) } } for , := range .m.GlobalSection { := .Type .declareWasmGlobal(.ValType, .Mutable) } // TODO: add tables. } func ( *Compiler) ( wasm.ValueType, bool) { var ssa.Type switch { case wasm.ValueTypeI32: = ssa.TypeI32 case wasm.ValueTypeI64, // Both externref and funcref are represented as I64 since we only support 64-bit platforms. wasm.ValueTypeExternref, wasm.ValueTypeFuncref: = ssa.TypeI64 case wasm.ValueTypeF32: = ssa.TypeF32 case wasm.ValueTypeF64: = ssa.TypeF64 case wasm.ValueTypeV128: = ssa.TypeV128 default: panic("TODO: " + wasm.ValueTypeName()) } := .ssaBuilder.DeclareVariable() := wasm.Index(len(.globalVariables)) .globalVariables = append(.globalVariables, ) .globalVariablesTypes = append(.globalVariablesTypes, ) if { .mutableGlobalVariablesIndexes = append(.mutableGlobalVariablesIndexes, ) } } // WasmTypeToSSAType converts wasm.ValueType to ssa.Type. func ( wasm.ValueType) ssa.Type { switch { case wasm.ValueTypeI32: return ssa.TypeI32 case wasm.ValueTypeI64, // Both externref and funcref are represented as I64 since we only support 64-bit platforms. wasm.ValueTypeExternref, wasm.ValueTypeFuncref: return ssa.TypeI64 case wasm.ValueTypeF32: return ssa.TypeF32 case wasm.ValueTypeF64: return ssa.TypeF64 case wasm.ValueTypeV128: return ssa.TypeV128 default: panic("TODO: " + wasm.ValueTypeName()) } } // addBlockParamsFromWasmTypes adds the block parameters to the given block. func ( *Compiler) ( []wasm.ValueType, ssa.BasicBlock) { for , := range { := WasmTypeToSSAType() .AddParam(.ssaBuilder, ) } } // formatBuilder outputs the constructed SSA function as a string with a source information. func ( *Compiler) () string { return .ssaBuilder.Format() } // SignatureForListener returns the signatures for the listener functions. func ( *wasm.FunctionType) (*ssa.Signature, *ssa.Signature) { := &ssa.Signature{} .Params = make([]ssa.Type, len(.Params)+2) .Params[0] = ssa.TypeI64 // Execution context. .Params[1] = ssa.TypeI32 // Function index. for , := range .Params { .Params[+2] = WasmTypeToSSAType() } := &ssa.Signature{} .Params = make([]ssa.Type, len(.Results)+2) .Params[0] = ssa.TypeI64 // Execution context. .Params[1] = ssa.TypeI32 // Function index. for , := range .Results { .Params[+2] = WasmTypeToSSAType() } return , } // isBoundSafe returns true if the given value is known to be safe to access up to the given bound. func ( *Compiler) ( ssa.ValueID) *knownSafeBound { if int() >= len(.knownSafeBounds) { return nil } return &.knownSafeBounds[] } // recordKnownSafeBound records the given safe bound for the given value. func ( *Compiler) ( ssa.ValueID, uint64, ssa.Value) { if int() >= len(.knownSafeBounds) { .knownSafeBounds = append(.knownSafeBounds, make([]knownSafeBound, +1)...) } if := .knownSafeBounds[]; .bound == 0 { .knownSafeBounds[] = knownSafeBound{ bound: , absoluteAddr: , } .knownSafeBoundsSet = append(.knownSafeBoundsSet, ) } else if > .bound { .knownSafeBounds[].bound = } } // clearSafeBounds clears the known safe bounds. func ( *Compiler) () { for , := range .knownSafeBoundsSet { := &.knownSafeBounds[] .bound = 0 .absoluteAddr = ssa.ValueInvalid } .knownSafeBoundsSet = .knownSafeBoundsSet[:0] } // resetAbsoluteAddressInSafeBounds resets the absolute addresses recorded in the known safe bounds. func ( *Compiler) () { for , := range .knownSafeBoundsSet { := &.knownSafeBounds[] .absoluteAddr = ssa.ValueInvalid } } func ( *knownSafeBound) () bool { return != nil && .bound > 0 } func ( *Compiler) ( int, ...ssa.Value) ssa.Values { := .ssaBuilder := .VarLengthPool() := .Allocate() = .Append(, ...) return } func ( *Compiler) ( ssa.BasicBlockID) { := int() if := len(.knownSafeBoundsAtTheEndOfBlocks); >= { .knownSafeBoundsAtTheEndOfBlocks = append(.knownSafeBoundsAtTheEndOfBlocks, make([]knownSafeBoundsAtTheEndOfBlock, +1-len(.knownSafeBoundsAtTheEndOfBlocks))...) for := ; < len(.knownSafeBoundsAtTheEndOfBlocks); ++ { .knownSafeBoundsAtTheEndOfBlocks[] = knownSafeBoundsAtTheEndOfBlockNil } } := &.varLengthKnownSafeBoundWithIDPool := len(.knownSafeBoundsSet) := .varLengthKnownSafeBoundWithIDPool.Allocate() // Sort the known safe bounds by the value ID so that we can use the intersection algorithm in initializeCurrentBlockKnownBounds. sortSSAValueIDs(.knownSafeBoundsSet) for , := range .knownSafeBoundsSet { := .knownSafeBounds[] = .Append(, knownSafeBoundWithID{ knownSafeBound: , id: , }) } .knownSafeBoundsAtTheEndOfBlocks[] = .clearSafeBounds() } func ( *Compiler) () { := .ssaBuilder.CurrentBlock() switch := .Preds(); { case 0: case 1: := .Pred(0).ID() for , := range .getKnownSafeBoundsAtTheEndOfBlocks().View() { // Unless the block is sealed, we cannot assume the absolute address is valid: // later we might add another predecessor that has no visibility of that value. := ssa.ValueInvalid if .Sealed() { = .absoluteAddr } .recordKnownSafeBound(.id, .bound, ) } default: .pointers = .pointers[:0] .bounds = .bounds[:0] for := 0; < ; ++ { .bounds = append(.bounds, .getKnownSafeBoundsAtTheEndOfBlocks(.Pred().ID()).View()) .pointers = append(.pointers, 0) } // If there are multiple predecessors, we need to find the intersection of the known safe bounds. : for { := ssa.ValueID(math.MaxUint32) for , := range .pointers { if >= len(.bounds[]) { break } := &.bounds[][] if := .id; < { = .id } } // Check if current elements are the same across all lists. := true := uint64(math.MaxUint64) for := 0; < ; ++ { := &.bounds[][.pointers[]] if .id != { = false } else { if .bound < { = .bound } .pointers[]++ } } if { // All elements are the same. // Absolute address cannot be used in the intersection since the value might be only defined in one of the predecessors. .recordKnownSafeBound(, , ssa.ValueInvalid) } } } } func ( *Compiler) ( ssa.BasicBlockID) knownSafeBoundsAtTheEndOfBlock { if int() >= len(.knownSafeBoundsAtTheEndOfBlocks) { return knownSafeBoundsAtTheEndOfBlockNil } return .knownSafeBoundsAtTheEndOfBlocks[] }