pybevy 0.2.1

PyBevy: A Python Real-Time Engine Built on Bevy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
# Safety in PyBevy

This document describes how PyBevy maintains memory safety when bridging Python's dynamic nature with Rust's strict safety guarantees.

## Overview

PyBevy implements multiple layers of safety mechanisms to prevent common memory safety bugs while maintaining acceptable performance:

1. **Runtime Validity Checking** - Prevents use-after-free bugs
2. **Parameter Validation** - Enforces Rust's borrowing rules (between parameters)
3. **Python Reference Semantics** - Standard Python behavior for references within parameters
4. **Free-Threading Mode** - Works with Python 3.13+ without GIL (mostly safe)
5. **Async Detection** - Actively blocks async systems that would break safety
6. **Borrowed Field Access** - Enables safe direct mutations
7. **Type Safety** - Leverages Python's type hints and runtime checks

---

## 1. Runtime Validity Checking

**Purpose**: Prevent use-after-free bugs when accessing ECS data after a system completes

### The Problem

In Bevy, system parameters like `Query`, `Commands`, and `ResMut` are only valid during system execution. If Python code stores a reference and tries to use it later, it would access freed memory:

```python
stored_query = None

def bad_system(query: Query[Transform]) -> None:
    global stored_query
    stored_query = query  # Dangerous!

# After system completes...
stored_query[0]  # Would be use-after-free without protection!
```

### The Solution: ValidityFlag with RAII Guards

PyBevy uses **runtime validity flags** with RAII guards to automatically invalidate all system parameters when execution completes:

```rust
// In DynamicSystem::run_unsafe() - src/dynamic_system.rs
let validity = ValidityFlag::new();
let _guard = ValidityGuard::new(validity.clone());

// All parameters share the same validity flag
let py_world = unsafe { PyWorld::new(world_mut, validity.clone()) };
let py_commands = unsafe { PyCommands::new(&mut commands, validity.clone()) };
let py_query = PyQuery::new(query_state, validity.clone());

// Execute Python system...
python_function.call()?;

// When scope ends, guard drops → validity flag set to false
// All parameters become invalid, even on panic!
```

### Runtime Checks on Every Access

Every method that accesses ECS data checks the validity flag first:

```rust
impl PyWorld {
    pub fn spawn_empty(&self) -> PyResult<PyEntityCommands> {
        self.check_valid()?;  // Raises RuntimeError if accessed after system
        // Safe to use world pointer
        let entity = unsafe { self.world_mut() }.spawn_empty();
        Ok(PyEntityCommands::new(entity, self.validity.clone()))
    }
}
```

**Result**: Attempting to use system parameters after the system completes raises a clear error instead of causing undefined behavior:

```python
def system(query: Query[Transform]) -> None:
    global stored_query
    stored_query = query

# Later...
stored_query[0]  # RuntimeError: Component access is no longer valid
```

### Performance

- **Overhead**: ~4ns per validity check (atomic load with Acquire ordering)
- **Impact**: ~4-8% of Query iteration cost (negligible compared to Python overhead)
- **Benefit**: Complete prevention of use-after-free bugs

---

## 2. Parameter Validation (Borrowing Rules)

**Purpose**: Enforce Rust's borrowing rules at system registration time for **all** parameter types

### The Problem

Rust's borrow checker prevents multiple mutable references or mixing mutable and immutable references to the same data:

```rust
// This won't compile in Rust:
fn bad_system(
    query1: Query<&mut Transform>,  // Mutable borrow
    query2: Query<&mut Transform>,  // Second mutable borrow - ERROR!
) { }
```

Python has no compile-time checks, so PyBevy must detect these conflicts at runtime.

### The Solution: Comprehensive Registration-Time Validation

PyBevy validates **all** system parameters when the system is **added** to the app (before it runs). This includes:

1. **Component Access** (Query/View parameters)
2. **Resource Access** (Res/ResMut parameters)
3. **Asset Access** (Assets[T] parameters)
4. **World Exclusivity** (World parameter)

**Implementation**: `src/ecs/dynamic_system.rs:1142-1209` (`validate_parameters()`)

The validation tracks three separate access maps:
- `component_access` - Query/View component conflicts
- `resource_access` - Res/ResMut resource conflicts
- `asset_access` - Assets[T] asset type conflicts

Plus special handling for World's exclusive access requirement.

**Called from**: `DynamicSystem::initialize()` when system is added to the app

### Validation Coverage

| Parameter Type | Validates | Test Coverage |
|----------------|-----------|---------------|
| **Query** | Component access conflicts | 10+ tests |
| **View** | Component access conflicts | 8 tests |
| **Resource** | Resource borrow conflicts | 9 tests |
| **Assets** | Asset type borrow conflicts | 8 tests |
| **Time** | Treated as resource conflict | 5 tests |
| **World** | Exclusive access enforcement | 14 tests |
| Commands | No validation (buffers only) | - |
| Local | No validation (isolated state) | - |
| AssetServer | No validation (read-only) | - |

