Skip to content

Commit 422b10d

Browse files
authored
Merge pull request #240 from stlab/worktree-adam-rs/planner-generalization-phase-b
Generalize planner seedfill and stage selected guard prerequisites
2 parents 57a4a88 + 21ec2be commit 422b10d

14 files changed

Lines changed: 3435 additions & 272 deletions

‎adam-lang/src/error_labels.rs‎

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ pub(crate) fn site_label(
3333
_ => generic_site_label(site, cell_name),
3434
},
3535

36+
Error::SeedCycle { .. } => match site {
37+
ErrorSite::Relationship(_) => {
38+
"this relationship is part of the seed dependency cycle".to_string()
39+
}
40+
ErrorSite::Cell(c) => match cell_name(*c) {
41+
Some(name) => format!("cell `{name}` is part of the seed dependency cycle"),
42+
None => "this cell is part of the seed dependency cycle".to_string(),
43+
},
44+
_ => generic_site_label(site, cell_name),
45+
},
46+
3647
Error::Conflict { .. } => match site {
3748
ErrorSite::Relationship(_) => {
3849
"this relationship is part of an overconstrained group".to_string()
@@ -50,14 +61,16 @@ pub(crate) fn site_label(
5061
_ => generic_site_label(site, cell_name),
5162
},
5263

53-
// Two site shapes share this variant: self-duplicate (a method's own outputs list
54-
// repeats a cell) is `[Method/MethodIndex, Cell(repeated output)]`; cross-method
55-
// (two methods claim the same output set) is `[Method(later), Method(earlier),
56-
// Cell(shared)…]`. A `Cell` site always means "this cell is claimed more than
57-
// once", in *both* shapes — checking it before the numeric-index arms below is
58-
// what keeps the self-duplicate case (whose `sites[1]` is a `Cell`, not the
59-
// earlier method) from being mislabelled as "collides with this earlier method".
60-
Error::DuplicateMethodOutputs { .. } => match site {
64+
// Three site shapes share this variant: self-duplicate (a method's own outputs
65+
// list repeats a cell) is `[Method/MethodIndex, Cell(repeated output)]`;
66+
// identical output sets are `[Method(later), Method(earlier), Cell(shared)…]`;
67+
// nested output sets are `[Method(later), Method(earlier), Cell(shared subset)…]`.
68+
// Overlapping non-nested output sets are valid and never reach this branch.
69+
// A `Cell` site always means "this cell is claimed more than once", in *all*
70+
// shapes — checking it before the numeric-index arms below keeps the self-duplicate
71+
// case (whose `sites[1]` is a `Cell`, not the earlier method) from being
72+
// mislabelled as "collides with this earlier method".
73+
Error::InvalidMethodOutputs { .. } => match site {
6174
ErrorSite::Cell(c) => match cell_name(*c) {
6275
Some(name) => format!("output cell `{name}` is claimed more than once"),
6376
None => "this output cell is claimed more than once".to_string(),
@@ -139,6 +152,22 @@ mod tests {
139152
assert_eq!(label, "cell `mode` is part of the cycle");
140153
}
141154

155+
#[test]
156+
fn seed_cycle_sites_are_labelled_as_part_of_the_seed_cycle() {
157+
let relationship = adam_rs::RelationshipId::default();
158+
let cell = CellId::default();
159+
let e = adam_rs::Error::SeedCycle {
160+
sites: vec![ErrorSite::Relationship(relationship), ErrorSite::Cell(cell)],
161+
};
162+
let relationship_label = site_label(&e, 0, &|_| Some("x".to_string()));
163+
let cell_label = site_label(&e, 1, &|_| Some("x".to_string()));
164+
assert_eq!(
165+
relationship_label,
166+
"this relationship is part of the seed dependency cycle"
167+
);
168+
assert_eq!(cell_label, "cell `x` is part of the seed dependency cycle");
169+
}
170+
142171
#[test]
143172
fn site_label_describes_mismatched_method_cells() {
144173
let e = adam_rs::Error::MismatchedMethodCells {
@@ -155,8 +184,8 @@ mod tests {
155184
}
156185

157186
#[test]
158-
fn site_label_describes_duplicate_method_outputs() {
159-
let e = adam_rs::Error::DuplicateMethodOutputs {
187+
fn site_label_describes_invalid_method_outputs() {
188+
let e = adam_rs::Error::InvalidMethodOutputs {
160189
sites: vec![
161190
adam_rs::ErrorSite::MethodIndex(1),
162191
adam_rs::ErrorSite::MethodIndex(0),
@@ -169,15 +198,15 @@ mod tests {
169198
assert!(site_label(&e, 2, &name).contains("out"));
170199
}
171200

172-
// Regression test for the self-duplicate shape of `DuplicateMethodOutputs`: a method's own
201+
// Regression test for the self-duplicate shape of `InvalidMethodOutputs`: a method's own
173202
// `outputs` list repeats a cell (e.g. `relationship { (b, b) := (a, a2); }`), which
174203
// `Sheet::add_relationship` reports as `sites: vec![MethodIndex(idx), Cell(o)]` -- only two
175204
// sites, with `sites[1]` a `Cell`, not a second method. Labelling `index == 1` by raw
176205
// position (as the cross-method case does) would wrongly print "collides with this earlier
177206
// method" onto the cell caret, even though there is no earlier method here.
178207
#[test]
179-
fn site_label_describes_duplicate_method_outputs_self_duplicate() {
180-
let e = adam_rs::Error::DuplicateMethodOutputs {
208+
fn site_label_describes_invalid_method_outputs_self_duplicate() {
209+
let e = adam_rs::Error::InvalidMethodOutputs {
181210
sites: vec![
182211
adam_rs::ErrorSite::MethodIndex(0),
183212
adam_rs::ErrorSite::Cell(adam_rs::CellId::default()),

‎adam-rs/src/error.rs‎

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ pub enum Error {
4242
/// A `CellId` or `RelationshipId` was not found in the sheet.
4343
InvalidId,
4444

45-
/// No valid method assignment exists (overconstrained).
45+
/// No valid method assignment exists, or staged prerequisite provenance conflicts
46+
/// with the final selected plan.
4647
Conflict {
47-
/// A subset-minimal group of relationships that together admit no valid
48-
/// method assignment: removing any member of the group makes the remainder
49-
/// feasible. Every entry is `ErrorSite::Relationship`.
48+
/// Relationships, methods, and cells involved in an impossible assignment or
49+
/// an incompatible staged prerequisite reuse.
5050
sites: Vec<ErrorSite>,
5151
},
5252

@@ -59,6 +59,15 @@ pub enum Error {
5959
sites: Vec<ErrorSite>,
6060
},
6161

62+
/// The seed dependency graph contains a non-self cycle.
63+
SeedCycle {
64+
/// The cycle's members in deterministic traversal order, starting at the first
65+
/// revisited `Cell` and alternating through the sibling `Relationship`s that
66+
/// force the cycle. The closing relationship appears last; the starting cell is
67+
/// not repeated at the end.
68+
sites: Vec<ErrorSite>,
69+
},
70+
6271
/// A method's function returned an error during execution.
6372
MethodFailed {
6473
/// The underlying error the method's function (or a requirement's/conditional's
@@ -91,13 +100,14 @@ pub enum Error {
91100
},
92101

93102
/// A method's own `outputs` list names a cell more than once, or two methods in
94-
/// the same relationship have identical `outputs` sets.
95-
DuplicateMethodOutputs {
103+
/// the same relationship have identical or nested `outputs` sets. Overlapping
104+
/// output sets are allowed when neither set nests the other.
105+
InvalidMethodOutputs {
96106
/// `sites[0]` is the method (by index within the `Vec` passed to
97107
/// `add_relationship`) whose output set collided. For a method's own outputs
98108
/// repeating a cell, further entries are the repeated cell(s). For two methods
99-
/// sharing an output set, `sites[1]` is the earlier method's index and further
100-
/// entries are the shared output cell(s).
109+
/// with identical or nested output sets, `sites[1]` is the earlier method's
110+
/// index and further entries are the duplicate or nested subset cell(s).
101111
sites: Vec<ErrorSite>,
102112
},
103113

@@ -169,16 +179,17 @@ impl std::fmt::Display for Error {
169179
Error::InvalidId => write!(f, "invalid cell or relationship id"),
170180
Error::Conflict { .. } => write!(f, "no valid method assignment (overconstrained)"),
171181
Error::Cycle { .. } => write!(f, "selected methods form a cycle"),
182+
Error::SeedCycle { .. } => write!(f, "seed dependency cycle"),
172183
Error::MethodFailed { error, .. } => write!(f, "method execution failed: {error}"),
173184
Error::InvalidMethod { .. } => write!(f, "method is structurally invalid"),
174185
Error::MismatchedMethodCells { .. } => write!(
175186
f,
176187
"methods in a relationship must reference the same set of cells"
177188
),
178-
Error::DuplicateMethodOutputs { .. } => write!(
189+
Error::InvalidMethodOutputs { .. } => write!(
179190
f,
180191
"a method's outputs must be duplicate-free, and no two methods in a \
181-
relationship may share an outputs set"
192+
relationship may have identical or nested output sets"
182193
),
183194
Error::InvalidConditional { .. } => write!(f, "conditional is structurally invalid"),
184195
Error::InvalidOutput => write!(f, "output is structurally invalid"),
@@ -218,10 +229,11 @@ impl Error {
218229
| Error::MethodFailed { sites, .. }
219230
| Error::InvalidMethod { sites }
220231
| Error::MismatchedMethodCells { sites }
221-
| Error::DuplicateMethodOutputs { sites }
232+
| Error::InvalidMethodOutputs { sites }
222233
| Error::InvalidCellKind { sites }
223234
| Error::Conflict { sites }
224235
| Error::Cycle { sites }
236+
| Error::SeedCycle { sites }
225237
| Error::DependencyCycle { sites }
226238
| Error::InvalidConditional { sites } => sites,
227239
_ => &[],
@@ -324,6 +336,7 @@ mod tests {
324336
assert!(std::error::Error::source(&Error::InvalidId).is_none());
325337
assert!(std::error::Error::source(&Error::Conflict { sites: vec![] }).is_none());
326338
assert!(std::error::Error::source(&Error::Cycle { sites: vec![] }).is_none());
339+
assert!(std::error::Error::source(&Error::SeedCycle { sites: vec![] }).is_none());
327340
assert!(std::error::Error::source(&Error::InvalidMethod { sites: vec![] }).is_none());
328341
assert!(
329342
std::error::Error::source(&Error::TypeMismatch {
@@ -337,7 +350,7 @@ mod tests {
337350
std::error::Error::source(&Error::MismatchedMethodCells { sites: vec![] }).is_none()
338351
);
339352
assert!(
340-
std::error::Error::source(&Error::DuplicateMethodOutputs { sites: vec![] }).is_none()
353+
std::error::Error::source(&Error::InvalidMethodOutputs { sites: vec![] }).is_none()
341354
);
342355
}
343356

@@ -365,9 +378,9 @@ mod tests {
365378
}
366379

367380
#[test]
368-
fn duplicate_method_outputs_display_contains_outputs() {
381+
fn invalid_method_outputs_display_contains_outputs() {
369382
assert!(
370-
Error::DuplicateMethodOutputs { sites: vec![] }
383+
Error::InvalidMethodOutputs { sites: vec![] }
371384
.to_string()
372385
.contains("outputs")
373386
);
@@ -442,6 +455,15 @@ mod tests {
442455
);
443456
}
444457

458+
#[test]
459+
fn seed_cycle_display_mentions_seed_dependency_cycle() {
460+
assert!(
461+
Error::SeedCycle { sites: vec![] }
462+
.to_string()
463+
.contains("seed dependency cycle")
464+
);
465+
}
466+
445467
#[test]
446468
fn dependency_cycle_has_no_source() {
447469
assert!(std::error::Error::source(&Error::DependencyCycle { sites: vec![] }).is_none());
@@ -454,6 +476,13 @@ mod tests {
454476
assert_eq!(e.sites(), &[site]);
455477
}
456478

479+
#[test]
480+
fn seed_cycle_exposes_its_sites() {
481+
let site = ErrorSite::Cell(CellId::default());
482+
let e = Error::SeedCycle { sites: vec![site] };
483+
assert_eq!(e.sites(), &[site]);
484+
}
485+
457486
#[test]
458487
fn error_site_variants_are_distinct() {
459488
let a = ErrorSite::MethodIndex(0);

‎adam-rs/src/planner.rs‎

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
//!
1616
//! Method selection is value-blind, including for self-referencing components: a
1717
//! self-referencing chain reaches the correct values through [`build_seeds`], which
18-
//! reconstructs each self-referencing input's value at execution time (see
18+
//! replays the release pass's elimination state to choose sibling seed methods,
19+
//! reconstructs each self-referencing input's value at execution time, and reports
20+
//! non-self seed dependency cycles as [`Error::SeedCycle`] rather than substituting a
21+
//! revisited source value (see
1922
//! `docs/superpowers/specs/2026-09-07-adam-rs-value-aware-self-ref-planning-design.md`),
2023
//! not through a value-aware assignment choice.
2124
//!
@@ -31,6 +34,7 @@
3134
3235
use std::any::Any;
3336
use std::collections::{HashMap, HashSet};
37+
use std::rc::Rc;
3438

3539
use slotmap::SlotMap;
3640

@@ -50,18 +54,18 @@ use digraph::{Node, add_filter_edges, build_digraph, topological_order};
5054
use matching::pure_outputs;
5155
use release::ReleaseFailure;
5256

53-
pub(crate) use seed::build_seeds;
57+
pub(crate) use seed::{SeedEvaluationCache, SeedSource, build_seeds, build_seeds_for_steps};
5458

5559
/// The seed value each self-referencing input should read this round, keyed by cell. A
5660
/// cell absent from the map reads its own `source`. See [`seed`] and
5761
/// `docs/superpowers/specs/2026-09-07-adam-rs-value-aware-self-ref-planning-design.md`.
58-
pub(crate) type Seeds = HashMap<CellId, Box<dyn Any>>;
62+
pub(crate) type Seeds = HashMap<CellId, Rc<dyn Any>>;
5963

6064
/// One step of a [`Plan`]'s `execution_order`: either a selected method, or reapplying a
6165
/// source cell's filter against its (now-settled) current argument values.
6266
///
6367
/// See `docs/superpowers/specs/2026-08-25-adam-rs-filter-revalidation-design.md` §2.2.
64-
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
68+
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
6569
pub(crate) enum PlanStep {
6670
/// Execute method `usize` of relationship `RelationshipId`.
6771
Method(RelationshipId, usize),
@@ -77,6 +81,11 @@ pub(crate) enum PlanStep {
7781
pub(crate) struct Plan {
7882
/// Selected steps (methods and filter reclamps) in execution order.
7983
pub(crate) execution_order: Vec<PlanStep>,
84+
/// The exact deterministic cell sequence the release pass evaluated when
85+
/// tentatively eliminating sources; [`seed::build_seeds`] replays sibling-local
86+
/// elimination against this order when choosing seed methods, so sibling selection
87+
/// follows actual elimination state rather than declaration order.
88+
pub(crate) elimination_order: Vec<CellId>,
8089
/// Cells that can never be a source under the relationships this plan considered.
8190
/// See [`forced_output_cells`].
8291
pub(crate) forced_outputs: HashSet<CellId>,
@@ -110,8 +119,8 @@ pub(crate) fn plan(
110119
) -> Result<Plan, Error> {
111120
let (forced_outputs, alive) = forced_output_cells(relationships, active);
112121

113-
let assignment = match release::resolve(cells, relationships, active) {
114-
Ok(a) => a,
122+
let release = match release::resolve(cells, relationships, active) {
123+
Ok(result) => result,
115124
Err(ReleaseFailure::NoAssignment) => {
116125
return Err(Error::Conflict {
117126
sites: conflict_sites(relationships, active),
@@ -123,6 +132,7 @@ pub(crate) fn plan(
123132
});
124133
}
125134
};
135+
let assignment = release.assignment;
126136

127137
let mut adj = build_digraph(&assignment, relationships);
128138
add_filter_edges(&mut adj, cells, &assignment);
@@ -171,11 +181,14 @@ pub(crate) fn plan(
171181
.map(|(&rel_id, _)| rel_id)
172182
.collect();
173183

174-
Ok(Plan {
184+
let plan = Plan {
175185
execution_order,
186+
elimination_order: release.elimination_order,
176187
forced_outputs,
177188
forced_relationships,
178-
})
189+
};
190+
debug_assert_eq!(plan.elimination_order.len(), cells.len());
191+
Ok(plan)
179192
}
180193

181194
/// Maps a cyclic assignment to `Relationship`/`Cell` sites in loop order.
@@ -595,6 +608,27 @@ mod tests {
595608
assert_eq!(plan.execution_order, vec![PlanStep::Method(rel, 0)]);
596609
}
597610

611+
#[test]
612+
fn plan_retains_release_elimination_order() {
613+
let mut sheet = Sheet::new();
614+
let a = sheet.add_cell(0_i32);
615+
let b = sheet.add_cell(0_i32);
616+
let c = sheet.add_cell(0_i32);
617+
sheet
618+
.add_relationship(vec![
619+
Method::from_fn_2_1([a, b], c, |x: &i32, y: &i32| Ok(*x + *y)),
620+
Method::from_fn_2_1([a, c], b, |x: &i32, y: &i32| Ok(*y - *x)),
621+
Method::from_fn_2_1([b, c], a, |x: &i32, y: &i32| Ok(*y - *x)),
622+
])
623+
.unwrap();
624+
sheet.write(a, 5_i32).unwrap();
625+
626+
let active: HashSet<_> = sheet.relationships().collect();
627+
let plan = crate::planner::plan(&sheet.cells, &sheet.relationships, &active).unwrap();
628+
629+
assert_eq!(plan.elimination_order, vec![a, c, b]);
630+
}
631+
598632
#[test]
599633
fn no_filter_reclamp_step_for_a_filtered_cell_that_is_derived_this_round() {
600634
let mut sheet = Sheet::new();

‎adam-rs/src/planner/matching.rs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -536,14 +536,15 @@ mod tests {
536536
let p_b = sheet.add_cell(0_i32);
537537
let u = sheet.add_cell(0_i32);
538538
let v = sheet.add_cell(0_i32);
539+
let w = sheet.add_cell(0_i32);
539540
let i32_ty = std::any::TypeId::of::<i32>();
540541

541542
let r_b = sheet
542543
.add_relationship(vec![
543544
Method::new(
544-
vec![p_b],
545+
vec![p_b, w],
545546
vec![u, v],
546-
vec![i32_ty],
547+
vec![i32_ty, i32_ty],
547548
vec![i32_ty, i32_ty],
548549
|args| {
549550
let p = *args[0].downcast_ref::<i32>().unwrap();
@@ -552,12 +553,12 @@ mod tests {
552553
),
553554
Method::new(
554555
vec![p_b, v],
555-
vec![u],
556+
vec![u, w],
557+
vec![i32_ty, i32_ty],
556558
vec![i32_ty, i32_ty],
557-
vec![i32_ty],
558559
|args| {
559560
let p = *args[0].downcast_ref::<i32>().unwrap();
560-
Ok(vec![Box::new(p)])
561+
Ok(vec![Box::new(p), Box::new(p)])
561562
},
562563
),
563564
])

0 commit comments

Comments
 (0)