Skip to content

Commit c2fe459

Browse files
committed
Update interpreter for better CPython compatibilities
1 parent b2489af commit c2fe459

File tree

1 file changed

+181
-95
lines changed

1 file changed

+181
-95
lines changed

monic/expressions/interpreter.py

Lines changed: 181 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ def __init__(self, context: ExpressionsContext | None = None) -> None:
180180
"StopIteration": StopIteration,
181181
"TimeoutError": TimeoutError,
182182
"RuntimeError": RuntimeError,
183+
"SyntaxError": SyntaxError,
184+
"IndentationError": IndentationError,
185+
"AttributeError": AttributeError,
183186
"SecurityError": SecurityError,
184187
}
185188

@@ -802,7 +805,7 @@ def visit_Try(self, node: ast.Try) -> None:
802805
handled = False
803806
for handler in node.handlers:
804807
if handler.type is None:
805-
exc_class = Exception
808+
exc_class: t.Type[BaseException] = Exception
806809
else:
807810
exc_class = self._get_exception_class(handler.type)
808811

@@ -827,51 +830,79 @@ def visit_Try(self, node: ast.Try) -> None:
827830
for stmt in node.finalbody:
828831
self.visit(stmt)
829832

830-
def _get_exception_class(self, node: ast.expr) -> t.Type[Exception]:
831-
if isinstance(node, ast.Name):
832-
class_name = node.id
833-
if class_name in globals()["__builtins__"]:
834-
exc_class = globals()["__builtins__"][class_name]
835-
if isinstance(exc_class, type) and issubclass(
836-
exc_class, Exception
837-
):
838-
return exc_class
839-
raise NameError(
840-
f"Name '{class_name}' is not defined or is not an exception "
841-
"class"
842-
)
843-
elif isinstance(node, ast.Attribute):
844-
value = self.visit(node.value)
845-
attr = node.attr
846-
if hasattr(value, attr):
847-
exc_class = getattr(value, attr)
848-
if isinstance(exc_class, type) and issubclass(
849-
exc_class, Exception
850-
):
851-
return exc_class
852-
raise NameError(f"'{attr}' is not a valid exception class")
853-
else:
854-
raise TypeError(
855-
f"Invalid exception class specification: {ast.dump(node)}"
856-
)
833+
def _get_exception_class(self, node: ast.expr) -> t.Type[BaseException]:
834+
"""Resolve the AST node to an actual exception class object."""
835+
# Use self.visit to evaluate the expression node (which might be a Name
836+
# like 'ValueError' or user-defined classes like 'ValidationError', or
837+
# even dotted attributes). This way, if the user code declared:
838+
#
839+
# class ValidationError(InputError): pass
840+
#
841+
# then we have that class in self.local_env, and self.visit(node)
842+
# will retrieve it.
843+
value = self.visit(node)
844+
845+
# Make sure it's a subclass of BaseException (i.e. an Exception).
846+
if isinstance(value, type) and issubclass(value, BaseException):
847+
return value
848+
849+
# Otherwise, it's not a valid exception class.
850+
# If "value" is, for instance, a string or a function, we raise.
851+
# (If "value" doesn't have a __name__, make a fallback name.)
852+
value_name = getattr(value, "__name__", repr(value))
853+
raise NameError(
854+
f"Name '{value_name}' is not defined or is not an exception class"
855+
)
857856

858857
def visit_Raise(self, node: ast.Raise) -> None:
858+
"""
859+
Handle raise statements that preserve exception chaining.
860+
861+
- If node.exc is a call (e.g. TypeError("msg")), we create an instance.
862+
- If node.exc is already an exception instance, we just raise it as-is.
863+
- If node.cause is specified, we set that as the __cause__.
864+
"""
859865
if node.exc is None:
860866
raise RuntimeError("No active exception to re-raise")
861867