**Total**: 58 safety tests (100% passing)

### Examples: Query Conflicts

**Rejected - Multiple mutable queries**:
```python
def bad_system(
    query1: Query[Mut[Transform]],
    query2: Query[Mut[Transform]]  # ERROR at registration!
) -> None:
    pass

app.add_systems(Update, bad_system)
# RuntimeError: System 'bad_system' has conflicting component access:
#  - Parameter 1 requests mutable access to Transform
#  - Parameter 0 already has mutable access to Transform
```

**Rejected - Read-write conflict**:
```python
def bad_system(
    read_query: Query[Transform],      # Immutable
    write_query: Query[Mut[Transform]]  # Mutable - ERROR!
) -> None:
    pass
```

**Allowed - Multiple read-only queries**:
```python
def good_system(
    query1: Query[Transform],  # Immutable
    query2: Query[Transform]   # Immutable - OK!
) -> None:
    pass  # Multiple readers allowed
```

**Allowed - Different components**:
```python
def good_system(
    transforms: Query[Mut[Transform]],  # Mutable Transform
    lights: Query[Mut[PointLight]]      # Mutable PointLight - OK!
) -> None:
    pass  # Different components don't conflict
```

### Examples: View Conflicts

View parameters follow the **same borrowing rules** as Query:

**Rejected - View + Query conflict**:
```python
def bad_system(
    view: View[Mut[Transform]],   # Mutable
    query: Query[Transform]        # Immutable - ERROR!
) -> None:
    pass

# RuntimeError: conflicting component access to Transform
```

**Rejected - Double mutable View**:
```python
def bad_system(
    view1: View[Mut[Transform]],
    view2: View[Mut[Transform]]  # ERROR!
) -> None:
    pass
```

**Allowed - Multiple read-only Views**:
```python
def good_system(
    view1: View[Transform],
    view2: View[Transform]  # OK!
) -> None:
    pass  # Multiple readers allowed
```

**Workaround - Split into separate systems**:
```python
# Instead of mixing View + Query in one system:
def modify_system(view: View[Mut[Transform]]) -> None:
    # Modify with View in Update
    pass

def verify_system(query: Query[Transform]) -> None:
    # Verify with Query in Last
    pass

app.add_systems(Update, modify_system)
app.add_systems(Last, verify_system)
```

### Examples: Resource Conflicts

**Rejected - Double mutable resources**:
```python
def bad_system(
    res1: ResMut[GameSettings],
    res2: ResMut[GameSettings]  # ERROR!
) -> None:
    pass

# RuntimeError: conflicting resource access to GameSettings
```

**Rejected - Resource read-write conflict**:
```python
def bad_system(
    res: Res[Time],
    res_mut: ResMut[Time]  # ERROR!
) -> None:
    pass
```

**Allowed - Multiple read-only resources**:
```python
def good_system(
    res1: Res[GameSettings],
    res2: Res[GameSettings]  # OK!
) -> None:
    pass  # Multiple readers allowed
```

**Allowed - Different resources**:
```python
def good_system(
    settings: ResMut[GameSettings],
    time: ResMut[Time]  # OK - different resources!
) -> None:
    pass
```

### Examples: Assets Conflicts

**Rejected - Double mutable assets**:
```python
def bad_system(
    mats1: ResMut[Assets[StandardMaterial]],
    mats2: ResMut[Assets[StandardMaterial]]  # ERROR!
) -> None:
    pass

# RuntimeError: conflicting asset access to Assets[StandardMaterial]
```

**Rejected - Assets read-write conflict**:
```python
def bad_system(
    images: Res[Assets[Image]],
    images_mut: ResMut[Assets[Image]]  # ERROR!
) -> None:
    pass
```

**Allowed - Different asset types**:
```python
def good_system(
    materials: ResMut[Assets[StandardMaterial]],
    images: ResMut[Assets[Image]]  # OK - different types!
) -> None:
    pass
```

### Examples: World Exclusive Access

World requires **EXCLUSIVE** access to the entire ECS. It conflicts with any parameter that accesses world state.

**Rejected - World + Query**:
```python
def bad_system(
    world: World,
    query: Query[Transform]  # ERROR!
) -> None:
    pass

# RuntimeError: World parameter requires EXCLUSIVE access to entire ECS
#  - Parameter 1 conflicts with World (type: Query)
#  World can only be used with Commands and Local parameters.
```

**Rejected - World + Resources**:
```python
def bad_system(
    world: World,
    time: Res[Time]  # ERROR!
) -> None:
    pass
```

