Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 5 additions & 138 deletions src/kida/compiler/partial_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from dataclasses import dataclass, replace
from typing import Any, final

from kida.compiler import partial_eval_constants as _constants
from kida.nodes import (
BinOp,
Block,
Expand Down Expand Up @@ -73,8 +74,10 @@
With,
)

# Sentinel for "evaluation failed" — distinct from None (which is a valid result)
_UNRESOLVED = object()
_UNRESOLVED = _constants.UNRESOLVED
_PARTIAL_EVAL_EXCEPTIONS = _constants.PARTIAL_EVAL_EXCEPTIONS
_compare_op = _constants.compare_op
_try_eval_const_only = _constants.try_eval_const_only

# Maximum number of iterations to unroll in a static for-loop
_MAX_UNROLL = 200
Expand Down Expand Up @@ -143,121 +146,11 @@ class _LoopProperties:
revindex0: int


# Expected errors when constant folding fails — fall back to runtime evaluation.
# Narrowing avoids silently swallowing KeyboardInterrupt, SystemExit, etc.
_PARTIAL_EVAL_EXCEPTIONS: tuple[type[BaseException], ...] = (
TypeError,
KeyError,
IndexError,
AttributeError,
ValueError,
OverflowError,
ZeroDivisionError,
)


# ---------------------------------------------------------------------------
# Dead code elimination (const-only, no static_context)
# ---------------------------------------------------------------------------


def _try_eval_const_only(expr: Expr) -> Any:
"""Evaluate expression using only literals and constant expressions.

Resolves Const, BinOp, UnaryOp, Compare, BoolOp. No Name/Getattr/Getitem
(those require static_context). Used for dead code elimination.
"""
match expr:
case Const():
return expr.value

case BinOp():
left = _try_eval_const_only(expr.left)
right = _try_eval_const_only(expr.right)
if left is _UNRESOLVED or right is _UNRESOLVED:
return _UNRESOLVED
try:
if expr.op == "+":
return left + right
if expr.op == "-":
return left - right
if expr.op == "*":
return left * right
if expr.op == "/":
return left / right
if expr.op == "//":
return left // right
if expr.op == "%":
return left % right
if expr.op == "**":
return left**right
if expr.op == "~":
# Compile-time only: operands are constants, never Markup.
# Runtime ~ uses _markup_concat for Markup preservation.
return str(left) + str(right)
except _PARTIAL_EVAL_EXCEPTIONS:
return _UNRESOLVED
return _UNRESOLVED

case UnaryOp():
operand = _try_eval_const_only(expr.operand)
if operand is _UNRESOLVED:
return _UNRESOLVED
try:
if expr.op == "-":
return -operand
if expr.op == "+":
return +operand
if expr.op == "not":
return not operand
except _PARTIAL_EVAL_EXCEPTIONS:
return _UNRESOLVED
return _UNRESOLVED

case Compare():
left = _try_eval_const_only(expr.left)
if left is _UNRESOLVED:
return _UNRESOLVED
for op, comp_node in zip(expr.ops, expr.comparators, strict=True):
right = _try_eval_const_only(comp_node)
if right is _UNRESOLVED:
return _UNRESOLVED
try:
result = _compare_op(op, left, right)
except _PARTIAL_EVAL_EXCEPTIONS:
return _UNRESOLVED
if not result:
return False
left = right
return True

case BoolOp():
if expr.op == "and":
for val_node in expr.values:
val = _try_eval_const_only(val_node)
if val is _UNRESOLVED:
return _UNRESOLVED
if not val:
return val
return val
for val_node in expr.values:
val = _try_eval_const_only(val_node)
if val is _UNRESOLVED:
return _UNRESOLVED
if val:
return val
return val

case CondExpr():
test = _try_eval_const_only(expr.test)
if test is _UNRESOLVED:
return _UNRESOLVED
return _try_eval_const_only(expr.if_true if test else expr.if_false)

case _:
return _UNRESOLVED


def _body_has_scoping_nodes(nodes: Sequence[Node]) -> bool:
"""True if body contains Set, Let, Capture, or Export (block-scoped)."""
for n in nodes:
Expand Down Expand Up @@ -2157,32 +2050,6 @@ class _InlinedBody(Node):
nodes: Sequence[Node] = ()


