Create multiple PODs where resource limits for a single POD are exceeded (#444)

* Create multiple PODs where resource limits for a single POD are exceeded

* HashSet -> BTreeSet determinism fix

* Fixed incorrect assignment of input PODs and added test

* Ensure only a single output POD

* Return error when reveal() called with unknown statement

* Use unreachable! for presumed-impossible cases

* Use assert_eq! rather than debug_assert_eq

* Use FIFO for topological sort

* Simplify bounds calculation

* Some more simplifications/comments

* Enforce dep_idx < idx invariant

* Incrementally solve rather than estimating slack

* Fix tests to correctly test dependencies between private and public statements

* More tidying

* Note possible optimisation of MainPodBuilder cloning of input PODs

* Fix tracking of total input POD count

* Refactor tests

* Formatting

* Small optimisation: use Vec in place of BTreeSet

* Account for automatically-inserted Contains statements

* Formatting

* Fix possible issue with copied statements

* Simplify result type given only a single result MainPod

* Remove unnecessary POD count estimate functionality

* Simplify dependency ordering and tracking

* Remove notion of multiple output PODs from solver

* Minor simplifications

* Use add_constraint instead of with

* Remove unnecessary check following assertion

* Fix handling of anchored keys given that Contains statements are not auto-inserted if they already exist

* Fix confusing dependency graph test

* Remove prove_order

* Fix deduplication and possible double-counting of public but not copied statements

* Reorder so that the output POD is the final POD

* Add more detailed tests

* Remove redundant tests

* Simplify POD counting

* More docs

* Flag more branches as unreachable

* Formatting

* Fix for changed custom batch parsing
This commit is contained in:
Rob Knight 2026-01-28 07:44:04 +01:00 committed by GitHub
parent d1b7b4d37e
commit 48aa004ae5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 3047 additions and 0 deletions

View file

@ -0,0 +1,178 @@
//! Dependency analysis for statements and operations.
//!
//! This module analyzes dependencies between statements to determine
//! which statements must be proved before others.
use std::collections::HashMap;
use crate::{
frontend::{Operation, OperationArg},
middleware::{Hash, Statement},
};
/// Represents a source of a statement dependency.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum StatementSource {
/// Statement created within this builder at the given index.
Internal(usize),
/// Statement from an external input POD (identified by POD hash).
External(Hash),
}
/// Dependency graph for all statements in a builder.
///
/// Each element `statement_deps[i]` is the list of dependencies for statement `i`.
#[derive(Clone, Debug)]
pub struct DependencyGraph {
/// Dependencies for each statement (indexed by statement index).
pub statement_deps: Vec<Vec<StatementSource>>,
}
impl DependencyGraph {
/// Build a dependency graph from statements and operations.
///
/// `statements` and `operations` should be parallel arrays where
/// `operations[i]` produces `statements[i]`.
///
/// `external_pod_statements` maps (pod_hash, statement) pairs to enable
/// recognizing references to external POD statements.
pub fn build(
statements: &[Statement],
operations: &[Operation],
external_pod_statements: &HashMap<Statement, Hash>,
) -> Self {
let mut statement_deps = Vec::with_capacity(statements.len());
// Build a map from statement to its index for internal lookup.
// Use entry().or_insert() to preserve the FIRST occurrence of each statement.
// This is important for CopyStatement: if statements[0] = A and statements[2] = copy(A) = A,
// we want statement_to_index[A] = 0 (the original), not 2 (the copy).
let mut statement_to_index: HashMap<&Statement, usize> = HashMap::new();
for (i, s) in statements.iter().enumerate() {
if !s.is_none() {
statement_to_index.entry(s).or_insert(i);
}
}
for (idx, op) in operations.iter().enumerate() {
let mut deps = Vec::new();
// Examine each argument to the operation
for arg in &op.1 {
if let OperationArg::Statement(ref dep_stmt) = arg {
if dep_stmt.is_none() {
continue;
}
// Check if this is an internal statement (created earlier in this builder)
if let Some(&dep_idx) = statement_to_index.get(dep_stmt) {
// Internal dependencies must always be from earlier statements
assert!(
dep_idx <= idx,
"Statement at index {} depends on future statement at index {}",
idx,
dep_idx
);
if dep_idx < idx {
// The statement was created by an earlier operation
deps.push(StatementSource::Internal(dep_idx));
continue;
}
// dep_idx == idx: The first occurrence of this statement is at the current index,
// meaning this operation both takes and produces this statement (e.g., CopyStatement
// copying from an external POD). Fall through to check external PODs for the source.
}
// Check if this is from an external POD
if let Some(&pod_hash) = external_pod_statements.get(dep_stmt) {
deps.push(StatementSource::External(pod_hash));
} else {
// Statement arguments should either be internal (created earlier)
// or from external PODs. If neither, something is wrong.
unreachable!(
"Statement argument not found in internal statements or external PODs: {:?}",
dep_stmt
);
}
}
}
statement_deps.push(deps);
}
Self { statement_deps }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
frontend::Operation as FrontendOp,
middleware::{NativeOperation, OperationAux, OperationType, Value, ValueRef},
};
fn equal_stmt(n: i64) -> Statement {
Statement::Equal(
ValueRef::Literal(Value::from(n)),
ValueRef::Literal(Value::from(n)),
)
}
/// None operation produces Statement::None
fn none_op() -> FrontendOp {
FrontendOp(
OperationType::Native(NativeOperation::None),
vec![],
OperationAux::None,
)
}
/// CopyStatement(s) produces s (the same statement)
fn copy_op(stmt: Statement) -> FrontendOp {
FrontendOp(
OperationType::Native(NativeOperation::CopyStatement),
vec![OperationArg::Statement(stmt)],
OperationAux::None,
)
}
#[test]
fn test_copy_creates_dependency_on_original() {
// CopyStatement(s) produces s. When we copy a statement, the copy
// depends on where that statement first appears.
//
// statements[0] = s (produced by none_op - not realistic, but we need a first occurrence)
// statements[1] = s (produced by copy_op(s))
//
// op1's argument s matches statements[0], so statement 1 depends on statement 0.
let s = equal_stmt(1);
let statements = vec![s.clone(), s.clone()];
let operations = vec![
none_op(), // Placeholder - in reality something else would produce s
copy_op(s), // Copies s, producing s. Depends on statements[0].
];
let graph = DependencyGraph::build(&statements, &operations, &HashMap::new());
assert!(graph.statement_deps[0].is_empty());
assert_eq!(graph.statement_deps[1], vec![StatementSource::Internal(0)]);
}
#[test]
fn test_multiple_copies_depend_on_original() {
// Multiple copies of the same statement all depend on where it first appears.
let s = equal_stmt(1);
let statements = vec![s.clone(), s.clone(), s.clone()];
let operations = vec![none_op(), copy_op(s.clone()), copy_op(s)];
let graph = DependencyGraph::build(&statements, &operations, &HashMap::new());
assert!(graph.statement_deps[0].is_empty());
assert_eq!(graph.statement_deps[1], vec![StatementSource::Internal(0)]);
assert_eq!(graph.statement_deps[2], vec![StatementSource::Internal(0)]);
}
}