Files
cdsl-cad/backend/engine/cdsl_engine/registry.py
T
ganjihong 5ffb106f36 refactor(cdsl_engine): executor registry + per-family executor package
Phase 3 of the decoupling refactor (behavior-preserving):
- registry.py: atomic_executor decorator, ALL_ATOMIC_IDS with
  fail-fast registration validation, execute_node dispatcher
- executors/: one module per family (extrude, revolve, surfaces,
  loft_sweep, bodies, context, primitives, parametric, holes,
  dressup, patterns) + shared helpers in executors/common
- executors/__init__: explicit aggregation + completeness check
  (registry must cover every declared atomic id at import time)
- runtime.py: slimmed to entry points (analyze_cdsl/rebuild_cdsl)
  plus full historical re-exports incl. test-referenced privates

Adding an atomic operation now touches only one executor module and
its schema contract; the shared registry never changes. Verified
against baseline: zero new failures.
2026-09-09 13:33:06 +08:00

84 lines
3.5 KiB
Python

"""Atomic executor registry and dispatch for the session runtime.
Executors register themselves with :func:`atomic_executor` from their own
modules; ``cdsl_engine.executors`` imports every executor module exactly once
to build the registry. Adding a new atomic operation therefore means adding
a decorated function in a family module -- the registry itself never changes.
``registry`` deliberately imports no executor module, so pattern executors
can re-enter :func:`execute_node` for replay without an import cycle.
"""
from __future__ import annotations
from typing import Any, Callable, Protocol
from .session import ExecutionSession
from .topology import CapabilityResult, FeaturePlanNode, FeatureResult
class AtomicExecutor(Protocol):
atomic_id: str
def preflight(self, node: FeaturePlanNode, session: ExecutionSession) -> CapabilityResult: ...
def execute(self, node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult: ...
#: The complete capability contract of this runtime. Every registered
#: executor must name an id from this set, so a mistyped registration fails
#: at import time instead of surfacing as an unknown-atomic blocker later.
ALL_ATOMIC_IDS = frozenset({
"extrude_add_blind", "extrude_add_blind_with_hole", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", "extrude_surface",
"extrude_cut_through", "extrude_from_face", "loft_add", "loft_add_with_cap_face", "sweep_add",
"revolve_add", "revolve_cut", "revolve_surface", "hole_blind", "hole_countersink",
"hole_counterbore", "sphere_add", "box_add", "cylinder_add",
"reference_plane", "reference_axis",
"hole_wizard", "fillet", "chamfer", "shell", "pattern_linear", "pattern_mirror",
"pattern_circular", "boolean_bodies", "transform_bodies", "delete_bodies",
"thread_add", "thread_cut",
"bend_add",
"gear_add", "rack_add",
})
#: ``atomic_id -> executor``. Populated by ``cdsl_engine.executors``.
EXECUTORS: dict[str, "ExecutorFunction"] = {}
ExecutorFunction = Callable[[FeaturePlanNode, ExecutionSession, dict[str, Any] | None], FeatureResult]
def atomic_executor(*atomic_ids: str) -> Callable[["ExecutorFunction"], "ExecutorFunction"]:
"""Register one executor function for one or more atomic ids.
Registration validates against ``ALL_ATOMIC_IDS`` and rejects duplicates,
keeping the registry consistent with the declared capability contract.
"""
unknown = [atomic_id for atomic_id in atomic_ids if atomic_id not in ALL_ATOMIC_IDS]
if unknown:
raise ValueError(f"cannot register executors for unknown atomic ids: {unknown}")
def decorator(func: "ExecutorFunction") -> "ExecutorFunction":
for atomic_id in atomic_ids:
if atomic_id in EXECUTORS:
raise ValueError(f"duplicate executor registration for {atomic_id!r}")
EXECUTORS[atomic_id] = func
return func
return decorator
def execute_node(
node: FeaturePlanNode,
session: ExecutionSession,
sketch_override: dict[str, Any] | None = None,
) -> FeatureResult:
"""Dispatch one plan node through its registered executor."""
executor = EXECUTORS.get(node.atomic_id)
if executor is None:
raise ValueError(f"No executor registered for {node.atomic_id!r}")
previous_feature_id = session.active_feature_id
session.active_feature_id = node.feature_id
try:
return executor(node, session, sketch_override)
finally:
session.active_feature_id = previous_feature_id