Repository: mlange-42/arche
Pull request #241 (2023-04-30 19:16:37)

Lock registered filters, partial recompile
* Prevent modifying registered generic filters
* Don't fully recompile after changing relation target

coveralls: ## Pull Request Test Coverage Report for [Build 4845995543](https://coveralls.io/builds/59470919)

* **136** of **136**   **(100.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/59470919/badge)](https://coveralls.io/builds/59470919) |
| :-- | --: |
| Change from base [Build 4841293696](https://coveralls.io/builds/59463951): |  0.0% |
| Covered Lines: | 3095 |
| Relevant Lines: | 3095 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0dac60f..5798863 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
 
 * Remove go-gameengine-ecs from Arche benchmarks (but not from competition!) (#228)
 * Reduce memory size of `Query` and internal archetype list by 8 bytes (#230)
+* Generic filters are locked when registered for caching (#241)
 
 ## [[v0.7.1]](https://github.com/mlange-42/arche/compare/v0.7.0...v0.7.1)
 
diff --git a/generic/compiled.go b/generic/compiled.go
index bb850b0..7e42efd 100644
--- a/generic/compiled.go
+++ b/generic/compiled.go
@@ -11,24 +11,30 @@ var relationType = reflect.TypeOf((*ecs.Relation)(nil)).Elem()
 
 // compiledQuery is a helper for compiling a generic filter into a [ecs.Filter].
 type compiledQuery struct {
-	maskFilter   ecs.MaskFilter
-	cachedFilter ecs.CachedFilter
-	filter       ecs.Filter
-	Ids          []ecs.ID
-	TargetID     int8
-	compiled     bool
+	maskFilter     ecs.MaskFilter
+	cachedFilter   ecs.CachedFilter
+	filter         ecs.Filter
+	Ids            []ecs.ID
+	TargetID       int8
+	compiled       bool
+	targetCompiled bool
+	locked         bool
 }
 
 // Compile compiles a generic filter.
 func (q *compiledQuery) Compile(w *ecs.World, include, optional, exclude []Comp, targetType Comp, target ecs.Entity) {
-	if q.compiled {
+	if q.targetCompiled {
 		return
 	}
-	q.Ids = toIds(w, include)
-	q.maskFilter = ecs.MaskFilter{
-		Include: toMaskOptional(w, q.Ids, optional),
-		Exclude: toMask(w, exclude),
+
+	if !q.compiled {
+		q.Ids = toIds(w, include)
+		q.maskFilter = ecs.MaskFilter{
+			Include: toMaskOptional(w, q.Ids, optional),
+			Exclude: toMask(w, exclude),
+		}
 	}
+
 	if targetType == nil {
 		q.filter = &q.maskFilter
 		q.TargetID = -1
@@ -53,10 +59,14 @@ func (q *compiledQuery) Compile(w *ecs.World, include, optional, exclude []Comp,
 			Target: target,
 		}
 	}
+	q.targetCompiled = true
 	q.compiled = true
 }
 
 // Reset sets the compiledQuery to not compiled.
-func (q *compiledQuery) Reset() {
-	q.compiled = false
+func (q *compiledQuery) Reset(targetOnly bool) {
+	q.targetCompiled = false
+	if !targetOnly {
+		q.compiled = false
+	}
 }
diff --git a/generic/generate/query.go.txt b/generic/generate/query.go.txt
index 1b918bf..8e3d45b 100644
--- a/generic/generate/query.go.txt
+++ b/generic/generate/query.go.txt
@@ -34,8 +34,11 @@ func NewFilter{{ .Index }}{{ .TypesFull }}() *Filter{{ .Index }}{{ .Types }} {
 //
 // Only affects component types that were specified in the query.
 func (q *Filter{{ .Index }}{{ .Types }}) Optional(mask ...Comp) *Filter{{ .Index }}{{ .Types }} {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 {{ end }}
@@ -44,8 +47,11 @@ func (q *Filter{{ .Index }}{{ .Types }}) Optional(mask ...Comp) *Filter{{ .Index
 //
 // Create the required mask items with [T].
 func (q *Filter{{ .Index }}{{ .Types }}) With(mask ...Comp) *Filter{{ .Index }}{{ .Types }} {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -53,8 +59,11 @@ func (q *Filter{{ .Index }}{{ .Types }}) With(mask ...Comp) *Filter{{ .Index }}{
 //
 // Create the required mask items with [T].
 func (q *Filter{{ .Index }}{{ .Types }}) Without(mask ...Comp) *Filter{{ .Index }}{{ .Types }} {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -62,9 +71,12 @@ func (q *Filter{{ .Index }}{{ .Types }}) Without(mask ...Comp) *Filter{{ .Index
 //
 // Create the required component ID with [T].
 func (q *Filter{{ .Index }}{{ .Types }}) WithRelation(comp Comp, target ecs.Entity) *Filter{{ .Index }}{{ .Types }} {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -94,6 +106,7 @@ func (q *Filter{{ .Index }}{{ .Types }}) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -105,6 +118,7 @@ func (q *Filter{{ .Index }}{{ .Types }}) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query{{ .Index }} is a generic query iterator for {{ .NumberStr }} components.
diff --git a/generic/query_generated.go b/generic/query_generated.go
index fa2a5c6..58da710 100644
--- a/generic/query_generated.go
+++ b/generic/query_generated.go
@@ -37,8 +37,11 @@ func NewFilter0() *Filter0 {
 //
 // Create the required mask items with [T].
 func (q *Filter0) With(mask ...Comp) *Filter0 {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -46,8 +49,11 @@ func (q *Filter0) With(mask ...Comp) *Filter0 {
 //
 // Create the required mask items with [T].
 func (q *Filter0) Without(mask ...Comp) *Filter0 {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -55,9 +61,12 @@ func (q *Filter0) Without(mask ...Comp) *Filter0 {
 //
 // Create the required component ID with [T].
 func (q *Filter0) WithRelation(comp Comp, target ecs.Entity) *Filter0 {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -86,6 +95,7 @@ func (q *Filter0) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -97,6 +107,7 @@ func (q *Filter0) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query0 is a generic query iterator for zero components.
@@ -164,8 +175,11 @@ func NewFilter1[A any]() *Filter1[A] {
 //
 // Only affects component types that were specified in the query.
 func (q *Filter1[A]) Optional(mask ...Comp) *Filter1[A] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -173,8 +187,11 @@ func (q *Filter1[A]) Optional(mask ...Comp) *Filter1[A] {
 //
 // Create the required mask items with [T].
 func (q *Filter1[A]) With(mask ...Comp) *Filter1[A] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -182,8 +199,11 @@ func (q *Filter1[A]) With(mask ...Comp) *Filter1[A] {
 //
 // Create the required mask items with [T].
 func (q *Filter1[A]) Without(mask ...Comp) *Filter1[A] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -191,9 +211,12 @@ func (q *Filter1[A]) Without(mask ...Comp) *Filter1[A] {
 //
 // Create the required component ID with [T].
 func (q *Filter1[A]) WithRelation(comp Comp, target ecs.Entity) *Filter1[A] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -223,6 +246,7 @@ func (q *Filter1[A]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -234,6 +258,7 @@ func (q *Filter1[A]) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query1 is a generic query iterator for one components.
@@ -310,8 +335,11 @@ func NewFilter2[A any, B any]() *Filter2[A, B] {
 //
 // Only affects component types that were specified in the query.
 func (q *Filter2[A, B]) Optional(mask ...Comp) *Filter2[A, B] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -319,8 +347,11 @@ func (q *Filter2[A, B]) Optional(mask ...Comp) *Filter2[A, B] {
 //
 // Create the required mask items with [T].
 func (q *Filter2[A, B]) With(mask ...Comp) *Filter2[A, B] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -328,8 +359,11 @@ func (q *Filter2[A, B]) With(mask ...Comp) *Filter2[A, B] {
 //
 // Create the required mask items with [T].
 func (q *Filter2[A, B]) Without(mask ...Comp) *Filter2[A, B] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -337,9 +371,12 @@ func (q *Filter2[A, B]) Without(mask ...Comp) *Filter2[A, B] {
 //
 // Create the required component ID with [T].
 func (q *Filter2[A, B]) WithRelation(comp Comp, target ecs.Entity) *Filter2[A, B] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -370,6 +407,7 @@ func (q *Filter2[A, B]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -381,6 +419,7 @@ func (q *Filter2[A, B]) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query2 is a generic query iterator for two components.
@@ -460,8 +499,11 @@ func NewFilter3[A any, B any, C any]() *Filter3[A, B, C] {
 //
 // Only affects component types that were specified in the query.
 func (q *Filter3[A, B, C]) Optional(mask ...Comp) *Filter3[A, B, C] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -469,8 +511,11 @@ func (q *Filter3[A, B, C]) Optional(mask ...Comp) *Filter3[A, B, C] {
 //
 // Create the required mask items with [T].
 func (q *Filter3[A, B, C]) With(mask ...Comp) *Filter3[A, B, C] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -478,8 +523,11 @@ func (q *Filter3[A, B, C]) With(mask ...Comp) *Filter3[A, B, C] {
 //
 // Create the required mask items with [T].
 func (q *Filter3[A, B, C]) Without(mask ...Comp) *Filter3[A, B, C] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -487,9 +535,12 @@ func (q *Filter3[A, B, C]) Without(mask ...Comp) *Filter3[A, B, C] {
 //
 // Create the required component ID with [T].
 func (q *Filter3[A, B, C]) WithRelation(comp Comp, target ecs.Entity) *Filter3[A, B, C] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -521,6 +572,7 @@ func (q *Filter3[A, B, C]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -532,6 +584,7 @@ func (q *Filter3[A, B, C]) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query3 is a generic query iterator for three components.
@@ -614,8 +667,11 @@ func NewFilter4[A any, B any, C any, D any]() *Filter4[A, B, C, D] {
 //
 // Only affects component types that were specified in the query.
 func (q *Filter4[A, B, C, D]) Optional(mask ...Comp) *Filter4[A, B, C, D] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -623,8 +679,11 @@ func (q *Filter4[A, B, C, D]) Optional(mask ...Comp) *Filter4[A, B, C, D] {
 //
 // Create the required mask items with [T].
 func (q *Filter4[A, B, C, D]) With(mask ...Comp) *Filter4[A, B, C, D] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -632,8 +691,11 @@ func (q *Filter4[A, B, C, D]) With(mask ...Comp) *Filter4[A, B, C, D] {
 //
 // Create the required mask items with [T].
 func (q *Filter4[A, B, C, D]) Without(mask ...Comp) *Filter4[A, B, C, D] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -641,9 +703,12 @@ func (q *Filter4[A, B, C, D]) Without(mask ...Comp) *Filter4[A, B, C, D] {
 //
 // Create the required component ID with [T].
 func (q *Filter4[A, B, C, D]) WithRelation(comp Comp, target ecs.Entity) *Filter4[A, B, C, D] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -676,6 +741,7 @@ func (q *Filter4[A, B, C, D]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -687,6 +753,7 @@ func (q *Filter4[A, B, C, D]) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query4 is a generic query iterator for four components.
@@ -772,8 +839,11 @@ func NewFilter5[A any, B any, C any, D any, E any]() *Filter5[A, B, C, D, E] {
 //
 // Only affects component types that were specified in the query.
 func (q *Filter5[A, B, C, D, E]) Optional(mask ...Comp) *Filter5[A, B, C, D, E] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -781,8 +851,11 @@ func (q *Filter5[A, B, C, D, E]) Optional(mask ...Comp) *Filter5[A, B, C, D, E]
 //
 // Create the required mask items with [T].
 func (q *Filter5[A, B, C, D, E]) With(mask ...Comp) *Filter5[A, B, C, D, E] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -790,8 +863,11 @@ func (q *Filter5[A, B, C, D, E]) With(mask ...Comp) *Filter5[A, B, C, D, E] {
 //
 // Create the required mask items with [T].
 func (q *Filter5[A, B, C, D, E]) Without(mask ...Comp) *Filter5[A, B, C, D, E] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -799,9 +875,12 @@ func (q *Filter5[A, B, C, D, E]) Without(mask ...Comp) *Filter5[A, B, C, D, E] {
 //
 // Create the required component ID with [T].
 func (q *Filter5[A, B, C, D, E]) WithRelation(comp Comp, target ecs.Entity) *Filter5[A, B, C, D, E] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -835,6 +914,7 @@ func (q *Filter5[A, B, C, D, E]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -846,6 +926,7 @@ func (q *Filter5[A, B, C, D, E]) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query5 is a generic query iterator for five components.
@@ -934,8 +1015,11 @@ func NewFilter6[A any, B any, C any, D any, E any, F any]() *Filter6[A, B, C, D,
 //
 // Only affects component types that were specified in the query.
 func (q *Filter6[A, B, C, D, E, F]) Optional(mask ...Comp) *Filter6[A, B, C, D, E, F] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -943,8 +1027,11 @@ func (q *Filter6[A, B, C, D, E, F]) Optional(mask ...Comp) *Filter6[A, B, C, D,
 //
 // Create the required mask items with [T].
 func (q *Filter6[A, B, C, D, E, F]) With(mask ...Comp) *Filter6[A, B, C, D, E, F] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -952,8 +1039,11 @@ func (q *Filter6[A, B, C, D, E, F]) With(mask ...Comp) *Filter6[A, B, C, D, E, F
 //
 // Create the required mask items with [T].
 func (q *Filter6[A, B, C, D, E, F]) Without(mask ...Comp) *Filter6[A, B, C, D, E, F] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -961,9 +1051,12 @@ func (q *Filter6[A, B, C, D, E, F]) Without(mask ...Comp) *Filter6[A, B, C, D, E
 //
 // Create the required component ID with [T].
 func (q *Filter6[A, B, C, D, E, F]) WithRelation(comp Comp, target ecs.Entity) *Filter6[A, B, C, D, E, F] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -998,6 +1091,7 @@ func (q *Filter6[A, B, C, D, E, F]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -1009,6 +1103,7 @@ func (q *Filter6[A, B, C, D, E, F]) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query6 is a generic query iterator for six components.
@@ -1100,8 +1195,11 @@ func NewFilter7[A any, B any, C any, D any, E any, F any, G any]() *Filter7[A, B
 //
 // Only affects component types that were specified in the query.
 func (q *Filter7[A, B, C, D, E, F, G]) Optional(mask ...Comp) *Filter7[A, B, C, D, E, F, G] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -1109,8 +1207,11 @@ func (q *Filter7[A, B, C, D, E, F, G]) Optional(mask ...Comp) *Filter7[A, B, C,
 //
 // Create the required mask items with [T].
 func (q *Filter7[A, B, C, D, E, F, G]) With(mask ...Comp) *Filter7[A, B, C, D, E, F, G] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -1118,8 +1219,11 @@ func (q *Filter7[A, B, C, D, E, F, G]) With(mask ...Comp) *Filter7[A, B, C, D, E
 //
 // Create the required mask items with [T].
 func (q *Filter7[A, B, C, D, E, F, G]) Without(mask ...Comp) *Filter7[A, B, C, D, E, F, G] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -1127,9 +1231,12 @@ func (q *Filter7[A, B, C, D, E, F, G]) Without(mask ...Comp) *Filter7[A, B, C, D
 //
 // Create the required component ID with [T].
 func (q *Filter7[A, B, C, D, E, F, G]) WithRelation(comp Comp, target ecs.Entity) *Filter7[A, B, C, D, E, F, G] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -1165,6 +1272,7 @@ func (q *Filter7[A, B, C, D, E, F, G]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -1176,6 +1284,7 @@ func (q *Filter7[A, B, C, D, E, F, G]) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query7 is a generic query iterator for seven components.
@@ -1270,8 +1379,11 @@ func NewFilter8[A any, B any, C any, D any, E any, F any, G any, H any]() *Filte
 //
 // Only affects component types that were specified in the query.
 func (q *Filter8[A, B, C, D, E, F, G, H]) Optional(mask ...Comp) *Filter8[A, B, C, D, E, F, G, H] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -1279,8 +1391,11 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) Optional(mask ...Comp) *Filter8[A, B,
 //
 // Create the required mask items with [T].
 func (q *Filter8[A, B, C, D, E, F, G, H]) With(mask ...Comp) *Filter8[A, B, C, D, E, F, G, H] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -1288,8 +1403,11 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) With(mask ...Comp) *Filter8[A, B, C, D
 //
 // Create the required mask items with [T].
 func (q *Filter8[A, B, C, D, E, F, G, H]) Without(mask ...Comp) *Filter8[A, B, C, D, E, F, G, H] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset()
+	q.compiled.Reset(false)
 	return q
 }
 
@@ -1297,9 +1415,12 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) Without(mask ...Comp) *Filter8[A, B, C
 //
 // Create the required component ID with [T].
 func (q *Filter8[A, B, C, D, E, F, G, H]) WithRelation(comp Comp, target ecs.Entity) *Filter8[A, B, C, D, E, F, G, H] {
+	if q.compiled.locked {
+		panic("can't modify a registered filter")
+	}
 	q.targetType = comp
 	q.target = target
-	q.compiled.Reset()
+	q.compiled.Reset(true)
 	return q
 }
 
@@ -1336,6 +1457,7 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
 	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
 	q.compiled.filter = &q.compiled.cachedFilter
+	q.compiled.locked = true
 }
 
 // Unregister the filter from caching.
@@ -1347,6 +1469,7 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) Unregister(w *ecs.World) {
 	} else {
 		panic("can't unregister a filter that is not cached")
 	}
+	q.compiled.locked = false
 }
 
 // Query8 is a generic query iterator for eight components.


Test patch:
diff --git a/generic/query_test.go b/generic/query_test.go
index 1227af6..5860eea 100644
--- a/generic/query_test.go
+++ b/generic/query_test.go
@@ -136,6 +136,12 @@ func TestQuery0(t *testing.T) {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
 	}
+
+	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
+	filter2.Unregister(&w)
 }
 
 func TestQuery1(t *testing.T) {
@@ -198,6 +204,13 @@ func TestQuery1(t *testing.T) {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
 	}
+
+	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
+	filter2.Unregister(&w)
 }
 
 func TestQuery2(t *testing.T) {
@@ -266,6 +279,13 @@ func TestQuery2(t *testing.T) {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
 	}
+
+	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
+	filter2.Unregister(&w)
 }
 
 func TestQuery3(t *testing.T) {
@@ -337,6 +357,13 @@ func TestQuery3(t *testing.T) {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
 	}
+
+	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
+	filter2.Unregister(&w)
 }
 
 func TestQuery4(t *testing.T) {
@@ -415,6 +442,13 @@ func TestQuery4(t *testing.T) {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
 	}
+
+	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
+	filter2.Unregister(&w)
 }
 
 func TestQuery5(t *testing.T) {
@@ -499,6 +533,13 @@ func TestQuery5(t *testing.T) {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
 	}
+
+	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
+	filter2.Unregister(&w)
 }
 
 func TestQuery6(t *testing.T) {
@@ -588,6 +629,13 @@ func TestQuery6(t *testing.T) {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
 	}
+
+	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
+	filter2.Unregister(&w)
 }
 
 func TestQuery7(t *testing.T) {
@@ -685,6 +733,13 @@ func TestQuery7(t *testing.T) {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
 	}
+
+	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
+	filter2.Unregister(&w)
 }
 
 func TestQuery8(t *testing.T) {
@@ -779,6 +834,13 @@ func TestQuery8(t *testing.T) {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
 	}
+
+	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
+	filter2.Unregister(&w)
 }
 
 func TestQueryGeneric(t *testing.T) {