wa-lang.org/wazero@v1.0.2/internal/engine/compiler/compiler_value_location.go (about)

     1  package compiler
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"wa-lang.org/wazero/internal/asm"
     8  	"wa-lang.org/wazero/internal/wasm"
     9  )
    10  
    11  var (
    12  	// unreservedGeneralPurposeRegisters contains unreserved general purpose registers of integer type.
    13  	unreservedGeneralPurposeRegisters []asm.Register
    14  
    15  	// unreservedVectorRegisters contains unreserved vector registers.
    16  	unreservedVectorRegisters []asm.Register
    17  )
    18  
    19  func isGeneralPurposeRegister(r asm.Register) bool {
    20  	return unreservedGeneralPurposeRegisters[0] <= r && r <= unreservedGeneralPurposeRegisters[len(unreservedGeneralPurposeRegisters)-1]
    21  }
    22  
    23  func isVectorRegister(r asm.Register) bool {
    24  	return unreservedVectorRegisters[0] <= r && r <= unreservedVectorRegisters[len(unreservedVectorRegisters)-1]
    25  }
    26  
    27  // runtimeValueLocation corresponds to each variable pushed onto the wazeroir (virtual) stack,
    28  // and it has the information about where it exists in the physical machine.
    29  // It might exist in registers, or maybe on in the non-virtual physical stack allocated in memory.
    30  type runtimeValueLocation struct {
    31  	valueType runtimeValueType
    32  	// register is set to asm.NilRegister if the value is stored in the memory stack.
    33  	register asm.Register
    34  	// conditionalRegister is set to conditionalRegisterStateUnset if the value is not on the conditional register.
    35  	conditionalRegister asm.ConditionalRegisterState
    36  	// stackPointer is the location of this value in the memory stack at runtime,
    37  	stackPointer uint64
    38  }
    39  
    40  func (v *runtimeValueLocation) getRegisterType() (ret registerType) {
    41  	switch v.valueType {
    42  	case runtimeValueTypeI32, runtimeValueTypeI64:
    43  		ret = registerTypeGeneralPurpose
    44  	case runtimeValueTypeF32, runtimeValueTypeF64,
    45  		runtimeValueTypeV128Lo, runtimeValueTypeV128Hi:
    46  		ret = registerTypeVector
    47  	}
    48  	return
    49  }
    50  
    51  type runtimeValueType byte
    52  
    53  const (
    54  	runtimeValueTypeI32 runtimeValueType = iota
    55  	runtimeValueTypeI64
    56  	runtimeValueTypeF32
    57  	runtimeValueTypeF64
    58  	runtimeValueTypeV128Lo
    59  	runtimeValueTypeV128Hi
    60  )
    61  
    62  func (r runtimeValueType) String() (ret string) {
    63  	switch r {
    64  	case runtimeValueTypeI32:
    65  		ret = "i32"
    66  	case runtimeValueTypeI64:
    67  		ret = "i64"
    68  	case runtimeValueTypeF32:
    69  		ret = "f32"
    70  	case runtimeValueTypeF64:
    71  		ret = "f64"
    72  	case runtimeValueTypeV128Lo:
    73  		ret = "v128.lo"
    74  	case runtimeValueTypeV128Hi:
    75  		ret = "v128.hi"
    76  	}
    77  	return
    78  }
    79  
    80  func (v *runtimeValueLocation) setRegister(reg asm.Register) {
    81  	v.register = reg
    82  	v.conditionalRegister = asm.ConditionalRegisterStateUnset
    83  }
    84  
    85  func (v *runtimeValueLocation) onRegister() bool {
    86  	return v.register != asm.NilRegister && v.conditionalRegister == asm.ConditionalRegisterStateUnset
    87  }
    88  
    89  func (v *runtimeValueLocation) onStack() bool {
    90  	return v.register == asm.NilRegister && v.conditionalRegister == asm.ConditionalRegisterStateUnset
    91  }
    92  
    93  func (v *runtimeValueLocation) onConditionalRegister() bool {
    94  	return v.conditionalRegister != asm.ConditionalRegisterStateUnset
    95  }
    96  
    97  func (v *runtimeValueLocation) String() string {
    98  	var location string
    99  	if v.onStack() {
   100  		location = fmt.Sprintf("stack(%d)", v.stackPointer)
   101  	} else if v.onConditionalRegister() {
   102  		location = fmt.Sprintf("conditional(%d)", v.conditionalRegister)
   103  	} else if v.onRegister() {
   104  		location = fmt.Sprintf("register(%s)", registerNameFn(v.register))
   105  	}
   106  	return fmt.Sprintf("{type=%s,location=%s}", v.valueType, location)
   107  }
   108  
   109  func newRuntimeValueLocationStack() *runtimeValueLocationStack {
   110  	return &runtimeValueLocationStack{
   111  		usedRegisters:                     map[asm.Register]struct{}{},
   112  		unreservedVectorRegisters:         unreservedVectorRegisters,
   113  		unreservedGeneralPurposeRegisters: unreservedGeneralPurposeRegisters,
   114  	}
   115  }
   116  
   117  // runtimeValueLocationStack represents the wazeroir virtual stack
   118  // where each item holds the location information about where it exists
   119  // on the physical machine at runtime.
   120  //
   121  // Notably this is only used in the compilation phase, not runtime,
   122  // and we change the state of this struct at every wazeroir operation we compile.
   123  // In this way, we can see where the operands of an operation (for example,
   124  // two variables for wazeroir add operation.) exist and check the necessity for
   125  // moving the variable to registers to perform actual CPU instruction
   126  // to achieve wazeroir's add operation.
   127  type runtimeValueLocationStack struct {
   128  	// stack holds all the variables.
   129  	stack []*runtimeValueLocation
   130  	// sp is the current stack pointer.
   131  	sp uint64
   132  	// usedRegisters stores the used registers.
   133  	usedRegisters map[asm.Register]struct{}
   134  	// stackPointerCeil tracks max(.sp) across the lifespan of this struct.
   135  	stackPointerCeil uint64
   136  	// unreservedGeneralPurposeRegisters and unreservedVectorRegisters hold
   137  	// architecture dependent unreserved register list.
   138  	unreservedGeneralPurposeRegisters, unreservedVectorRegisters []asm.Register
   139  }
   140  
   141  func (v *runtimeValueLocationStack) String() string {
   142  	var stackStr []string
   143  	for i := uint64(0); i < v.sp; i++ {
   144  		stackStr = append(stackStr, v.stack[i].String())
   145  	}
   146  	var usedRegisters []string
   147  	for reg := range v.usedRegisters {
   148  		usedRegisters = append(usedRegisters, registerNameFn(reg))
   149  	}
   150  	return fmt.Sprintf("sp=%d, stack=[%s], used_registers=[%s]", v.sp, strings.Join(stackStr, ","), strings.Join(usedRegisters, ","))
   151  }
   152  
   153  func (v *runtimeValueLocationStack) clone() *runtimeValueLocationStack {
   154  	ret := &runtimeValueLocationStack{}
   155  	ret.sp = v.sp
   156  	ret.usedRegisters = make(map[asm.Register]struct{}, len(ret.usedRegisters))
   157  	for r := range v.usedRegisters {
   158  		ret.markRegisterUsed(r)
   159  	}
   160  	ret.stack = make([]*runtimeValueLocation, len(v.stack))
   161  	for i, v := range v.stack {
   162  		ret.stack[i] = &runtimeValueLocation{
   163  			valueType:           v.valueType,
   164  			conditionalRegister: v.conditionalRegister,
   165  			stackPointer:        v.stackPointer,
   166  			register:            v.register,
   167  		}
   168  	}
   169  	ret.stackPointerCeil = v.stackPointerCeil
   170  	ret.unreservedGeneralPurposeRegisters = v.unreservedGeneralPurposeRegisters
   171  	ret.unreservedVectorRegisters = v.unreservedVectorRegisters
   172  	return ret
   173  }
   174  
   175  // pushRuntimeValueLocationOnRegister creates a new runtimeValueLocation with a given register and pushes onto
   176  // the location stack.
   177  func (v *runtimeValueLocationStack) pushRuntimeValueLocationOnRegister(reg asm.Register, vt runtimeValueType) (loc *runtimeValueLocation) {
   178  	loc = &runtimeValueLocation{register: reg, conditionalRegister: asm.ConditionalRegisterStateUnset}
   179  	loc.valueType = vt
   180  
   181  	v.push(loc)
   182  	return
   183  }
   184  
   185  // pushRuntimeValueLocationOnRegister creates a new runtimeValueLocation and pushes onto the location stack.
   186  func (v *runtimeValueLocationStack) pushRuntimeValueLocationOnStack() (loc *runtimeValueLocation) {
   187  	loc = &runtimeValueLocation{register: asm.NilRegister, conditionalRegister: asm.ConditionalRegisterStateUnset}
   188  	v.push(loc)
   189  	return
   190  }
   191  
   192  // pushRuntimeValueLocationOnRegister creates a new runtimeValueLocation with a given conditional register state
   193  // and pushes onto the location stack.
   194  func (v *runtimeValueLocationStack) pushRuntimeValueLocationOnConditionalRegister(state asm.ConditionalRegisterState) (loc *runtimeValueLocation) {
   195  	loc = &runtimeValueLocation{register: asm.NilRegister, conditionalRegister: state}
   196  	v.push(loc)
   197  	return
   198  }
   199  
   200  // push a runtimeValueLocation onto the stack.
   201  func (v *runtimeValueLocationStack) push(loc *runtimeValueLocation) {
   202  	loc.stackPointer = v.sp
   203  	if v.sp >= uint64(len(v.stack)) {
   204  		// This case we need to grow the stack capacity by appending the item,
   205  		// rather than indexing.
   206  		v.stack = append(v.stack, loc)
   207  	} else {
   208  		v.stack[v.sp] = loc
   209  	}
   210  	v.sp++
   211  	// stackPointerCeil must be set after sp is incremented since
   212  	// we skip the stack grow if len(stack) >= basePointer+stackPointerCeil.
   213  	if v.sp > v.stackPointerCeil {
   214  		v.stackPointerCeil = v.sp
   215  	}
   216  }
   217  
   218  func (v *runtimeValueLocationStack) pop() (loc *runtimeValueLocation) {
   219  	v.sp--
   220  	loc = v.stack[v.sp]
   221  	return
   222  }
   223  
   224  func (v *runtimeValueLocationStack) popV128() (loc *runtimeValueLocation) {
   225  	v.sp -= 2
   226  	loc = v.stack[v.sp]
   227  	return
   228  }
   229  
   230  func (v *runtimeValueLocationStack) peek() (loc *runtimeValueLocation) {
   231  	loc = v.stack[v.sp-1]
   232  	return
   233  }
   234  
   235  func (v *runtimeValueLocationStack) releaseRegister(loc *runtimeValueLocation) {
   236  	v.markRegisterUnused(loc.register)
   237  	loc.register = asm.NilRegister
   238  	loc.conditionalRegister = asm.ConditionalRegisterStateUnset
   239  }
   240  
   241  func (v *runtimeValueLocationStack) markRegisterUnused(regs ...asm.Register) {
   242  	for _, reg := range regs {
   243  		delete(v.usedRegisters, reg)
   244  	}
   245  }
   246  
   247  func (v *runtimeValueLocationStack) markRegisterUsed(regs ...asm.Register) {
   248  	for _, reg := range regs {
   249  		v.usedRegisters[reg] = struct{}{}
   250  	}
   251  }
   252  
   253  type registerType byte
   254  
   255  const (
   256  	registerTypeGeneralPurpose registerType = iota
   257  	// registerTypeVector represents a vector register which can be used for either scalar float
   258  	// operation or SIMD vector operation depending on the instruction by which the register is used.
   259  	//
   260  	// Note: In normal assembly language, scalar float and vector register have different notations as
   261  	// Vn is for vectors and Qn is for scalar floats on arm64 for example. But on physical hardware,
   262  	// they are placed on the same locations. (Qn means the lower 64-bit of Vn vector register on arm64).
   263  	//
   264  	// In wazero, for the sake of simplicity in the register allocation, we intentionally conflate these two types
   265  	// and delegate the decision to the assembler which is aware of the instruction types for which these registers are used.
   266  	registerTypeVector
   267  )
   268  
   269  func (tp registerType) String() (ret string) {
   270  	switch tp {
   271  	case registerTypeGeneralPurpose:
   272  		ret = "int"
   273  	case registerTypeVector:
   274  		ret = "vector"
   275  	}
   276  	return
   277  }
   278  
   279  // takeFreeRegister searches for unused registers. Any found are marked used and returned.
   280  func (v *runtimeValueLocationStack) takeFreeRegister(tp registerType) (reg asm.Register, found bool) {
   281  	var targetRegs []asm.Register
   282  	switch tp {
   283  	case registerTypeVector:
   284  		targetRegs = v.unreservedVectorRegisters
   285  	case registerTypeGeneralPurpose:
   286  		targetRegs = v.unreservedGeneralPurposeRegisters
   287  	}
   288  	for _, candidate := range targetRegs {
   289  		if _, ok := v.usedRegisters[candidate]; ok {
   290  			continue
   291  		}
   292  		return candidate, true
   293  	}
   294  	return 0, false
   295  }
   296  
   297  func (v *runtimeValueLocationStack) takeFreeRegisters(tp registerType, num int) (regs []asm.Register, found bool) {
   298  	var targetRegs []asm.Register
   299  	switch tp {
   300  	case registerTypeVector:
   301  		targetRegs = v.unreservedVectorRegisters
   302  	case registerTypeGeneralPurpose:
   303  		targetRegs = v.unreservedGeneralPurposeRegisters
   304  	}
   305  
   306  	regs = make([]asm.Register, 0, num)
   307  	for _, candidate := range targetRegs {
   308  		if _, ok := v.usedRegisters[candidate]; ok {
   309  			continue
   310  		}
   311  		regs = append(regs, candidate)
   312  		if len(regs) == num {
   313  			found = true
   314  			break
   315  		}
   316  	}
   317  	return
   318  }
   319  
   320  // Search through the stack, and steal the register from the last used
   321  // variable on the stack.
   322  func (v *runtimeValueLocationStack) takeStealTargetFromUsedRegister(tp registerType) (*runtimeValueLocation, bool) {
   323  	for i := uint64(0); i < v.sp; i++ {
   324  		loc := v.stack[i]
   325  		if loc.onRegister() {
   326  			switch tp {
   327  			case registerTypeVector:
   328  				if loc.valueType == runtimeValueTypeV128Hi {
   329  					panic("BUG: V128Hi must be above the corresponding V128Lo")
   330  				}
   331  				if isVectorRegister(loc.register) {
   332  					return loc, true
   333  				}
   334  			case registerTypeGeneralPurpose:
   335  				if isGeneralPurposeRegister(loc.register) {
   336  					return loc, true
   337  				}
   338  			}
   339  		}
   340  	}
   341  	return nil, false
   342  }
   343  
   344  // init sets up the runtimeValueLocationStack which reflects the state of
   345  // the stack at the beginning of the function.
   346  //
   347  // See the diagram in callEngine.stack.
   348  func (v *runtimeValueLocationStack) init(sig *wasm.FunctionType) {
   349  	for _, t := range sig.Params {
   350  		loc := v.pushRuntimeValueLocationOnStack()
   351  		switch t {
   352  		case wasm.ValueTypeI32:
   353  			loc.valueType = runtimeValueTypeI32
   354  		case wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref:
   355  			loc.valueType = runtimeValueTypeI64
   356  		case wasm.ValueTypeF32:
   357  			loc.valueType = runtimeValueTypeF32
   358  		case wasm.ValueTypeF64:
   359  			loc.valueType = runtimeValueTypeF64
   360  		case wasm.ValueTypeV128:
   361  			loc.valueType = runtimeValueTypeV128Lo
   362  			hi := v.pushRuntimeValueLocationOnStack()
   363  			hi.valueType = runtimeValueTypeV128Hi
   364  		default:
   365  			panic("BUG")
   366  		}
   367  	}
   368  
   369  	// If the len(results) > len(args), the slots for all results are reserved after
   370  	// arguments, so we reflect that into the location stack.
   371  	for i := 0; i < sig.ResultNumInUint64-sig.ParamNumInUint64; i++ {
   372  		_ = v.pushRuntimeValueLocationOnStack()
   373  	}
   374  
   375  	// Then push the control frame fields.
   376  	for i := 0; i < callFrameDataSizeInUint64; i++ {
   377  		loc := v.pushRuntimeValueLocationOnStack()
   378  		loc.valueType = runtimeValueTypeI64
   379  	}
   380  }
   381  
   382  // getCallFrameLocations returns each field of callFrame's runtime location.
   383  //
   384  // See the diagram in callEngine.stack.
   385  func (v *runtimeValueLocationStack) getCallFrameLocations(sig *wasm.FunctionType) (
   386  	returnAddress, callerStackBasePointerInBytes, callerFunction *runtimeValueLocation,
   387  ) {
   388  	offset := callFrameOffset(sig)
   389  	return v.stack[offset], v.stack[offset+1], v.stack[offset+2]
   390  }
   391  
   392  // pushCallFrame pushes a call frame's runtime locations onto the stack assuming that
   393  // the function call parameters are already pushed there.
   394  //
   395  // See the diagram in callEngine.stack.
   396  func (v *runtimeValueLocationStack) pushCallFrame(callTargetFunctionType *wasm.FunctionType) (
   397  	returnAddress, callerStackBasePointerInBytes, callerFunction *runtimeValueLocation,
   398  ) {
   399  	// If len(results) > len(args), we reserve the slots for the results below the call frame.
   400  	reservedSlotsBeforeCallFrame := callTargetFunctionType.ResultNumInUint64 - callTargetFunctionType.ParamNumInUint64
   401  	for i := 0; i < reservedSlotsBeforeCallFrame; i++ {
   402  		v.pushRuntimeValueLocationOnStack()
   403  	}
   404  
   405  	// Push the runtime location for each field of callFrame struct. Note that each of them has
   406  	// uint64 type, and therefore must be treated as runtimeValueTypeI64.
   407  
   408  	// callFrame.returnAddress
   409  	returnAddress = v.pushRuntimeValueLocationOnStack()
   410  	returnAddress.valueType = runtimeValueTypeI64
   411  	// callFrame.returnStackBasePointerInBytes
   412  	callerStackBasePointerInBytes = v.pushRuntimeValueLocationOnStack()
   413  	callerStackBasePointerInBytes.valueType = runtimeValueTypeI64
   414  	// callFrame.function
   415  	callerFunction = v.pushRuntimeValueLocationOnStack()
   416  	callerFunction.valueType = runtimeValueTypeI64
   417  	return
   418  }