+++ /dev/null
-#!/usr/bin/env python3
-# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
-
-"""
-ObjectRegistry
-
-Maps Python runtime objects to ProcessLocalId.
-
-Strategies:
- 1. Weak Identity (Preferred): For objects that support weakrefs (instances ,classes).
- - ID lifetime is bound to object lifetime.
- - Auto-cleaned on GC.
-
- 2. Value Identity (Fallback): For immutable primitives (int ,str ,tuple).
- - ID is bound to the *value* (hash/equality).
- - Stored strongly (since values like 42 or "red" are conceptually eternal).
-"""
-
-from __future__ import annotations
-
-import weakref
-from typing import Any ,Callable ,Dict ,Optional
-
-from .ProcessLocalId import ProcessLocalIdGenerator ,ProcessLocalId
-
-
-class ObjectRegistry:
- def __init__(self ,id_gen: ProcessLocalIdGenerator):
- self._id_gen = id_gen
-
- # Strategy 1: Entities (Weakref-able)
- self._obj_to_id_wkd: "weakref.WeakKeyDictionary[Any ,ProcessLocalId]" = weakref.WeakKeyDictionary()
- self._id_to_obj_ref: Dict[ProcessLocalId ,weakref.ref] = {}
-
- # Strategy 2: Values (Hashable ,not weakref-able)
- self._value_to_id: Dict[Any ,ProcessLocalId] = {}
-
- self._finalizers: Dict[ProcessLocalId ,Callable[[ProcessLocalId] ,None]] = {}
- self._global_finalizer: Optional[Callable[[ProcessLocalId] ,None]] = None
-
- def register_finalizer(self ,fn: Callable[[ProcessLocalId] ,None]):
- """
- Registers a finalizer callback invoked when any registered *weak-refable* object is GC'd.
- """
- self._global_finalizer = fn
-
- def _on_collect(self ,obj_id: ProcessLocalId):
- # Only called for Strategy 1 objects
- ref = self._id_to_obj_ref.pop(obj_id ,None)
- if(ref is not None):
- # The WeakKeyDictionary auto-cleans the forward mapping ,
- # but we double check or clean any edge cases if needed.
- pass
-
- fn = self._global_finalizer
- if(fn is not None):
- fn(obj_id)
-
- def get_id(self ,obj: Any) -> ProcessLocalId:
- """
- Returns the ProcessLocalId for `obj` ,registering it if needed.
- """
- # 1. Try WeakRef Strategy (Entities)
- try:
- existing = self._obj_to_id_wkd.get(obj)
- if(existing is not None):
- return existing
- except TypeError:
- # obj is not weakref-able (e.g. int ,str ,tuple ,or list).
- # Fall through to Strategy 2.
- pass
- else:
- # It IS weakref-able ,but wasn't in the dictionary. Register it.
- obj_id = self._id_gen.next_id()
- self._obj_to_id_wkd[obj] = obj_id
- # Create reverse lookup with callback
- self._id_to_obj_ref[obj_id] = weakref.ref(obj ,lambda _ref ,oid=obj_id: self._on_collect(oid))
- return obj_id
-
- # 2. Try Value Strategy (Primitives)
- # Note: Mutable non-weakrefables (like standard lists) will fail here because they are unhashable.
- try:
- existing = self._value_to_id.get(obj)
- if(existing is not None):
- return existing
-
- # Register new value
- obj_id = self._id_gen.next_id()
- self._value_to_id[obj] = obj_id
- return obj_id
- except TypeError:
- # It is neither weakref-able NOR hashable (e.g. standard list ,dict).
- raise TypeError(
- f"ObjectRegistry: cannot track object of type {type(obj)!r}. "
- "It is neither weakref-able (Entity) nor hashable (Value)."
- )
-
- def try_get_object(self ,obj_id: ProcessLocalId) -> Optional[Any]:
- """
- Best-effort: returns the live object/value.
- """
- # Check Entities
- ref = self._id_to_obj_ref.get(obj_id)
- if(ref is not None):
- return ref()
-
- # Check Values
- # For now ,we assume values identify themselves.
- for val ,pid in self._value_to_id.items():
- if(pid == obj_id):
- return val
-
- return None
+++ /dev/null
-#!/usr/bin/env python3
-# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
-
-
-"""
-ProcessLocalId
-
-A process-local identifier used as an internal key.
-
-Design constraint:
- - NOT intended to be serialized or persisted.
- - `repr()` intentionally does not reveal the numeric token, to discourage logging/persistence.
-"""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-
-
-@dataclass(frozen=True ,slots=True)
-class ProcessLocalId:
- _n: int
-
- def __repr__(self) -> str:
- return "<ProcessLocalId>"
-
- def __str__(self) -> str:
- return "<ProcessLocalId>"
-
- def as_int_UNSAFE(self) -> int:
- """
- Returns the raw integer token.
-
- UNSAFE because:
- - tokens are process-local
- - do not write these into files/databases/logs as stable identifiers
- """
- return self._n
-
-
-class ProcessLocalIdGenerator:
- """
- Monotonic generator; ids are never recycled.
- """
- def __init__(self ,start: int = 1):
- if start < 1: raise ValueError("start must be >= 1")
- self._next_n: int = start
-
- def next_id(self) -> ProcessLocalId:
- n = self._next_n
- self._next_n += 1
- return ProcessLocalId(n)
+++ /dev/null
-#!/usr/bin/env python3
-# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
-
-
-"""
-Property
-
-A Property is itself an entity (it has an Identity id) so that:
- - properties can have properties
- - properties can be members of semantic sets
-"""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-from typing import Optional ,Tuple
-
-from .ProcessLocalId import ProcessLocalId
-
-
-@dataclass(frozen=True ,slots=True)
-class Property:
- id: ProcessLocalId
- name_path: Tuple[str ,...]
- doc: str = ""
-
- def __repr__(self) -> str:
- # name_path is safe to reveal; id token is not.
- return f"<Property {'.'.join(self.name_path)!r}>"
+++ /dev/null
-#!/usr/bin/env python3
-# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
-
-
-"""
-PropertyManager
-
-Core RT property system.
-
-Key decisions vs the earlier `property_manager.py`:
- - do NOT key by `object_path()` strings (avoids collisions) fileciteturn3file4
- - runtime objects are keyed by weak identity (ProcessLocalId assigned by ObjectRegistry)
- - properties are first-class entities (Property has an id), so properties can have properties
-
-This remains process-local and in-memory.
-"""
-
-from __future__ import annotations
-
-from typing import Any ,Dict ,Iterable ,List ,Optional ,Tuple ,Union
-
-from .ProcessLocalId import ProcessLocalIdGenerator ,ProcessLocalId
-from .ObjectRegistry import ObjectRegistry
-from .PropertyStore import PropertyStore
-from .Property import Property
-from .SemanticSets import SemanticSet ,SemanticSetStore
-from .Syntax import SyntaxInstance
-
-
-NamePathLike = Union[str ,List[str] ,Tuple[str ,...]]
-
-
-class PropertyManager:
- def __init__(self):
- self._id_gen = ProcessLocalIdGenerator()
- self._obj_reg = ObjectRegistry(self._id_gen)
- self._store = PropertyStore()
- self._sets = SemanticSetStore()
-
- # Declare-by-name registry
- self._name_path_to_property: Dict[Tuple[str ,...],Property] = {}
- self._property_id_to_property: Dict[ProcessLocalId ,Property] = {}
-
- self._name_path_to_set: Dict[Tuple[str ,...],SemanticSet] = {}
- self._set_id_to_set: Dict[ProcessLocalId ,SemanticSet] = {}
-
- # Optional syntax instances (if user chooses to model them)
- self._syntax_id_to_instance: Dict[ProcessLocalId ,SyntaxInstance] = {}
-
- # Finalization cleanup
- self._obj_reg.register_finalizer(self._on_subject_finalized)
-
- def _on_subject_finalized(self ,subject_id: ProcessLocalId):
- self._store.remove_subject(subject_id)
- self._sets.remove_subject(subject_id)
-
- def _normalize_name_path(self ,name_path: NamePathLike) -> Tuple[str ,...]:
- if isinstance(name_path ,tuple): return name_path
- if isinstance(name_path ,list): return tuple(name_path)
- if isinstance(name_path ,str): return tuple(name_path.split("."))
- raise TypeError("name_path must be str ,list[str] ,or tuple[str ,...]")
-
- # -------------------------
- # Identity acquisition
- # -------------------------
- def id_of_py_object(self ,obj: Any) -> ProcessLocalId:
- return self._obj_reg.get_id(obj)
-
- def create_syntax_identity(self ,syntax: SyntaxInstance) -> ProcessLocalId:
- sid = self._id_gen.next_id()
- self._syntax_id_to_instance[sid] = syntax
- return sid
-
- def try_get_syntax(self ,syntax_id: ProcessLocalId) -> Optional[SyntaxInstance]:
- return self._syntax_id_to_instance.get(syntax_id)
-
- # -------------------------
- # Property declaration
- # -------------------------
- def declare_property(self ,name_path: NamePathLike ,doc: str = "") -> ProcessLocalId:
- np = self._normalize_name_path(name_path)
- existing = self._name_path_to_property.get(np)
- if existing is not None: return existing.id
- pid = self._id_gen.next_id()
- p = Property(pid ,np ,doc)
- self._name_path_to_property[np] = p
- self._property_id_to_property[pid] = p
- return pid
-
- def property_id(self ,name_path: NamePathLike) -> ProcessLocalId:
- np = self._normalize_name_path(name_path)
- p = self._name_path_to_property.get(np)
- if p is None: raise KeyError(f"Property not declared: {np!r}")
- return p.id
-
- def try_get_property(self ,prop_id: ProcessLocalId) -> Optional[Property]:
- return self._property_id_to_property.get(prop_id)
-
- # -------------------------
- # Semantic sets
- # -------------------------
- def declare_set(self ,name_path: NamePathLike ,doc: str = "") -> ProcessLocalId:
- np = self._normalize_name_path(name_path)
- existing = self._name_path_to_set.get(np)
- if existing is not None: return existing.id
- sid = self._id_gen.next_id()
- s = SemanticSet(sid ,np ,doc)
- self._name_path_to_set[np] = s
- self._set_id_to_set[sid] = s
- return sid
-
- def add_to_set(self ,subject: Any ,set_id: ProcessLocalId):
- subject_id = self._coerce_subject_id(subject)
- self._sets.add_member(set_id ,subject_id)
-
- def is_in_set(self ,subject: Any ,set_id: ProcessLocalId) -> bool:
- subject_id = self._coerce_subject_id(subject)
- return self._sets.has_member(set_id ,subject_id)
-
- def members(self ,set_id: ProcessLocalId) -> List[ProcessLocalId]:
- return list(self._sets.members(set_id))
-
- # -------------------------
- # Set/get properties
- # -------------------------
- def set(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike] ,value: Any):
- subject_id = self._coerce_subject_id(subject)
- prop_id = self._coerce_property_id(prop)
- self._store.set(subject_id ,prop_id ,value)
-
- def get(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike] ,default: Any = None) -> Any:
- subject_id = self._coerce_subject_id(subject)
- prop_id = self._coerce_property_id(prop)
- return self._store.get(subject_id ,prop_id ,default)
-
- def has(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike]) -> bool:
- subject_id = self._coerce_subject_id(subject)
- prop_id = self._coerce_property_id(prop)
- return self._store.has(subject_id ,prop_id)
-
- def subjects_with(self ,prop: Union[ProcessLocalId ,NamePathLike]) -> List[ProcessLocalId]:
- prop_id = self._coerce_property_id(prop)
- return list(self._store.subjects_with(prop_id))
-
- # -------------------------
- # Coercions
- # -------------------------
- def _coerce_subject_id(self ,subject: Any) -> ProcessLocalId:
- if isinstance(subject ,ProcessLocalId): return subject
- # For Python runtime objects, we require weakref-able instances.
- return self._obj_reg.get_id(subject)
-
- def _coerce_property_id(self ,prop: Union[ProcessLocalId ,NamePathLike]) -> ProcessLocalId:
- if isinstance(prop ,ProcessLocalId): return prop
- return self.property_id(prop)
+++ /dev/null
-#!/usr/bin/env python3
-# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
-
-
-"""
-PropertyStore
-
-Stores property values and maintains reverse lookups.
-
-This is intentionally process-local and in-memory.
-"""
-
-from __future__ import annotations
-
-from typing import Any ,Dict ,Optional ,Set ,Tuple
-
-from .ProcessLocalId import ProcessLocalId
-
-
-class PropertyStore:
- def __init__(self):
- # (subject_id ,property_id) -> value
- self._values: Dict[Tuple[ProcessLocalId ,ProcessLocalId] ,Any] = {}
-
- # subject_id -> set(property_id)
- self._subject_to_props: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {}
-
- # property_id -> set(subject_id)
- self._prop_to_subjects: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {}
-
- def set(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId ,value: Any):
- key = (subject_id ,prop_id)
- self._values[key] = value
- self._subject_to_props.setdefault(subject_id ,set()).add(prop_id)
- self._prop_to_subjects.setdefault(prop_id ,set()).add(subject_id)
-
- def get(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId ,default: Any = None) -> Any:
- return self._values.get((subject_id ,prop_id) ,default)
-
- def has(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId) -> bool:
- return (subject_id ,prop_id) in self._values
-
- def subjects_with(self ,prop_id: ProcessLocalId) -> Set[ProcessLocalId]:
- return set(self._prop_to_subjects.get(prop_id ,set()))
-
- def props_of(self ,subject_id: ProcessLocalId) -> Set[ProcessLocalId]:
- return set(self._subject_to_props.get(subject_id ,set()))
-
- def remove_subject(self ,subject_id: ProcessLocalId):
- """
- Remove all stored properties for a subject (used on finalization).
- """
- prop_ids = self._subject_to_props.pop(subject_id ,set())
- for prop_id in prop_ids:
- self._values.pop((subject_id ,prop_id) ,None)
- s = self._prop_to_subjects.get(prop_id)
- if s is not None:
- s.discard(subject_id)
- if not s: self._prop_to_subjects.pop(prop_id ,None)
+++ /dev/null
-#!/usr/bin/env python3
-# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
-
-
-"""
-SemanticSets
-
-Membership sets over identities. Used for semantic typing.
-
-Design:
- - set_id identifies the set
- - members are subject ids
- - reverse index for cleanup
-"""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-from typing import Dict ,Optional ,Set
-
-from .ProcessLocalId import ProcessLocalId
-
-
-@dataclass(frozen=True ,slots=True)
-class SemanticSet:
- id: ProcessLocalId
- name_path: tuple[str ,...]
- doc: str = ""
-
- def __repr__(self) -> str:
- return f"<SemanticSet {'.'.join(self.name_path)!r}>"
-
-
-class SemanticSetStore:
- def __init__(self):
- self._members: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {}
- self._subject_to_sets: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {}
-
- def add_member(self ,set_id: ProcessLocalId ,subject_id: ProcessLocalId):
- self._members.setdefault(set_id ,set()).add(subject_id)
- self._subject_to_sets.setdefault(subject_id ,set()).add(set_id)
-
- def has_member(self ,set_id: ProcessLocalId ,subject_id: ProcessLocalId) -> bool:
- return subject_id in self._members.get(set_id ,set())
-
- def members(self ,set_id: ProcessLocalId) -> Set[ProcessLocalId]:
- return set(self._members.get(set_id ,set()))
-
- def remove_subject(self ,subject_id: ProcessLocalId):
- set_ids = self._subject_to_sets.pop(subject_id ,set())
- for set_id in set_ids:
- m = self._members.get(set_id)
- if m is not None:
- m.discard(subject_id)
- if not m: self._members.pop(set_id ,None)
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
+
+"""
+Symbol
+
+The Definition of Identity.
+
+Architecture:
+ - Symbol: The namespace and manager (Factory).
+ - Symbol.Instance: The atomic unit of identity (The Product).
+
+Constraints:
+ - Distinctness: Instances are unique.
+ - Opaque: No internal state leakage.
+ - Immutable Metadata: Name and Doc can be set exactly once.
+ - Static Manager: The Symbol class cannot be instantiated.
+"""
+
+from __future__ import annotations
+
+import weakref
+from typing import Optional
+
+
+class Symbol:
+ """
+ The Manager class.
+ Maintains the global state for symbol metadata and token generation.
+ """
+
+ def __new__(cls):
+ """
+ Prevent instantiation. Symbol is a static namespace/factory.
+ """
+ raise TypeError("The Symbol class is a static namespace and cannot be instantiated.")
+
+ # Global registry for metadata.
+ # We use class-level storage so Instances remain lightweight (no back-ref needed).
+ _names: weakref.WeakKeyDictionary[Symbol.Instance ,str] = weakref.WeakKeyDictionary()
+ _docs: weakref.WeakKeyDictionary[Symbol.Instance ,str] = weakref.WeakKeyDictionary()
+
+ # Inclusive bounding: Start at 0 (The Null Token)
+ _current_token: int = 0
+
+ class Instance:
+ """
+ The atomic unit of identity.
+ """
+ # __weakref__ required for WeakKeyDictionary keys
+ __slots__ = (
+ '_token'
+ ,'__weakref__'
+ )
+
+ def __init__(self ,token: int):
+ self._token = token
+
+ def __repr__(self) -> str:
+ return "<Symbol.Instance>"
+
+ def __eq__(self ,other: object) -> bool:
+ if( isinstance(other ,Symbol.Instance) ):
+ return self._token == other._token
+ return NotImplemented
+
+ def __hash__(self) -> int:
+ return hash(self._token)
+
+ # ----------------------------------------------------------------------
+ # Metadata Accessors
+ # ----------------------------------------------------------------------
+
+ @property
+ def name(self) -> Optional[str]:
+ """
+ Returns the name of the symbol, or None if anonymous.
+ """
+ return Symbol._names.get(self)
+
+ @name.setter
+ def name(self ,value: str):
+ """
+ Sets the name.
+ Raises RuntimeError if the name has already been set.
+ """
+ if( self in Symbol._names ):
+ raise RuntimeError(f"Symbol name is immutable. Already set to '{Symbol._names[self]}'.")
+ Symbol._names[self] = value
+
+ @property
+ def doc(self) -> str:
+ """
+ Returns the docstring of the symbol, or "" if none.
+ """
+ return Symbol._docs.get(self ,"")
+
+ @doc.setter
+ def doc(self ,value: str):
+ """
+ Sets the docstring.
+ Raises RuntimeError if the docstring has already been set.
+ Ignores empty strings (setting to "" is a no-op).
+ """
+ if( not value ): return
+
+ if( self in Symbol._docs ):
+ raise RuntimeError("Symbol docstring is immutable. Already set.")
+
+ Symbol._docs[self] = value
+
+ # ------------------------------------------------------------------------
+ # Factory Methods
+ # ------------------------------------------------------------------------
+
+ @classmethod
+ def make(cls ,name: Optional[str] = None ,doc: str = "") -> Instance:
+ """
+ Mints a new, distinct Original Symbol Instance.
+
+ Args:
+ name: Optional name. If provided, it becomes immutable.
+ doc: Optional docstring. If provided (non-empty), it becomes immutable.
+ """
+ # The Rest: Increment first, so the first public symbol is 1.
+ cls._current_token += 1
+ token = cls._current_token
+
+ instance = cls.Instance(token)
+
+ if( name is not None ):
+ # Direct injection to bypass the "check if set" logic of the setter
+ cls._names[instance] = name
+
+ if( doc ):
+ cls._docs[instance] = doc
+
+ return instance
+
+ # ------------------------------------------------------------------------
+ # Static Initialization (The First)
+ # ------------------------------------------------------------------------
+ # We perform this inside the class body using local names.
+ # This creates the Null Symbol immediately upon class definition.
+
+ # Note: We must use 'Instance' (local scope) not 'Symbol.Instance'
+ # because 'Symbol' is not yet bound.
+ null = Instance(0)
+ _names[null] = "Null"
+ _docs[null] = "The Null Symbol"
+
+
+# LocalWords: Accessors
+++ /dev/null
-#!/usr/bin/env python3
-# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
-
-
-"""
-Syntax
-
-RT syntax identity instances.
-
-We treat "syntax" as AST-level objects:
- - kind: official-ish AST node kind name (e.g., "ast.FunctionDef")
- - location: file + span
- - scope: enclosing syntax identity id (optional)
- - parts: mapping of part-name to literal or referenced syntax identity id(s)
-
-This module does NOT traverse Python programs. It only defines the data model.
-"""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-from typing import Any ,Dict ,Optional ,Tuple ,Union ,List
-
-from .ProcessLocalId import ProcessLocalId
-
-
-@dataclass(frozen=True ,slots=True)
-class SourceSpan:
- file_path: str
- lineno: int
- col: int
- end_lineno: int
- end_col: int
-
-
-SyntaxPartValue = Union[
- None
- ,bool
- ,int
- ,float
- ,str
- ,ProcessLocalId
- ,List["SyntaxPartValue"]
- ,Dict[str ,"SyntaxPartValue"]
-]
-
-
-@dataclass(frozen=True ,slots=True)
-class SyntaxInstance:
- """
- A single syntax node instance.
-
- NOTE: many syntax nodes have no identifier-name. Name-like things (identifiers)
- appear as child nodes or literals inside `parts`.
- """
- kind: str
- span: SourceSpan
- scope_id: Optional[ProcessLocalId] = None
- parts: Dict[str ,SyntaxPartValue] = None
-from .PropertyManager import PropertyManager
+from .Epimetheus import Epimetheus
+
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
+
+"""
+ObjectRegistry
+
+Maps Python runtime objects to ProcessLocalId.
+
+Strategies:
+ 1. Weak Identity (Preferred): For objects that support weakrefs (instances ,classes).
+ - ID lifetime is bound to object lifetime.
+ - Auto-cleaned on GC.
+
+ 2. Value Identity (Fallback): For immutable primitives (int ,str ,tuple).
+ - ID is bound to the *value* (hash/equality).
+ - Stored strongly (since values like 42 or "red" are conceptually eternal).
+"""
+
+from __future__ import annotations
+
+import weakref
+from typing import Any ,Callable ,Dict ,Optional
+
+from .ProcessLocalId import ProcessLocalIdGenerator ,ProcessLocalId
+
+
+class ObjectRegistry:
+ def __init__(self ,id_gen: ProcessLocalIdGenerator):
+ self._id_gen = id_gen
+
+ # Strategy 1: Entities (Weakref-able)
+ self._obj_to_id_wkd: "weakref.WeakKeyDictionary[Any ,ProcessLocalId]" = weakref.WeakKeyDictionary()
+ self._id_to_obj_ref: Dict[ProcessLocalId ,weakref.ref] = {}
+
+ # Strategy 2: Values (Hashable ,not weakref-able)
+ self._value_to_id: Dict[Any ,ProcessLocalId] = {}
+
+ self._finalizers: Dict[ProcessLocalId ,Callable[[ProcessLocalId] ,None]] = {}
+ self._global_finalizer: Optional[Callable[[ProcessLocalId] ,None]] = None
+
+ def register_finalizer(self ,fn: Callable[[ProcessLocalId] ,None]):
+ """
+ Registers a finalizer callback invoked when any registered *weak-refable* object is GC'd.
+ """
+ self._global_finalizer = fn
+
+ def _on_collect(self ,obj_id: ProcessLocalId):
+ # Only called for Strategy 1 objects
+ ref = self._id_to_obj_ref.pop(obj_id ,None)
+ if(ref is not None):
+ # The WeakKeyDictionary auto-cleans the forward mapping ,
+ # but we double check or clean any edge cases if needed.
+ pass
+
+ fn = self._global_finalizer
+ if(fn is not None):
+ fn(obj_id)
+
+ def get_id(self ,obj: Any) -> ProcessLocalId:
+ """
+ Returns the ProcessLocalId for `obj` ,registering it if needed.
+ """
+ # 1. Try WeakRef Strategy (Entities)
+ try:
+ existing = self._obj_to_id_wkd.get(obj)
+ if(existing is not None):
+ return existing
+ except TypeError:
+ # obj is not weakref-able (e.g. int ,str ,tuple ,or list).
+ # Fall through to Strategy 2.
+ pass
+ else:
+ # It IS weakref-able ,but wasn't in the dictionary. Register it.
+ obj_id = self._id_gen.next_id()
+ self._obj_to_id_wkd[obj] = obj_id
+ # Create reverse lookup with callback
+ self._id_to_obj_ref[obj_id] = weakref.ref(obj ,lambda _ref ,oid=obj_id: self._on_collect(oid))
+ return obj_id
+
+ # 2. Try Value Strategy (Primitives)
+ # Note: Mutable non-weakrefables (like standard lists) will fail here because they are unhashable.
+ try:
+ existing = self._value_to_id.get(obj)
+ if(existing is not None):
+ return existing
+
+ # Register new value
+ obj_id = self._id_gen.next_id()
+ self._value_to_id[obj] = obj_id
+ return obj_id
+ except TypeError:
+ # It is neither weakref-able NOR hashable (e.g. standard list ,dict).
+ raise TypeError(
+ f"ObjectRegistry: cannot track object of type {type(obj)!r}. "
+ "It is neither weakref-able (Entity) nor hashable (Value)."
+ )
+
+ def try_get_object(self ,obj_id: ProcessLocalId) -> Optional[Any]:
+ """
+ Best-effort: returns the live object/value.
+ """
+ # Check Entities
+ ref = self._id_to_obj_ref.get(obj_id)
+ if(ref is not None):
+ return ref()
+
+ # Check Values
+ # For now ,we assume values identify themselves.
+ for val ,pid in self._value_to_id.items():
+ if(pid == obj_id):
+ return val
+
+ return None
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
+
+
+"""
+ProcessLocalId
+
+A process-local identifier used as an internal key.
+
+Design constraint:
+ - NOT intended to be serialized or persisted.
+ - `repr()` intentionally does not reveal the numeric token, to discourage logging/persistence.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+
+@dataclass(frozen=True ,slots=True)
+class ProcessLocalId:
+ _n: int
+
+ def __repr__(self) -> str:
+ return "<ProcessLocalId>"
+
+ def __str__(self) -> str:
+ return "<ProcessLocalId>"
+
+ def as_int_UNSAFE(self) -> int:
+ """
+ Returns the raw integer token.
+
+ UNSAFE because:
+ - tokens are process-local
+ - do not write these into files/databases/logs as stable identifiers
+ """
+ return self._n
+
+
+class ProcessLocalIdGenerator:
+ """
+ Monotonic generator; ids are never recycled.
+ """
+ def __init__(self ,start: int = 1):
+ if start < 1: raise ValueError("start must be >= 1")
+ self._next_n: int = start
+
+ def next_id(self) -> ProcessLocalId:
+ n = self._next_n
+ self._next_n += 1
+ return ProcessLocalId(n)
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
+
+
+"""
+Property
+
+A Property is itself an entity (it has an Identity id) so that:
+ - properties can have properties
+ - properties can be members of semantic sets
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional ,Tuple
+
+from .ProcessLocalId import ProcessLocalId
+
+
+@dataclass(frozen=True ,slots=True)
+class Property:
+ id: ProcessLocalId
+ name_path: Tuple[str ,...]
+ doc: str = ""
+
+ def __repr__(self) -> str:
+ # name_path is safe to reveal; id token is not.
+ return f"<Property {'.'.join(self.name_path)!r}>"
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
+
+
+"""
+PropertyManager
+
+Core RT property system.
+
+Key decisions vs the earlier `property_manager.py`:
+ - do NOT key by `object_path()` strings (avoids collisions) fileciteturn3file4
+ - runtime objects are keyed by weak identity (ProcessLocalId assigned by ObjectRegistry)
+ - properties are first-class entities (Property has an id), so properties can have properties
+
+This remains process-local and in-memory.
+"""
+
+from __future__ import annotations
+
+from typing import Any ,Dict ,Iterable ,List ,Optional ,Tuple ,Union
+
+from .ProcessLocalId import ProcessLocalIdGenerator ,ProcessLocalId
+from .ObjectRegistry import ObjectRegistry
+from .PropertyStore import PropertyStore
+from .Property import Property
+from .SemanticSets import SemanticSet ,SemanticSetStore
+from .Syntax import SyntaxInstance
+
+
+NamePathLike = Union[str ,List[str] ,Tuple[str ,...]]
+
+
+class PropertyManager:
+ def __init__(self):
+ self._id_gen = ProcessLocalIdGenerator()
+ self._obj_reg = ObjectRegistry(self._id_gen)
+ self._store = PropertyStore()
+ self._sets = SemanticSetStore()
+
+ # Declare-by-name registry
+ self._name_path_to_property: Dict[Tuple[str ,...],Property] = {}
+ self._property_id_to_property: Dict[ProcessLocalId ,Property] = {}
+
+ self._name_path_to_set: Dict[Tuple[str ,...],SemanticSet] = {}
+ self._set_id_to_set: Dict[ProcessLocalId ,SemanticSet] = {}
+
+ # Optional syntax instances (if user chooses to model them)
+ self._syntax_id_to_instance: Dict[ProcessLocalId ,SyntaxInstance] = {}
+
+ # Finalization cleanup
+ self._obj_reg.register_finalizer(self._on_subject_finalized)
+
+ def _on_subject_finalized(self ,subject_id: ProcessLocalId):
+ self._store.remove_subject(subject_id)
+ self._sets.remove_subject(subject_id)
+
+ def _normalize_name_path(self ,name_path: NamePathLike) -> Tuple[str ,...]:
+ if isinstance(name_path ,tuple): return name_path
+ if isinstance(name_path ,list): return tuple(name_path)
+ if isinstance(name_path ,str): return tuple(name_path.split("."))
+ raise TypeError("name_path must be str ,list[str] ,or tuple[str ,...]")
+
+ # -------------------------
+ # Identity acquisition
+ # -------------------------
+ def id_of_py_object(self ,obj: Any) -> ProcessLocalId:
+ return self._obj_reg.get_id(obj)
+
+ def create_syntax_identity(self ,syntax: SyntaxInstance) -> ProcessLocalId:
+ sid = self._id_gen.next_id()
+ self._syntax_id_to_instance[sid] = syntax
+ return sid
+
+ def try_get_syntax(self ,syntax_id: ProcessLocalId) -> Optional[SyntaxInstance]:
+ return self._syntax_id_to_instance.get(syntax_id)
+
+ # -------------------------
+ # Property declaration
+ # -------------------------
+ def declare_property(self ,name_path: NamePathLike ,doc: str = "") -> ProcessLocalId:
+ np = self._normalize_name_path(name_path)
+ existing = self._name_path_to_property.get(np)
+ if existing is not None: return existing.id
+ pid = self._id_gen.next_id()
+ p = Property(pid ,np ,doc)
+ self._name_path_to_property[np] = p
+ self._property_id_to_property[pid] = p
+ return pid
+
+ def property_id(self ,name_path: NamePathLike) -> ProcessLocalId:
+ np = self._normalize_name_path(name_path)
+ p = self._name_path_to_property.get(np)
+ if p is None: raise KeyError(f"Property not declared: {np!r}")
+ return p.id
+
+ def try_get_property(self ,prop_id: ProcessLocalId) -> Optional[Property]:
+ return self._property_id_to_property.get(prop_id)
+
+ # -------------------------
+ # Semantic sets
+ # -------------------------
+ def declare_set(self ,name_path: NamePathLike ,doc: str = "") -> ProcessLocalId:
+ np = self._normalize_name_path(name_path)
+ existing = self._name_path_to_set.get(np)
+ if existing is not None: return existing.id
+ sid = self._id_gen.next_id()
+ s = SemanticSet(sid ,np ,doc)
+ self._name_path_to_set[np] = s
+ self._set_id_to_set[sid] = s
+ return sid
+
+ def add_to_set(self ,subject: Any ,set_id: ProcessLocalId):
+ subject_id = self._coerce_subject_id(subject)
+ self._sets.add_member(set_id ,subject_id)
+
+ def is_in_set(self ,subject: Any ,set_id: ProcessLocalId) -> bool:
+ subject_id = self._coerce_subject_id(subject)
+ return self._sets.has_member(set_id ,subject_id)
+
+ def members(self ,set_id: ProcessLocalId) -> List[ProcessLocalId]:
+ return list(self._sets.members(set_id))
+
+ # -------------------------
+ # Set/get properties
+ # -------------------------
+ def set(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike] ,value: Any):
+ subject_id = self._coerce_subject_id(subject)
+ prop_id = self._coerce_property_id(prop)
+ self._store.set(subject_id ,prop_id ,value)
+
+ def get(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike] ,default: Any = None) -> Any:
+ subject_id = self._coerce_subject_id(subject)
+ prop_id = self._coerce_property_id(prop)
+ return self._store.get(subject_id ,prop_id ,default)
+
+ def has(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike]) -> bool:
+ subject_id = self._coerce_subject_id(subject)
+ prop_id = self._coerce_property_id(prop)
+ return self._store.has(subject_id ,prop_id)
+
+ def subjects_with(self ,prop: Union[ProcessLocalId ,NamePathLike]) -> List[ProcessLocalId]:
+ prop_id = self._coerce_property_id(prop)
+ return list(self._store.subjects_with(prop_id))
+
+ # -------------------------
+ # Coercions
+ # -------------------------
+ def _coerce_subject_id(self ,subject: Any) -> ProcessLocalId:
+ if isinstance(subject ,ProcessLocalId): return subject
+ # For Python runtime objects, we require weakref-able instances.
+ return self._obj_reg.get_id(subject)
+
+ def _coerce_property_id(self ,prop: Union[ProcessLocalId ,NamePathLike]) -> ProcessLocalId:
+ if isinstance(prop ,ProcessLocalId): return prop
+ return self.property_id(prop)
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
+
+
+"""
+PropertyStore
+
+Stores property values and maintains reverse lookups.
+
+This is intentionally process-local and in-memory.
+"""
+
+from __future__ import annotations
+
+from typing import Any ,Dict ,Optional ,Set ,Tuple
+
+from .ProcessLocalId import ProcessLocalId
+
+
+class PropertyStore:
+ def __init__(self):
+ # (subject_id ,property_id) -> value
+ self._values: Dict[Tuple[ProcessLocalId ,ProcessLocalId] ,Any] = {}
+
+ # subject_id -> set(property_id)
+ self._subject_to_props: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {}
+
+ # property_id -> set(subject_id)
+ self._prop_to_subjects: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {}
+
+ def set(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId ,value: Any):
+ key = (subject_id ,prop_id)
+ self._values[key] = value
+ self._subject_to_props.setdefault(subject_id ,set()).add(prop_id)
+ self._prop_to_subjects.setdefault(prop_id ,set()).add(subject_id)
+
+ def get(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId ,default: Any = None) -> Any:
+ return self._values.get((subject_id ,prop_id) ,default)
+
+ def has(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId) -> bool:
+ return (subject_id ,prop_id) in self._values
+
+ def subjects_with(self ,prop_id: ProcessLocalId) -> Set[ProcessLocalId]:
+ return set(self._prop_to_subjects.get(prop_id ,set()))
+
+ def props_of(self ,subject_id: ProcessLocalId) -> Set[ProcessLocalId]:
+ return set(self._subject_to_props.get(subject_id ,set()))
+
+ def remove_subject(self ,subject_id: ProcessLocalId):
+ """
+ Remove all stored properties for a subject (used on finalization).
+ """
+ prop_ids = self._subject_to_props.pop(subject_id ,set())
+ for prop_id in prop_ids:
+ self._values.pop((subject_id ,prop_id) ,None)
+ s = self._prop_to_subjects.get(prop_id)
+ if s is not None:
+ s.discard(subject_id)
+ if not s: self._prop_to_subjects.pop(prop_id ,None)
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
+
+
+"""
+SemanticSets
+
+Membership sets over identities. Used for semantic typing.
+
+Design:
+ - set_id identifies the set
+ - members are subject ids
+ - reverse index for cleanup
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Dict ,Optional ,Set
+
+from .ProcessLocalId import ProcessLocalId
+
+
+@dataclass(frozen=True ,slots=True)
+class SemanticSet:
+ id: ProcessLocalId
+ name_path: tuple[str ,...]
+ doc: str = ""
+
+ def __repr__(self) -> str:
+ return f"<SemanticSet {'.'.join(self.name_path)!r}>"
+
+
+class SemanticSetStore:
+ def __init__(self):
+ self._members: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {}
+ self._subject_to_sets: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {}
+
+ def add_member(self ,set_id: ProcessLocalId ,subject_id: ProcessLocalId):
+ self._members.setdefault(set_id ,set()).add(subject_id)
+ self._subject_to_sets.setdefault(subject_id ,set()).add(set_id)
+
+ def has_member(self ,set_id: ProcessLocalId ,subject_id: ProcessLocalId) -> bool:
+ return subject_id in self._members.get(set_id ,set())
+
+ def members(self ,set_id: ProcessLocalId) -> Set[ProcessLocalId]:
+ return set(self._members.get(set_id ,set()))
+
+ def remove_subject(self ,subject_id: ProcessLocalId):
+ set_ids = self._subject_to_sets.pop(subject_id ,set())
+ for set_id in set_ids:
+ m = self._members.get(set_id)
+ if m is not None:
+ m.discard(subject_id)
+ if not m: self._members.pop(set_id ,None)
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
+
+
+"""
+Syntax
+
+RT syntax identity instances.
+
+We treat "syntax" as AST-level objects:
+ - kind: official-ish AST node kind name (e.g., "ast.FunctionDef")
+ - location: file + span
+ - scope: enclosing syntax identity id (optional)
+ - parts: mapping of part-name to literal or referenced syntax identity id(s)
+
+This module does NOT traverse Python programs. It only defines the data model.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any ,Dict ,Optional ,Tuple ,Union ,List
+
+from .ProcessLocalId import ProcessLocalId
+
+
+@dataclass(frozen=True ,slots=True)
+class SourceSpan:
+ file_path: str
+ lineno: int
+ col: int
+ end_lineno: int
+ end_col: int
+
+
+SyntaxPartValue = Union[
+ None
+ ,bool
+ ,int
+ ,float
+ ,str
+ ,ProcessLocalId
+ ,List["SyntaxPartValue"]
+ ,Dict[str ,"SyntaxPartValue"]
+]
+
+
+@dataclass(frozen=True ,slots=True)
+class SyntaxInstance:
+ """
+ A single syntax node instance.
+
+ NOTE: many syntax nodes have no identifier-name. Name-like things (identifiers)
+ appear as child nodes or literals inside `parts`.
+ """
+ kind: str
+ span: SourceSpan
+ scope_id: Optional[ProcessLocalId] = None
+ parts: Dict[str ,SyntaxPartValue] = None
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*-
+
+"""
+Tests for Symbol.Instance constraints.
+"""
+
+import unittest
+from Symbol import Symbol
+
+class TestSymbol(unittest.TestCase):
+
+ def test_null_symbol_exists(self):
+ """
+ Requirement: Symbol.null exists and is Token 0.
+ """
+ null_sym = Symbol.null
+ self.assertIsNotNone(null_sym)
+ self.assertEqual(null_sym.name ,"Null")
+ self.assertEqual(null_sym.doc ,"The Null Symbol")
+
+ def test_distinctness(self):
+ """
+ Requirement: Two distinct originals will always be not equal.
+ """
+ s1 = Symbol.make()
+ s2 = Symbol.make()
+
+ self.assertNotEqual(s1 ,s2)
+ self.assertIsNot(s1 ,s2)
+ self.assertNotEqual(s1 ,Symbol.null)
+
+ def test_name_immutability(self):
+ """
+ Requirement: Name can be set once, but never changed.
+ """
+ # Case 1: Set at creation
+ s1 = Symbol.make(name="Alpha")
+ self.assertEqual(s1.name ,"Alpha")
+
+ # Attempt to change
+ with self.assertRaises(RuntimeError):
+ s1.name = "Beta"
+
+ # Case 2: Late binding
+ s2 = Symbol.make()
+ self.assertIsNone(s2.name)
+
+ s2.name = "Gamma"
+ self.assertEqual(s2.name ,"Gamma")
+
+ # Attempt to change after late bind
+ with self.assertRaises(RuntimeError):
+ s2.name = "Delta"
+
+ def test_doc_immutability(self):
+ """
+ Requirement: Doc can be set once, but never changed.
+ """
+ # Case 1: Set at creation
+ s1 = Symbol.make(doc="Original Doc")
+ self.assertEqual(s1.doc ,"Original Doc")
+
+ with self.assertRaises(RuntimeError):
+ s1.doc = "New Doc"
+
+ # Case 2: Late binding
+ s2 = Symbol.make()
+ self.assertEqual(s2.doc ,"") # Default is empty string
+
+ s2.doc = "Late Doc"
+ self.assertEqual(s2.doc ,"Late Doc")
+
+ with self.assertRaises(RuntimeError):
+ s2.doc = "Changed Doc"
+
+ def test_doc_empty_string_behavior(self):
+ """
+ Requirement: Setting doc to "" is ignored and does not count as 'setting' it.
+ """
+ s1 = Symbol.make()
+
+ # Setting empty string should be a no-op
+ s1.doc = ""
+ self.assertEqual(s1.doc ,"")
+
+ # Should still be able to set it later because "" didn't lock it
+ s1.doc = "Real Doc"
+ self.assertEqual(s1.doc ,"Real Doc")
+
+ # NOW it is locked
+ with self.assertRaises(RuntimeError):
+ s1.doc = "Trying to change"
+
+ def test_property_access(self):
+ """
+ Requirement: Read/Write via properties.
+ """
+ s = Symbol.make()
+ s.name = "Velocity"
+ s.doc = "m/s"
+
+ self.assertEqual(s.name ,"Velocity")
+ self.assertEqual(s.doc ,"m/s")
+ self.assertIsInstance(s ,Symbol.Instance)
+
+ def test_opaque_representation(self):
+ """
+ Requirement: repr() reveals no internal token.
+ """
+ s1 = Symbol.make()
+ self.assertEqual(repr(s1) ,"<Symbol.Instance>")
+
+
+if __name__ == '__main__':
+ unittest.main()
In Epimetheus, we use a dictionary to give a <RT-term>symbol</RT-term> a name. The names need not meet the requirements imposed on a <RT-term>symbol instance</RT-term>, but it can be confusing when they don't. This is avoided in other <RT-term>symbol</RT-term> systems by implementing <RT-term>symbol</RT-term>s over character strings, and using the character string as both the name and the instance.
</p>
- <h2>Required Properties</h2>
+ <h3>Required Properties</h3>
<p>
Any two, or by transitivity, any number of, <RT-term>symbol instance</RT-term>s can be compared for equality, say <RT-code>x == y</RT-code>. A comparison between <RT-term>symbol instance</RT-term>s will always return <RT-code>True</RT-code> or <RT-code>False</RT-code>.
</p>
- <h2>What <em>is</em> a Symbol?</h2>
+ <h3>What <em>is</em> a Symbol?</h3>
<p>
Only <RT-term>symbol instance</RT-term>s exist at runtime, so a <RT-term>symbol</RT-term> is never manipulated directly.
Think about it this way: you are sitting in a pub of an inn, and a stranger walks in. Though you know the stranger's current <RT-term>destination</RT-term>, you might not know where he came from.
</blockquote>
- <h3>Discrete Function</h3>
+ <h2>Discrete Function</h2>
<p>
Now consider a <RT-term>tape</RT-term>, say called <var>F</var>, where each cell in F holds a <RT-term>path</RT-term>. As a <RT-term>path</RT-term> is a <RT-term>tape</RT-term>, the structure of <var>F</var> is that of a <RT-term>tape</RT-term> of <RT-term>tape</RT-term>s.
</p>