Skip to content
This repository was archived by the owner on Sep 9, 2025. It is now read-only.

Commit 9bfef2c

Browse files
author
Hendrik van Antwerpen
committed
Fix cycle detection logic, and add tests for productive paths
1 parent 0ef964b commit 9bfef2c

File tree

4 files changed

+332
-49
lines changed

4 files changed

+332
-49
lines changed

stack-graphs/src/cycles.rs

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ use std::collections::{HashMap, VecDeque};
3434
use smallvec::SmallVec;
3535

3636
use crate::arena::Handle;
37+
use crate::graph::Edge;
3738
use crate::graph::Node;
3839
use crate::graph::StackGraph;
3940
use crate::partial::PartialPath;
4041
use crate::partial::PartialPaths;
42+
use crate::paths::Extend;
4143
use crate::paths::Path;
4244
use crate::stitching::Database;
4345
use crate::stitching::OwnedOrDatabasePath;
@@ -148,34 +150,48 @@ impl AppendingCycleDetector {
148150
&mut self,
149151
graph: &StackGraph,
150152
partials: &mut PartialPaths,
151-
node: Handle<Node>,
152-
new_path: &PartialPath,
153+
appended_node: Handle<Node>,
153154
) -> bool {
154-
let i = match self.nodes.iter().position(|n| *n == node) {
155+
// find last occurrence of the appended node
156+
let i = self.nodes.iter().rposition(|n| *n == appended_node);
157+
158+
// add new node
159+
self.nodes.push(appended_node);
160+
161+
// check if the appended node was found
162+
let i = match i {
155163
Some(i) => i,
156164
None => return true,
157165
};
158-
let mut rhs = PartialPath::from_node(graph, partials, node);
166+
167+
// construct cyclic path
168+
let mut cyclic_path = PartialPath::from_node(graph, partials, self.nodes[i]);
159169
for node in self.nodes.range(i + 1..) {
160-
graph[*node]
161-
.apply_to_partial_stacks(
170+
if cyclic_path.ends_in_jump(graph) {
171+
// restore jump target
172+
cyclic_path
173+
.scope_stack_precondition
174+
.push_back(partials, *node);
175+
cyclic_path
176+
.scope_stack_postcondition
177+
.push_front(partials, *node);
178+
cyclic_path.resolve(graph, partials).unwrap();
179+
}
180+
cyclic_path
181+
.append(
162182
graph,
163183
partials,
164-
&mut rhs.symbol_stack_precondition,
165-
&mut rhs.scope_stack_precondition,
166-
&mut rhs.symbol_stack_postcondition,
167-
&mut rhs.scope_stack_postcondition,
184+
Edge {
185+
source: cyclic_path.end_node,
186+
sink: *node,
187+
precedence: 0,
188+
},
168189
)
169190
.unwrap();
170191
}
171-
let mut loop_path = new_path.clone();
172-
if loop_path.concatenate(graph, partials, &rhs).is_ok()
173-
&& loop_path.symbol_stack_postcondition.len()
174-
> new_path.symbol_stack_postcondition.len()
175-
{
176-
return false;
177-
}
178-
true
192+
193+
// process path if it is productive
194+
cyclic_path.is_productive(graph, partials)
179195
}
180196
}
181197

@@ -191,7 +207,7 @@ impl JoiningCycleDetector {
191207
path: PartialPath,
192208
) -> Self {
193209
Self {
194-
paths: vec![OwnedOrDatabasePath::Owned(path)].into(),
210+
paths: vec![path.into()].into(),
195211
}
196212
}
197213

@@ -202,7 +218,7 @@ impl JoiningCycleDetector {
202218
path: Handle<PartialPath>,
203219
) -> Self {
204220
Self {
205-
paths: vec![OwnedOrDatabasePath::Db(path)].into(),
221+
paths: vec![path.into()].into(),
206222
}
207223
}
208224

@@ -211,25 +227,43 @@ impl JoiningCycleDetector {
211227
graph: &StackGraph,
212228
partials: &mut PartialPaths,
213229
db: &Database,
214-
new_path: &PartialPath,
230+
joined_path: OwnedOrDatabasePath,
215231
) -> bool {
216-
if let Some(i) = self
232+
// add new fragment
233+
self.paths.push(joined_path.into());
234+
let joined_path = self.paths.back().unwrap().get(db);
235+
236+
// find a fragment starting with our end node
237+
// (includes new fragment, in case that is cyclic)
238+
let end_node = joined_path.end_node;
239+
let i = match self
217240
.paths
218241
.iter()
219-
.rposition(|p| p.get(db).start_node == new_path.end_node)
242+
.rposition(|p| p.get(db).start_node == end_node)
220243
{
221-
let mut rhs = self.paths[i].get(db).clone();
222-
for path in self.paths.range(i + 1..) {
223-
rhs.concatenate(graph, partials, path.get(db)).unwrap();
224-
}
225-
let mut loop_path = new_path.clone();
226-
if loop_path.concatenate(graph, partials, &rhs).is_ok()
227-
&& loop_path.symbol_stack_postcondition.len()
228-
> new_path.symbol_stack_postcondition.len()
229-
{
230-
return false;
244+
Some(i) => i,
245+
None => return true,
246+
};
247+
248+
// construct the cyclic path
249+
let mut cyclic_path = self.paths[i].get(db).clone();
250+
for path in self.paths.range(i + 1..) {
251+
let path = path.get(db);
252+
if cyclic_path.ends_in_jump(graph) {
253+
// restore jump target
254+
cyclic_path
255+
.scope_stack_precondition
256+
.push_back(partials, path.start_node);
257+
cyclic_path
258+
.scope_stack_postcondition
259+
.push_front(partials, path.start_node);
260+
cyclic_path.resolve(graph, partials).unwrap();
231261
}
262+
cyclic_path.ensure_no_overlapping_variables(partials, path);
263+
cyclic_path.concatenate(graph, partials, path).unwrap();
232264
}
233-
true
265+
266+
// process path if it is productive
267+
cyclic_path.is_productive(graph, partials)
234268
}
235269
}

stack-graphs/src/partial.rs

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,26 +2068,82 @@ impl PartialPath {
20682068
graph[self.end_node].is_jump_to()
20692069
}
20702070

2071-
/// Returns whether a partial path is "productive" that is, whether it adds useful
2072-
/// information to a path. Non-productive paths are ignored.
2073-
pub fn is_productive(&self, partials: &mut PartialPaths) -> bool {
2071+
/// Returns whether a partial path is "productive" --- that is, it is not cyclic, or it consumes
2072+
/// symbols from the stack, possible replacing them by different symbols.
2073+
pub fn is_productive(&self, graph: &StackGraph, partials: &mut PartialPaths) -> bool {
20742074
// StackGraph ensures that there are no nodes with duplicate IDs, so we can do a simple
20752075
// comparison of node handles here.
20762076
if self.start_node != self.end_node {
20772077
return true;
20782078
}
2079-
if !self
2079+
2080+
let lhs = self;
2081+
let mut rhs = self.clone();
2082+
rhs.ensure_no_overlapping_variables(partials, lhs);
2083+
2084+
// If the join node operates on the stack, the effect is present in both sides.
2085+
// For correct joining, we must undo the effect on one of the sides. We match
2086+
// the end node of the lhs, and the start node of the rhs separately, depending
2087+
// on whether we must manipulate the lhs postcondition or rhs precondition,
2088+
// respectively. The reason we cannot use only one of the lhs end or rhs start
2089+
// node is that the variables used in them may differ.
2090+
let mut lhs_symbol_stack_postcondition = lhs.symbol_stack_postcondition;
2091+
let mut lhs_scope_stack_postcondition = lhs.scope_stack_postcondition;
2092+
let mut rhs_symbol_stack_precondition = rhs.symbol_stack_precondition;
2093+
let mut rhs_scope_stack_precondition = rhs.scope_stack_precondition;
2094+
graph[lhs.end_node].halfopen_closed_postcondition(
2095+
partials,
2096+
&mut lhs_symbol_stack_postcondition,
2097+
&mut lhs_scope_stack_postcondition,
2098+
);
2099+
graph[rhs.start_node].halfopen_closed_precondition(
2100+
partials,
2101+
&mut rhs_symbol_stack_precondition,
2102+
&mut rhs_scope_stack_precondition,
2103+
);
2104+
2105+
let mut symbol_bindings = PartialSymbolStackBindings::new();
2106+
let mut scope_bindings = PartialScopeStackBindings::new();
2107+
if lhs_symbol_stack_postcondition
2108+
.unify(
2109+
partials,
2110+
rhs_symbol_stack_precondition,
2111+
&mut symbol_bindings,
2112+
&mut scope_bindings,
2113+
)
2114+
.is_err()
2115+
{
2116+
// symbol stacks are incompatible
2117+
return true;
2118+
}
2119+
if lhs_scope_stack_postcondition
2120+
.unify(partials, rhs_scope_stack_precondition, &mut scope_bindings)
2121+
.is_err()
2122+
{
2123+
// scope stacks are incompatible
2124+
return true;
2125+
}
2126+
2127+
if lhs
20802128
.symbol_stack_precondition
2081-
.matches(partials, self.symbol_stack_postcondition)
2129+
.variable
2130+
.into_option()
2131+
.map_or(false, |v| symbol_bindings.get(v).iter().len() > 0)
20822132
{
2133+
// symbol stack precondition strengthened
20832134
return true;
20842135
}
2085-
if !self
2136+
2137+
if lhs
20862138
.scope_stack_precondition
2087-
.matches(partials, self.scope_stack_postcondition)
2139+
.variable
2140+
.into_option()
2141+
.map_or(false, |v| scope_bindings.get(v).iter().len() > 0)
20882142
{
2143+
// scope stack precondition strengthened
20892144
return true;
20902145
}
2146+
20912147
false
20922148
}
20932149

@@ -2350,17 +2406,25 @@ impl PartialPath {
23502406
let extensions = graph.outgoing_edges(self.end_node);
23512407
result.reserve(extensions.size_hint().0);
23522408
for extension in extensions {
2409+
copious_debugging!(
2410+
" -> with edge {} -> {}",
2411+
extension.source.display(graph),
2412+
extension.sink.display(graph)
2413+
);
23532414
if !graph[extension.sink].is_in_file(file) {
2415+
copious_debugging!(" * outside file");
23542416
continue;
23552417
}
23562418
let mut new_path = self.clone();
23572419
let mut new_cycle_detector = path_cycle_detector.clone();
23582420
// If there are errors adding this edge to the partial path, or resolving the resulting
23592421
// partial path, just skip the edge — it's not a fatal error.
23602422
if new_path.append(graph, partials, extension).is_err() {
2423+
copious_debugging!(" * invalid extension");
23612424
continue;
23622425
}
2363-
if !new_cycle_detector.appended(graph, partials, extension.sink, &new_path) {
2426+
if !new_cycle_detector.appended(graph, partials, extension.sink) {
2427+
copious_debugging!(" * cycle");
23642428
continue;
23652429
}
23662430
result.push((new_path, new_cycle_detector));

stack-graphs/src/stitching.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -903,23 +903,23 @@ impl ForwardPartialPathStitcher {
903903
self.next_iteration.0.reserve(extension_count);
904904
self.next_iteration.1.reserve(extension_count);
905905
for extension in &self.candidate_partial_paths {
906-
let mut extension = db[*extension].clone();
906+
let mut extension_path = db[*extension].clone();
907907
copious_debugging!(" Extend {}", partial_path.display(graph, partials));
908-
copious_debugging!(" with {}", extension.display(graph, partials));
909-
extension.ensure_no_overlapping_variables(partials, partial_path);
910-
copious_debugging!(" -> {}", extension.display(graph, partials));
908+
copious_debugging!(" with {}", extension_path.display(graph, partials));
909+
extension_path.ensure_no_overlapping_variables(partials, partial_path);
910+
copious_debugging!(" -> {}", extension_path.display(graph, partials));
911911

912912
let mut new_partial_path = partial_path.clone();
913913
let mut new_cycle_detector = cycle_detector.clone();
914914
// If there are errors concatenating these partial paths, or resolving the resulting
915915
// partial path, just skip the extension — it's not a fatal error.
916916
#[cfg_attr(not(feature = "copious-debugging"), allow(unused_variables))]
917917
{
918-
if let Err(err) = new_partial_path.concatenate(graph, partials, &extension) {
918+
if let Err(err) = new_partial_path.concatenate(graph, partials, &extension_path) {
919919
copious_debugging!(" is invalid: {:?}", err);
920920
continue;
921921
}
922-
if !new_cycle_detector.joined(graph, partials, db, &extension) {
922+
if !new_cycle_detector.joined(graph, partials, db, extension.into()) {
923923
copious_debugging!(" is invalid: cyclic");
924924
continue;
925925
}
@@ -1032,7 +1032,7 @@ impl ForwardPartialPathStitcher {
10321032
}
10331033

10341034
#[derive(Clone)]
1035-
pub(crate) enum OwnedOrDatabasePath {
1035+
pub enum OwnedOrDatabasePath {
10361036
Db(Handle<PartialPath>),
10371037
Owned(PartialPath),
10381038
}
@@ -1045,3 +1045,21 @@ impl OwnedOrDatabasePath {
10451045
}
10461046
}
10471047
}
1048+
1049+
impl From<Handle<PartialPath>> for OwnedOrDatabasePath {
1050+
fn from(value: Handle<PartialPath>) -> Self {
1051+
Self::Db(value)
1052+
}
1053+
}
1054+
1055+
impl From<&Handle<PartialPath>> for OwnedOrDatabasePath {
1056+
fn from(value: &Handle<PartialPath>) -> Self {
1057+
Self::Db(*value)
1058+
}
1059+
}
1060+
1061+
impl From<PartialPath> for OwnedOrDatabasePath {
1062+
fn from(value: PartialPath) -> Self {
1063+
Self::Owned(value)
1064+
}
1065+
}

0 commit comments

Comments
 (0)