def _compare_op(op: str, left: Any, right: Any) -> bool:
"""Evaluate a comparison operator."""
if op == "==":
return left == right
if op == "!=":
return left != right
if op == "<":
return left < right
if op == "<=":
return left <= right
if op == ">":
return left > right
if op == ">=":
return left >= right
if op == "in":
return left in right
if op == "not in":
return left not in right
if op == "is":
return left is right
if op == "is not":
return left is not right
msg = f"Unknown comparison operator: {op}"
raise ValueError(msg)


def partial_evaluate(
template: Template,
static_context: dict[str, Any],
Expand Down
151 changes: 151 additions & 0 deletions src/kida/compiler/partial_eval_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""Constant-expression primitives shared by partial-evaluation phases.

This module owns the literal-only evaluator used by dead-code elimination and
the sentinel/error policy used by the static-context evaluator. Keeping these
primitives separate makes the phase boundary explicit without changing the
public ``partial_evaluate()`` or ``eliminate_dead_code()`` entrypoints.
"""

from __future__ import annotations

from typing import Any

from kida.nodes import BinOp, BoolOp, Compare, CondExpr, Const, Expr, UnaryOp

# Distinct from None, which is a valid compile-time result.
UNRESOLVED = object()

# Expected failures fall back to runtime evaluation. Process-control exceptions
# such as KeyboardInterrupt and SystemExit must continue to propagate.
PARTIAL_EVAL_EXCEPTIONS: tuple[type[BaseException], ...] = (
TypeError,
KeyError,
IndexError,
AttributeError,
ValueError,
OverflowError,
ZeroDivisionError,
)


def compare_op(op: str, left: Any, right: Any) -> bool:
"""Evaluate a comparison operator."""
if op == "==":
return left == right
if op == "!=":
return left != right
if op == "<":
return left < right
if op == "<=":
return left <= right
if op == ">":
return left > right
if op == ">=":
return left >= right
if op == "in":
return left in right
if op == "not in":
return left not in right
if op == "is":
return left is right
if op == "is not":
return left is not right
msg = f"Unknown comparison operator: {op}"
raise ValueError(msg)


def try_eval_const_only(expr: Expr) -> Any:
"""Evaluate an expression using only literals and constant operators.

Name, attribute, and item lookups intentionally remain unresolved because
they require a static context. Dead-code elimination uses this narrower
evaluator even when no static context is configured.
"""
match expr:
case Const():
return expr.value

case BinOp():
left = try_eval_const_only(expr.left)
right = try_eval_const_only(expr.right)
if left is UNRESOLVED or right is UNRESOLVED:
return UNRESOLVED
try:
if expr.op == "+":
return left + right
if expr.op == "-":
return left - right
if expr.op == "*":
return left * right
if expr.op == "/":
return left / right
if expr.op == "//":
return left // right
if expr.op == "%":
return left % right
if expr.op == "**":
return left**right
if expr.op == "~":
# Compile-time operands are constants, never Markup.
return str(left) + str(right)
except PARTIAL_EVAL_EXCEPTIONS:
return UNRESOLVED
return UNRESOLVED

case UnaryOp():
operand = try_eval_const_only(expr.operand)
if operand is UNRESOLVED:
return UNRESOLVED
try:
if expr.op == "-":
return -operand
if expr.op == "+":
return +operand
if expr.op == "not":
return not operand
except PARTIAL_EVAL_EXCEPTIONS:
return UNRESOLVED
return UNRESOLVED

case Compare():
left = try_eval_const_only(expr.left)
if left is UNRESOLVED:
return UNRESOLVED
for op, comp_node in zip(expr.ops, expr.comparators, strict=True):
right = try_eval_const_only(comp_node)
if right is UNRESOLVED:
return UNRESOLVED
try:
result = compare_op(op, left, right)
except PARTIAL_EVAL_EXCEPTIONS:
return UNRESOLVED
if not result:
return False
left = right
return True

case BoolOp():
if expr.op == "and":
for val_node in expr.values:
val = try_eval_const_only(val_node)
if val is UNRESOLVED:
return UNRESOLVED
if not val:
return val
return val
for val_node in expr.values:
val = try_eval_const_only(val_node)
if val is UNRESOLVED:
return UNRESOLVED
if val:
return val
return val

case CondExpr():
test = try_eval_const_only(expr.test)
if test is UNRESOLVED:
return UNRESOLVED
return try_eval_const_only(expr.if_true if test else expr.if_false)

case _:
return UNRESOLVED
Loading
Loading