Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/ogar-loco/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,14 @@ pub mod node;
pub mod pool;
pub mod program;
pub mod statements;
pub mod telemetry;
pub mod vocabulary;

pub use node::FunctionNode;
pub use pool::{Constant, ConstantPool, PoolError};
pub use program::{Program, branches_of};
pub use statements::{StatementBounds, StatementError, statement_bounds};
pub use telemetry::{FunnelTally, RefusalGate};
pub use vocabulary::conformance::CheckedVocabulary;
pub use vocabulary::{FnSpec, Vocabulary, VocabularyTable};

Expand Down
38 changes: 38 additions & 0 deletions crates/ogar-loco/src/statements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,44 @@
//! A template wanting more than 64 statements is a split signal, not a
//! mask-widening use case.
//!
//! # The >64-statement split contract (wishlist W-5)
//!
//! `statement_bounds` reports the count; it has no opinion on what a
//! lowerer does past 64, because that choice belongs to the vocabulary, not
//! the core. What IS fixed, so N lowerers do not converge on N divergent
//! conventions:
//!
//! - **The split unit is a function, never the mask.** `StepMask` stays a
//! `u64` forever (see `lance_graph_contract::step_mask`); it is never
//! widened to reach a 65th statement. A body over 64 statements is lowered
//! as **two (or more) sibling function bodies**, referenced the same way
//! any nested body is referenced — by index, in the value bytes of a
//! dispatching call in the FIRST body. This is literally the same
//! overflow remedy the ABI uses everywhere else (`BodyError::Overflow`,
//! `PoolError::Full`): split, never widen.
//! - **Statement ordinals restart at 0 in each split function.** A `StepMask`
//! is scoped to the function it selects over, exactly like it already is
//! for a single body — a global statement numbering across the split
//! would smuggle a second addressing scheme past the classid.
//! - **The split point falls on a statement boundary, never mid-statement.**
//! `statement_bounds` already gives the lowerer exactly the boundaries
//! `[first_call, call_count]` a split must respect — cutting inside one
//! would separate an operand run from its consumer, the same
//! desynchronization masking a raw call would cause.
//! - **Whether the sibling body is entered by an unconditional dispatch
//! call (a `CONTINUATION`-shaped hop, always taken) or the vocabulary's
//! own control flow (e.g. a template's own sequencing verb) is a
//! vocabulary decision** — this crate does not mint that call. What is
//! fixed is only the shape (function split, statement-aligned, forward
//! reference) so every vocabulary's split is interoperable at the level a
//! generic tool (a renderer, a step-mask dispatcher) needs to reason
//! about it.
//!
//! A 65-statement body is therefore never a hard error at this layer — it
//! is a signal a vocabulary-side lowering pass must act on, the same way
//! `BodyError::Overflow` is a signal `Program`'s caller must act on rather
//! than something this crate resolves for it.
//!
//! # The segmentation rule
//!
//! Walk the calls simulating stack depth (`depth -= arity; depth += 1` if
Expand Down
242 changes: 242 additions & 0 deletions crates/ogar-loco/src/telemetry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
//! Funnel telemetry — refusal statistics as data (wishlist W-4).
//!
//! # The oracle contract's safe half
//!
//! A generate-and-filter loop (an LLM emitting N candidate bodies, this
//! crate's parse/validate/cast refusing the garbage) needs to know WHICH
//! gate killed each surviving-or-not candidate, so the loop can react —
//! re-prompt, narrow the vocabulary shown, adjust the shape. That is
//! **validity feedback**: "candidate 7 underflowed the stack at call 3." It
//! is safe to hand back to a generator raw; it names a structural defect in
//! the emitted bytes, not a judgment about the candidate's quality.
//!
//! **Fitness feedback — how WELL a surviving candidate performed — is
//! deliberately out of scope here.** That is a downstream instrument's
//! concern (lance-graph's observer-effect payload law: distribution shape ×
//! rank, never a raw scalar looped back into the generator), and this crate
//! has no fitness signal to report in the first place — it only knows
//! whether a candidate parses, casts, and segments.
//!
//! This module is therefore a **pure tally**: fold a batch of
//! [`Result`]s from the crate's own error types into counts per variant.
//! No scoring, no ranking, no scalar feedback — the safe slice, and
//! nothing past it.

