Add modular primitive libraries and fix Symbol class compatibility

- Add primitive_libs/ with modular primitive loading (core, math, image,
  color, color_ops, filters, geometry, drawing, blending, arrays, ascii)
- Effects now explicitly declare dependencies via (require-primitives "...")
- Convert ascii-fx-zone from hardcoded special form to loadable primitive
- Add _is_symbol/_is_keyword helpers for duck typing to support both
  sexp_effects.parser.Symbol and artdag.sexp.parser.Symbol classes
- Auto-inject _interp and _env for primitives that need them
- Remove silent error swallowing in cell_effect evaluation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-20 09:02:34 +00:00
parent 6ceaa37ab6
commit d574d5badd
24 changed files with 2056 additions and 46 deletions

View File

@@ -348,9 +348,9 @@ def get_encoding(recipe_encoding: dict, step_config: dict) -> dict:
class SexpEffectModule:
"""Wrapper for S-expression effects to provide process_frame interface."""
def __init__(self, effect_path: Path, effects_registry: dict = None, recipe_dir: Path = None):
def __init__(self, effect_path: Path, effects_registry: dict = None, recipe_dir: Path = None, minimal_primitives: bool = False):
from sexp_effects import get_interpreter
self.interp = get_interpreter()
self.interp = get_interpreter(minimal_primitives=minimal_primitives)
# Load only explicitly declared effects from the recipe's registry
# No auto-loading from directory - everything must be explicit
@@ -371,10 +371,10 @@ class SexpEffectModule:
return self.interp.run_effect(self.effect_name, frame, params, state or {})
def load_effect(effect_path: Path, effects_registry: dict = None, recipe_dir: Path = None):
def load_effect(effect_path: Path, effects_registry: dict = None, recipe_dir: Path = None, minimal_primitives: bool = False):
"""Load an effect module from a local path (.py or .sexp)."""
if effect_path.suffix == ".sexp":
return SexpEffectModule(effect_path, effects_registry, recipe_dir)
return SexpEffectModule(effect_path, effects_registry, recipe_dir, minimal_primitives)
spec = importlib.util.spec_from_file_location("effect", effect_path)
module = importlib.util.module_from_spec(spec)
@@ -981,6 +981,11 @@ def execute_plan(plan_path: Path = None, output_path: Path = None, recipe_dir: P
if effects_registry:
print(f"Effects registry: {list(effects_registry.keys())}", file=sys.stderr)
# Check for minimal primitives mode
minimal_primitives = plan.get("minimal_primitives", False)
if minimal_primitives:
print(f"Minimal primitives mode: enabled", file=sys.stderr)
# Execute steps
results = {} # step_id -> output_path
work_dir = Path(tempfile.mkdtemp(prefix="artdag_exec_"))
@@ -1124,7 +1129,7 @@ def execute_plan(plan_path: Path = None, output_path: Path = None, recipe_dir: P
if effect_path:
full_path = recipe_dir / effect_path
effect_module = load_effect(full_path, effects_registry, recipe_dir)
effect_module = load_effect(full_path, effects_registry, recipe_dir, minimal_primitives)
params = {k: v for k, v in config.items()
if k not in ("effect", "effect_path", "cid", "encoding", "multi_input")}
print(f" Effect: {effect_name}", file=sys.stderr)
@@ -1420,7 +1425,7 @@ def execute_plan(plan_path: Path = None, output_path: Path = None, recipe_dir: P
if effect_path:
full_path = recipe_dir / effect_path
effect_module = load_effect(full_path, effects_registry, recipe_dir)
effect_module = load_effect(full_path, effects_registry, recipe_dir, minimal_primitives)
params = {k: v for k, v in effect_config.items()
if k not in ("effect", "effect_path", "cid", "encoding", "type")}
print(f" COMPOUND [{i+1}/{len(effects)}]: {effect_name} (Python)", file=sys.stderr)