**Rejected - World + Assets**:
```python
def bad_system(
    world: World,
    assets: ResMut[Assets[Mesh]]  # ERROR!
) -> None:
    pass
```

**Allowed - World + Commands**:
```python
def good_system(
    world: World,
    commands: Commands  # OK - Commands just buffers!
) -> None:
    pass  # Commands doesn't access world state during system
```

**Allowed - World alone**:
```python
def good_system(world: World) -> None:
    # Can use world.spawn(), world.query(), world.resource(), etc.
    pass
```

---

## 3. Python Reference Semantics vs Rust Borrowing

**Purpose**: Clarify differences between Rust's compile-time guarantees and Python's runtime behavior

### The Difference

Parameter validation (Section 2) prevents conflicts **between** parameters, which matches Rust's borrowing rules. However, **within** a single parameter, PyBevy follows normal Python reference semantics:

```python
def system(query: Query[Mut[Transform]]) -> None:
    transforms = list(query)  # Convert to Python list

    # Multiple references to the same entity - normal Python!
    ref1 = transforms[0]
    ref2 = transforms[0]

    # Both reference the same object (standard Python behavior)
    ref1.translation.x = 100.0
    print(ref2.translation.x)  # Prints 100.0 - same object

    ref2.translation.x = 200.0
    print(ref1.translation.x)  # Prints 200.0 - same object
```

**This is normal Python behavior**:
```python
# Regular Python works exactly the same way:
class Point:
    def __init__(self, x):
        self.x = x

my_list = [Point(10), Point(20)]
ref1 = my_list[0]
ref2 = my_list[0]  # Same reference, not a copy

ref1.x = 999
print(ref2.x)  # Prints 999 - this is how Python works!
```

**Why this is NOT a safety hole**:
- Standard Python reference semantics (expected behavior)
- No memory corruption, data races, or undefined behavior
- ValidityFlag still prevents use-after-free
- Python GIL prevents concurrent access issues
- Easy to debug (wrong values are obvious, not silent corruption)

### Behavioral Differences from Rust Bevy

**What Rust prevents (compile-time)**:
- Multiple mutable borrows **between system parameters**
- Rust: `fn system(q1: Query<&mut T>, q2: Query<&mut T>)` Won't compile
- PyBevy: Prevented by parameter validation (Section 2)

**What Rust prevents (compile-time) but Python allows (runtime)**:
- Multiple references to same object **within a parameter**
- Rust: Borrow checker tracks individual entity borrows
- Python: Reference semantics allow multiple refs to same object

**The only semi-realistic edge case**:

```python
def system(query: Query[Mut[Transform]]) -> None:
    entities = list(query)

    # Nested iteration - might process same entity twice
    for e1 in entities:
        for e2 in entities:
            if distance(e1, e2) < 10.0:
                apply_force(e1, e2)  # Double-applies if e1 == e2
```

**But this is just normal Python**:
```python
# Same issue exists in any Python code:
points = [Point(1, 2), Point(3, 4)]
for p1 in points:
    for p2 in points:
        # p1 and p2 might be the same - check if needed
        if p1 is not p2:  # Standard Python pattern
            process(p1, p2)
```

### Test Coverage

Three tests in `tests/safety/test_intra_query_aliasing.py` document this behavior:

1. **`test_aliasing_via_list()`** - Multiple refs to same entity work (normal Python)
2. **`test_aliasing_during_iteration()`** - Stored refs see modifications (expected)
3. **`test_double_modification_through_aliases()`** - Both refs affect same object (correct)

These tests **PASS** to document that this follows standard Python reference semantics.

### Why PyBevy Doesn't Add Runtime Checks

**Decision**: Use standard Python reference semantics (no additional tracking)

**Reasoning**:
- This is how Python works - not a bug to fix
- Adding runtime tracking would add overhead to ALL queries for a non-issue
- Python developers expect reference semantics
- No actual safety violation (no memory corruption, data races, or UB)
- Easy to reason about and debug

### Common Usage Patterns

**Standard iteration (most common)**:
```python
def system(query: Query[Mut[Transform]]) -> None:
    for transform in query:
        transform.translation.x += 1.0  # Standard pattern
```

**List conversion for random access**:
```python
def system(query: Query[Mut[Transform]]) -> None:
    transforms = list(query)

    for transform in transforms:
        transform.translation.x += 1.0  # Works fine
```

**Nested iteration (use identity check if needed)**:
```python
def system(query: Query[tuple[Mut[Transform], Entity]]) -> None:
    entities = [(t, e) for t, e in query]

    for t1, e1 in entities:
        for t2, e2 in entities:
            if e1 != e2:  # Standard Python pattern - check identity
                process(t1, t2)
```