use crate::pool::PoolError;
use crate::statements::StatementError;
use crate::vocabulary::conformance::ConformanceError;

/// Which named gate refused a candidate — a flat key so counts can be
/// reported without matching on three different error enums downstream.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum RefusalGate {
/// [`ConformanceError::SharedCoreDrift`].
ConformanceDrift,
/// [`ConformanceError::ShapeTooNarrowForRefs`].
ConformanceShapeTooNarrow,
/// [`StatementError::Uncovered`].
StatementUncovered,
/// [`StatementError::StackUnderflow`].
StatementUnderflow,
/// [`StatementError::DanglingOperands`].
StatementDangling,
/// [`PoolError::Full`].
PoolFull,
/// [`PoolError::TooWide`].
PoolTooWide,
}

impl RefusalGate {
/// Every gate, for a stable iteration/report order.
pub const ALL: [RefusalGate; 7] = [
RefusalGate::ConformanceDrift,
RefusalGate::ConformanceShapeTooNarrow,
RefusalGate::StatementUncovered,
RefusalGate::StatementUnderflow,
RefusalGate::StatementDangling,
RefusalGate::PoolFull,
RefusalGate::PoolTooWide,
];
}

impl From<&ConformanceError> for RefusalGate {
fn from(e: &ConformanceError) -> Self {
match e {
ConformanceError::SharedCoreDrift { .. } => RefusalGate::ConformanceDrift,
ConformanceError::ShapeTooNarrowForRefs { .. } => {
RefusalGate::ConformanceShapeTooNarrow
}
}
}
}

impl From<&StatementError> for RefusalGate {
fn from(e: &StatementError) -> Self {
match e {
StatementError::Uncovered { .. } => RefusalGate::StatementUncovered,
StatementError::StackUnderflow { .. } => RefusalGate::StatementUnderflow,
StatementError::DanglingOperands { .. } => RefusalGate::StatementDangling,
}
}
}

impl From<&PoolError> for RefusalGate {
fn from(e: &PoolError) -> Self {
match e {
PoolError::Full => RefusalGate::PoolFull,
PoolError::TooWide { .. } => RefusalGate::PoolTooWide,
}
}
}

/// A batch's refusal tally — how many candidates survived, and how many the
/// funnel refused, broken down by [`RefusalGate`].
///
/// Deliberately data-only: no ranking, no scoring, no fitness signal. Build
/// with [`FunnelTally::default`] and [`record`](Self::record) each
/// candidate's outcome as the batch runs, or fold a slice of results with
/// [`FunnelTally::from_results`].
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FunnelTally {
/// How many candidates survived every gate.
pub survived: u32,
/// Refusal counts, one entry per [`RefusalGate::ALL`] member in order.
counts: [u32; RefusalGate::ALL.len()],
}

