diff options
Diffstat (limited to 'vendor/github.com/tetratelabs/wazero/internal/wasm/store.go')
-rw-r--r-- | vendor/github.com/tetratelabs/wazero/internal/wasm/store.go | 679 |
1 files changed, 0 insertions, 679 deletions
diff --git a/vendor/github.com/tetratelabs/wazero/internal/wasm/store.go b/vendor/github.com/tetratelabs/wazero/internal/wasm/store.go deleted file mode 100644 index c7909c67c..000000000 --- a/vendor/github.com/tetratelabs/wazero/internal/wasm/store.go +++ /dev/null @@ -1,679 +0,0 @@ -package wasm - -import ( - "context" - "encoding/binary" - "errors" - "fmt" - "sync" - "sync/atomic" - - "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/experimental" - "github.com/tetratelabs/wazero/internal/expctxkeys" - "github.com/tetratelabs/wazero/internal/internalapi" - "github.com/tetratelabs/wazero/internal/leb128" - internalsys "github.com/tetratelabs/wazero/internal/sys" - "github.com/tetratelabs/wazero/sys" -) - -// nameToModuleShrinkThreshold is the size the nameToModule map can grow to -// before it starts to be monitored for shrinking. -// The capacity will never be smaller than this once the threshold is met. -const nameToModuleShrinkThreshold = 100 - -type ( - // Store is the runtime representation of "instantiated" Wasm module and objects. - // Multiple modules can be instantiated within a single store, and each instance, - // (e.g. function instance) can be referenced by other module instances in a Store via Module.ImportSection. - // - // Every type whose name ends with "Instance" suffix belongs to exactly one store. - // - // Note that store is not thread (concurrency) safe, meaning that using single Store - // via multiple goroutines might result in race conditions. In that case, the invocation - // and access to any methods and field of Store must be guarded by mutex. - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#store%E2%91%A0 - Store struct { - // moduleList ensures modules are closed in reverse initialization order. - moduleList *ModuleInstance // guarded by mux - - // nameToModule holds the instantiated Wasm modules by module name from Instantiate. - // It ensures no race conditions instantiating two modules of the same name. - nameToModule map[string]*ModuleInstance // guarded by mux - - // nameToModuleCap tracks the growth of the nameToModule map in order to - // track when to shrink it. - nameToModuleCap int // guarded by mux - - // EnabledFeatures are read-only to allow optimizations. - EnabledFeatures api.CoreFeatures - - // Engine is a global context for a Store which is in responsible for compilation and execution of Wasm modules. - Engine Engine - - // typeIDs maps each FunctionType.String() to a unique FunctionTypeID. This is used at runtime to - // do type-checks on indirect function calls. - typeIDs map[string]FunctionTypeID - - // functionMaxTypes represents the limit on the number of function types in a store. - // Note: this is fixed to 2^27 but have this a field for testability. - functionMaxTypes uint32 - - // mux is used to guard the fields from concurrent access. - mux sync.RWMutex - } - - // ModuleInstance represents instantiated wasm module. - // The difference from the spec is that in wazero, a ModuleInstance holds pointers - // to the instances, rather than "addresses" (i.e. index to Store.Functions, Globals, etc) for convenience. - // - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-moduleinst - // - // This implements api.Module. - ModuleInstance struct { - internalapi.WazeroOnlyType - - ModuleName string - Exports map[string]*Export - Globals []*GlobalInstance - MemoryInstance *MemoryInstance - Tables []*TableInstance - - // Engine implements function calls for this module. - Engine ModuleEngine - - // TypeIDs is index-correlated with types and holds typeIDs which is uniquely assigned to a type by store. - // This is necessary to achieve fast runtime type checking for indirect function calls at runtime. - TypeIDs []FunctionTypeID - - // DataInstances holds data segments bytes of the module. - // This is only used by bulk memory operations. - // - // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/runtime.html#data-instances - DataInstances []DataInstance - - // ElementInstances holds the element instance, and each holds the references to either functions - // or external objects (unimplemented). - ElementInstances []ElementInstance - - // Sys is exposed for use in special imports such as WASI, assemblyscript. - // - // # Notes - // - // - This is a part of ModuleInstance so that scope and Close is coherent. - // - This is not exposed outside this repository (as a host function - // parameter) because we haven't thought through capabilities based - // security implications. - Sys *internalsys.Context - - // Closed is used both to guard moduleEngine.CloseWithExitCode and to store the exit code. - // - // The update value is closedType + exitCode << 32. This ensures an exit code of zero isn't mistaken for never closed. - // - // Note: Exclusively reading and updating this with atomics guarantees cross-goroutine observations. - // See /RATIONALE.md - Closed atomic.Uint64 - - // CodeCloser is non-nil when the code should be closed after this module. - CodeCloser api.Closer - - // s is the Store on which this module is instantiated. - s *Store - // prev and next hold the nodes in the linked list of ModuleInstance held by Store. - prev, next *ModuleInstance - // Source is a pointer to the Module from which this ModuleInstance derives. - Source *Module - - // CloseNotifier is an experimental hook called once on close. - CloseNotifier experimental.CloseNotifier - } - - // DataInstance holds bytes corresponding to the data segment in a module. - // - // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/runtime.html#data-instances - DataInstance = []byte - - // GlobalInstance represents a global instance in a store. - // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-instances%E2%91%A0 - GlobalInstance struct { - Type GlobalType - // Val holds a 64-bit representation of the actual value. - // If me is non-nil, the value will not be updated and the current value is stored in the module engine. - Val uint64 - // ValHi is only used for vector type globals, and holds the higher bits of the vector. - // If me is non-nil, the value will not be updated and the current value is stored in the module engine. - ValHi uint64 - // Me is the module engine that owns this global instance. - // The .Val and .ValHi fields are only valid when me is nil. - // If me is non-nil, the value is stored in the module engine. - Me ModuleEngine - Index Index - } - - // FunctionTypeID is a uniquely assigned integer for a function type. - // This is wazero specific runtime object and specific to a store, - // and used at runtime to do type-checks on indirect function calls. - FunctionTypeID uint32 -) - -// The wazero specific limitations described at RATIONALE.md. -const maximumFunctionTypes = 1 << 27 - -// GetFunctionTypeID is used by emscripten. -func (m *ModuleInstance) GetFunctionTypeID(t *FunctionType) FunctionTypeID { - id, err := m.s.GetFunctionTypeID(t) - if err != nil { - // This is not recoverable in practice since the only error GetFunctionTypeID returns is - // when there's too many function types in the store. - panic(err) - } - return id -} - -func (m *ModuleInstance) buildElementInstances(elements []ElementSegment) { - m.ElementInstances = make([][]Reference, len(elements)) - for i, elm := range elements { - if elm.Type == RefTypeFuncref && elm.Mode == ElementModePassive { - // Only passive elements can be access as element instances. - // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/syntax/modules.html#element-segments - inits := elm.Init - inst := make([]Reference, len(inits)) - m.ElementInstances[i] = inst - for j, idx := range inits { - if index, ok := unwrapElementInitGlobalReference(idx); ok { - global := m.Globals[index] - inst[j] = Reference(global.Val) - } else { - if idx != ElementInitNullReference { - inst[j] = m.Engine.FunctionInstanceReference(idx) - } - } - } - } - } -} - -func (m *ModuleInstance) applyElements(elems []ElementSegment) { - for elemI := range elems { - elem := &elems[elemI] - if !elem.IsActive() || - // Per https://github.com/WebAssembly/spec/issues/1427 init can be no-op. - len(elem.Init) == 0 { - continue - } - var offset uint32 - if elem.OffsetExpr.Opcode == OpcodeGlobalGet { - // Ignore error as it's already validated. - globalIdx, _, _ := leb128.LoadUint32(elem.OffsetExpr.Data) - global := m.Globals[globalIdx] - offset = uint32(global.Val) - } else { - // Ignore error as it's already validated. - o, _, _ := leb128.LoadInt32(elem.OffsetExpr.Data) - offset = uint32(o) - } - - table := m.Tables[elem.TableIndex] - references := table.References - if int(offset)+len(elem.Init) > len(references) { - // ErrElementOffsetOutOfBounds is the error raised when the active element offset exceeds the table length. - // Before CoreFeatureReferenceTypes, this was checked statically before instantiation, after the proposal, - // this must be raised as runtime error (as in assert_trap in spectest), not even an instantiation error. - // https://github.com/WebAssembly/spec/blob/d39195773112a22b245ffbe864bab6d1182ccb06/test/core/linking.wast#L264-L274 - // - // In wazero, we ignore it since in any way, the instantiated module and engines are fine and can be used - // for function invocations. - return - } - - if table.Type == RefTypeExternref { - for i := 0; i < len(elem.Init); i++ { - references[offset+uint32(i)] = Reference(0) - } - } else { - for i, init := range elem.Init { - if init == ElementInitNullReference { - continue - } - - var ref Reference - if index, ok := unwrapElementInitGlobalReference(init); ok { - global := m.Globals[index] - ref = Reference(global.Val) - } else { - ref = m.Engine.FunctionInstanceReference(index) - } - references[offset+uint32(i)] = ref - } - } - } -} - -// validateData ensures that data segments are valid in terms of memory boundary. -// Note: this is used only when bulk-memory/reference type feature is disabled. -func (m *ModuleInstance) validateData(data []DataSegment) (err error) { - for i := range data { - d := &data[i] - if !d.IsPassive() { - offset := int(executeConstExpressionI32(m.Globals, &d.OffsetExpression)) - ceil := offset + len(d.Init) - if offset < 0 || ceil > len(m.MemoryInstance.Buffer) { - return fmt.Errorf("%s[%d]: out of bounds memory access", SectionIDName(SectionIDData), i) - } - } - } - return -} - -// applyData uses the given data segments and mutate the memory according to the initial contents on it -// and populate the `DataInstances`. This is called after all the validation phase passes and out of -// bounds memory access error here is not a validation error, but rather a runtime error. -func (m *ModuleInstance) applyData(data []DataSegment) error { - m.DataInstances = make([][]byte, len(data)) - for i := range data { - d := &data[i] - m.DataInstances[i] = d.Init - if !d.IsPassive() { - offset := executeConstExpressionI32(m.Globals, &d.OffsetExpression) - if offset < 0 || int(offset)+len(d.Init) > len(m.MemoryInstance.Buffer) { - return fmt.Errorf("%s[%d]: out of bounds memory access", SectionIDName(SectionIDData), i) - } - copy(m.MemoryInstance.Buffer[offset:], d.Init) - } - } - return nil -} - -// GetExport returns an export of the given name and type or errs if not exported or the wrong type. -func (m *ModuleInstance) getExport(name string, et ExternType) (*Export, error) { - exp, ok := m.Exports[name] - if !ok { - return nil, fmt.Errorf("%q is not exported in module %q", name, m.ModuleName) - } - if exp.Type != et { - return nil, fmt.Errorf("export %q in module %q is a %s, not a %s", name, m.ModuleName, ExternTypeName(exp.Type), ExternTypeName(et)) - } - return exp, nil -} - -func NewStore(enabledFeatures api.CoreFeatures, engine Engine) *Store { - return &Store{ - nameToModule: map[string]*ModuleInstance{}, - nameToModuleCap: nameToModuleShrinkThreshold, - EnabledFeatures: enabledFeatures, - Engine: engine, - typeIDs: map[string]FunctionTypeID{}, - functionMaxTypes: maximumFunctionTypes, - } -} - -// Instantiate uses name instead of the Module.NameSection ModuleName as it allows instantiating the same module under -// different names safely and concurrently. -// -// * ctx: the default context used for function calls. -// * name: the name of the module. -// * sys: the system context, which will be closed (SysContext.Close) on ModuleInstance.Close. -// -// Note: Module.Validate must be called prior to instantiation. -func (s *Store) Instantiate( - ctx context.Context, - module *Module, - name string, - sys *internalsys.Context, - typeIDs []FunctionTypeID, -) (*ModuleInstance, error) { - // Instantiate the module and add it to the store so that other modules can import it. - m, err := s.instantiate(ctx, module, name, sys, typeIDs) - if err != nil { - return nil, err - } - - // Now that the instantiation is complete without error, add it. - if err = s.registerModule(m); err != nil { - _ = m.Close(ctx) - return nil, err - } - return m, nil -} - -func (s *Store) instantiate( - ctx context.Context, - module *Module, - name string, - sysCtx *internalsys.Context, - typeIDs []FunctionTypeID, -) (m *ModuleInstance, err error) { - m = &ModuleInstance{ModuleName: name, TypeIDs: typeIDs, Sys: sysCtx, s: s, Source: module} - - m.Tables = make([]*TableInstance, int(module.ImportTableCount)+len(module.TableSection)) - m.Globals = make([]*GlobalInstance, int(module.ImportGlobalCount)+len(module.GlobalSection)) - m.Engine, err = s.Engine.NewModuleEngine(module, m) - if err != nil { - return nil, err - } - - if err = m.resolveImports(ctx, module); err != nil { - return nil, err - } - - err = m.buildTables(module, - // As of reference-types proposal, boundary check must be done after instantiation. - s.EnabledFeatures.IsEnabled(api.CoreFeatureReferenceTypes)) - if err != nil { - return nil, err - } - - allocator, _ := ctx.Value(expctxkeys.MemoryAllocatorKey{}).(experimental.MemoryAllocator) - - m.buildGlobals(module, m.Engine.FunctionInstanceReference) - m.buildMemory(module, allocator) - m.Exports = module.Exports - for _, exp := range m.Exports { - if exp.Type == ExternTypeTable { - t := m.Tables[exp.Index] - t.involvingModuleInstances = append(t.involvingModuleInstances, m) - } - } - - // As of reference types proposal, data segment validation must happen after instantiation, - // and the side effect must persist even if there's out of bounds error after instantiation. - // https://github.com/WebAssembly/spec/blob/d39195773112a22b245ffbe864bab6d1182ccb06/test/core/linking.wast#L395-L405 - if !s.EnabledFeatures.IsEnabled(api.CoreFeatureReferenceTypes) { - if err = m.validateData(module.DataSection); err != nil { - return nil, err - } - } - - // After engine creation, we can create the funcref element instances and initialize funcref type globals. - m.buildElementInstances(module.ElementSection) - - // Now all the validation passes, we are safe to mutate memory instances (possibly imported ones). - if err = m.applyData(module.DataSection); err != nil { - return nil, err - } - - m.applyElements(module.ElementSection) - - m.Engine.DoneInstantiation() - - // Execute the start function. - if module.StartSection != nil { - funcIdx := *module.StartSection - ce := m.Engine.NewFunction(funcIdx) - _, err = ce.Call(ctx) - if exitErr, ok := err.(*sys.ExitError); ok { // Don't wrap an exit error! - return nil, exitErr - } else if err != nil { - return nil, fmt.Errorf("start %s failed: %w", module.funcDesc(SectionIDFunction, funcIdx), err) - } - } - return -} - -func (m *ModuleInstance) resolveImports(ctx context.Context, module *Module) (err error) { - // Check if ctx contains an ImportResolver. - resolveImport, _ := ctx.Value(expctxkeys.ImportResolverKey{}).(experimental.ImportResolver) - - for moduleName, imports := range module.ImportPerModule { - var importedModule *ModuleInstance - if resolveImport != nil { - if v := resolveImport(moduleName); v != nil { - importedModule = v.(*ModuleInstance) - } - } - if importedModule == nil { - importedModule, err = m.s.module(moduleName) - if err != nil { - return err - } - } - - for _, i := range imports { - var imported *Export - imported, err = importedModule.getExport(i.Name, i.Type) - if err != nil { - return - } - - switch i.Type { - case ExternTypeFunc: - expectedType := &module.TypeSection[i.DescFunc] - src := importedModule.Source - actual := src.typeOfFunction(imported.Index) - if !actual.EqualsSignature(expectedType.Params, expectedType.Results) { - err = errorInvalidImport(i, fmt.Errorf("signature mismatch: %s != %s", expectedType, actual)) - return - } - - m.Engine.ResolveImportedFunction(i.IndexPerType, i.DescFunc, imported.Index, importedModule.Engine) - case ExternTypeTable: - expected := i.DescTable - importedTable := importedModule.Tables[imported.Index] - if expected.Type != importedTable.Type { - err = errorInvalidImport(i, fmt.Errorf("table type mismatch: %s != %s", - RefTypeName(expected.Type), RefTypeName(importedTable.Type))) - return - } - - if expected.Min > importedTable.Min { - err = errorMinSizeMismatch(i, expected.Min, importedTable.Min) - return - } - - if expected.Max != nil { - expectedMax := *expected.Max - if importedTable.Max == nil { - err = errorNoMax(i, expectedMax) - return - } else if expectedMax < *importedTable.Max { - err = errorMaxSizeMismatch(i, expectedMax, *importedTable.Max) - return - } - } - m.Tables[i.IndexPerType] = importedTable - importedTable.involvingModuleInstancesMutex.Lock() - if len(importedTable.involvingModuleInstances) == 0 { - panic("BUG: involvingModuleInstances must not be nil when it's imported") - } - importedTable.involvingModuleInstances = append(importedTable.involvingModuleInstances, m) - importedTable.involvingModuleInstancesMutex.Unlock() - case ExternTypeMemory: - expected := i.DescMem - importedMemory := importedModule.MemoryInstance - - if expected.Min > memoryBytesNumToPages(uint64(len(importedMemory.Buffer))) { - err = errorMinSizeMismatch(i, expected.Min, importedMemory.Min) - return - } - - if expected.Max < importedMemory.Max { - err = errorMaxSizeMismatch(i, expected.Max, importedMemory.Max) - return - } - m.MemoryInstance = importedMemory - m.Engine.ResolveImportedMemory(importedModule.Engine) - case ExternTypeGlobal: - expected := i.DescGlobal - importedGlobal := importedModule.Globals[imported.Index] - - if expected.Mutable != importedGlobal.Type.Mutable { - err = errorInvalidImport(i, fmt.Errorf("mutability mismatch: %t != %t", - expected.Mutable, importedGlobal.Type.Mutable)) - return - } - - if expected.ValType != importedGlobal.Type.ValType { - err = errorInvalidImport(i, fmt.Errorf("value type mismatch: %s != %s", - ValueTypeName(expected.ValType), ValueTypeName(importedGlobal.Type.ValType))) - return - } - m.Globals[i.IndexPerType] = importedGlobal - } - } - } - return -} - -func errorMinSizeMismatch(i *Import, expected, actual uint32) error { - return errorInvalidImport(i, fmt.Errorf("minimum size mismatch: %d > %d", expected, actual)) -} - -func errorNoMax(i *Import, expected uint32) error { - return errorInvalidImport(i, fmt.Errorf("maximum size mismatch: %d, but actual has no max", expected)) -} - -func errorMaxSizeMismatch(i *Import, expected, actual uint32) error { - return errorInvalidImport(i, fmt.Errorf("maximum size mismatch: %d < %d", expected, actual)) -} - -func errorInvalidImport(i *Import, err error) error { - return fmt.Errorf("import %s[%s.%s]: %w", ExternTypeName(i.Type), i.Module, i.Name, err) -} - -// executeConstExpressionI32 executes the ConstantExpression which returns ValueTypeI32. -// The validity of the expression is ensured when calling this function as this is only called -// during instantiation phrase, and the validation happens in compilation (validateConstExpression). -func executeConstExpressionI32(importedGlobals []*GlobalInstance, expr *ConstantExpression) (ret int32) { - switch expr.Opcode { - case OpcodeI32Const: - ret, _, _ = leb128.LoadInt32(expr.Data) - case OpcodeGlobalGet: - id, _, _ := leb128.LoadUint32(expr.Data) - g := importedGlobals[id] - ret = int32(g.Val) - } - return -} - -// initialize initializes the value of this global instance given the const expr and imported globals. -// funcRefResolver is called to get the actual funcref (engine specific) from the OpcodeRefFunc const expr. -// -// Global initialization constant expression can only reference the imported globals. -// See the note on https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#constant-expressions%E2%91%A0 -func (g *GlobalInstance) initialize(importedGlobals []*GlobalInstance, expr *ConstantExpression, funcRefResolver func(funcIndex Index) Reference) { - switch expr.Opcode { - case OpcodeI32Const: - // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md - v, _, _ := leb128.LoadInt32(expr.Data) - g.Val = uint64(uint32(v)) - case OpcodeI64Const: - // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md - v, _, _ := leb128.LoadInt64(expr.Data) - g.Val = uint64(v) - case OpcodeF32Const: - g.Val = uint64(binary.LittleEndian.Uint32(expr.Data)) - case OpcodeF64Const: - g.Val = binary.LittleEndian.Uint64(expr.Data) - case OpcodeGlobalGet: - id, _, _ := leb128.LoadUint32(expr.Data) - importedG := importedGlobals[id] - switch importedG.Type.ValType { - case ValueTypeI32: - g.Val = uint64(uint32(importedG.Val)) - case ValueTypeI64: - g.Val = importedG.Val - case ValueTypeF32: - g.Val = importedG.Val - case ValueTypeF64: - g.Val = importedG.Val - case ValueTypeV128: - g.Val, g.ValHi = importedG.Val, importedG.ValHi - case ValueTypeFuncref, ValueTypeExternref: - g.Val = importedG.Val - } - case OpcodeRefNull: - switch expr.Data[0] { - case ValueTypeExternref, ValueTypeFuncref: - g.Val = 0 // Reference types are opaque 64bit pointer at runtime. - } - case OpcodeRefFunc: - v, _, _ := leb128.LoadUint32(expr.Data) - g.Val = uint64(funcRefResolver(v)) - case OpcodeVecV128Const: - g.Val, g.ValHi = binary.LittleEndian.Uint64(expr.Data[0:8]), binary.LittleEndian.Uint64(expr.Data[8:16]) - } -} - -// String implements api.Global. -func (g *GlobalInstance) String() string { - switch g.Type.ValType { - case ValueTypeI32, ValueTypeI64: - return fmt.Sprintf("global(%d)", g.Val) - case ValueTypeF32: - return fmt.Sprintf("global(%f)", api.DecodeF32(g.Val)) - case ValueTypeF64: - return fmt.Sprintf("global(%f)", api.DecodeF64(g.Val)) - default: - panic(fmt.Errorf("BUG: unknown value type %X", g.Type.ValType)) - } -} - -func (g *GlobalInstance) Value() (uint64, uint64) { - if g.Me != nil { - return g.Me.GetGlobalValue(g.Index) - } - return g.Val, g.ValHi -} - -func (g *GlobalInstance) SetValue(lo, hi uint64) { - if g.Me != nil { - g.Me.SetGlobalValue(g.Index, lo, hi) - } else { - g.Val, g.ValHi = lo, hi - } -} - -func (s *Store) GetFunctionTypeIDs(ts []FunctionType) ([]FunctionTypeID, error) { - ret := make([]FunctionTypeID, len(ts)) - for i := range ts { - t := &ts[i] - inst, err := s.GetFunctionTypeID(t) - if err != nil { - return nil, err - } - ret[i] = inst - } - return ret, nil -} - -func (s *Store) GetFunctionTypeID(t *FunctionType) (FunctionTypeID, error) { - s.mux.RLock() - key := t.key() - id, ok := s.typeIDs[key] - s.mux.RUnlock() - if !ok { - s.mux.Lock() - defer s.mux.Unlock() - // Check again in case another goroutine has already added the type. - if id, ok = s.typeIDs[key]; ok { - return id, nil - } - l := len(s.typeIDs) - if uint32(l) >= s.functionMaxTypes { - return 0, fmt.Errorf("too many function types in a store") - } - id = FunctionTypeID(l) - s.typeIDs[key] = id - } - return id, nil -} - -// CloseWithExitCode implements the same method as documented on wazero.Runtime. -func (s *Store) CloseWithExitCode(ctx context.Context, exitCode uint32) error { - s.mux.Lock() - defer s.mux.Unlock() - // Close modules in reverse initialization order. - var errs []error - for m := s.moduleList; m != nil; m = m.next { - // If closing this module errs, proceed anyway to close the others. - if err := m.closeWithExitCode(ctx, exitCode); err != nil { - errs = append(errs, err) - } - } - s.moduleList = nil - s.nameToModule = nil - s.nameToModuleCap = 0 - s.typeIDs = nil - return errors.Join(errs...) -} |