79 lines
2.5 KiB
Python
79 lines
2.5 KiB
Python
|
|
"""
|
||
|
|
Safe predicate evaluator for XIC v2 control flow.
|
||
|
|
Supports simple expressions referencing fused symbol fields and helper functions.
|
||
|
|
Allowed: comparisons, boolean ops, dominant_contains('glyph://id').
|
||
|
|
"""
|
||
|
|
import ast
|
||
|
|
from typing import Any, Dict
|
||
|
|
|
||
|
|
ALLOWED_NODE_TYPES = (
|
||
|
|
ast.Expression, ast.BoolOp, ast.BinOp, ast.UnaryOp, ast.Compare,
|
||
|
|
ast.Name, ast.Load, ast.Constant, ast.Call, ast.Attribute,
|
||
|
|
ast.And, ast.Or, ast.Not,
|
||
|
|
ast.Gt, ast.Lt, ast.GtE, ast.LtE, ast.Eq, ast.NotEq, ast.In, ast.NotIn
|
||
|
|
)
|
||
|
|
|
||
|
|
def _validate_node(node: ast.AST):
|
||
|
|
"""Recursively validate AST node is safe for eval."""
|
||
|
|
if not isinstance(node, ALLOWED_NODE_TYPES):
|
||
|
|
raise ValueError(f"Unsafe predicate node: {type(node).__name__}")
|
||
|
|
for child in ast.iter_child_nodes(node):
|
||
|
|
_validate_node(child)
|
||
|
|
|
||
|
|
class DotDict:
|
||
|
|
"""Helper class that allows dict access via dot notation."""
|
||
|
|
def __init__(self, data: Dict[str, Any]):
|
||
|
|
self.__dict__.update(data)
|
||
|
|
|
||
|
|
def _build_context(fused: Dict[str, Any], dominant: list):
|
||
|
|
"""Build safe evaluation context with helpers and fused symbol fields."""
|
||
|
|
def dominant_contains(glyph_id: str) -> bool:
|
||
|
|
"""Check if a glyph is in the dominant list."""
|
||
|
|
return any(g == glyph_id for g, _ in dominant)
|
||
|
|
|
||
|
|
safe = {
|
||
|
|
"dominant_contains": dominant_contains,
|
||
|
|
"fused": DotDict(fused or {}),
|
||
|
|
}
|
||
|
|
return safe
|
||
|
|
|
||
|
|
def eval_predicate(
|
||
|
|
expr: str,
|
||
|
|
fused: Dict[str, Any] | None = None,
|
||
|
|
dominant: list | None = None
|
||
|
|
) -> bool:
|
||
|
|
"""
|
||
|
|
Evaluate predicate expression safely.
|
||
|
|
|
||
|
|
Example predicates:
|
||
|
|
"fused.global_resonance_score > 0.7"
|
||
|
|
"dominant_contains('glyph://entropy') and fused.global_resonance_score > 0.5"
|
||
|
|
|
||
|
|
Args:
|
||
|
|
expr: Predicate expression string
|
||
|
|
fused: Fused symbol dict with fields like global_resonance_score, glyph_ids
|
||
|
|
dominant: List of (glyph_id, weight) tuples for dominant glyphs
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Boolean result of predicate evaluation
|
||
|
|
"""
|
||
|
|
if dominant is None:
|
||
|
|
dominant = []
|
||
|
|
|
||
|
|
# Parse and validate AST
|
||
|
|
try:
|
||
|
|
expr_ast = ast.parse(expr, mode="eval")
|
||
|
|
except SyntaxError as e:
|
||
|
|
raise ValueError(f"Invalid predicate syntax: {e}")
|
||
|
|
|
||
|
|
_validate_node(expr_ast)
|
||
|
|
|
||
|
|
# Compile and evaluate
|
||
|
|
compiled = compile(expr_ast, "<predicate>", "eval")
|
||
|
|
safe_ctx = _build_context(fused or {}, dominant)
|
||
|
|
|
||
|
|
try:
|
||
|
|
return bool(eval(compiled, {}, safe_ctx))
|
||
|
|
except Exception as e:
|
||
|
|
raise ValueError(f"Predicate evaluation error: {e}")
|