**Using `is` operator for identity checks**:
```python
def system(query: Query[Mut[Transform]]) -> None:
    transforms = list(query)

    for t1 in transforms:
        for t2 in transforms:
            if t1 is not t2:  # Python identity check
                # Different entities
                process(t1, t2)
```

---

## 4. Free-Threading Mode (Python 3.13+)

**Purpose**: Explain PyBevy's behavior in Python's free-threaded mode (no GIL)

### Overview

Python 3.13+ introduced optional free-threaded mode where the Global Interpreter Lock (GIL) is disabled, allowing true parallel execution of Python code. PyBevy declares `gil_used = false` on the extension module so the free-threaded interpreter does not re-enable the GIL at import. Initial validation on CPython 3.14t passes 3,533/3,534 tests and confirms genuine parallel execution of non-conflicting CPU-bound Python systems. PyBevy's core safety mechanisms remain effective in this mode, with some additional considerations.

**Risk Level**: 15/100 (Low-Medium)

Most of PyBevy's safety comes from **Bevy's ECS scheduler** and **parameter validation**, not the GIL. The GIL was defensive redundancy.

### What Remains Safe

**All core PyBevy safety mechanisms work without GIL**:

1. **ValidityFlag** - Uses `Arc<AtomicU8>` (already thread-safe, tracks Read/Write/Invalid)
   ```rust
   pub struct ValidityFlag(Arc<AtomicU8>);  // Atomic operations
   ```

2. **Parameter validation** - Prevents concurrent access at registration time
   ```python
   # These conflicts are REJECTED regardless of GIL:
   def bad(q1: Query[Mut[Transform]], q2: Query[Mut[Transform]]) -> None:
       pass  # RuntimeError - conflicting access
   ```

3. **Bevy's scheduler** - Ensures systems with conflicting access never run in parallel
   - `Query[Mut[T]]` + `Query[Mut[T]]` → Never concurrent
   - `ResMut[R]` + `ResMut[R]` → Rejected by validation
   - Each entity accessed by at most one system at a time

4. **Wrapper storage components** - Mutations go through Rust memory
   ```python
   @component
   class Health(Component):
       value: float  # Stored as Rust f64 - thread-safe

   def system(query: Query[Mut[Health]]) -> None:
       for h in query:
           h.value += 1.0  # Safe - writes to Rust memory
   ```

5. **Borrowed field access** - Protected by Bevy's exclusive access
   ```python
   def system(query: Query[Mut[Transform]]) -> None:
       for t in query:
           t.translation.x = 100.0  # Safe - Bevy prevents conflicts
   ```

### What Requires Caution

**CAUTION:Global Python state (User responsibility)**:

```python
# Global state - NOT protected by PyBevy!
scores = []

def system1(query: Query[Transform]) -> None:
    scores.append(1)  # Thread 1

def system2(query: Query[Transform]) -> None:
    scores.append(2)  # Thread 2 - DATA RACE without GIL!
```

**Why it's unsafe**:
- Python lists/dicts are NOT thread-safe without GIL
- PyBevy cannot detect or prevent global state access
- This is standard Python threading responsibility

**Workaround - Use Local parameters**:
```python
from dataclasses import dataclass, field
from pybevy.ecs import Local

@dataclass
class Scores:
    values: list = field(default_factory=list)

def system(scores: Local[Scores]) -> None:
    scores.value.values.append(1)  # Each system instance gets its own list
```

**CAUTION:Custom resource complex fields (Usually safe)**:

```python
@resource
class GameState(Resource):
    enemies: list[Enemy]  # Python list

def system(state: ResMut[GameState]) -> None:
    state.enemies.append(enemy)  # Usually safe
```

**Why it's usually safe**:
- Parameter validation prevents two systems from getting `ResMut[GameState]`
- Only ONE system can mutate GameState at a time
- Bevy's scheduler enforces exclusive access

**Edge case - Still unsafe**:
```python
# If GameState is accessed from OUTSIDE systems (direct Python code)
# then it's not protected by Bevy's scheduler
```

**CAUTION: Spawning threads inside systems**:

Passing component references to threads spawned within a system bypasses PyBevy's safety guarantees. Without the GIL, two threads calling `as_mut()` on the same component simultaneously would create a data race on the underlying Rust memory. In practice the effect is limited to corrupted field values (torn writes on non-atomic types). A thread-id check in the validity flag could detect this, but would add a branch to every component access on the hot path.

### Safe Patterns

**Standard component mutations**:
```python
def system(query: Query[Mut[Transform]]) -> None:
    for transform in query:
        transform.translation.x += 1.0  # Safe - Bevy prevents conflicts
```

**Resource access with parameter validation**:
```python
def system(time: Res[Time], settings: ResMut[GameSettings]) -> None:
    if time.elapsed_secs() > 10.0:
        settings.difficulty = "hard"  # Safe - exclusive access
```

