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

Commit 7e0a48f

Browse files
author
Hendrik van Antwerpen
authored
Merge pull request #204 from github/productivity-cult
Non-terminating path finding with current definition of `is_productive`
2 parents 71bf812 + 82c24e5 commit 7e0a48f

File tree

11 files changed

+534
-274
lines changed

11 files changed

+534
-274
lines changed

stack-graphs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ test = false
2424
bitvec = "1.0"
2525
controlled-option = "0.4"
2626
either = "1.6"
27+
enumset = "1.0"
2728
fxhash = "0.2"
2829
itertools = "0.10"
2930
libc = "0.2"

stack-graphs/src/cycles.rs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
3232
use std::collections::HashMap;
3333

34+
use enumset::EnumSet;
3435
use smallvec::SmallVec;
3536

3637
use crate::arena::Handle;
@@ -39,6 +40,7 @@ use crate::arena::ListArena;
3940
use crate::graph::Edge;
4041
use crate::graph::Node;
4142
use crate::graph::StackGraph;
43+
use crate::partial::Cyclicity;
4244
use crate::partial::PartialPath;
4345
use crate::partial::PartialPaths;
4446
use crate::paths::Path;
@@ -173,16 +175,25 @@ impl<A: Appendable + Clone> AppendingCycleDetector<A> {
173175
result
174176
}
175177

176-
pub fn append(
177-
&mut self,
178+
pub fn append(&mut self, appendables: &mut Appendables<A>, appendage: A) {
179+
self.appendages.push_front(appendables, appendage);
180+
}
181+
182+
/// Tests if the path is cyclic. Returns a vector indicating the kind of cycles that were found.
183+
/// If appending or concatenating all fragments succeeds, this function will never raise and error.
184+
pub fn is_cyclic(
185+
&self,
178186
graph: &StackGraph,
179187
partials: &mut PartialPaths,
180188
ctx: &mut A::Ctx,
181189
appendables: &mut Appendables<A>,
182-
appendage: A,
183-
) -> Result<(), PathResolutionError> {
184-
let end_node = appendage.end_node(ctx);
185-
self.appendages.push_front(appendables, appendage);
190+
) -> Result<EnumSet<Cyclicity>, PathResolutionError> {
191+
let mut cycles = EnumSet::new();
192+
193+
let end_node = match self.appendages.clone().pop_front(appendables) {
194+
Some(appendage) => appendage.end_node(ctx),
195+
None => return Ok(cycles),
196+
};
186197

187198
let mut maybe_cyclic_path = None;
188199
let mut appendages = self.appendages;
@@ -199,33 +210,25 @@ impl<A: Appendable + Clone> AppendingCycleDetector<A> {
199210
break;
200211
}
201212
}
202-
None => return Ok(()),
213+
None => return Ok(cycles),
203214
}
204215
}
205216

206217
// build prefix path -- prefix starts at end_node, because this is a cycle
207218
let mut prefix_path = PartialPath::from_node(graph, partials, end_node);
208219
while let Some(appendage) = prefix_appendages.pop_front(appendables) {
209-
prefix_path
210-
.resolve_to(graph, partials, appendage.start_node(ctx))
211-
.expect("resolving cycle prefix path failed");
212-
appendage
213-
.append_to(graph, partials, ctx, &mut prefix_path)
214-
.expect("appending cycle prefix path failed");
220+
prefix_path.resolve_to_node(graph, partials, appendage.start_node(ctx))?;
221+
appendage.append_to(graph, partials, ctx, &mut prefix_path)?;
215222
}
216223

217224
// build cyclic path
218225
let cyclic_path = maybe_cyclic_path
219226
.unwrap_or_else(|| PartialPath::from_node(graph, partials, end_node));
220-
prefix_path
221-
.resolve_to(graph, partials, cyclic_path.start_node)
222-
.expect("resolving cyclic path failed");
227+
prefix_path.resolve_to_node(graph, partials, cyclic_path.start_node)?;
223228
prefix_path.ensure_no_overlapping_variables(partials, &cyclic_path);
224-
prefix_path
225-
.concatenate(graph, partials, &cyclic_path)
226-
.expect("concatenating cyclic path failed ");
227-
if !prefix_path.is_productive(graph, partials) {
228-
return Err(PathResolutionError::DisallowedCycle);
229+
prefix_path.concatenate(graph, partials, &cyclic_path)?;
230+
if let Some(cyclicity) = prefix_path.is_cyclic(graph, partials) {
231+
cycles |= cyclicity;
229232
}
230233
maybe_cyclic_path = Some(prefix_path);
231234
}

