Child groups That's a useful rule, and it can get us pretty far. But let's make it even more specific, so we can prove more programs memory-safe.
For example, look at this snippet: rs ref hp_ref = d.hp # Ref to contents damage = a.calculate_damage(d) a_energy_cost = a.calculate_attack_cost(d) d_energy_cost = d.calculate_defend_cost(a) a.use_energy(a_energy_cost) d.use_energy(d_energy_cost) d.damage(damage) print(hp_ref) # Valid!
The previous (invalid) program had a ring_ref referring to an element in a ring array. This new (correct) program has an hp_ref that's pointing to a mere integer instead. This is actually safe, and the compiler should correctly accept this. After all, since none of these methods can delete an Entity, then they can't delete its contained hp integer.
Good news, Nick's approach takes that into account!
But wait, how? Wouldn't that violate our rule? We might have used a reference (damage may have used d) to mutate an object (the Entity that d is pointing to). So why didn't we invalidate all references to the Entity's contents, like that hp_ref?
So, at long last, let's relax our rule, and replace it with something more precise.
Old rule: When you might have used a reference to mutate an object, don't invalidate any other references to the object's group, but do invalidate any references to its contents.
Better rule: When you might have used a reference to mutate an object, don't invalidate any other references to the object's group, but do invalidate any references to anything in its contents that might have been destroyed.
Or, to have more precise terms: Even better rule: When you might have used a reference to mutate an object, don't invalidate any other references to the object's group, but do invalidate any references to its "child groups".
So what's a "child group", and how is it different from the "contents" from the old rule?
If Entity was defined like this: rs struct Entity: var hp: Int var rings: ArrayList[Ring] var armor: Box[IArmor] # An owning pointer to heap (C++ "unique_ptr") var hand: Variant[Shield, Sword] # A tagged union (Rust "enum") struct Ring: var power: int struct Shield: var durability: int struct Sword: var sharpness: int struct SteelArmor: var hardness: int
Then these things would be part of an Entity's group: hp: Int
rings: ArrayList[Ring]
armor: Box[IArmor] 11
11 hand: Variant[Shield, Sword] 12
However, these would be in Entity's child groups: The Ring s inside that rings list.
s that list. The IArmor object that armor points to.
object that The Shield or Sword inside the hand variant.
For example, if we had this code: rs fn attack[mut r: group Entity]( ref[r] a: Entity, ref[r] d: Entity): ref hp_ref = d.hp ref rings_list_ref = d.rings ref ring_ref = d.rings[rand() % len(d.rings)] ref armor_ref = d.armor[] # Dereferences armor pointer match ref d.hand: case Shield as ref s: ...
Then these are the groups the compiler knows about:
Some observations: d , hp_ref , and rings_list_ref all point to the r group (in blue).
, , and all point to the group (in blue). ring_ref points to the r.rings.items[*] group (in green). That group represents all the rings, because the compiler doesn't know the index rand() % len(d.rings) . This is different than the r.rings[0] from before.
points to the group (in green). That group represents the rings, because the compiler doesn't know the index . This is different than the from before. armor_ref points to the r.armor[] group (in red).
points to the group (in red). s points to the r.hand.Shield group (in yellow). 13
As a user, you can use this rule-of-thumb: any element of a Variant or a collection (List, String, Dict, etc) or Box will be in a child group.
If you want to go deeper, the real rule might be something like: "a Variant's element or anything owned by a pointer will be in a child group." After all, String / List / Dict / Box own things with a pointer under the hood.
That all sounds abstract, so I'll state it in more familiar terms: if an object (even indirectly) owns something that could be independently destroyed, it must be in a child group.
Now, let's see what happens to the groups when we add a damage call in. Remember: Entity.damage mutates the entity, so it has the potential to destroy the rings, armor, shields and/or swords that the entity is holding:
rs fn attack[mut r: group Entity]( ref[r] a: Entity, ref[r] d: Entity): ref hp_ref = d.hp # Group r ref rings_list_ref = d.rings # Group r ref ring_ref = d.rings[rand() % len(d.rings)] # Group r.rings.items[*] ref armor_ref = d.armor[] # Group r.armor[] match ref d.hand: case Shield as ref s: # Group r.hand.Shield ... d.damage(10) # Invalidates refs to r's child groups # Group r.rings.items[*] is invalidated # Group r.armor[] is invalidated # Group r.hand.Shield is invalidated print(hp_ref) # Okay print(len(rings_list_ref)) # Okay print(ring_ref.power) # Error, used invalidated group print(s.durability) # Error, used invalidated group print(armor_ref) # Error, used invalidated group