**Primitive fields in custom components**:
```python
@component
class Player(Component):
    health: float  # Stored efficiently
    score: int

def system(query: Query[Mut[Player]]) -> None:
    for player in query:
        player.health -= 1.0  # Safe
```

**Using Local for thread-local state**:
```python
from dataclasses import dataclass
from pybevy.ecs import Local

@dataclass
class Counter:
    count: int = 0

def system(counter: Local[Counter], query: Query[Transform]) -> None:
    # Each system instance gets its own Counter
    counter.value.count += len(list(query))
```

### Explicitly Prevented: Async Systems

**PyBevy actively blocks async systems at registration time:**

```python
import asyncio

async def async_system(query: Query[Mut[Transform]]) -> None:
    transforms = list(query)
    await asyncio.sleep(0.1)  # Would suspend execution
    transforms[0].translation.x = 100.0

# This raises RuntimeError at registration:
app.add_systems(Update, async_system)
# RuntimeError: Async systems (async def) are not supported.
# Use synchronous 'def' instead of 'async def'.
# Async functions would break PyBevy's safety guarantees
# (ValidityFlag becomes invalid when function suspends).
```

**Why async is dangerous:**
- ValidityGuard uses RAII - drops when async function suspends at `await`
- When function resumes, ValidityFlag is invalid but execution continues
- Parameters point to freed/moved memory → use-after-free
- Would completely bypass PyBevy's safety architecture

**Detection:**
- Checked at system registration using `inspect.iscoroutinefunction()`
- Zero runtime overhead (one-time check)
- Clear error message explains the safety issue

**Alternative for async operations:**
```python
# Instead of async systems, use regular systems with async libraries
def system(commands: Commands) -> None:
    # Start async task in background
    asyncio.create_task(load_assets())
    # System completes immediately - ValidityFlag safe

# Or use callbacks
def system(commands: Commands, loader: ResMut[AssetLoader]) -> None:
    loader.queue_load("texture.png", on_complete=lambda tex: ...)
```

### Unsafe Patterns (Free-Threaded Mode)

**UNSAFE:Global mutable state**:
```python
cache = {}  # Global - NOT thread-safe!

def system(query: Query[Entity]) -> None:
    cache[entity.id()] = data  # DATA RACE!
```

**UNSAFE:Shared closures capturing mutable state**:
```python
shared_list = []

def make_system():
    def system(query: Query[Transform]) -> None:
        shared_list.append(1)  # Captures shared_list - unsafe!
    return system
```

**UNSAFE:Accessing resources outside Bevy systems**:
```python
@resource
class GameState(Resource):
    data: list[int]

game_state = GameState()
app.insert_resource(game_state)

# Later, outside any system:
game_state.data.append(1)  # NOT protected by Bevy scheduler!
```

### PyO3 Thread Safety

**PyO3's behavior** (free-threaded mode):
- `Python::attach()` does NOT acquire GIL (multiple can run concurrently)
- `Py<T>` is `Send + Sync` - can be sent between threads
- **Users** must handle synchronization for shared Python objects

**PyBevy's approach**:
- Relies on Bevy's scheduler to prevent concurrent access
- Parameter validation enforces exclusive access at registration time
- ValidityFlag uses atomics (thread-safe regardless of PyO3 behavior)

### Detection

PyBevy automatically detects free-threaded mode:

```rust
fn detect_gil_status() -> bool {
    Python::attach(|py| {
        if let Ok(sys) = py.import("sys") {
            if let Ok(enabled) = sys.getattr("_is_gil_enabled") {
                return enabled.call0().extract().unwrap_or(true);
            }
        }
        true  // GIL enabled by default
    })
}
```

Used for diagnostics/logging. The extension module declares `gil_used = false` via `#[pymodule(gil_used = false)]`, so the free-threaded interpreter does not re-enable the GIL at import time.

### Thread Safety Summary

| Component | GIL Mode | Free-Threaded | Notes |
|-----------|----------|---------------|-------|
| ValidityFlag | Safe | Safe | Uses atomics |
| Parameter validation | Safe | Safe | Registration-time |
| Wrapper components | Safe | Safe | Rust storage |
| PyObject components | Safe | Safe | Bevy prevents conflicts |
| Built-in resources | Safe | Safe | Exclusive access enforced |
| Custom resources | Safe | Safe* | *If only accessed in systems |
| Global Python state | Discouraged | Unsafe | User responsibility |
| Borrowed fields | Safe | Safe | Bevy prevents conflicts |

### Recommendations

**For most users**: Free-threading works fine with PyBevy. Follow normal ECS patterns:
- DO: Access data through system parameters
- DO: Use Local for thread-local state
- DON'T: Use global Python mutable state
- DON'T: Access resources outside systems