stack-graphs/src/partial.rs

Lines changed: 116 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use std::num::NonZeroU32;
4040

4141
use controlled_option::ControlledOption;
4242
use controlled_option::Niche;
43+
use enumset::EnumSetType;
4344
use smallvec::SmallVec;
4445

4546
use crate::arena::Deque;
@@ -2069,13 +2070,14 @@ impl PartialPath {
20692070
graph[self.end_node].is_jump_to()
20702071
}
20712072

2072-
/// Returns whether a partial path is "productive" --- that is, it is not cyclic, or its
2073-
/// pre- and postcondition are incompatible.
2074-
pub fn is_productive(&self, graph: &StackGraph, partials: &mut PartialPaths) -> bool {
2073+
/// Returns whether a partial path is cyclic---that is, it starts and ends at the same node,
2074+
/// and its postcondition is compatible with its precondition. If the path is cyclic, a
2075+
/// tuple is returned indicating whether cycle requires strengthening the pre- or postcondition.
2076+
pub fn is_cyclic(&self, graph: &StackGraph, partials: &mut PartialPaths) -> Option<Cyclicity> {
20752077
// StackGraph ensures that there are no nodes with duplicate IDs, so we can do a simple
20762078
// comparison of node handles here.
20772079
if self.start_node != self.end_node {
2078-
return true;
2080+
return None;
20792081
}
20802082

20812083
let lhs = self;
@@ -2084,30 +2086,36 @@ impl PartialPath {
20842086

20852087
let join = match Self::compute_join(graph, partials, lhs, &rhs) {
20862088
Ok(join) => join,
2087-
Err(_) => return true,
2089+
Err(_) => return None,
20882090
};
20892091

20902092
if lhs
20912093
.symbol_stack_precondition
20922094
.variable
20932095
.into_option()
20942096
.map_or(false, |v| join.symbol_bindings.get(v).iter().len() > 0)
2097+
|| lhs
2098+
.scope_stack_precondition
2099+
.variable
2100+
.into_option()
2101+
.map_or(false, |v| join.scope_bindings.get(v).iter().len() > 0)
20952102
{
2096-
// symbol stack precondition strengthened
2097-
return true;
2098-
}
2099-
2100-
if lhs
2101-
.scope_stack_precondition
2103+
Some(Cyclicity::StrengthensPrecondition)
2104+
} else if rhs
2105+
.symbol_stack_postcondition
21022106
.variable
21032107
.into_option()
2104-
.map_or(false, |v| join.scope_bindings.get(v).iter().len() > 0)
2108+
.map_or(false, |v| join.symbol_bindings.get(v).iter().len() > 0)
2109+
|| rhs
2110+
.scope_stack_postcondition
2111+
.variable
2112+
.into_option()
2113+
.map_or(false, |v| join.scope_bindings.get(v).iter().len() > 0)
21052114
{
2106-
// scope stack precondition strengthened
2107-
return true;
2115+
Some(Cyclicity::StrengthensPostcondition)
2116+
} else {
2117+
Some(Cyclicity::Free)
21082118
}
2109-
2110-
false
21112119
}
21122120

21132121
/// Ensures that the content of this partial path is available in both forwards and backwards
@@ -2222,6 +2230,16 @@ impl PartialPath {
22222230
}
22232231
}
22242232

