From: Thomas Walker Lynch Date: Fri, 16 Jan 2026 11:45:20 +0000 (+0000) Subject: adds Symbol.py X-Git-Url: https://git.reasoningtechnology.com/style/RT_math.js?a=commitdiff_plain;h=refs%2Fheads%2Fcore_developer_branch;p=Epimetheus%2F.git adds Symbol.py --- diff --git a/developer/authored/ObjectRegistry.py b/developer/authored/ObjectRegistry.py deleted file mode 100644 index 9d30fd9..0000000 --- a/developer/authored/ObjectRegistry.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/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 diff --git a/developer/authored/ProcessLocalId.py b/developer/authored/ProcessLocalId.py deleted file mode 100644 index b57c47c..0000000 --- a/developer/authored/ProcessLocalId.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/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 "" - - def __str__(self) -> str: - return "" - - 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) diff --git a/developer/authored/Property.py b/developer/authored/Property.py deleted file mode 100644 index 5b33226..0000000 --- a/developer/authored/Property.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/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"" diff --git a/developer/authored/PropertyManager.py b/developer/authored/PropertyManager.py deleted file mode 100644 index 36e2b9e..0000000 --- a/developer/authored/PropertyManager.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/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) fileciteturn3file4 - - 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) diff --git a/developer/authored/PropertyStore.py b/developer/authored/PropertyStore.py deleted file mode 100644 index bc8f3f3..0000000 --- a/developer/authored/PropertyStore.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/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) diff --git a/developer/authored/SemanticSets.py b/developer/authored/SemanticSets.py deleted file mode 100644 index f0fdf48..0000000 --- a/developer/authored/SemanticSets.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/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"" - - -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) diff --git a/developer/authored/Symbol.py b/developer/authored/Symbol.py new file mode 100644 index 0000000..1240480 --- /dev/null +++ b/developer/authored/Symbol.py @@ -0,0 +1,153 @@ +#!/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 "" + + 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 diff --git a/developer/authored/Syntax.py b/developer/authored/Syntax.py deleted file mode 100644 index 4fcfa7c..0000000 --- a/developer/authored/Syntax.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/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 diff --git a/developer/authored/__init__.py b/developer/authored/__init__.py index ce046ae..dce65e6 100644 --- a/developer/authored/__init__.py +++ b/developer/authored/__init__.py @@ -1 +1,2 @@ -from .PropertyManager import PropertyManager +from .Epimetheus import Epimetheus + diff --git a/developer/authored/deprecated/ObjectRegistry.py b/developer/authored/deprecated/ObjectRegistry.py new file mode 100644 index 0000000..9d30fd9 --- /dev/null +++ b/developer/authored/deprecated/ObjectRegistry.py @@ -0,0 +1,113 @@ +#!/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 diff --git a/developer/authored/deprecated/ProcessLocalId.py b/developer/authored/deprecated/ProcessLocalId.py new file mode 100644 index 0000000..b57c47c --- /dev/null +++ b/developer/authored/deprecated/ProcessLocalId.py @@ -0,0 +1,52 @@ +#!/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 "" + + def __str__(self) -> str: + return "" + + 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) diff --git a/developer/authored/deprecated/Property.py b/developer/authored/deprecated/Property.py new file mode 100644 index 0000000..5b33226 --- /dev/null +++ b/developer/authored/deprecated/Property.py @@ -0,0 +1,29 @@ +#!/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"" diff --git a/developer/authored/deprecated/PropertyManager.py b/developer/authored/deprecated/PropertyManager.py new file mode 100644 index 0000000..36e2b9e --- /dev/null +++ b/developer/authored/deprecated/PropertyManager.py @@ -0,0 +1,155 @@ +#!/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) fileciteturn3file4 + - 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) diff --git a/developer/authored/deprecated/PropertyStore.py b/developer/authored/deprecated/PropertyStore.py new file mode 100644 index 0000000..bc8f3f3 --- /dev/null +++ b/developer/authored/deprecated/PropertyStore.py @@ -0,0 +1,59 @@ +#!/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) diff --git a/developer/authored/deprecated/SemanticSets.py b/developer/authored/deprecated/SemanticSets.py new file mode 100644 index 0000000..f0fdf48 --- /dev/null +++ b/developer/authored/deprecated/SemanticSets.py @@ -0,0 +1,55 @@ +#!/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"" + + +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) diff --git a/developer/authored/deprecated/Syntax.py b/developer/authored/deprecated/Syntax.py new file mode 100644 index 0000000..4fcfa7c --- /dev/null +++ b/developer/authored/deprecated/Syntax.py @@ -0,0 +1,59 @@ +#!/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 diff --git a/developer/authored/test_SymbolFactory.py b/developer/authored/test_SymbolFactory.py new file mode 100755 index 0000000..8bc21dc --- /dev/null +++ b/developer/authored/test_SymbolFactory.py @@ -0,0 +1,116 @@ +#!/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) ,"") + + +if __name__ == '__main__': + unittest.main() diff --git a/document/Epimetheus.html b/document/Epimetheus.html index 5661740..85ee1ad 100644 --- a/document/Epimetheus.html +++ b/document/Epimetheus.html @@ -88,7 +88,7 @@ In Epimetheus, we use a dictionary to give a symbol a name. The names need not meet the requirements imposed on a symbol instance, but it can be confusing when they don't. This is avoided in other symbol systems by implementing symbols over character strings, and using the character string as both the name and the instance.

-

Required Properties

+

Required Properties

Any two, or by transitivity, any number of, symbol instances can be compared for equality, say x == y. A comparison between symbol instances will always return True or False. @@ -119,7 +119,7 @@

-

What is a Symbol?

+

What is a Symbol?

Only symbol instances exist at runtime, so a symbol is never manipulated directly. @@ -205,7 +205,7 @@ 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 destination, you might not know where he came from. -

Discrete Function

+

Discrete Function

Now consider a tape, say called F, where each cell in F holds a path. As a path is a tape, the structure of F is that of a tape of tapes.