Skip to content

Commit eeec9ff

Browse files
Merge pull request #89 from InauguralSystems/copilot/improve-existing-features
Improve predicate-driven observation tracking in observer analyzer
2 parents 7976376 + fe29a57 commit eeec9ff

2 files changed

Lines changed: 73 additions & 25 deletions

File tree

src/eigenscript/compiler/analysis/observer.py

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
This implements zero-cost abstraction: pay for geometric semantics only when used.
1111
"""
1212

13-
from typing import Set
13+
from typing import Optional, Set
1414
from eigenscript.parser.ast_builder import (
1515
ASTNode,
1616
Identifier,
@@ -26,6 +26,7 @@
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."""

tests/test_observer_predicates.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
Tests for predicate handling in the ObserverAnalyzer.
3+
4+
Ensure that predicate usage marks the relevant variables as observed so
5+
predicate checks operate on EigenValue-tracked variables.
6+
"""
7+
8+
import textwrap
9+
10+
from eigenscript.lexer import Tokenizer
11+
from eigenscript.parser import Parser
12+
from eigenscript.compiler.analysis.observer import ObserverAnalyzer
13+
14+
15+
def _analyze(code: str):
16+
"""Helper to run observer analysis on EigenScript code."""
17+
source = textwrap.dedent(code).strip()
18+
tokens = Tokenizer(source).tokenize()
19+
ast = Parser(tokens).parse()
20+
analyzer = ObserverAnalyzer()
21+
return analyzer.analyze(ast.statements)
22+
23+
24+
def test_predicate_marks_last_assignment_observed():
25+
"""A predicate condition should mark the last assigned variable as observed."""
26+
observed = _analyze(
27+
"""
28+
x is 1
29+
if converged:
30+
x is x + 1
31+
"""
32+
)
33+
assert "x" in observed
34+
35+
36+
def test_predicate_with_not_marks_last_assignment_observed():
37+
"""NOT predicate conditions should also mark the last assigned variable."""
38+
observed = _analyze(
39+
"""
40+
value is 0
41+
loop while not converged:
42+
value is value + 1
43+
"""
44+
)
45+
assert "value" in observed

0 commit comments

Comments
 (0)