862-
exc = self.visit(node.exc)
863-
if isinstance(exc, type) and issubclass(exc, Exception):
864-
if node.cause:
865-
cause = self.visit(node.cause)
866-
raise exc from cause
867-
raise exc()
868-
else:
869-
if isinstance(exc, BaseException):
870-
raise exc
868+
# Evaluate the main 'exc'
869+
# Could be a class like ValueError, or an instance like ValueError()
870+
exc_obj = self.visit(node.exc)
871+
872+
# Evaluate the 'from' cause if present
873+
cause_obj = self.visit(node.cause) if node.cause else None
874+
875+
# If exc_obj is already an exception instance
876+
if isinstance(exc_obj, BaseException):
877+
if cause_obj is not None:
878+
if not isinstance(cause_obj, BaseException):
879+
raise TypeError(
880+
"Expected an exception instance for 'from' cause, "
881+
f"got {type(cause_obj).__name__}"
882+
)
883+
# Raise existing exception instance with cause
884+
raise exc_obj from cause_obj
871885
else:
872-
raise TypeError(
873-
f"Expected an exception instance, got {type(exc).__name__}"
874-
)
886+
raise exc_obj
887+
888+
# If exc_obj is a class (like ValueError) rather than an instance
889+
if isinstance(exc_obj, type) and issubclass(exc_obj, BaseException):
890+
new_exc = exc_obj() # Instantiate
891+
if cause_obj is not None:
892+
if not isinstance(cause_obj, BaseException):
893+
raise TypeError(
894+
"Expected an exception instance for 'from' cause, "
895+
f"got {type(cause_obj).__name__}"
896+
)
897+
raise new_exc from cause_obj
898+
else:
899+
raise new_exc
900+
901+
# Otherwise it's not a valid exception type or instance
902+
raise TypeError(
903+
"Expected an exception instance or class, got "
904+
f"{type(exc_obj).__name__}"
905+
)
875906

