1//! The batch planner: how each row reaches the model (contract 2//! *Execution*, "Request layout"). 3//! 4//! A row with two or more distinct questions is sent as the state, each 5//! question a branch. A row with exactly one would be sent in that 6//! question's instructions, beside other rows' questions, but that layout 7//! has no accuracy measurement against row as state, so it waits on the 8//! layout-parity release gate (*Testing*). Until the gate passes it is 9//! unreachable: [`LayoutParity`] has no values, so no [`Planner`] can hold 10//! one, and every row is sent as the state. 11 12use std::collections::{HashMap, HashSet}; 13use std::hash::Hash; 14 15use jev_protocol::{Json, ProtocolError}; 16 17use crate::Layout; 18 19/// Evidence that row in instructions passed the layout-parity release 20/// gate: jev-orderby-bench's mean |Δp|, Spearman and position effect 21/// against row as state, recorded against the pinned model version. 22/// 23/// It has not been run, so the type is uninhabited. Giving it a value is 24/// the change that enables row in instructions, and it belongs with the 25/// recorded result. 26#[derive(Clone, Copy, Debug)] 27pub enum LayoutParity {} 28 29/// Chooses each row's layout, and builds its state. 30#[derive(Clone, Copy, Debug, Default)] 31pub struct Planner { 32 /// Always `None` while [`LayoutParity`] is uninhabited. 33 parity: Option<LayoutParity>, 34} 35 36/// A row's layout as the [`Planner`] chose it. Only a planner makes one, 37/// and row in instructions carries the [`LayoutParity`] evidence, so a 38/// layout the gate has not passed cannot be written down, let alone 39/// handed to [`Planner::place`]. 40#[derive(Clone, Copy, Debug)] 41pub struct Planned(Plan); 42 43#[derive(Clone, Copy, Debug)] 44enum Plan { 45 RowAsState, 46 RowInInstructions(LayoutParity), 47} 48 49impl Planned { 50 pub fn layout(self) -> Layout { 51 match self.0 { 52 Plan::RowAsState => Layout::RowAsState, 53 Plan::RowInInstructions(gate) => match gate {}, 54 } 55 } 56} 57 58/// Where a row goes in its request: its layout and the request's 59/// `state`. Only a [`Planner`] makes one, so a layout the gate has not 60/// passed never reaches a request or a cache key. 61#[derive(Clone)] 62pub struct Placement { 63 layout: Layout, 64 state: Json, 65} 66 67impl Placement { 68 pub(crate) fn new(layout: Layout, state: Json) -> Self { 69 Placement { layout, state } 70 } 71 72 pub fn layout(&self) -> Layout { 73 self.layout 74 } 75 76 /// Under row as state, the row's canonical JSON. 77 pub fn state(&self) -> &Json { 78 &self.state 79 } 80} 81 82impl Planner { 83 pub const fn new() -> Self { 84 Planner { parity: None } 85 } 86 87 /// The layout for a row with `questions` distinct questions. 88 pub fn layout(&self, questions: usize) -> Planned { 89 match (questions, self.parity) { 90 (1, Some(gate)) => Planned(Plan::RowInInstructions(gate)), 91 _ => Planned(Plan::RowAsState), 92 } 93 } 94 95 /// Each row's layout, from the questions asked about it: `asks` pairs 96 /// a row with one question's serialized bytes, and a question asked 97 /// twice about the same row counts once. 98 pub fn layouts<'q, R: Hash + Eq>(&self, asks: impl IntoIterator<Item = (R, &'q [u8])>) -> HashMap<R, Planned> { 99 let mut questions: HashMap<R, HashSet<&'q [u8]>> = HashMap::new(); 100 for (row, question) in asks { 101 questions.entry(row).or_default().insert(question); 102 } 103 questions.into_iter().map(|(row, q)| (row, self.layout(q.len()))).collect() 104 } 105 106 /// `row`, a JSON text, placed as `planned`. Refused when the row is 107 /// not JSON. 108 pub fn place(&self, planned: Planned, row: &str) -> Result<Placement, ProtocolError> { 109 match planned.0 { 110 Plan::RowAsState => Ok(Placement::new(Layout::RowAsState, Json::canonical(row)?)), 111 Plan::RowInInstructions(gate) => match gate {}, 112 } 113 } 114} 115 116#[cfg(test)] 117mod tests { 118 use super::*; 119 120 #[test] 121 fn two_or_more_questions_are_row_as_state() { 122 let p = Planner::new(); 123 assert_eq!(p.layout(2).layout(), Layout::RowAsState); 124 assert_eq!(p.layout(255).layout(), Layout::RowAsState); 125 } 126 127 #[test] 128 fn one_question_is_row_as_state_until_the_gate_passes() { 129 assert_eq!(Planner::new().layout(1).layout(), Layout::RowAsState); 130 assert_eq!(Planner::default().layout(1).layout(), Layout::RowAsState); 131 } 132 133 #[test] 134 fn layouts_count_distinct_questions_per_row() { 135 let (a, b): (&[u8], &[u8]) = (b"{\"noul\":1}", b"{\"noul\":2}"); 136 let layouts = Planner::new().layouts([("r1", a), ("r1", b), ("r2", a), ("r2", a)]); 137 assert_eq!(layouts.len(), 2); 138 assert_eq!(layouts["r1"].layout(), Layout::RowAsState); 139 assert_eq!(layouts["r2"].layout(), Layout::RowAsState); 140 } 141 142 #[test] 143 fn row_as_state_sends_the_canonical_row() { 144 let p = Planner::new(); 145 let placed = p.place(p.layout(1), r#"{ "b": 1, "a": "x" }"#).unwrap(); 146 assert_eq!(placed.layout(), Layout::RowAsState); 147 assert_eq!(placed.state().as_str(), r#"{"a":"x","b":1}"#); 148 } 149 150 #[test] 151 fn a_row_that_is_not_json_is_refused() { 152 let p = Planner::new(); 153 assert!(p.place(p.layout(2), "{").is_err()); 154 } 155}