Skip to content

Commit 0929859

Browse files
authored
Merge pull request #96 from OpenQuantumDesign/type_checking
feat: Type checker for Analog frontend
2 parents b69d5e6 + b937437 commit 0929859

8 files changed

Lines changed: 1981 additions & 1157 deletions

File tree

docs/frontend/analog_frontend.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
The analog frontend provides tools to convert analog source text into a parsed tree and to convert a parsed tree back into source text.
1+
The analog frontend provides tools to convert analog source text into a typed Abstract Syntax Tree (AST) and to convert a typed AST back into source text.
22

33
## Frontend Components
44

@@ -21,7 +21,7 @@ For more details on the analog grammar, see [`analog_grammar.md`](../grammar/ana
2121

2222
### AST Builder
2323

24-
[`AnalogCircuitAST.py`](../../src/oqd_core/frontend/analog/AnalogCircuitAST.py) contains the implementation of the AST Builder. The `parse_analog` function uses the `AnalogASTBuilder` class to convert the analog source text into an [`AnalogCircuit`][oqd_core.interface.analog.circuit.AnalogCircuit].
24+
[`AnalogCircuitAST.py`](../../src/oqd_core/frontend/analog/AnalogCircuitAST.py) contains the implementation of the AST Builder. The `parse_analog` function uses the `AnalogASTBuilder` class to convert the analog source text into an [`AnalogCircuit`][oqd_core.interface.analog.circuit.AnalogCircuit]. It produces a typed AST of the source code.
2525

2626
### Serializer
2727

@@ -65,3 +65,12 @@ serialized = serialize_analog(circuit)
6565
```
6666

6767
///
68+
69+
### Type Checker
70+
71+
The analog type checker is implemented by the `AnalogTypeChecker` class in [`type_checker.py`](../../src/oqd_core/frontend/analog/type_checker.py). The `AnalogTypeLattice` class defines a concrete lattice for analog types with `leq`, `join`, and `meet` methods. The type checker builds a Control Flow Graph (CFG) using methods from `cfg.py` from the typed AST, and runs the forward dataflow analysis on the CFG.
72+
73+
### Control Flow Graph
74+
75+
The analog Control Flow Graph (CFG) is implemented by the `CFGNode` and `AnalogCFGBuilder` classes in [`cfg.py`](../../src/oqd_core/frontend/analog/cfg.py). This script also implements an infinite loop checker by identifying strongly connected components (SCCs) in the CFG.
76+

examples/analog/test.analog

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ I = %I
2525
C = %C
2626
A = %A
2727
J = %J
28-
H_single = 0.5 * %X + 0.5 * %Z
28+
H_single = 0.5 %* %X %+ 0.5 %* %Z
2929
H_pair = (%X %@ %I) %+ (%I %@ %X)
30-
H_rabi = (0.5 * #t) * (%X %@ %I) %+ 0.5 * (%I %@ %Y)
30+
H_rabi = (0.5 * #t) %* (%X %@ %I) %+ 0.5 %* (%I %@ %Y)
3131

3232
//control flow
3333
x = 1
@@ -69,7 +69,7 @@ initialize(r)
6969
// evolve(hamiltonian, duration, target)
7070
evolve(H_single, 2.0, r)
7171
evolve(H_pair, 1.0, targets)
72-
evolve(%X * 0.5, pi, q0)
72+
evolve(%X %* 0.5, pi, q0)
7373

7474
measure(r)
7575
measurement = measure(q0)

src/oqd_core/analysis/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .utils import CFGNode, SCCAnalysis
2+
3+
########################################################################################
4+
__all__ = [
5+
"CFGNode",
6+
"SCCAnalysis",
7+
]

src/oqd_core/analysis/utils.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Copyright 2024-2025 Open Quantum Design
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
17+
class CFGNode:
18+
def __init__(self, register_id, stmt, preds = None, kind = "stmt"):
19+
self.register_id = register_id
20+
self.stmt = stmt
21+
self.preds = list(preds) if preds is not None else []
22+
self.succs = []
23+
self.kind = kind
24+
self.exit_nodes = []
25+
self.edge_labels = {}
26+
27+
def add_succ(self, succ, label=None):
28+
if succ not in self.succs:
29+
self.succs.append(succ)
30+
if label is not None:
31+
self.edge_labels[succ.register_id] = label
32+
33+
def add_pred(self, pred, label=None):
34+
if pred not in self.preds:
35+
self.preds.append(pred)
36+
pred.add_succ(self, label=label)
37+
38+
def add_preds(self, preds, label=None):
39+
for pred in preds:
40+
self.add_pred(pred, label=label)
41+
42+
def to_dict(self):
43+
if isinstance(self.stmt, str):
44+
stmt_repr = self.stmt
45+
elif hasattr(self.stmt, "class_"):
46+
stmt_repr = self.stmt.class_
47+
else:
48+
stmt_repr = type(self.stmt).__name__
49+
return {
50+
"id": self.register_id,
51+
"kind": self.kind,
52+
"stmt": stmt_repr,
53+
"preds": [p.register_id for p in self.preds],
54+
"succs": [c.register_id for c in self.succs],
55+
"edges": [
56+
{"to": c.register_id, "label": self.edge_labels.get(c.register_id)}
57+
for c in self.succs
58+
],
59+
"exit_nodes": [n.register_id for n in self.exit_nodes],
60+
}
61+
62+
63+
64+
65+
class SCCAnalysis:
66+
def __init__(self, cfg):
67+
self.cfg = cfg
68+
self.time = 0
69+
self.disc = {nid: -1 for nid in cfg}
70+
self.low = {nid: -1 for nid in cfg}
71+
self.on_stack = {nid: False for nid in cfg}
72+
self.stack = []
73+
self.sccs = []
74+
75+
def dfs(self, u):
76+
self.disc[u] = self.time
77+
self.low[u] = self.time
78+
self.time += 1
79+
self.stack.append(u)
80+
self.on_stack[u] = True
81+
for succ in self.cfg[u].succs:
82+
v = succ.register_id
83+
if self.disc[v] == -1:
84+
self.dfs(v)
85+
self.low[u] = min(self.low[u], self.low[v])
86+
elif self.on_stack[v]:
87+
self.low[u] = min(self.low[u], self.disc[v])
88+
if self.low[u] == self.disc[u]:
89+
comp = set()
90+
while True:
91+
w = self.stack.pop()
92+
self.on_stack[w] = False
93+
comp.add(w)
94+
if w == u:
95+
break
96+
self.sccs.append(comp)
97+
98+
def run(self):
99+
for nid in self.cfg:
100+
if self.disc[nid] == -1:
101+
self.dfs(nid)
102+
return self.sccs
103+
104+
def edge_feasible(self, src, dst_id):
105+
if src.kind == "branch":
106+
label = src.edge_labels.get(dst_id)
107+
if src.stmt.value is True and label == "false":
108+
return False
109+
if src.stmt.value is False and label == "true":
110+
return False
111+
return True
112+
113+
def infinite_loop_check(self):
114+
sccs = self.run()
115+
stop_ids = {nid for nid, node in self.cfg.items() if node.kind == "stop"}
116+
for comp in sccs:
117+
has_cycle = len(comp) > 1 or any(
118+
succ.register_id == nid
119+
for nid in comp
120+
for succ in self.cfg[nid].succs
121+
)
122+
if not has_cycle:
123+
continue
124+
125+
has_exit = any(
126+
(succ.register_id not in comp) and self.edge_feasible(self.cfg[nid], succ.register_id)
127+
for nid in comp
128+
for succ in self.cfg[nid].succs
129+
)
130+
131+
stack = list(comp)
132+
seen = set(comp)
133+
can_reach_stop = False
134+
while stack:
135+
curr = stack.pop()
136+
if curr in stop_ids:
137+
can_reach_stop = True
138+
break
139+
for succ in self.cfg[curr].succs:
140+
sid = succ.register_id
141+
if not self.edge_feasible(self.cfg[curr], sid):
142+
continue
143+
if sid not in seen:
144+
seen.add(sid)
145+
stack.append(sid)
146+
147+
if not has_exit and not can_reach_stop:
148+
raise TypeError(
149+
f"Infinite loop detected in circuit: {sorted(comp)}"
150+
)
151+
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright 2024-2025 Open Quantum Design
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License")
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
from __future__ import annotations
17+
18+
from oqd_compiler_infrastructure import RewriteRule
19+
20+
from oqd_core.analysis.utils import CFGNode
21+
from oqd_core.interface.analog import (
22+
AnalogCircuit,
23+
Break,
24+
Continue,
25+
IfElse,
26+
While,
27+
)
28+
29+
30+
class AnalogCFGBuilder(RewriteRule):
31+
def __init__(self):
32+
super().__init__()
33+
self.registry = 0
34+
self.cache = {}
35+
self.loop_stack = []
36+
self.preds = []
37+
self.founder = None
38+
self.last_node = None
39+
self.edge_labels = None
40+
self.fallthrough_labels = {}
41+
42+
43+
def new_node(self, preds, stmt, kind = "stmt"):
44+
node = CFGNode(register_id=self.registry, stmt=stmt,preds=preds, kind=kind)
45+
self.cache[node.register_id] = node
46+
self.registry += 1
47+
48+
explicit_labels = self.edge_labels or {}
49+
self.edge_labels = None
50+
51+
for pred in node.preds:
52+
label = explicit_labels.get(pred.register_id)
53+
if label is None:
54+
label = self.fallthrough_labels.pop(pred.register_id, None)
55+
pred.add_succ(node, label=label)
56+
57+
return node
58+
59+
def walk_stmt(self, stmt, preds, edge_labels=None):
60+
old = self.preds
61+
old_labels = self.edge_labels
62+
self.preds = preds
63+
self.edge_labels = edge_labels
64+
result = self(stmt)
65+
self.preds = old
66+
self.edge_labels = old_labels
67+
return result
68+
69+
def walk_block(self, statements, preds, entry_label=None):
70+
pred = preds
71+
first = True
72+
for stmt in statements:
73+
edge_labels = None
74+
if first and entry_label is not None:
75+
edge_labels = {p.register_id: entry_label for p in pred}
76+
pred = self.walk_stmt(stmt, pred, edge_labels=edge_labels)
77+
first = False
78+
return pred
79+
80+
def run(self, circuit: AnalogCircuit):
81+
self.registry = 0
82+
self.cache = {}
83+
self.loop_stack = []
84+
self.edge_labels = None
85+
self.fallthrough_labels = {}
86+
self.founder = self.new_node([], "start", kind="start")
87+
exits = self.walk_stmt(circuit, [self.founder])
88+
self.last_node = self.new_node(exits, "stop", kind="stop")
89+
return self.cache
90+
91+
def map_AnalogCircuit(self, model: AnalogCircuit):
92+
return self.walk_block(model.statements, self.preds)
93+
94+
def map_IfElse(self, model: IfElse):
95+
node = self.new_node(self.preds, model.condition, kind="branch")
96+
then_branch = self.walk_block(model.then_branch, [node], entry_label="true")
97+
if model.else_branch:
98+
else_branch = self.walk_block(model.else_branch, [node], entry_label="false")
99+
return list(then_branch) + (list(else_branch))
100+
101+
self.fallthrough_labels[node.register_id] = "false"
102+
103+
return list(then_branch) + [node]
104+
105+
def map_While(self, model: While):
106+
node = self.new_node(self.preds, model.condition, kind="branch")
107+
self.loop_stack.append(node)
108+
body = self.walk_block(model.body, [node], entry_label="true")
109+
self.loop_stack.pop()
110+
111+
node.add_preds(body)
112+
self.fallthrough_labels[node.register_id] = "false"
113+
114+
return node.exit_nodes + [node]
115+
116+
def map_Break(self, model: Break):
117+
if not self.loop_stack:
118+
raise TypeError("break statement used outside loop")
119+
break_node = self.new_node(self.preds, model)
120+
self.loop_stack[-1].exit_nodes.append(break_node)
121+
self.fallthrough_labels[break_node.register_id] = "break"
122+
return []
123+
124+
def map_Continue(self, model: Continue):
125+
if not self.loop_stack:
126+
raise TypeError("continue statement used outside loop")
127+
continue_node = self.new_node(self.preds, model)
128+
self.loop_stack[-1].add_pred(continue_node, label="continue")
129+
return []
130+
131+
def generic_map(self, model):
132+
return [self.new_node(self.preds, model)]
133+
134+

0 commit comments

Comments
 (0)