2233+
#[derive(Debug, EnumSetType)]
2234+
pub enum Cyclicity {
2235+
/// The path can be freely concatenated to itself.
2236+
Free,
2237+
/// Concatenating the path to itself strengthens the precondition---symbols are eliminated from the stack.
2238+
StrengthensPrecondition,
2239+
/// Concatenating the path to itself strengthens the postcondition---symbols are introduced on the stack.
2240+
StrengthensPostcondition,
2241+
}
2242+
22252243
impl<'a> DisplayWithPartialPaths for &'a PartialPath {
22262244
fn prepare(&mut self, graph: &StackGraph, partials: &mut PartialPaths) {
22272245
self.symbol_stack_precondition
@@ -2285,6 +2303,45 @@ impl PartialPath {
22852303
.with_offset(scope_variable_offset);
22862304
}
22872305

2306+
/// Replaces stack variables in the precondition with empty stacks.
2307+
pub fn eliminate_precondition_stack_variables(&mut self, partials: &mut PartialPaths) {
2308+
let mut symbol_bindings = PartialSymbolStackBindings::new();
2309+
let mut scope_bindings = PartialScopeStackBindings::new();
2310+
if let Some(symbol_variable) = self.symbol_stack_precondition.variable() {
2311+
symbol_bindings
2312+
.add(
2313+
partials,
2314+
symbol_variable,
2315+
PartialSymbolStack::empty(),
2316+
&mut scope_bindings,
2317+
)
2318+
.unwrap();
2319+
}
2320+
if let Some(scope_variable) = self.scope_stack_precondition.variable() {
2321+
scope_bindings
2322+
.add(partials, scope_variable, PartialScopeStack::empty())
2323+
.unwrap();
2324+
}
2325+
2326+
self.symbol_stack_precondition = self
2327+
.symbol_stack_precondition
2328+
.apply_partial_bindings(partials, &symbol_bindings, &scope_bindings)
2329+
.unwrap();
2330+
self.scope_stack_precondition = self
2331+
.scope_stack_precondition
2332+
.apply_partial_bindings(partials, &scope_bindings)
2333+
.unwrap();
2334+
2335+
self.symbol_stack_postcondition = self
2336+
.symbol_stack_postcondition
2337+
.apply_partial_bindings(partials, &symbol_bindings, &scope_bindings)
2338+
.unwrap();
2339+
self.scope_stack_postcondition = self
2340+
.scope_stack_postcondition
2341+
.apply_partial_bindings(partials, &scope_bindings)
2342+
.unwrap();
2343+
}
2344+
22882345
/// Attempts to append an edge to the end of a partial path. If the edge is not a valid
22892346
/// extension of this partial path, we return an error describing why.
22902347
pub fn append(
@@ -2315,15 +2372,15 @@ impl PartialPath {
23152372
},
23162373
);
23172374

2318-
self.resolve(graph, partials)?;
2375+
self.resolve_from_postcondition(graph, partials)?;
23192376

23202377
Ok(())
23212378
}
23222379

2323-
/// Attempts to resolve any _jump to scope_ node at the end of a partial path. If the partial
2324-
/// path does not end in a _jump to scope_ node, we do nothing. If it does, and we cannot
2325-
/// resolve it, then we return an error describing why.
2326-
pub fn resolve(
2380+
/// Attempts to resolve any _jump to scope_ node at the end of a partial path from the postcondition
2381+
/// scope stack. If the partial path does not end in a _jump to scope_ node, we do nothing. If it
2382+
/// does, and we cannot resolve it, then we return an error describing why.
2383+
pub fn resolve_from_postcondition(
23272384
&mut self,
23282385
graph: &StackGraph,
23292386
partials: &mut PartialPaths,
@@ -2349,10 +2406,11 @@ impl PartialPath {
23492406
Ok(())
23502407
}
23512408

2352-
/// Attempts to resolve any _jump to scope_ node at the end of a partial path to the given node.
2353-
/// If the partial path does not end in a _jump to scope_ node, we do nothing. If it does, and we
2354-
/// cannot resolve it, then we return an error describing why.
2355-
pub fn resolve_to(
2409+
/// Resolve any _jump to scope_ node at the end of a partial path to the given node, updating the
2410+
/// precondition to include the given node. If the partial path does not end in a _jump to scope_
2411+
/// node, we do nothing. If it does, and we cannot resolve it, then we return an error describing
2412+
/// why.
2413+
pub fn resolve_to_node(
23562414
&mut self,
23572415
graph: &StackGraph,
23582416
partials: &mut PartialPaths,
@@ -2361,11 +2419,31 @@ impl PartialPath {
23612419
if !graph[self.end_node].is_jump_to() {
23622420
return Ok(());
23632421
}
2364-
if self.scope_stack_postcondition.contains_scopes() {
2365-
return Err(PathResolutionError::ScopeStackUnsatisfied); // this path was not properly resolved
2366-
}
2367-
self.scope_stack_precondition.push_back(partials, node);
2422+
2423+
let scope_variable = match self.scope_stack_postcondition.variable() {
2424+
Some(scope_variable) => scope_variable,
2425+
None => return Err(PathResolutionError::ScopeStackUnsatisfied),
2426+
};
2427+
let mut scope_stack = PartialScopeStack::from_variable(scope_variable);
2428+
scope_stack.push_front(partials, node);
2429+
2430+
let symbol_bindings = PartialSymbolStackBindings::new();
2431+
let mut scope_bindings = PartialScopeStackBindings::new();
2432+
scope_bindings
2433+
.add(partials, scope_variable, scope_stack)
2434+
.unwrap();
2435+
2436+
self.symbol_stack_precondition = self
2437+
.symbol_stack_precondition
2438+
.apply_partial_bindings(partials, &symbol_bindings, &scope_bindings)
2439+
.unwrap();
2440+
self.scope_stack_precondition = self
2441+
.scope_stack_precondition
2442+
.apply_partial_bindings(partials, &scope_bindings)
2443+
.unwrap();
2444+
23682445
self.end_node = node;
2446+
23692447
Ok(())
23702448
}
23712449

@@ -2407,13 +2485,7 @@ impl PartialPath {
24072485
copious_debugging!(" * invalid extension");
24082486
continue;
24092487
}
2410-
if new_cycle_detector
2411-
.append(graph, partials, &mut (), edges, extension)
2412-
.is_err()
2413-
{
2414-
copious_debugging!(" * cycle");
2415-
continue;
2416-
}
2488+
new_cycle_detector.append(edges, extension);
24172489
result.push((new_path, new_cycle_detector));
24182490
}
24192491
}
@@ -2550,9 +2622,7 @@ impl Node {
25502622
scope_stack: &mut PartialScopeStack,
25512623
) {
25522624
match self {
2553-
Node::DropScopes(_) => {
2554-
*scope_stack = PartialScopeStack::empty();
2555-
}
2625+
Node::DropScopes(_) => {}
25562626
Node::JumpTo(_) => {}
25572627
Node::PopScopedSymbol(node) => {
25582628
let symbol = symbol_stack
@@ -2676,10 +2746,6 @@ impl PartialPaths {
26762746
/// (a) is minimal, no path can be constructed by stitching other paths in the set, and
26772747
/// (b) covers all complete paths, from references to definitions, when used for path stitching
26782748
///
2679-
/// Callers are advised _not_ to filter this set in the visitor using functions like
2680-
/// [`PartialPath::is_productive`][] as that may interfere with implementation changes of this
2681-
/// function.
2682-
///
26832749
/// This function will not return until all reachable partial paths have been processed, so
26842750
/// `graph` must already contain a complete stack graph. If you have a very large stack graph
26852751
/// stored in some other storage system, and want more control over lazily loading only the
@@ -2734,6 +2800,12 @@ impl PartialPaths {
27342800
copious_debugging!(" * visit");
27352801
visit(graph, self, path);
27362802
}
2803+
} else if !path_cycle_detector
2804+
.is_cyclic(graph, self, &mut (), &mut edges)
2805+
.expect("cyclic test failed when finding partial paths")
2806+
.is_empty()
2807+
{
2808+
copious_debugging!(" * cycle");
27372809
} else {
27382810
copious_debugging!(" * extend");
27392811
path.extend_from_file(
@@ -2901,7 +2973,7 @@ impl PartialPath {
29012973
}
29022974
lhs.end_node = rhs.end_node;
29032975

2904-
lhs.resolve(graph, partials)?;
2976+
lhs.resolve_from_postcondition(graph, partials)?;
29052977

29062978
Ok(())
29072979
}

0 commit comments

Comments
 (0)