Shapes are basically what Python's adaptive LOAD_ATTR (in Python's bytecode) and modern JavaScript VMs do to optimize attribute lookup
When an object is first built, it starts at "empty shape". Each time an attribute X is written, it transitions to the shape of "I had this list of attributes written to me", e.g.
val foo = Box(); # empty shape
foo.x = 1; # shape [x]
foo.y = 2; # shape [x,y]
foo.z = 3; # shape [x,y,z]
foo.x = 2; # shape [x,y,z]
Hashtables of attributes by name are replaced by symbols (which are globally uniqued int mappings of attribute names); symbols are then stored in integer-indexed slots and looked up by shape, so in our example
shape [x] -> {slot(x) = 0}
shape [x,y] -> {slot(x) = 0 slot(y) = 1}
shape [x,y,z] -> {slot(x) = 0 slot(y) = 1 slot(z) = 2}
With all that plumbing, you can then go to each READ_ATTRIBUTE (in Aria's bytecode) and discover that, for example, you're always reading the attribute at slot 2, so you can replace READ_ATTRIBUTE Symbol(z) with LOAD_SLOT 2 and make attribute read O(1) in the happy case
Shapes are basically what Python's adaptive
LOAD_ATTR(in Python's bytecode) and modern JavaScript VMs do to optimize attribute lookupWhen an object is first built, it starts at "empty shape". Each time an attribute
Xis written, it transitions to the shape of "I had this list of attributes written to me", e.g.Hashtables of attributes by name are replaced by symbols (which are globally uniqued int mappings of attribute names); symbols are then stored in integer-indexed slots and looked up by shape, so in our example
shape [x] -> {slot(x) = 0}shape [x,y] -> {slot(x) = 0 slot(y) = 1}shape [x,y,z] -> {slot(x) = 0 slot(y) = 1 slot(z) = 2}With all that plumbing, you can then go to each
READ_ATTRIBUTE(in Aria's bytecode) and discover that, for example, you're always reading the attribute at slot 2, so you can replaceREAD_ATTRIBUTE Symbol(z)withLOAD_SLOT 2and make attribute read O(1) in the happy case