# # A class which knows how to parse objects in the object tree. # Implements section 12 of Z-code specification. # # For the license of this file, please consult the LICENSE file in the # root directory of this distribution. # # This part of of the z-machine is where it becomes really clear that # the original authoris were MIT Lisp-heads. :-) They've got a tree # of objects going, where each object is basically a linked list of # siblings. Specifically, each object contains a pointer to a parent, # a pointer to its "next sibling" in the list, and a pointer to the # head of its own children-list. from .bitfield import BitField from .zlogging import log from .zstring import ZStringFactory class ZObjectError(Exception): "General exception for ZObject class" pass class ZObjectIllegalObjectNumber(ZObjectError): "Illegal object number given." pass class ZObjectIllegalAttributeNumber(ZObjectError): "Illegal attribute number given." pass class ZObjectIllegalPropertyNumber(ZObjectError): "Illegal property number given." pass class ZObjectIllegalPropertySet(ZObjectError): "Illegal set of a property whose size is not 1 or 2." pass class ZObjectIllegalVersion(ZObjectError): "Unsupported z-machine version." pass class ZObjectIllegalPropLength(ZObjectError): "Illegal property length." pass class ZObjectMalformedTree(ZObjectError): "Object tree is malformed." pass # The interpreter should only need exactly one instance of this class. class ZObjectParser: def __init__(self, zmem): self._memory = zmem self._propdefaults_addr = zmem.read_word(0x0A) self._stringfactory = ZStringFactory(self._memory) if 1 <= self._memory.version <= 3: self._objecttree_addr = self._propdefaults_addr + 62 elif 4 <= self._memory.version <= 5 or self._memory.version == 8: self._objecttree_addr = self._propdefaults_addr + 126 else: raise ZObjectIllegalVersion def _get_object_addr(self, objectnum): """Return address of object number OBJECTNUM.""" result = 0 if 1 <= self._memory.version <= 3: if not (1 <= objectnum <= 255): raise ZObjectIllegalObjectNumber result = self._objecttree_addr + (9 * (objectnum - 1)) elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (1 <= objectnum <= 65535): log(f"error: there is no object {objectnum}") raise ZObjectIllegalObjectNumber result = self._objecttree_addr + (14 * (objectnum - 1)) else: raise ZObjectIllegalVersion log(f"address of object {objectnum} is {result}") return result def _get_parent_sibling_child(self, objectnum): """Return [parent, sibling, child] object numbers of object OBJECTNUM.""" addr = self._get_object_addr(objectnum) result = 0 if 1 <= self._memory.version <= 3: addr += 4 # skip past attributes result = self._memory[addr : addr + 3] elif 4 <= self._memory.version <= 5 or self._memory.version == 8: addr += 6 # skip past attributes result = [ self._memory.read_word(addr), self._memory.read_word(addr + 2), self._memory.read_word(addr + 4), ] else: raise ZObjectIllegalVersion log( f"parent/sibling/child of object {objectnum} is " f"{result[0]}, {result[1]}, {result[2]}" ) return result def _get_proptable_addr(self, objectnum): """Return address of property table of object OBJECTNUM.""" addr = self._get_object_addr(objectnum) # skip past attributes and relatives if 1 <= self._memory.version <= 3: addr += 7 elif 4 <= self._memory.version <= 5 or self._memory.version == 8: addr += 12 else: raise ZObjectIllegalVersion return self._memory.read_word(addr) def _get_default_property_addr(self, propnum): """Return address of default value for property PROPNUM.""" addr = self._propdefaults_addr if 1 <= self._memory.version <= 3: if not (1 <= propnum <= 31): raise ZObjectIllegalPropertyNumber elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (1 <= propnum <= 63): raise ZObjectIllegalPropertyNumber else: raise ZObjectIllegalVersion return addr + (2 * (propnum - 1)) # --------- Public APIs ----------- def get_attribute(self, objectnum, attrnum): """Return value (0 or 1) of attribute number ATTRNUM of object number OBJECTNUM.""" if objectnum == 0: return 0 object_addr = self._get_object_addr(objectnum) if 1 <= self._memory.version <= 3: if not (0 <= attrnum <= 31): raise ZObjectIllegalAttributeNumber bf = BitField(self._memory[object_addr + (attrnum // 8)]) elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (0 <= attrnum <= 47): raise ZObjectIllegalAttributeNumber bf = BitField(self._memory[object_addr + (attrnum // 8)]) else: raise ZObjectIllegalVersion return bf[7 - (attrnum % 8)] def set_attribute(self, objectnum, attrnum): """Set attribute number ATTRNUM of object number OBJECTNUM to 1.""" if objectnum == 0: return object_addr = self._get_object_addr(objectnum) if 1 <= self._memory.version <= 3: if not (0 <= attrnum <= 31): raise ZObjectIllegalAttributeNumber byte_offset = attrnum // 8 bf = BitField(self._memory[object_addr + byte_offset]) elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (0 <= attrnum <= 47): raise ZObjectIllegalAttributeNumber byte_offset = attrnum // 8 bf = BitField(self._memory[object_addr + byte_offset]) else: raise ZObjectIllegalVersion bf[7 - (attrnum % 8)] = 1 self._memory[object_addr + byte_offset] = int(bf) def clear_attribute(self, objectnum, attrnum): """Clear attribute number ATTRNUM of object number OBJECTNUM to 0.""" if objectnum == 0: return object_addr = self._get_object_addr(objectnum) if 1 <= self._memory.version <= 3: if not (0 <= attrnum <= 31): raise ZObjectIllegalAttributeNumber byte_offset = attrnum // 8 bf = BitField(self._memory[object_addr + byte_offset]) elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (0 <= attrnum <= 47): raise ZObjectIllegalAttributeNumber byte_offset = attrnum // 8 bf = BitField(self._memory[object_addr + byte_offset]) else: raise ZObjectIllegalVersion bf[7 - (attrnum % 8)] = 0 self._memory[object_addr + byte_offset] = int(bf) def get_all_attributes(self, objectnum): """Return a list of all attribute numbers that are set on object OBJECTNUM""" if 1 <= self._memory.version <= 3: max = 32 elif 4 <= self._memory.version <= 5 or self._memory.version == 8: max = 48 else: raise ZObjectIllegalVersion # really inefficient, but who cares? attrs = [] for i in range(0, max): if self.get_attribute(objectnum, i): attrs.append(i) return attrs def get_parent(self, objectnum): """Return object number of parent of object number OBJECTNUM.""" if objectnum == 0: return 0 [parent, sibling, child] = self._get_parent_sibling_child(objectnum) return parent def get_child(self, objectnum): """Return object number of child of object number OBJECTNUM.""" if objectnum == 0: return 0 [parent, sibling, child] = self._get_parent_sibling_child(objectnum) return child def get_sibling(self, objectnum): """Return object number of sibling of object number OBJECTNUM.""" if objectnum == 0: return 0 [parent, sibling, child] = self._get_parent_sibling_child(objectnum) return sibling def set_parent(self, objectnum, new_parent_num): """Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM.""" addr = self._get_object_addr(objectnum) if 1 <= self._memory.version <= 3: self._memory[addr + 4] = new_parent_num elif 4 <= self._memory.version <= 5 or self._memory.version == 8: self._memory.write_word(addr + 6, new_parent_num) else: raise ZObjectIllegalVersion def set_child(self, objectnum, new_child_num): """Make OBJECTNUM's child pointer point to NEW_PARENT_NUM.""" addr = self._get_object_addr(objectnum) if 1 <= self._memory.version <= 3: self._memory[addr + 6] = new_child_num elif 4 <= self._memory.version <= 5 or self._memory.version == 8: self._memory.write_word(addr + 10, new_child_num) else: raise ZObjectIllegalVersion def set_sibling(self, objectnum, new_sibling_num): """Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM.""" addr = self._get_object_addr(objectnum) if 1 <= self._memory.version <= 3: self._memory[addr + 5] = new_sibling_num elif 4 <= self._memory.version <= 5 or self._memory.version == 8: self._memory.write_word(addr + 8, new_sibling_num) else: raise ZObjectIllegalVersion def remove_object(self, objectnum): """Detach object OBJECTNUM from its parent (unlink from sibling chain).""" if objectnum == 0: return parent = self.get_parent(objectnum) if parent == 0: # Object has no parent, nothing to remove return sibling = self.get_sibling(objectnum) # Check if this object is the first child if self.get_child(parent) == objectnum: # Make sibling the new first child self.set_child(parent, sibling) else: # Walk the sibling chain to find the object before this one prev = self.get_child(parent) current = self.get_sibling(prev) while current != 0: if current == objectnum: # Link prev to our sibling, removing us from chain self.set_sibling(prev, sibling) break prev = current current = self.get_sibling(current) else: # Shouldn't happen - object claimed parent but not in chain raise ZObjectMalformedTree # Clear this object's parent self.set_parent(objectnum, 0) self.set_sibling(objectnum, 0) def insert_object(self, parent_object, new_child): """Prepend object NEW_CHILD to the list of PARENT_OBJECT's children. Per the Z-spec: if new_child already has a parent, it is first removed from that parent's child list, then made the first child of parent_object.""" if parent_object == 0 or new_child == 0: return # Remove from old parent first (spec says "first removed") self.remove_object(new_child) # Now insert as first child of parent_object original_child = self.get_child(parent_object) self.set_sibling(new_child, original_child) self.set_parent(new_child, parent_object) self.set_child(parent_object, new_child) def get_shortname(self, objectnum): """Return 'short name' of object number OBJECTNUM as ascii string.""" if objectnum == 0: return "" addr = self._get_proptable_addr(objectnum) return self._stringfactory.get(addr + 1) def get_prop(self, objectnum, propnum): """Return either a byte or word value of property PROPNUM of object OBJECTNUM.""" if objectnum == 0: return 0 (addr, size) = self.get_prop_addr_len(objectnum, propnum) if size == 1: return self._memory[addr] elif size == 2: return self._memory.read_word(addr) else: raise ZObjectIllegalPropLength def get_prop_addr_len(self, objectnum, propnum): """Return address & length of value for property number PROPNUM of object number OBJECTNUM. If object has no such property, then return the address & length of the 'default' value for the property.""" # start at the beginning of the object's proptable addr = self._get_proptable_addr(objectnum) # skip past the shortname of the object addr += 1 + 2 * self._memory[addr] pnum = 0 if 1 <= self._memory.version <= 3: while self._memory[addr] != 0: bf = BitField(self._memory[addr]) addr += 1 pnum = bf[0:5] size = bf[5:8] + 1 if pnum == propnum: return (addr, size) addr += size elif 4 <= self._memory.version <= 5 or self._memory.version == 8: while self._memory[addr] != 0: bf = BitField(self._memory[addr]) addr += 1 pnum = bf[0:6] if bf[7]: bf2 = BitField(self._memory[addr]) addr += 1 size = bf2[0:6] else: size = 2 if bf[6] else 1 if pnum == propnum: return (addr, size) addr += size else: raise ZObjectIllegalVersion # property list ran out, so return default propval instead. default_value_addr = self._get_default_property_addr(propnum) return (default_value_addr, 2) def get_all_properties(self, objectnum): """Return a dictionary of all properties listed in the property table of object OBJECTNUM. (Obviously, this discounts 'default' property values.). The dictionary maps property numbers to (addr, len) propval tuples.""" proplist = {} # start at the beginning of the object's proptable addr = self._get_proptable_addr(objectnum) # skip past the shortname of the object shortname_length = self._memory[addr] addr += 1 addr += 2 * shortname_length if 1 <= self._memory.version <= 3: while self._memory[addr] != 0: bf = BitField(self._memory[addr]) addr += 1 pnum = bf[0:5] size = bf[5:8] + 1 proplist[pnum] = (addr, size) addr += size elif 4 <= self._memory.version <= 5 or self._memory.version == 8: while self._memory[addr] != 0: bf = BitField(self._memory[addr]) addr += 1 pnum = bf[0:6] if bf[7]: bf2 = BitField(self._memory[addr]) addr += 1 size = bf2[0:6] if size == 0: size = 64 else: size = 2 if bf[6] else 1 proplist[pnum] = (addr, size) addr += size else: raise ZObjectIllegalVersion return proplist def set_property(self, objectnum, propnum, value): """Set a property on an object.""" proplist = self.get_all_properties(objectnum) if propnum not in proplist: raise ZObjectIllegalPropertyNumber addr, size = proplist[propnum] if size == 1: self._memory[addr] = value & 0xFF elif size == 2: self._memory.write_word(addr, value) else: raise ZObjectIllegalPropertySet def get_property_data_address(self, objectnum, propnum): """Return the address of property PROPNUM's data bytes for object OBJECTNUM. Return 0 if the object doesn't have that property.""" if objectnum == 0: return 0 try: addr, size = self.get_prop_addr_len(objectnum, propnum) # get_prop_addr_len returns default property addr if not found # We need to check if this is the actual property or default proplist = self.get_all_properties(objectnum) if propnum in proplist: return addr else: return 0 except ZObjectIllegalPropLength: return 0 def get_next_property(self, objectnum, propnum): """If PROPNUM is 0, return the first property number of object OBJECTNUM. Otherwise, return the property number after PROPNUM in the property list. Return 0 if there are no more properties.""" if objectnum == 0: return 0 if propnum == 0: # Return first property number addr = self._get_proptable_addr(objectnum) # Skip past the shortname addr += 1 + 2 * self._memory[addr] # Read first property number if self._memory[addr] == 0: return 0 if 1 <= self._memory.version <= 3: bf = BitField(self._memory[addr]) return bf[0:5] elif 4 <= self._memory.version <= 5 or self._memory.version == 8: bf = BitField(self._memory[addr]) return bf[0:6] else: raise ZObjectIllegalVersion else: # Find the property after propnum proplist = self.get_all_properties(objectnum) if propnum not in proplist: raise ZObjectIllegalPropertyNumber # Properties are stored in descending order # Find the next lower property number sorted_props = sorted(proplist.keys(), reverse=True) try: idx = sorted_props.index(propnum) if idx + 1 < len(sorted_props): return sorted_props[idx + 1] else: return 0 except ValueError: return 0 def get_property_length(self, data_address): """Given a property DATA address, return the length of that property's data. Return 0 if data_address is 0.""" if data_address == 0: return 0 # The size byte is just before the data address size_addr = data_address - 1 if 1 <= self._memory.version <= 3: bf = BitField(self._memory[size_addr]) size = bf[5:8] + 1 return size elif 4 <= self._memory.version <= 5 or self._memory.version == 8: bf = BitField(self._memory[size_addr]) if bf[7]: # Two-byte header: size is in bits 0-5 of this byte size = bf[0:6] if size == 0: size = 64 return size else: # One size byte return 2 if bf[6] else 1 else: raise ZObjectIllegalVersion def describe_object(self, objectnum): """For debugging purposes, pretty-print everything known about object OBJECTNUM.""" print("Object number:", objectnum) print(" Short name:", self.get_shortname(objectnum)) print(" Parent:", self.get_parent(objectnum), end=" ") print(" Sibling:", self.get_sibling(objectnum), end=" ") print(" Child:", self.get_child(objectnum)) print(" Attributes:", self.get_all_attributes(objectnum)) print(" Properties:") proplist = self.get_all_properties(objectnum) for key in list(proplist.keys()): (addr, len) = proplist[key] print(f" [{key:2d}] :", end=" ") for i in range(0, len): print(f"{self._memory[addr + i]:02X}", end=" ") print()