876907
def visit_With(self, node: ast.With) -> None:
877908
"""
@@ -1595,72 +1626,127 @@ def visit_Call(self, node: ast.Call) -> t.Any:
15951626
# Call the function
15961627
return self._call_function(func, pos_args, kwargs)
15971628

1598-
def visit_GeneratorExp(
1599-
self, node: ast.GeneratorExp
1600-
) -> t.Generator[t.Any, None, None]:
1601-
"""Handle generator expressions."""
1602-
# Create new scope for the generator expression
1603-
gen_scope = Scope()
1604-
self.scope_stack.append(gen_scope)
1629+
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> t.Any:
1630+
"""
1631+
Handle generator expressions with closure semantics.
16051632
1606-
# Copy the outer environment
1607-
outer_env = self.local_env
1608-
self.local_env = outer_env.copy()
1633+
Capture the current local_env when the generator expression is created
1634+
so that references to local variables (like 'y=2') remain valid even
1635+
after returning from the enclosing function.
16091636
1610-
try:
1637+
Example user code that requires closure:
1638+
def gen():
1639+
y = 2
1640+
return (x + y + i for i in range(3))
1641+
1642+
Without closure capturing, "y" would appear undefined once 'gen'
1643+
returns.
1644+
"""
1645+
# Copy (snapshot) the current local environment for closure
1646+
closure_env = dict(self.local_env)
1647+
1648+
def _generator_expression_runner():
1649+
"""
1650+
A helper that reinstalls the 'closure_env' as 'self.local_env'
1651+
whenever we iterate the generator, mimicking how Python closures
1652+
keep references to their defining scopes.
1653+
"""
1654+
prev_env = self.local_env
1655+
self.local_env = closure_env
1656+
try:
1657+
# Actually produce the items by calling the comprehension logic
1658+
yield from self._evaluate_generator_exp(node)
1659+
finally:
1660+
self.local_env = prev_env
16111661

1612-
def generator() -> t.Generator[t.Any, None, None]:
1613-
def process_generator(
1614-
generators: list, index: int = 0
1615-
) -> t.Generator[t.Any, None, None]:
1616-
if index >= len(generators):
1617-
# Base case: all generators processed, yield element
1618-
value = self.visit(node.elt)
1619-
yield value
1620-
return
1662+
return _generator_expression_runner()
16211663

1622-
generator = generators[index]
1623-
iter_obj = self.visit(generator.iter)
1664+
def _evaluate_generator_exp(
1665+
self, node: ast.GeneratorExp
1666+
) -> t.Iterator[t.Any]:
1667+
"""
1668+
Evaluate an already-constructed generator expression node under the
1669+
current (already snapshotted) self.local_env.
1670+
"""
16241671

1625-
# Save the current environment before processing this
1626-
# generator
1627-
current_env = self.local_env.copy()
1672+
def inner():
1673+
# The logic below closely matches typical generator expression
1674+
# evaluation: we handle node.generators (the "for" clauses, possibly
1675+
# with if-filters) and node.elt (the yield expression).
1676+
generators = node.generators
1677+
1678+
# We can reuse your existing "process_generator" or whatever method
1679+
# you have. The snippet below shows a direct approach:
1680+
def process(gens, index=0):
1681+
"""
1682+
Recursively handle each comprehension generator in 'gens'.
1683+
Once we exhaust them, evaluate node.elt and yield it.
1684+
"""
1685+
if index >= len(gens):
1686+
# If no more loops, the expression is node.elt
1687+
yield self.visit(node.elt)
1688+
return
16281689

1629-
for item in iter_obj:
1630-
# Restore environment from before this generator's loop
1631-
self.local_env = current_env.copy()
1690+
gen = gens[index]
1691+
iter_obj = self.visit(gen.iter)
16321692

1633-
try:
1634-
self._handle_unpacking_target(
1635-
generator.target, item
1636-
)
1637-
except (TypeError, ValueError, SyntaxError):
1638-
if isinstance(generator.target, ast.Name):
1639-
self._set_name_value(generator.target.id, item)
1640-
else:
1641-
raise
1693+
try:
1694+
iterator = iter(iter_obj)
1695+
except TypeError as e:
1696+
# If not iterable
1697+
raise TypeError(
1698+
f"object is not iterable: {iter_obj}"
1699+
) from e
1700+
1701+
for item in iterator:
1702+
# Assign the loop target
1703+
self._assign_comprehension_target(gen.target, item)
1704+
# Check any if-conditions
1705+
if gen.ifs:
1706+
skip = False
1707+
for if_test in gen.ifs:
1708+
condition = self.visit(if_test)
1709+
if not condition:
1710+
skip = True
1711+
break
1712+
if skip:
1713+
continue
16421714

1643-
# Check if conditions
1644-
if all(
1645-
self.visit(if_clause) for if_clause in generator.ifs
1646-
):
1647-
# Process next generator or yield result
1648-
yield from process_generator(generators, index + 1)
1715+
# Recurse to handle the next generator
1716+
yield from process(gens, index + 1)
16491717

1650-
# Update outer environment with any named expression
1651-
# bindings
1652-
for name, value in self.local_env.items():
1653-
if name not in current_env:
1654-
outer_env[name] = value
1718+
yield from process(generators)
16551719

1656-
# Start processing generators recursively
1657-
yield from process_generator(node.generators)
1720+
yield from inner()
16581721

1659-
return generator()
1660-
finally:
1661-
# Restore the outer environment and pop the scope
1662-
self.local_env = outer_env
1663-
self.scope_stack.pop()
1722+
def _assign_comprehension_target(
1723+
self, target: ast.expr, value: t.Any
1724+
) -> None:
1725+
"""
1726+
Assign 'value' to the target node within a comprehension. This can be
1727+
something like 'for i in range(5)', or 'for k, v in items'.
1728+
If it's just a Name node, we store in self.local_env. If it's a tuple
1729+
or list, we destructure.
1730+
"""
1731+
if isinstance(target, ast.Name):
1732+
# This is a simple Name like: for i in ...
1733+
self._set_name_value(target.id, value)
1734+
elif isinstance(target, (ast.Tuple, ast.List)):
1735+
# Destructure the value into the sub-targets
1736+
if not isinstance(value, (tuple, list)):
1737+
raise TypeError(f"cannot unpack non-iterable value {value}")
1738+
if len(value) != len(target.elts):
1739+
raise ValueError(
1740+
f"cannot unpack {len(value)} values into "
1741+
f"{len(target.elts)} targets"
1742+
)
1743+
for t_elt, v_elt in zip(target.elts, value):
1744+
self._assign_comprehension_target(t_elt, v_elt)
1745+
else:
1746+
# For something more complex, handle similarly or raise an error
1747+
raise NotImplementedError(
1748+
f"unsupported comprehension target type: {type(target)}"
1749+
)
16641750

16651751
def visit_List(self, node: ast.List) -> list:
16661752
return [self.visit(elt) for elt in node.elts]

0 commit comments

Comments
 (0)