return
# Still rightmost in new front context; keep popping.
- # ------------------------------------------------------------------
- # Introspection helper
- # ------------------------------------------------------------------
def has_node(self) -> bool:
- """True if there is a current node to read."""
+ """
+ True if there is a current node to read.
+ """
return self._has_node and self._tree_machine.context_depth() > 0
def build_breadth_machine(root_dp: str) -> BreadthMachine:
- """Worker: construct and return a BreadthMachine for root_dp."""
+ """
+ Worker: construct and return a BreadthMachine for root_dp.
+ """
return BreadthMachine(root_dp)
return
# Still rightmost in new front context; keep popping.
- # ------------------------------------------------------------------
- # Introspection helper
- # ------------------------------------------------------------------
def has_node(self) -> bool:
- """True if there is a current node to read."""
+ """
+ True if there is a current node to read.
+ """
return self._has_node and self._tree_machine.context_depth() > 0
def build_depth_machine(root_dp: str) -> DepthMachine:
- """Worker: construct and return a DepthMachine for root_dp."""
+ """
+ Worker: construct and return a DepthMachine for root_dp.
+ """
return DepthMachine(root_dp)
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+from __future__ import annotations
+from typing import Any
+import json as pyjson
+import duck
+import builtins
+
+# ----------------------------------------------------------------------
+def to_JSON_TM(
+ tm: Any
+) -> str:
+ """
+ Convert a Tape Machine (TM_ND_A or compatible) to a JSON string.
+
+ JSON contains only:
+ - "tape": list of cell values
+ - "head": integer head position
+ - "extent": last valid index
+ """
+
+ head = tm.address()
+ tm.cue_leftmost()
+ tape_data = [tm.read(n) for n in range(0 ,tm.extent + 1)]
+
+ obj = {
+ "tape" : tape_data
+ ,"head" : tm.address()
+ ,"extent": tm.extent
+ }
+
+ return pyjson.dumps(tm ,ensure_ascii=False)
+
+
+# ----------------------------------------------------------------------
+def print_TM(
+ tm: Any
+ ,indent: int = 0
+) -> None:
+ """
+ Print the JSON representation of the TM nicely indented.
+ """
+ s = to_JSON_TM(tm)
+ parsed = pyjson.loads(s)
+ out = pyjson.dumps(parsed ,indent=2 ,ensure_ascii=False)
+ prefix = " " * indent
+ for line in out.split("\n"):
+ print(prefix + line)
+
+
+# ----------------------------------------------------------------------
+def pretty_TM(
+ tm0: Any
+ ,indent: int = 0
+) -> None:
+ """
+ Walk the tape cells, printing:
+
+ <indent><mark><value>
+
+ mark::
+ ' ' normal cell
+ '*' head is here
+ '␀' empty (None, non-head)
+ """
+ prefix = " " * indent
+
+ tm1 = tm0.entangle()
+ tm1.cue_leftmost()
+
+ while True:
+ v = tm1.read()
+
+ if tm1.address() == tm0.address():
+ mark = "*"
+ elif v is None:
+ mark = "␀"
+ else:
+ mark = " "
+
+ s = "" if v is None else str(v)
+ print(f"{prefix}{mark}{s}")
+
+ if not tm1.can_step(): break
+ tm1.step()
+
+
+# ----------------------------------------------------------------------
+def is_a_TM(
+ value: Any
+) -> bool:
+ """
+ A Tape Machine is anything providing the attributes:
+
+ entangle
+ cue_leftmost
+ read
+ can_step
+ step
+ address
+
+ These are exactly what pretty_TM() uses.
+ """
+ return duck.has(
+ value
+ ,"entangle"
+ ,"cue_leftmost"
+ ,"read"
+ ,"can_step"
+ ,"step"
+ ,"address"
+ )
+
+# ----------------------------------------------------------------------
+def print(
+ value: Any
+ ,indent: int = 0
+) -> None:
+ """
+ Print logic:
+
+ - If TM: call print_TM
+ - Else: print raw value using Python print
+ (JSON primitives remain one-line)
+ """
+ if is_a_TM(value):
+ print_TM(value ,indent)
+ else:
+ prefix = " " * indent
+ print(prefix + str(value))
+
+
+# ----------------------------------------------------------------------
+def pretty(
+ value: Any
+ ,indent: int = 0
+) -> None:
+ """
+ Pretty-print logic:
+
+ - If TM: pretty_TM
+ - If JSON primitive: print raw
+ - If non-JSON type: wrap into { "value": <repr> } and pretty-print
+ - If JSON object/array: pretty-print with indentation
+ """
+ prefix = " " * indent
+
+ # TM case
+ if is_a_TM(value):
+ pretty_TM(value ,indent)
+ return
+
+ # JSON primitive: str, int, float, bool, None
+ if isinstance(value ,(str ,int ,float ,bool)) or value is None:
+ print(prefix + str(value))
+ return
+
+ # Try to JSON-dump; if it fails, wrap in {"value": repr}
+ try:
+ js = pyjson.dumps(value ,ensure_ascii=False)
+ parsed = pyjson.loads(js)
+ except Exception:
+ wrapped = { "value": repr(value) }
+ parsed = wrapped
+
+ # Pretty-print JSON
+ out = pyjson.dumps(parsed ,indent=2 ,ensure_ascii=False)
+ for line in out.split("\n"):
+ print(prefix + line)
+
+
+
+# ----------------------------------------------------------------------
+def CLI() -> None:
+ """
+ Demo for json helpers. Prints examples for:
+
+ - to_JSON_TM
+ - print_TM
+ - pretty_TM
+ - is_a_TM
+ - print
+ - pretty
+ """
+ p = builtins.print
+
+ class DemoTM:
+ def __init__(self) -> None:
+ self._tape = [1 ,None ,"three"]
+ self._head_position = 1 # on the None cell
+
+ @property
+ def extent(self) -> int: return len(self._tape) - 1
+ def address(self) -> int: return self._head_position
+ def cue_leftmost(self) -> None: self._head_position = 0
+ def can_step(self ,n: int = 1) -> bool:
+ return 0 <= self._head_position + n <= self.extent
+ def step(self ,n: int = 1) -> None:
+ self._head_position += n
+ def read(self ,n: int = 0) -> Any:
+ return self._tape[self._head_position + n]
+ def write(self ,value: Any ,n: int = 0) -> None:
+ self._tape[self._head_position + n] = value
+ def entangle(self) -> "DemoTM":
+ tm = DemoTM()
+ tm._tape = self._tape
+ tm._head_position = self._head_position
+ return tm
+
+ tm = DemoTM()
+
+ p("json.is_a_TM examples:")
+ p(" is_a_TM(tm) ->" ,is_a_TM(tm))
+ p(" is_a_TM(123) ->" ,is_a_TM(123))
+ p()
+
+ p("json.to_JSON_TM example:")
+ p(" to_JSON_TM(tm) ->")
+ p(" " + to_JSON_TM(tm))
+ p()
+
+ p("json.print_TM example:")
+ print_TM(tm ,indent=2)
+ p()
+
+ p("json.pretty_TM example:")
+ pretty_TM(tm ,indent=2)
+ p()
+
+ p("json.print() examples:")
+ print(tm ,indent=2) # TM case
+ print(42 ,indent=2) # primitive
+ print({"a": 1 ,"b": 2} ,indent=2) # dict, one-line
+ p()
+
+ p("json.pretty() examples:")
+ pretty(tm ,indent=2) # TM
+ p("---")
+ pretty(42 ,indent=2) # primitive
+ p("---")
+ pretty({"a": 1 ,"b": [2 ,3]} ,indent=2) # JSON object
+ p("---")
+ pretty(object() ,indent=2) # non-JSON -> wrapped
+
+
+# ----------------------------------------------------------------------
+if __name__ == "__main__":
+ CLI()
+
from __future__ import annotations
-from typing import Any, Generic, Iterable, List, Optional, TypeVar
-
import meta
-
-
-T = TypeVar("T")
-
-
-def _format_item_for_debug(
- item: Any
-) -> str:
- """
- Convert a queued item to a human-readable string for debug output.
-
- If the item has a callable .print() method, we call it and use
- the returned value (converted to str). Otherwise we fall back
- to repr(item).
- """
- if hasattr(item, "print"):
- candidate = getattr(item, "print")
- if callable(candidate):
- try:
- text = candidate()
- if text is None:
- return repr(item)
- return str(text)
- except Exception:
- return repr(item)
- return repr(item)
+from typing import Any, List
def _queue_debug(
- queue: Any
- ,op: str
- ,item: Any | None = None
+ q: Any
+ ,msg: str
) -> None:
"""
- Internal helper: emit a queue-related debug message if
- the meta.DEBUG set contains the tag "Queue".
-
- Includes the queue instance pointer in hex so we can
- distinguish multiple Queue instances.
+ Emit a Queue-related debug message if meta.DEBUG has 'Queue'.
"""
- if "Queue" not in meta.DEBUG:
- return
-
- qid = hex(id(queue))
-
- if item is None:
- print(f"[Queue {qid}] {op}")
- else:
- desc = _format_item_for_debug(item)
- print(f"[Queue {qid}] {op}: {desc}")
+ if "Queue" not in meta.DEBUG: return
+ print(f"[Queue {hex(id(q))}] {msg}")
-class Queue(Generic[T]):
+class Queue:
"""
- Queue — ordered queue of items (e.g., Context objects).
+ Simple FIFO queue with tagged push operations.
- Semantics:
+ Items are stored in a Python list, with index 0 as the "front".
+ """
- - The "front" of the queue is the current item.
- - The "back" of the queue will eventually be visited.
- - Popping an empty queue is an error.
+ def __init__(self) -> None:
+ self._items: List[Any] = []
- Interface:
+ def push(
+ self
+ ,where_tag: str
+ ,item: Any
+ ) -> None:
+ """
+ Push an item into the queue.
- front() -> T : current item (head), does not remove it.
- back() -> T : last item in the queue, does not remove it.
- top() -> T : alias for front().
- depth() -> int : number of entries in the queue.
- pop() -> T : remove and return the front item.
+ where_tag:
+ - 'front' -> insert at front
+ - 'back' -> append at back
+ - 'throw-away' -> discard item (for debugging)
+ """
+ if hasattr(item, "print"):
+ desc = item.print()
+ else:
+ desc = repr(item)
- push(where_tag, item: T) -> None:
+ if where_tag == "throw-away":
+ _queue_debug(self, f"push throw-away (discard): {desc}")
+ return
- where_tag:
- "front" -> insert item at front of queue
- "back" -> append item at back of queue
- "throw-away" -> do not store, treated as intentionally ignored
+ if where_tag == "front":
+ self._items.insert(0, item)
+ _queue_debug(self, f"push front: {desc}")
+ return
- clone() -> Queue[T]
+ if where_tag == "back":
+ self._items.append(item)
+ _queue_debug(self, f"push back: {desc}")
+ return
- Creates a copy of the Queue. For each item:
- - If item has a .clone() method, it is called.
- - Otherwise the item reference is reused.
- """
+ raise ValueError(f"Queue.push: unknown where_tag {where_tag!r}")
- def __init__(
- self
- ,initial_iter: Optional[Iterable[T]] = None
- ) -> None:
- if initial_iter is None:
- self._queue_list: List[T] = []
+ def pop(self) -> Any:
+ """
+ Pop and return the front item.
+ """
+ if not self._items:
+ raise IndexError("Queue.pop on empty queue.")
+ item = self._items.pop(0)
+ if hasattr(item, "print"):
+ desc = item.print()
else:
- self._queue_list = list(initial_iter)
+ desc = repr(item)
+ _queue_debug(self, f"pop: {desc}")
+ return item
- def snapshot(self) -> list:
+ def front(self) -> Any:
"""
- Return a shallow copy of the items in queue order (front to back).
-
- This is intended for read-only traversal by higher-level logic
- such as TreeMachine.can_step().
+ Return the front item without removing it.
"""
- return list(self._items)
-
- # ----------------------------------------------------------------------
- # Basic properties
- # ----------------------------------------------------------------------
- def is_empty(self) -> bool:
- return len(self._queue_list) == 0
-
- def depth(self) -> int:
- return len(self._queue_list)
+ if not self._items:
+ raise IndexError("Queue.front on empty queue.")
+ return self._items[0]
- # ----------------------------------------------------------------------
- # Cloning
- # ----------------------------------------------------------------------
- def clone(self) -> "Queue[T]":
+ def back(self) -> Any:
"""
- Create a copy of this Queue.
-
- The new queue has its own internal list. If an item provides
- a .clone() method, that is used to copy the item; otherwise
- the item reference is reused.
+ Return the back item without removing it.
"""
- new_item_list: List[T] = []
- for item in self._queue_list:
- if hasattr(item, "clone") and callable(getattr(item, "clone")):
- new_item_list.append(item.clone()) # type: ignore[arg-type]
- else:
- new_item_list.append(item)
- new_q = Queue[T](new_item_list)
- _queue_debug(self, f"clone -> new {hex(id(new_q))}")
- return new_q
-
- # ----------------------------------------------------------------------
- # Accessors
- # ----------------------------------------------------------------------
- def front(self) -> T:
- if self.is_empty():
- raise RuntimeError("Queue.front() on empty queue.")
- return self._queue_list[0]
-
- def back(self) -> T:
- if self.is_empty():
- raise RuntimeError("Queue.back() on empty queue.")
- return self._queue_list[-1]
+ if not self._items:
+ raise IndexError("Queue.back on empty queue.")
+ return self._items[-1]
- def top(self) -> T:
+ def depth(self) -> int:
"""
- Alias for front().
+ Number of items in the queue.
"""
- return self.front()
+ return len(self._items)
- # ----------------------------------------------------------------------
- # Mutating operations
- # ----------------------------------------------------------------------
- def pop(self) -> T:
+ def is_empty(self) -> bool:
"""
- Remove and return the front item.
-
- Popping an empty queue is an error.
+ True if the queue is empty.
"""
- if self.is_empty():
- raise RuntimeError("Queue.pop() on empty queue.")
- item = self._queue_list.pop(0)
- _queue_debug(self, "pop", item)
- return item
+ return not self._items
- def push(
- self
- ,where_tag: str
- ,item: T
- ) -> None:
+ def snapshot(self) -> list:
"""
- Push an item according to where_tag:
-
- where_tag == "front":
- insert item at front of the queue.
-
- where_tag == "back":
- append item at back of the queue.
+ Return a shallow copy of items (front to back).
- where_tag == "throw-away":
- do not store item; treated as intentionally ignored.
-
- Any other where_tag is an error.
+ Intended for read-only inspection (e.g. TreeMachine.can_step()).
"""
- if where_tag == "front":
- self._queue_list.insert(0, item)
- _queue_debug(self, "push front", item)
- return
-
- if where_tag == "back":
- self._queue_list.append(item)
- _queue_debug(self, "push back", item)
- return
-
- if where_tag == "throw-away":
- _queue_debug(self, "push throw-away (discard)", item)
- return
-
- raise ValueError(f"Unknown Queue push tag: {where_tag!r}")
+ return list(self._items)
- # ----------------------------------------------------------------------
- # Dispatcher
- # ----------------------------------------------------------------------
- def dispatch(
- self
- ,op: str
- ,*args
- ,**kwargs
- ):
+ def clone(self) -> "Queue":
"""
- Generic dispatcher for method-tag control.
-
- Supported op values:
-
- "front" -> front()
- "back" -> back()
- "top" -> top()
- "depth" -> depth()
- "pop" -> pop()
+ Clone the queue.
- "push" -> push(where_tag, item)
-
- Any unknown op raises ValueError.
+ If an item has a .clone() method, use it. Otherwise copy the
+ reference directly.
"""
- if op == "front":
- return self.front()
- if op == "back":
- return self.back()
- if op == "top":
- return self.top()
- if op == "depth":
- return self.depth()
- if op == "pop":
- return self.pop()
- if op == "push":
- if len(args) != 2:
- raise ValueError("dispatch('push', where_tag, item) requires 2 arguments.")
- where_tag = args[0]
- item = args[1]
- self.push(where_tag, item)
- return None
-
- raise ValueError(f"Unknown Queue operation: {op!r}")
-
-
-# ----------------------------------------------------------------------
-# Simple test harness
-# ----------------------------------------------------------------------
-class TestItem:
- """
- Minimal test item for the __main__ test().
-
- It implements .print() so Queue can use it in debug messages.
- """
-
- def __init__(
- self
- ,name: str
- ) -> None:
- self.name = name
-
- def print(self) -> str:
- return f"TestItem({self.name})"
-
- def clone(self) -> "TestItem":
- # For the test, clone is just another instance with same name.
- return TestItem(self.name)
+ new_q = Queue()
+ for item in self._items:
+ if hasattr(item, "clone"):
+ new_q._items.append(item.clone())
+ else:
+ new_q._items.append(item)
+ return new_q
-def test() -> None:
+def CLI(argv: list[str]) -> int:
"""
- Simple test for Queue with debug enabled.
+ Simple self-test for Queue.
"""
- meta.DEBUG.add("Queue")
-
print("Queue test starting...")
meta.version()
+ meta.DEBUG.add("Queue")
- q = Queue[TestItem]()
- q2 = q.clone() # exercise clone-debug once at the start
-
- q.push("front", TestItem("root"))
- q.push("back", TestItem("child-A"))
- q.push("back", TestItem("child-B"))
- q.push("throw-away", TestItem("ignored-node"))
+ q = Queue()
+ q.push("back", "apple")
+ q.push("back", "banana")
+ q.push("front", "cherry")
+ q.push("throw-away", "ignored")
print(f"depth = {q.depth()}")
- print(f"front = {q.front().print()}")
- print(f"back = {q.back().print()}")
+ print(f"front = {q.front()!r}")
+ print(f"back = {q.back()!r}")
popped = q.pop()
- print(f"popped = {popped.print()}")
+ print(f"popped = {popped!r}")
print(f"depth = {q.depth()}")
print("Queue test done.")
+ return 0
if __name__ == "__main__":
- test()
+ import sys
+ raise SystemExit(CLI(sys.argv))
# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
from __future__ import annotations
+from typing import Any
+import duck
-from typing import Any, Optional
-
-import meta
-
+class TM_ND_A:
+ """
+ TM_ND_A
-def _tape_debug(tm: Any, msg: str) -> None:
- """Emit a TapeMachine-related debug message if meta.DEBUG has 'TapeMachine'."""
- if "TapeMachine" not in meta.DEBUG: return
- print(f"[TapeMachine {hex(id(tm))}] {msg}")
+ TM Tape Machine
+ ND Non Destructive, no cell deletion or insertion
+ A Address supports addressing of cells
+ Machine does not support status, so if the machine exists, the tape has
+ at least one cell.
-class TapeMachine:
+ Head starts on cell 0 and by contract with the user is always on a valid cell.
+ I.e. it is the user's responsibility to check `can_step` before stepping.
"""
- TapeMachine — single-tape, single-head, first-rest pattern.
- - Tape is non-empty.
- - Head starts on cell 0 and is always on a valid cell.
- - Movement contract: caller checks can_step(n) before step(n).
- """
-
- def __init__(self, tape: Optional[Any] = None) -> None:
- if tape is None: tape = []
- for name in ("__len__", "__getitem__", "__setitem__"):
- if not hasattr(tape, name): raise TypeError(f"TapeMachine tape must support {name}: {tape!r}")
- self._length: int = len(tape)
- if self._length == 0: raise ValueError("TapeMachine requires a non-empty tape (first-rest pattern).")
+ # ----------------------------------------------------------------------
+ def __init__(self ,tape):
+ duck.require(duck.class_of(tape) ,"__len__" ,"__getitem__" ,"__setitem__")
+ self._extent: int = len(tape) - 1
self._tape = tape
self._head_position: int = 0
- _tape_debug(self, f"init length={self._length}, head_position=0")
+ # ----------------------------------------------------------------------
@property
- def length(self) -> int:
- """Length of the tape (number of cells)."""
- return self._length
-
- def cue(self, new_position: Optional[int] = None) -> int:
- """Get or set head (cue) position."""
- if new_position is None: return self._head_position
- if not 0 <= new_position < self._length: raise IndexError(f"TapeMachine.cue: out of range ({new_position}).")
- self._head_position = new_position
- return self._head_position
-
- def can_step(self, n: int = 1) -> bool:
- """True if step(n) would keep the head on the tape."""
- proposed_position = self._head_position + n
- return 0 <= proposed_position < self.length
-
- def rewind(self) -> None:
- """Move head to the first cell (index 0)."""
- self._head_position = 0
-
- def cue_end(self) -> None:
- """Move head to the last cell (index length-1)."""
- self._head_position = self._length - 1
-
- def step(self, n: int = 1) -> None:
- """Move head by n cells (n>0 right, n<0 left, n=0 no-op); caller must ensure can_step(n)."""
- self._head_position += n
-
- def read(self) -> Any:
- """Read value under head."""
- return self._tape[self._head_position]
-
- def write(self, value: Any) -> None:
- """Write a new value into the current cell."""
- self._tape[self._head_position] = value
-
- def print(self) -> str:
- """
- Printable view of the tape.
-
- Two spaces then value per line; line with the head is prefixed '* '.
- """
- line_list = []
- for idx in range(self._length):
- prefix = "* " if idx == self._head_position else " "
- line_list.append(f"{prefix}{self._tape[idx]}")
- return "\n".join(line_list)
-
+ def extent(self) -> int: return self._extent
+ def tape(self) -> Any: return self._tape
+ def where(self) -> int: return self._head_position
+ def cue_leftmost(self) -> None: self._head_position = 0
+ def cue_rightmost(self) -> None: self._head_position = self._extent
+ def cue(self ,new_position: int) -> int: self._head_position = new_position
+ def can_step(self ,n: int = 1) -> bool: return 0 <= self._head_position + n <= self._extent
+ def step(self ,n: int = 1) -> None: self._head_position += n
+ def read(self ,n: int = 0) -> Any: return self._tape[self._head_position + n]
+ def write(self ,value: Any ,n: int = 0) -> None: self._tape[self._head_position + n] = value
+
+ # because this is an ND machine it is safe to entangle heads
def entangle(self) -> "TapeMachine":
"""Create an entangled copy: shared tape, independent head_position."""
new_tm = object.__new__(TapeMachine)
new_tm._tape = self._tape
new_tm._length = self._length
new_tm._head_position = self._head_position
- _tape_debug(self, f"entangle -> new {hex(id(new_tm))} head_position={self._head_position}")
return new_tm
-
-def test() -> None:
- """Simple TapeMachine test with first-rest semantics."""
- meta.DEBUG.add("TapeMachine")
- print("TapeMachine test starting..."); meta.version()
-
- tm = TapeMachine(["apple", "orange", "tangerine"])
- print(f"initial: cue={tm.cue()}, value={tm.read()!r}")
-
- while tm.can_step(1):
- tm.step(1)
- print(f"step(+1): cue={tm.cue()}, value={tm.read()!r}")
-
- if tm.can_step(-1):
- tm.step(-1)
- print(f"step(-1): cue={tm.cue()}, value={tm.read()!r}")
-
- tm.rewind()
- print(f"rewind: cue={tm.cue()}, value={tm.read()!r}")
-
- tm2 = tm.entangle()
- if tm.can_step(2):
- tm.step(2)
- print(f"tm step(+2): cue={tm.cue()}, value={tm.read()!r}")
- if tm2.can_step(1):
- tm2.step(1)
- print(f"tm2 step(+1): cue={tm2.cue()}, value={tm2.read()!r}")
-
- tm.cue_end()
- print(f"cue_end: cue={tm.cue()}, value={tm.read()!r}")
-
- print("print(tm):")
- print(tm.print())
- print("TapeMachine test done.")
-
-
-if __name__ == "__main__":
- test()
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+from typing import Any, Optional
+
+
+# ----------------------------------------------------------------------
+def class_of(
+ value: Any
+) -> Optional[type]:
+ """
+ Return the runtime class for a value.
+
+ If the value has a __class__ attribute that is a real type, return it.
+ Otherwise return None.
+ """
+ cls = getattr(value ,"__class__" ,None)
+ return cls if isinstance(cls ,type) else None
+
+
+# ----------------------------------------------------------------------
+def has(
+ inspected_class: Any
+ ,*attribute_names: str
+) -> bool:
+ """
+ True iff `inspected_class` has *all* listed attributes.
+
+ Usage:
+ has(MyClass ,"x" ,"y" ,"z")
+ """
+ for name in attribute_names:
+ if not hasattr(inspected_class ,name):
+ return False
+ return True
+
+
+# ----------------------------------------------------------------------
+def require(
+ inspected_class: Any
+ ,*attribute_names: str
+) -> None:
+ """
+ Guard: ensure `inspected_class` has the required attributes.
+
+ Usage:
+ require(MyClass ,"x" ,"y" ,"z")
+
+ Raise TypeError if any required attribute is missing.
+ """
+ if has(inspected_class ,*attribute_names):
+ return
+
+ # Collect missing names for detailed error.
+ missing_names = [
+ name
+ for name in attribute_names
+ if not hasattr(inspected_class ,name)
+ ]
+
+ raise TypeError(
+ f"require() failed: {inspected_class!r} "
+ f"is missing attributes {missing_names!r}"
+ )
+
+# ----------------------------------------------------------------------
+def CLI() -> None:
+ """
+ Demo for duck helpers. Prints examples for:
+
+ - class_of
+ - has
+ - require
+ """
+
+ class Demo:
+ demo_attr = 1
+ def demo_method(self) -> None: # short-stuff
+ pass
+
+ print("duck.class_of examples:")
+ print(" class_of(Demo) ->" ,class_of(Demo))
+ print(" class_of(Demo()) ->" ,class_of(Demo()))
+ print(" class_of(None) ->" ,class_of(None))
+ print()
+
+ print("duck.has examples:")
+ print(" has(Demo ,'demo_attr') ->"
+ ,has(Demo ,"demo_attr"))
+ print(" has(Demo ,'demo_method') ->"
+ ,has(Demo ,"demo_method"))
+ print(" has(Demo ,'missing_attr') ->"
+ ,has(Demo ,"missing_attr"))
+ print(" has(Demo ,'demo_attr','missing_attr') ->"
+ ,has(Demo ,"demo_attr" ,"missing_attr"))
+ print()
+
+ print("duck.require examples:")
+ print(" require(Demo ,'demo_attr') (no error)")
+ require(Demo ,"demo_attr")
+
+ print(" require(Demo ,'demo_attr','demo_method') (no error)")
+ require(Demo ,"demo_attr" ,"demo_method")
+
+ print(" require(Demo ,'missing_attr') (should raise)")
+ try:
+ require(Demo ,"missing_attr")
+ except TypeError as exc:
+ print(" caught TypeError:" ,exc)
+
+
+# ----------------------------------------------------------------------
+if __name__ == "__main__":
+ CLI()
--- /dev/null
+
+ to_JSON:
+ For each cell of the tape:
+ 1. If the value has a to_JSON() method, use value.to_JSON().
+ 2. If the value is a JSON primitive (str,int,float,bool,None), output it directly.
+ 3. If the value is a Python primitive that is not a JSON primitive, stringify it
+ with JSON-safe escaping.
+ 4. Otherwise: error — cannot represent in JSON.
+
+ # ----------------------------------------------------------------------
+ # JSON representation
+ # ----------------------------------------------------------------------
+
+ @classmethod
+ def is_a_JSON_primitive(
+ cls
+ ,value: Any
+ ) -> bool:
+ return (
+ isinstance(value ,(str ,int ,float ,bool))
+ or value is None
+ )
+
+ @classmethod
+ def has_to_JSON(
+ cls
+ ,value: Any
+ ) -> bool:
+ return hasattr(value ,"to_JSON") and callable(value.to_JSON)
+
+ @classmethod
+ def normalize_value(
+ cls
+ ,value: Any
+ ) -> Any:
+ """
+ Normalize a tape cell value into JSON-compatible output.
+
+ Rules:
+ 1. If the value has to_JSON, call it.
+ 2. If the value is a JSON primitive, return it.
+ 3. If the value is a Python primitive but not JSON-safe, stringify it with escaping.
+ 4. Else: raise TypeError.
+ """
+ # Rule 1: to_JSON
+ if cls.has_to_JSON(value):
+ return value.to_JSON()
+
+ # Rule 2: JSON primitive
+ if cls.is_a_JSON_primitive(value):
+ return value
+
+ # Rule 3: Python primitive → stringify
+ # Note: repr() is safe because JSONPretty will escape the resulting string.
+ if isinstance(value ,(bytes ,bytearray)):
+ # represent bytes safely in hex
+ hexval = value.hex()
+ return f"<bytes len={len(value)} hex={hexval}>"
+
+ if isinstance(value ,(complex ,range)):
+ return repr(value)
+
+ # Rule 4: No idea what this is
+ raise TypeError(f"TapeMachine cannot JSON-normalize value: {value!r} of type {type(value)}")
+
+ def to_JSON(
+ self
+ ) -> dict[str ,Any]:
+ """
+ JSON representation:
+
+ {
+ "TapeMachine": "<hex id>"
+ ,"head": <int>
+ ,"Tape": [ <Value0> ,<Value1> , ... ]
+ }
+
+ Where each Value is JSON-normalized by normalize_value().
+ """
+ normalized = []
+ for raw in self._tape:
+ normalized.append(self.normalize_value(raw))
+
+ return {
+ "TapeMachine": hex(id(self))
+ ,"head": self._head_position
+ ,"Tape": normalized
+ }
+
+def _tape_debug(
+ tm: Any
+ ,msg: str
+) -> None:
+ """Emit a TapeMachine-related debug message if meta.DEBUG has 'TapeMachine'."""
+ if "TapeMachine" not in meta.DEBUG: return
+ print(f"[TapeMachine {hex(id(tm))}] {msg}")
+
+
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; -*-
+
+from TapeMachine import TapeMachine
+from JSONPretty import print_JSON ,pretty_JSON
+import meta
+
+
+def test_TapeMachine() -> None:
+ """Unit test for TapeMachine using JSONPretty output."""
+ meta.DEBUG.add("TapeMachine")
+ print("TapeMachine test starting..."); meta.version()
+
+ print("----------------------------------------")
+ print("instantiating tape machine")
+ print("")
+
+ tm = TapeMachine(["apple", "orange", "tangerine"])
+
+ print("----------------------------------------")
+ print("printing newly instantiated tape machine")
+ print("")
+
+ print_JSON(tm.to_JSON())
+
+
+ print("----------------------------------------")
+ print("pretty printing newly instantiated tape machine")
+ print("")
+
+ pretty_JSON(tm.to_JSON())
+
+ print("----------------------------------------")
+ print("one rightward step at a time through the tape")
+ print("")
+
+ while tm.can_step(1):
+ tm.step(1)
+ print_JSON(tm.to_JSON())
+
+ print("----------------------------------------")
+ print("one leftward step at a time through the tape")
+ print("")
+
+ if tm.can_step(-1):
+ tm.step(-1)
+ print_JSON(tm.to_JSON())
+
+ print("----------------------------------------")
+ print("printing machine again")
+ print("")
+
+ tm.rewind() # this won't do anything
+ print_JSON(tm.to_JSON())
+
+ print("----------------------------------------")
+ print("making entangled copy")
+ print("")
+
+ tm2 = tm.entangle()
+
+ print("----------------------------------------")
+ print("stepping original two steps")
+ print("")
+ if tm.can_step(2):
+ tm.step(2)
+ print_JSON(tm.to_JSON())
+
+ print("----------------------------------------")
+ print("stepping copy one step")
+ print("")
+ if tm2.can_step(1):
+ tm2.step(1)
+ print_JSON(tm2.to_JSON())
+
+ tm.cue_end()
+ print_JSON(tm.to_JSON())
+
+ print("TapeMachine test done.")
+
+
+if __name__ == "__main__":
+ test_TapeMachine()