223 lines
7.0 KiB
Rust
223 lines
7.0 KiB
Rust
//! Verified pipeline composition.
|
|
//!
|
|
//! Provides `VerifiedStage` for type-safe pipeline stages and `compose_stages`
|
|
//! for proving that two stages can be composed (output type matches input type).
|
|
|
|
use crate::error::{Result, VerificationError};
|
|
use crate::ProofEnvironment;
|
|
use std::marker::PhantomData;
|
|
|
|
/// A verified pipeline stage with proven input/output type compatibility.
|
|
///
|
|
/// `A` and `B` are phantom type parameters representing the stage's
|
|
/// logical input and output types (compile-time markers, not runtime).
|
|
///
|
|
/// The `proof_id` field references the proof term that the stage's
|
|
/// implementation correctly transforms `A` to `B`.
|
|
#[derive(Debug)]
|
|
pub struct VerifiedStage<A, B> {
|
|
/// Human-readable stage name (e.g., "kmer_embedding", "variant_call").
|
|
pub name: String,
|
|
/// Proof term ID.
|
|
pub proof_id: u32,
|
|
/// Input type term ID in the environment.
|
|
pub input_type_id: u32,
|
|
/// Output type term ID in the environment.
|
|
pub output_type_id: u32,
|
|
_phantom: PhantomData<(A, B)>,
|
|
}
|
|
|
|
impl<A, B> VerifiedStage<A, B> {
|
|
/// Create a new verified stage with its correctness proof.
|
|
pub fn new(
|
|
name: impl Into<String>,
|
|
proof_id: u32,
|
|
input_type_id: u32,
|
|
output_type_id: u32,
|
|
) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
proof_id,
|
|
input_type_id,
|
|
output_type_id,
|
|
_phantom: PhantomData,
|
|
}
|
|
}
|
|
|
|
/// Get the stage name.
|
|
pub fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
}
|
|
|
|
/// Compose two verified stages, producing a proof that the pipeline is type-safe.
|
|
///
|
|
/// Checks that `f.output_type_id == g.input_type_id` (pointer equality via
|
|
/// hash-consing). If they match, constructs a composed stage `A -> C`.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `TypeCheckFailed` if the output type of `f` does not match
|
|
/// the input type of `g`.
|
|
pub fn compose_stages<A, B, C>(
|
|
f: &VerifiedStage<A, B>,
|
|
g: &VerifiedStage<B, C>,
|
|
env: &mut ProofEnvironment,
|
|
) -> Result<VerifiedStage<A, C>> {
|
|
// Verify output(f) = input(g) via ID equality (hash-consed)
|
|
if f.output_type_id != g.input_type_id {
|
|
return Err(VerificationError::TypeCheckFailed(format!(
|
|
"pipeline type mismatch: stage '{}' output (type#{}) != stage '{}' input (type#{})",
|
|
f.name, f.output_type_id, g.name, g.input_type_id,
|
|
)));
|
|
}
|
|
|
|
// Construct composed proof
|
|
let proof_id = env.alloc_term();
|
|
env.stats.proofs_verified += 1;
|
|
|
|
Ok(VerifiedStage::new(
|
|
format!("{} >> {}", f.name, g.name),
|
|
proof_id,
|
|
f.input_type_id,
|
|
g.output_type_id,
|
|
))
|
|
}
|
|
|
|
/// Compose a chain of stages, verifying each connection.
|
|
///
|
|
/// Takes a list of (name, input_type_id, output_type_id) and produces
|
|
/// a single composed stage spanning the entire chain.
|
|
pub fn compose_chain(
|
|
stages: &[(String, u32, u32)],
|
|
env: &mut ProofEnvironment,
|
|
) -> Result<(u32, u32, u32)> {
|
|
if stages.is_empty() {
|
|
return Err(VerificationError::ProofConstructionFailed(
|
|
"empty pipeline chain".into(),
|
|
));
|
|
}
|
|
|
|
let mut current_output = stages[0].2;
|
|
let mut proof_ids = Vec::with_capacity(stages.len());
|
|
proof_ids.push(env.alloc_term());
|
|
|
|
for (i, stage) in stages.iter().enumerate().skip(1) {
|
|
if current_output != stage.1 {
|
|
return Err(VerificationError::TypeCheckFailed(format!(
|
|
"chain break at stage {}: type#{} != type#{}",
|
|
i, current_output, stage.1,
|
|
)));
|
|
}
|
|
proof_ids.push(env.alloc_term());
|
|
current_output = stage.2;
|
|
}
|
|
|
|
env.stats.proofs_verified += stages.len() as u64;
|
|
let final_proof = env.alloc_term();
|
|
Ok((stages[0].1, current_output, final_proof))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// Marker types for phantom parameters
|
|
#[derive(Debug)]
|
|
struct KmerInput;
|
|
#[derive(Debug)]
|
|
struct EmbeddingOutput;
|
|
#[derive(Debug)]
|
|
struct AlignmentOutput;
|
|
#[derive(Debug)]
|
|
struct VariantOutput;
|
|
|
|
#[test]
|
|
fn test_verified_stage_creation() {
|
|
let stage: VerifiedStage<KmerInput, EmbeddingOutput> =
|
|
VerifiedStage::new("kmer_embed", 0, 1, 2);
|
|
assert_eq!(stage.name(), "kmer_embed");
|
|
assert_eq!(stage.input_type_id, 1);
|
|
assert_eq!(stage.output_type_id, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compose_stages_matching() {
|
|
let mut env = ProofEnvironment::new();
|
|
|
|
let f: VerifiedStage<KmerInput, EmbeddingOutput> = VerifiedStage::new("embed", 0, 1, 2);
|
|
let g: VerifiedStage<EmbeddingOutput, AlignmentOutput> =
|
|
VerifiedStage::new("align", 1, 2, 3);
|
|
|
|
let composed = compose_stages(&f, &g, &mut env);
|
|
assert!(composed.is_ok());
|
|
let c = composed.unwrap();
|
|
assert_eq!(c.name(), "embed >> align");
|
|
assert_eq!(c.input_type_id, 1);
|
|
assert_eq!(c.output_type_id, 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compose_stages_mismatch() {
|
|
let mut env = ProofEnvironment::new();
|
|
|
|
let f: VerifiedStage<KmerInput, EmbeddingOutput> = VerifiedStage::new("embed", 0, 1, 2);
|
|
let g: VerifiedStage<EmbeddingOutput, AlignmentOutput> =
|
|
VerifiedStage::new("align", 1, 99, 3); // 99 != 2
|
|
|
|
let composed = compose_stages(&f, &g, &mut env);
|
|
assert!(composed.is_err());
|
|
let err = composed.unwrap_err();
|
|
assert!(matches!(err, VerificationError::TypeCheckFailed(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_compose_three_stages() {
|
|
let mut env = ProofEnvironment::new();
|
|
|
|
let f: VerifiedStage<KmerInput, EmbeddingOutput> = VerifiedStage::new("embed", 0, 1, 2);
|
|
let g: VerifiedStage<EmbeddingOutput, AlignmentOutput> =
|
|
VerifiedStage::new("align", 1, 2, 3);
|
|
let h: VerifiedStage<AlignmentOutput, VariantOutput> = VerifiedStage::new("call", 2, 3, 4);
|
|
|
|
let fg = compose_stages(&f, &g, &mut env).unwrap();
|
|
let fgh = compose_stages(&fg, &h, &mut env).unwrap();
|
|
assert_eq!(fgh.name(), "embed >> align >> call");
|
|
assert_eq!(fgh.input_type_id, 1);
|
|
assert_eq!(fgh.output_type_id, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compose_chain() {
|
|
let mut env = ProofEnvironment::new();
|
|
let stages = vec![
|
|
("embed".into(), 1u32, 2u32),
|
|
("align".into(), 2, 3),
|
|
("call".into(), 3, 4),
|
|
];
|
|
let result = compose_chain(&stages, &mut env);
|
|
assert!(result.is_ok());
|
|
let (input, output, _proof) = result.unwrap();
|
|
assert_eq!(input, 1);
|
|
assert_eq!(output, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compose_chain_break() {
|
|
let mut env = ProofEnvironment::new();
|
|
let stages = vec![
|
|
("embed".into(), 1u32, 2u32),
|
|
("align".into(), 99, 3), // break: 99 != 2
|
|
];
|
|
let result = compose_chain(&stages, &mut env);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_compose_chain_empty() {
|
|
let mut env = ProofEnvironment::new();
|
|
let result = compose_chain(&[], &mut env);
|
|
assert!(result.is_err());
|
|
}
|
|
}
|