1010This implements zero-cost abstraction: pay for geometric semantics only when used.
1111"""
1212
13- from typing import Set
13+ from typing import Optional , Set
1414from eigenscript .parser .ast_builder import (
1515 ASTNode ,
1616 Identifier ,
2626 ListLiteral ,
2727 Index ,
2828 Program ,
29+ TentativeAssignment ,
2930)
3031
3132
@@ -40,10 +41,19 @@ class ObserverAnalyzer:
4041 Unobserved variables can be compiled to raw doubles for maximum performance.
4142 """
4243
44+ PREDICATE_NAMES = {
45+ "converged" ,
46+ "diverging" ,
47+ "oscillating" ,
48+ "stable" ,
49+ "improving" ,
50+ }
51+
4352 def __init__ (self ):
4453 self .observed : Set [str ] = set ()
4554 self .user_functions : Set [str ] = set ()
4655 self .current_function : str = None
56+ self .last_assigned : Optional [str ] = None
4757
4858 def analyze (self , ast_nodes : list [ASTNode ]) -> Set [str ]:
4959 """Analyze AST and return set of variable names that need EigenValue tracking.
@@ -58,6 +68,7 @@ def analyze(self, ast_nodes: list[ASTNode]) -> Set[str]:
5868 self .observed = set ()
5969 self .user_functions = set ()
6070 self .current_function = None
71+ self .last_assigned = None
6172
6273 # First pass: collect all user-defined function names
6374 for node in ast_nodes :
@@ -82,17 +93,22 @@ def _visit(self, node: ASTNode):
8293 elif isinstance (node , FunctionDef ):
8394 # Function parameters are always observed (might be interrogated inside)
8495 prev_function = self .current_function
96+ prev_last_assigned = self .last_assigned
8597 self .current_function = node .name
98+ self .last_assigned = None
8699
87100 # In EigenScript, functions implicitly have parameter 'n'
88101 self .observed .add ("n" )
89102
90103 for stmt in node .body :
91104 self ._visit (stmt )
92105
106+ self .last_assigned = prev_last_assigned
93107 self .current_function = prev_function
94108
95- elif isinstance (node , Assignment ):
109+ elif isinstance (node , (Assignment , TentativeAssignment )):
110+ # Assignment/TentativeAssignment identifier is a string name of the target
111+ self .last_assigned = node .identifier
96112 self ._visit (node .expression )
97113
98114 elif isinstance (node , Interrogative ):
@@ -152,32 +168,19 @@ def _visit(self, node: ASTNode):
152168 self ._visit (node .list_expr )
153169 self ._visit (node .index_expr )
154170
155- elif isinstance (node , Identifier ):
156- # Check if this identifier is a predicate
157- if node .name in [
158- "converged" ,
159- "diverging" ,
160- "oscillating" ,
161- "stable" ,
162- "improving" ,
163- ]:
164- # Predicates require the last variable to be observed
165- # This is a simplified heuristic - ideally we'd track scope
166- pass
167-
168171 def _check_for_predicates (self , node : ASTNode ):
169172 """Check if condition uses predicates (converged, diverging, etc.)."""
173+ if node is None :
174+ return
175+
170176 if isinstance (node , Identifier ):
171- if node .name in [
172- "converged" ,
173- "diverging" ,
174- "oscillating" ,
175- "stable" ,
176- "improving" ,
177- ]:
178- # TODO: Mark the variable being tested as observed
179- # For now, this is handled by the codegen heuristic of "last variable"
180- pass
177+ if node .name in self .PREDICATE_NAMES and self .last_assigned :
178+ self .observed .add (self .last_assigned )
179+ elif isinstance (node , UnaryOp ):
180+ self ._check_for_predicates (node .operand )
181+ elif isinstance (node , BinaryOp ):
182+ self ._check_for_predicates (node .left )
183+ self ._check_for_predicates (node .right )
181184
182185 def _mark_expression_observed (self , node : ASTNode ):
183186 """Recursively mark all identifiers in an expression as observed."""
0 commit comments