**Advanced users**: If you need shared Python state:
1. Use `threading.Lock` explicitly
2. Store state in Bevy resources (accessed via parameters)
3. Use message passing instead of shared mutable state

### Why PyBevy Is Mostly GIL-Independent

PyBevy's safety architecture was designed around **Bevy's guarantees**, not Python's:

1. **Bevy's scheduler** prevents concurrent mutable access to components/resources
2. **Parameter validation** enforces borrowing rules at registration time
3. **ValidityFlag** uses atomics (works without GIL)
4. **Borrowed fields** point to Bevy-managed memory (not Python objects)

The GIL was **defensive redundancy** - an extra layer of protection against user code doing unsafe things (global state). The core safety mechanisms work regardless of GIL status.

---

## 5. Borrowed Field Access

**Purpose**: Enable direct field mutations that persist to ECS memory

### The Problem

Python's property access model creates copies:

```python
# This DOESN'T work without borrowed field access:
for transform in Query[Mut[Transform]]:
    transform.translation.x = 100.0  # Modifies a COPY
    # Original ECS data unchanged!
```

The issue: `transform.translation` returns a copy of the Vec3. Modifying `copy.x` doesn't affect the ECS.

### The Solution: Borrowed Field Pattern

PyBevy implements an enum-based storage pattern where property getters return **borrowed** references pointing directly to component fields:

```rust
// Vec3 can be either owned or borrowed - src/math/vec3.rs
enum Vec3Storage {
    Owned(Vec3),  // Created via Vec3(1.0, 2.0, 3.0)
    Borrowed {
        ptr: *mut Vec3,                // Points to component field
        validity: ValidityFlagWithMode, // Tracks read/write permissions
    },
}

#[pyclass(name = "Vec3")]
pub struct PyVec3 {
    storage: Vec3Storage,
}

impl PyVec3 {
    // Create borrowed Vec3 pointing to a component's field
    pub(crate) fn from_borrowed(ptr: *mut Vec3, validity: ValidityFlagWithMode) -> Self {
        PyVec3 {
            storage: Vec3Storage::Borrowed { ptr, validity },
        }
    }

    // Get mutable reference with safety checks
    fn as_mut(&mut self) -> PyResult<&mut Vec3> {
        match &mut self.storage {
            Vec3Storage::Owned(vec) => Ok(vec),
            Vec3Storage::Borrowed { ptr, validity } => {
                validity.check_write()?;  // Ensures mutable access allowed
                Ok(unsafe { &mut **ptr })
            }
        }
    }
}

#[pymethods]
impl PyVec3 {
    #[setter]
    pub fn set_x(&mut self, value: f32) -> PyResult<()> {
        self.as_mut()?.x = value;  // Writes directly to ECS memory!
        Ok(())
    }
}
```

**Transform property returns borrowed Vec3**:
```rust
// In Transform::translation() - src/transform/transform.rs
#[getter]
pub fn translation(&self) -> PyResult<PyVec3> {
    self.check_valid()?;
    match &self.storage {
        TransformStorage::Owned(boxed) => {
            Ok(PyVec3::from(boxed.translation))  // Copy for owned
        }
        TransformStorage::Borrowed { ptr, validity } => {
            // Return borrowed Vec3 pointing directly to the field
            let translation_ptr = unsafe {
                let transform_ptr = *ptr as *const Transform;
                &(*transform_ptr).translation as *const Vec3 as *mut Vec3
            };
            Ok(PyVec3::from_borrowed(translation_ptr, validity.clone()))
        }
    }
}
```

### How It Works

1. `Query[Mut[Transform]]` iteration creates borrowed Transform pointing to ECS memory
2. `transform.translation` getter returns borrowed Vec3 pointing to transform's field
3. `translation.x = 100.0` calls `set_x()`, which writes directly to ECS memory
4. `ValidityFlagWithMode` ensures write access is allowed (would fail for read-only query)

**Result**: Field mutations persist to ECS!

```python
# This WORKS with borrowed field access:
for transform in Query[Mut[Transform]]:
    transform.translation.x = 100.0  # Writes to ECS memory
    transform.rotation.w = 0.5       # Persists!
```

### Implemented Types

Types with borrowed field access:

- **Vec2** (`src/math/vec2.rs`) - 2D vectors with `.x`, `.y` setters
- **Vec3** (`src/math/vec3.rs`) - 3D vectors with `.x`, `.y`, `.z` setters
- **Quat** (`src/math/quat.rs`) - Quaternions with `.x`, `.y`, `.z`, `.w` setters
- **Transform** (`src/transform/transform.rs`) - Uses borrowed Vec3/Quat for properties
- **PointLight** (`src/light/point_light.rs`) - Direct field mutations