impl FunnelTally {
/// A tally over an already-collected batch of outcomes.
#[must_use]
pub fn from_results<'a, E, I>(results: I) -> Self
where
E: 'a,
RefusalGate: for<'b> From<&'b E>,
I: IntoIterator<Item = &'a Result<(), E>>,
{
let mut t = Self::default();
for r in results {
t.record(r.as_ref().map(|_| ()).map_err(RefusalGate::from));
}
t
}

/// Record one candidate's outcome: `Ok(())` for a survivor, `Err(gate)`
/// for a refusal at the named gate.
pub fn record(&mut self, outcome: Result<(), RefusalGate>) {
match outcome {
Ok(()) => self.survived += 1,
Err(gate) => {
let i = RefusalGate::ALL
.iter()
.position(|g| *g == gate)
.expect("RefusalGate::ALL is exhaustive over the enum");
self.counts[i] += 1;
}
}
}

/// How many candidates this tally has seen in total.
#[must_use]
pub fn total(&self) -> u32 {
self.survived + self.counts.iter().sum::<u32>()
}

/// Refusals at one gate.
#[must_use]
pub fn at(&self, gate: RefusalGate) -> u32 {
let i = RefusalGate::ALL
.iter()
.position(|g| *g == gate)
.expect("RefusalGate::ALL is exhaustive over the enum");
self.counts[i]
}

/// `(gate, count)` for every gate that refused at least one candidate,
/// in [`RefusalGate::ALL`] order — the report a caller actually wants
/// (a batch that never hit `PoolFull` should not print a zero row).
pub fn nonzero_gates(&self) -> impl Iterator<Item = (RefusalGate, u32)> + '_ {
RefusalGate::ALL
.into_iter()
.zip(self.counts)
.filter(|(_, n)| *n > 0)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn a_mixed_batch_tallies_survivors_and_gates_separately() {
let mut t = FunnelTally::default();
t.record(Ok(()));
t.record(Ok(()));
t.record(Err(RefusalGate::StatementUnderflow));
t.record(Err(RefusalGate::StatementUnderflow));
t.record(Err(RefusalGate::PoolFull));

assert_eq!(t.survived, 2);
assert_eq!(t.at(RefusalGate::StatementUnderflow), 2);
assert_eq!(t.at(RefusalGate::PoolFull), 1);
// Silence twin: a gate that never fired reports zero, not absence
// masquerading as failure to look it up.
assert_eq!(t.at(RefusalGate::PoolTooWide), 0);
assert_eq!(t.total(), 5);
}

#[test]
fn nonzero_gates_omits_gates_that_never_fired() {
let mut t = FunnelTally::default();
t.record(Ok(()));
t.record(Err(RefusalGate::ConformanceDrift));
let report: Vec<_> = t.nonzero_gates().collect();
assert_eq!(report, vec![(RefusalGate::ConformanceDrift, 1)]);
// Anti-vacuity: a fully-clean batch reports an EMPTY nonzero list,
// not a list of every gate at zero.
let clean = FunnelTally {
survived: 3,
..Default::default()
};
assert_eq!(clean.nonzero_gates().count(), 0);
}

#[test]
fn real_error_types_map_to_the_gate_that_actually_fired() {
// This is the point: a caller running the real funnel does not
// hand-translate three enums into RefusalGate — `.into()` does it,
// and it must land on the RIGHT gate, not just *a* gate.
let underflow = StatementError::StackUnderflow {
index: 0,
f: crate::FnIndex::ADD,
};
assert_eq!(
RefusalGate::from(&underflow),
RefusalGate::StatementUnderflow
);
let dangling = StatementError::DanglingOperands { depth: 2 };
assert_eq!(RefusalGate::from(&dangling), RefusalGate::StatementDangling);
let full = PoolError::Full;
assert_eq!(RefusalGate::from(&full), RefusalGate::PoolFull);
let drift = ConformanceError::SharedCoreDrift {
f: crate::FnIndex::ADD,
what: "stack_arity",
};
assert_eq!(RefusalGate::from(&drift), RefusalGate::ConformanceDrift);
}

#[test]
fn from_results_folds_a_batch_without_hand_rolled_matching() {
let batch: Vec<Result<(), StatementError>> = vec![
Ok(()),
Err(StatementError::StackUnderflow {
index: 0,
f: crate::FnIndex::ADD,
}),
Ok(()),
];
let t = FunnelTally::from_results(batch.iter());
assert_eq!(t.survived, 2);
assert_eq!(t.at(RefusalGate::StatementUnderflow), 1);
}
}
Loading
Loading