Skip to content

Commit 143a7f3

Browse files
authored
Merge pull request #20 from stlab:sean-parent/planner-pre-claim-flood-fill
fix: extend planner to handle multi-input constraints via pre-claiming
2 parents 074eb63 + a6fd531 commit 143a7f3

3 files changed

Lines changed: 77 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ git checkout -b <username>/<feature-name>
5353

5454
Never commit directly to `main`.
5555

56+
Before creating a PR, run the full check suite locally (fmt, clippy, and test from the
57+
Commands section above).
58+
5659
## Project Status
5760

5861
This project has not been released yet and has no clients. The API is not stable and may change at

property-model/src/planner.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
//! at most one method per relationship can be eligible at any given point (the inputs of
1010
//! each method are the outputs of the other methods), so selection is deterministic.
1111
//!
12+
//! **Pre-claiming**: when a determined cell eliminates all but one feasible method for a
13+
//! relationship (a method is infeasible if it would overwrite a determined or pre-claimed
14+
//! cell), that sole method's outputs are *pre-claimed*: they can never become sources, even
15+
//! before all of the method's inputs are determined. Excluding pre-claimed outputs from
16+
//! feasibility prevents a method whose output is pre-claimed by the current flood-fill pass
17+
//! from being counted as a second viable option. This allows the planner to correctly handle
18+
//! constraints where the highest-strength cell is one of several inputs to the selected
19+
//! method.
20+
//!
1221
//! Because a method is only selected once all its inputs are determined, any method that
1322
//! writes an input to a later method necessarily appears earlier in the selection order.
1423
//! The selection order is therefore already a valid topological execution order.
@@ -43,14 +52,15 @@ pub(crate) fn plan(
4352
relationships: &SlotMap<RelationshipId, RelationshipData>,
4453
) -> Result<Plan, Error> {
4554
let mut determined: HashSet<CellId> = HashSet::new();
55+
let mut pre_claimed: HashSet<CellId> = HashSet::new();
4656
let mut selected: Vec<(RelationshipId, usize)> = Vec::new();
4757
let mut selected_set: HashSet<RelationshipId> = HashSet::new();
4858

4959
let mut cells_sorted: Vec<CellId> = cells.keys().collect();
5060
cells_sorted.sort_by_key(|&id| Reverse(cells[id].strength));
5161

5262
for &source in &cells_sorted {
53-
if determined.contains(&source) {
63+
if determined.contains(&source) || pre_claimed.contains(&source) {
5464
continue;
5565
}
5666
determined.insert(source);
@@ -77,10 +87,37 @@ pub(crate) fn plan(
7787
);
7888
for &output in &method.outputs {
7989
determined.insert(output);
90+
pre_claimed.remove(&output);
8091
queue.push_back(output);
8192
}
8293
selected_set.insert(rel_id);
8394
selected.push((rel_id, method_idx));
95+
} else {
96+
// No method is immediately selectable. If exactly one method remains
97+
// feasible — meaning no other method can run without overwriting a cell
98+
// that is already determined or pre-claimed — and the current cell is
99+
// one of its inputs, the flow continues along that method: pre-claim its
100+
// outputs and enqueue them so the flood-fill propagates further.
101+
//
102+
// Both `determined` and `pre_claimed` are excluded from feasibility so
103+
// that a method whose output is pre-claimed by this same flood-fill pass
104+
// is not counted as a second viable option.
105+
let mut feasible = rel.methods.iter().filter(|m| {
106+
m.outputs
107+
.iter()
108+
.all(|o| !determined.contains(o) && !pre_claimed.contains(o))
109+
});
110+
let first = feasible.next();
111+
let second = feasible.next();
112+
if let (Some(sole), None) = (first, second)
113+
&& sole.inputs.contains(&cell)
114+
{
115+
for &output in &sole.outputs {
116+
if pre_claimed.insert(output) {
117+
queue.push_back(output);
118+
}
119+
}
120+
}
84121
}
85122
}
86123
}

property-model/tests/integration.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! End-to-end integration tests for the property-model crate.
22
3+
use std::any::TypeId;
4+
35
use property_model::{Error, Method, Sheet};
46

57
#[test]
@@ -163,3 +165,37 @@ fn mutually_dependent_relationships_return_conflict() {
163165

164166
assert!(matches!(sheet.propagate(), Err(Error::Conflict)));
165167
}
168+
169+
#[test]
170+
fn arity_3_2_1() {
171+
let mut sheet = Sheet::new();
172+
let a = sheet.add_cell("a".to_string());
173+
let c = sheet.add_cell("ab".to_string());
174+
let b = sheet.add_cell("b".to_string());
175+
sheet
176+
.add_relationship(vec![
177+
Method::from_fn_2_1([a, b], c, |x: &String, y: &String| Ok(x.clone() + y)),
178+
Method::new(
179+
vec![c],
180+
vec![a, b],
181+
vec![TypeId::of::<String>()],
182+
vec![TypeId::of::<String>(), TypeId::of::<String>()],
183+
|args| {
184+
let z = args[0]
185+
.downcast_ref::<String>()
186+
.expect("type checked at add_relationship");
187+
let mut chars = z.chars();
188+
let first = chars.next().unwrap_or_default().to_string();
189+
let rest = chars.collect::<String>();
190+
Ok(vec![Box::new(first), Box::new(rest)])
191+
},
192+
),
193+
])
194+
.unwrap();
195+
196+
sheet.propagate().unwrap();
197+
198+
assert_eq!(sheet.read::<String>(a).unwrap(), "a");
199+
assert_eq!(sheet.read::<String>(b).unwrap(), "b");
200+
assert_eq!(sheet.read::<String>(c).unwrap(), "ab");
201+
}

0 commit comments

Comments
 (0)