### Safety Guarantees

1. **ValidityFlag**: Prevents access after system completion
2. **ValidityFlagWithMode**: Enforces read-only vs mutable access:
   - `Query[Transform]` → Borrowed fields are read-only
   - `Query[Mut[Transform]]` → Borrowed fields allow writes
3. **Pointer safety**: Only dereferenced after validity check passes
4. **Lifetime coupling**: Borrowed field lifetime tied to parent component

---

## 6. Thread Safety

PyBevy types that wrap raw pointers implement `Send + Sync` to allow them to cross thread boundaries:

```rust
// SAFETY: ValidityFlag ensures borrowed pointers only accessed during system execution
unsafe impl Send for PyVec3 {}
unsafe impl Sync for PyVec3 {}
```

**Why this is safe**:
- Pointers are never dereferenced without validity check
- Validity check fails if accessed from wrong context
- ECS scheduler ensures exclusive access during system execution

---

## Safety Precautions for Users

### Do This

**Store components temporarily during system execution**:
```python
def system(query: Query[Mut[Transform]]) -> None:
    transforms = list(query)  # OK within system
    for t in transforms:
        t.translation.x += 1.0  # OK - still in system
```

**Use multiple read-only queries**:
```python
def system(
    transforms: Query[Transform],
    lights: Query[PointLight]
) -> None:
    pass  # OK - multiple readers allowed
```

### Don't Do This

**Store system parameters globally**:
```python
stored_query = None

def system(query: Query[Transform]) -> None:
    global stored_query
    stored_query = query  # DON'T!

# Later...
stored_query[0]  # RuntimeError: Component no longer valid
```

**Multiple mutable queries for same component**:
```python
def system(
    q1: Query[Mut[Transform]],
    q2: Query[Mut[Transform]]  # RuntimeError at registration!
) -> None:
    pass
```

**Mix mutable and immutable queries**:
```python
def system(
    read: Query[Transform],
    write: Query[Mut[Transform]]  # RuntimeError at registration!
) -> None:
    pass
```

---

## Error Messages

PyBevy provides clear error messages for safety violations:

**Use-after-free attempt**:
```
RuntimeError: Component access is no longer valid (system has completed execution)
```

**Multiple mutable queries**:
```
RuntimeError: System 'update_positions' has conflicting component access:
 - Parameter 1 requests mutable access to Transform
 - Parameter 0 already has mutable access to Transform
 Rust's borrowing rules forbid multiple mutable references or
 mixing mutable and immutable references to the same data.
```

**Write to read-only field**:
```
RuntimeError: Cannot write to component - query does not have mutable access
```

---

## Performance Impact

| Safety Mechanism | Overhead | Impact |
|------------------|----------|--------|
| Validity check | ~4ns per access | ~4-8% of query iteration |
| Parameter validation | One-time at registration | Zero runtime cost |
| Borrowed field access | Zero (inline match) | No overhead vs direct access |

**Comparison**: Python's method call overhead is ~100-500ns, making the 4ns validity check negligible (~1% of method call cost).

---

## Implementation Details

### Key Files

- **Core Storage Layer** (`crates/pybevy_core/src/`):
  - `validity_guard.rs` - ValidityFlag, ValidityGuard (RAII), AccessMode
  - `component_storage.rs` - Owned/Borrowed component storage
  - `value_storage.rs` - Copy-type field storage (Vec3, Quat, f32, etc.)
  - `field_storage.rs` - Non-Copy field storage (TextureAtlas, WindowResolution, etc.)
  - `list_storage.rs` - Vec<T> field storage with Python list interface
  - `storage/asset_storage.rs` - Asset storage (read-only vs mutable variants)
- **Parameter Validation**: `src/ecs/dynamic_system.rs` (`validate_parameters()`, `validate_component_access_internal()`)
- **Borrowed Fields**: `src/math/vec2.rs`, `src/math/vec3.rs`, `src/math/quat.rs`
- **Component Pattern**: `src/transform/transform.rs`, `src/light/point_light.rs`
- **Python Safety Tests**: `tests/safety/` (15 test files)
- **Rust Unit Tests**: `cargo test -p pybevy_core` (63 tests covering all storage types)

### Related Documentation

- **[Enum Component Pattern](tech/enum-component-pattern.md)** - Detailed guide to borrowed field pattern

---

## 7. Rust Unit Tests (Storage Layer)

**Purpose**: Validate the core unsafe storage layer in pure Rust, independently from Python

### Overview

The `pybevy_core` crate contains all the unsafe pointer logic (owned/borrowed storage, validity flags, field borrowing). These are tested with pure Rust unit tests that can run without the Python interpreter.

```bash
cargo test -p pybevy_core    # 63 tests, ~0s runtime
```

### Coverage

| Module | Tests | What's Tested |
|--------|-------|---------------|
| `validity_guard` | 10 | Flag lifecycle, read/write/invalid modes, RAII guard drop, clone propagation, ValidityFlagWithMode |
| `component_storage` | 9 | Owned/borrowed modes, mutation persistence, validity enforcement, write permissions, into_owned |
| `value_storage` | 8 | Same as component_storage for Copy types (Vec3, f32, etc.) |
| `field_storage` | 12 | Owned/borrowed, borrow_field chains (owned→field, borrowed→field), mutation persistence through nested borrows, Drop invalidation, clone independence |
| `list_storage` | 11 | Vec storage, mutation persistence, validity/write enforcement, normalize_index edge cases |
| `asset_storage` | 8 | Read-only vs mutable variants, take/consume, validity enforcement |
| `handle` | 2 | UUID handling |
| `global_registry` | 2 | Registry basics |
| **Total** | **63** | |

### Key Safety Properties Tested

- **Use-after-free prevention**: Validity flag becomes Invalid after guard drops; access raises error
- **Write permission enforcement**: Read-only borrows reject mutation attempts
- **Mutation persistence**: Borrowed storage mutations propagate through raw pointers to original data
- **Drop invalidation**: Owned storage invalidates its validity flag before freeing memory
- **Nested borrow chains**: Field borrows from borrowed parents share the same validity flag
- **Clone independence**: Cloned owned storage has independent data and validity flags

---

## 8. AddressSanitizer Testing

**Purpose**: Detect memory errors (use-after-free, buffer overflow, etc.) at runtime using compiler instrumentation

### Running

```bash
./scripts/check_asan.sh                          # Phase 1 only (Rust unit tests under ASan)
./scripts/check_asan.sh --full                   # Phase 1 + Phase 2 (build .so + run pytest)
./scripts/check_asan.sh --full tests/safety/     # Phase 2 with specific test directory
./scripts/check_asan.sh --full -k test_transform # Phase 2 with pytest args
```

### Requirements

```bash
rustup toolchain install nightly
sudo apt install libasan8    # only needed for --full (Phase 2)
```

### What It Does

1. **Phase 1** (default): Runs `cargo +nightly test -p pybevy_core` with `-Z sanitizer=address` — validates the Rust storage layer under ASan. Always works because `pybevy_core` has minimal dependencies.
2. **Phase 2** (`--full`): Builds the full PyO3 `.so` with ASan instrumentation via `maturin develop`, then runs `pytest` with `LD_PRELOAD=libasan.so` — catches memory errors during actual Python-to-Rust interaction.
3. **Cleanup** (Phase 2 only): Rebuilds without ASan to restore the normal dev environment.

### What ASan Detects

- Use-after-free (dangling pointer dereference)
- Heap/stack buffer overflow
- Stack use after return
- Memory leaks (disabled by default — Python's GC makes this noisy)

### Limitations

- Requires Rust nightly toolchain
- Phase 2 may fail on some nightly versions due to transitive dependency breakage (e.g., `zerocopy`, `futures-util` not compiling on bleeding-edge nightly). Phase 1 results are still valid in this case.
- Miri (`cargo miri`) cannot be used because `pybevy_core` depends on `pyo3`/`bevy` which have FFI that Miri can't interpret
- ASan adds ~2x runtime overhead (not for production use)

---

## Summary

PyBevy achieves production-ready safety through:

1. **Runtime validity checking** - Prevents use-after-free (~4ns overhead)
2. **Comprehensive parameter validation** - Enforces Rust borrowing rules for all parameter types (zero runtime cost)
   - Query/View component access
   - Res/ResMut resource access
   - Assets[T] asset access
   - World exclusive access
3. **Standard Python reference semantics** - Within parameters, follows normal Python behavior
4. **Free-threading compatible** - Works with Python 3.13+ without GIL
5. **Async system detection** - Blocks async functions at registration (would break ValidityFlag)
6. **Borrowed field access** - Direct mutations persist safely (zero overhead)
7. **Rust unit tests** - 63 tests covering all storage types in `pybevy_core`
8. **AddressSanitizer support** - `scripts/check_asan.sh` for runtime memory error detection

### Test Coverage

| Layer | What's Tested | Count |
|-------|---------------|-------|
| Rust storage (pybevy_core) | Validity flags, owned/borrowed storage, field borrows, Drop safety | 63 tests |
| Python safety (tests/safety/) | Parameter conflicts, aliasing, async detection, field mutations | 15 test files |
| ASan (scripts/check_asan.sh) | Runtime memory errors across Rust + Python boundary | On-demand |

These mechanisms ensure that PyBevy maintains Rust's safety guarantees at runtime, making it safe for production use while keeping performance impact minimal.