From 118849d0c93d241c7f45af74c62bf28988e029aa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 20:30:48 +0000 Subject: [PATCH 1/4] =?UTF-8?q?loco:=20the=20orchestration=20engine=20?= =?UTF-8?q?=E2=80=94=20control=20flow,=20and=20deliberately=20nothing=20el?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ogar-loco could describe a program and refuse an ill-formed one, but it could not RUN one. The engine lived only in `examples/interpret_probe.rs`, stranded where no consumer can reach it and no `cargo test` exercises it. This lifts orchestration into the library, scoped hard: Interpreter owns the pc walk, IF / IF_ELSE / REPEAT / WHILE / REPEAT_UNTIL, the condition-span re-run, the iteration cap, recursion into referenced bodies. Dialect owns every semantic — what a value is (`type Value`), when one is true (`truthy`), how many times a REPEAT runs (`repeat_count`), and what any non-branching call does (`call`). That split is the claim "loco speaks all dialects", made mechanical. Blockly plugs in with i64 arithmetic; a thinking dialect plugs in with masks and truth values; the engine never learns which it is running. The masking algebra exists to make the reasoning tactics cheap — which is exactly what lets a tactic be a PROGRAM here rather than a bespoke function somewhere below. Scope held to the five control-flow ops an earlier probe validated against independent reference implementations. FOREVER / BREAK / CONTINUE / STOP / RETURN / FOR_EACH / FOR_RANGE / PROC_DEF are REFUSED (`RunError::UnhandledControlFlow`), not approximated. Shipping them would ship behaviour nothing has executed; they land when a falsifier lands with them. The subtlety worth keeping: a WHILE's condition is not a separate body, it is the calls immediately PRECEDING the loop call in the same body. Re-testing means re-running that local span, so `operand_span_start` walks backwards over the arities to find where it begins. Testing the stack top instead would loop forever on the first truthy condition — and would pass any test whose loop runs zero or one times. Six falsifiers, each with an input that makes it fail: REPEAT against the closed form n(n+1)/2; WHILE computing gcd(1071, 462) = 21 (a real multi-iteration re-run); a never-falsifying loop hitting the cap rather than hanging; a branch to a missing body refused; an unproven control-flow call refused; and the engine's own emptiness — the dialect sees every non-branching call, control flow reaches it never. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X6y3drwKSE2zSgoexheLFX --- crates/ogar-loco/src/interpret.rs | 652 ++++++++++++++++++++++++++++++ crates/ogar-loco/src/lib.rs | 2 + 2 files changed, 654 insertions(+) create mode 100644 crates/ogar-loco/src/interpret.rs diff --git a/crates/ogar-loco/src/interpret.rs b/crates/ogar-loco/src/interpret.rs new file mode 100644 index 0000000..6d52b51 --- /dev/null +++ b/crates/ogar-loco/src/interpret.rs @@ -0,0 +1,652 @@ +//! The **orchestration engine** — control flow, and deliberately nothing else. +//! +//! # Why this owns so little +//! +//! This crate's promise is that it *"can speak all dialects"*. An engine that +//! knew what `ADD` meant could only ever speak one. So the split here is +//! total: [`Interpreter`] owns the program counter, the branch decisions, the +//! loop bounds and the recursion into referenced bodies, and a [`Dialect`] +//! owns **every semantic** — what a value is, when one is true, what a +//! non-branching call does. +//! +//! Blockly plugs in with `i64` arithmetic. A thinking dialect plugs in with +//! masks and truth values, whose primitives are cheap for a reason — the +//! masking algebra exists to make the reasoning tactics cheap, which is what +//! lets a tactic be a *program here* rather than a bespoke function +//! somewhere below. The engine never learns which dialect it is running. +//! +//! # What it owns, exactly +//! +//! Five control-flow functions: [`FnIndex::IF`], [`FnIndex::IF_ELSE`], +//! [`FnIndex::REPEAT`], [`FnIndex::WHILE`], [`FnIndex::REPEAT_UNTIL`]. +//! +//! That is not the whole control-flow block, and the gap is deliberate. +//! `FOREVER`, `BREAK`, `CONTINUE`, `STOP`, `RETURN`, `FOR_EACH`, `FOR_RANGE` +//! and `PROC_DEF` are **refused**, not guessed +//! ([`RunError::UnhandledControlFlow`]). These five are the set an earlier +//! probe ran against real algorithms — iterative GCD, summation, nested +//! classification, Collatz step-counting — each checked against an +//! independent reference implementation. Shipping the rest would be shipping +//! behaviour nothing has executed. They land when a falsifier lands with +//! them. +//! +//! # The loop subtlety, which is easy to lose +//! +//! A `WHILE`'s condition is not a separate body. It is the calls immediately +//! *preceding* the loop call in the same body, already evaluated by the +//! forward walk that reached it. So re-testing means re-running that local +//! span — [`Interpreter::operand_span_start`] walks backwards over the +//! arities to find where it begins. An implementation that re-tested the +//! stack top without re-running the span would loop forever on the first +//! truthy condition, and would pass any test whose loop runs zero or one +//! times. + +use crate::vocabulary::conformance::CheckedVocabulary; +use crate::{Call, FnIndex, FunctionBody, Program, Vocabulary}; + +/// Default ceiling on iterations of one loop call, before +/// [`RunError::IterationCap`]. +/// +/// A cap rather than trust: a `WHILE` whose dialect never falsifies its +/// condition is a hang, and a hang in an orchestration layer is +/// indistinguishable from work. +pub const DEFAULT_ITERATION_CAP: u32 = 100_000; + +/// Everything the engine refuses to know. +/// +/// The three reads are separated on purpose. `truthy` is a branch decision, +/// `repeat_count` is a loop bound, and they are different questions a dialect +/// may answer differently — a mask is "true" when it is non-empty, and its +/// repeat count is its population. +pub trait Dialect { + /// What this dialect's stack holds. + type Value; + /// What its calls can fail with. + type Error; + + /// Does this value branch a conditional? + fn truthy(&self, value: &Self::Value) -> bool; + + /// How many times should a [`FnIndex::REPEAT`] carrying this value run? + fn repeat_count(&self, value: &Self::Value) -> u32; + + /// Execute one NON-branching call: pop its operands, push its result. + /// + /// The engine has already established that `f` does not branch. Whether + /// `f` is a shared-core byte or one of this dialect's own is the + /// dialect's business — the engine does not look. + fn call( + &mut self, + f: FnIndex, + values: [u8; 3], + stack: &mut Vec, + ) -> Result<(), Self::Error>; +} + +/// Why a run stopped short. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum RunError { + /// The dialect refused a call. + Dialect(E), + /// A control-flow call wanted an operand and the stack was empty. + StackUnderflow { + /// The call that asked. + call: FnIndex, + }, + /// The vocabulary does not cover this call's arity, so the engine cannot + /// know how many operands it consumes — and a wrong arity does not give a + /// slightly wrong answer, it reattributes every later operand. + UncoveredArity { + /// The call whose arity is unknown. + call: FnIndex, + }, + /// A branch named a function index the program does not contain. + UnresolvedBody { + /// The branching call. + call: FnIndex, + /// The index it named. + target: u8, + }, + /// A loop ran past [`Interpreter::iteration_cap`]. + IterationCap { + /// The loop call. + call: FnIndex, + /// The cap it exceeded. + cap: u32, + }, + /// A control-flow call outside the five this engine executes. Refused + /// rather than approximated — see the module doc. + UnhandledControlFlow { + /// The call that branches but has no execution rule here. + call: FnIndex, + }, +} + +/// A running program: the engine's own state, plus the dialect's. +pub struct Interpreter<'a, V: Vocabulary, D: Dialect> { + vocab: &'a CheckedVocabulary, + program: &'a Program, + dialect: D, + stack: Vec, + iteration_cap: u32, +} + +impl<'a, V: Vocabulary, D: Dialect> Interpreter<'a, V, D> { + /// Bind a dialect to a program. + pub fn new(vocab: &'a CheckedVocabulary, program: &'a Program, dialect: D) -> Self { + Self { + vocab, + program, + dialect, + stack: Vec::new(), + iteration_cap: DEFAULT_ITERATION_CAP, + } + } + + /// Replace the per-loop iteration ceiling. + pub fn with_iteration_cap(mut self, cap: u32) -> Self { + self.iteration_cap = cap; + self + } + + /// The ceiling a loop may not exceed. + pub fn iteration_cap(&self) -> u32 { + self.iteration_cap + } + + /// Borrow the dialect (its own state is where results usually land). + pub fn dialect(&self) -> &D { + &self.dialect + } + + /// Borrow the operand stack. + pub fn stack(&self) -> &[D::Value] { + &self.stack + } + + /// Run the program's entry function. + pub fn run(&mut self) -> Result<(), RunError> { + self.run_function(0) + } + + /// Run one function body to completion. + fn run_function(&mut self, index: usize) -> Result<(), RunError> { + let Some(body) = self.program.functions.get(index) else { + return Ok(()); + }; + let mut pc = 0usize; + while let Some(call) = body.call(pc) { + let f = call.function; + if !self.vocab.table().branches(f) { + self.dialect + .call(f, call.values, &mut self.stack) + .map_err(RunError::Dialect)?; + pc += 1; + continue; + } + self.run_branching(body, pc, call)?; + pc += 1; + } + Ok(()) + } + + /// Execute one branching call at `pc`. + fn run_branching( + &mut self, + body: &FunctionBody, + pc: usize, + call: Call, + ) -> Result<(), RunError> { + let f = call.function; + let arity = self + .vocab + .table() + .stack_arity(f) + .ok_or(RunError::UncoveredArity { call: f })?; + + match f { + FnIndex::IF => { + let cond = self.pop(f)?; + if self.dialect.truthy(&cond) { + self.branch(f, call.values[0])?; + } + } + FnIndex::IF_ELSE => { + let cond = self.pop(f)?; + let target = if self.dialect.truthy(&cond) { + call.values[0] + } else { + call.values[1] + }; + self.branch(f, target)?; + } + FnIndex::REPEAT => { + let n = self.pop(f)?; + let count = self.dialect.repeat_count(&n).min(self.iteration_cap); + for _ in 0..count { + self.branch(f, call.values[0])?; + } + } + FnIndex::WHILE | FnIndex::REPEAT_UNTIL => { + let until = f == FnIndex::REPEAT_UNTIL; + // Where the condition's own calls begin — re-running THEM is + // what makes the next test a new test. + let cond_start = self.operand_span_start(body, pc, arity)?; + let mut iters = 0u32; + loop { + let cond = self.pop(f)?; + let truthy = self.dialect.truthy(&cond); + if truthy == until { + break; + } + self.branch(f, call.values[0])?; + iters += 1; + if iters >= self.iteration_cap { + return Err(RunError::IterationCap { + call: f, + cap: self.iteration_cap, + }); + } + for i in cond_start..pc { + let c = body.call(i).expect("in bounds: span already walked"); + if self.vocab.table().branches(c.function) { + return Err(RunError::UnhandledControlFlow { call: c.function }); + } + self.dialect + .call(c.function, c.values, &mut self.stack) + .map_err(RunError::Dialect)?; + } + } + } + other => return Err(RunError::UnhandledControlFlow { call: other }), + } + Ok(()) + } + + /// Recurse into the body a branch names. + fn branch(&mut self, call: FnIndex, target: u8) -> Result<(), RunError> { + let idx = usize::from(target); + if idx == 0 || idx >= self.program.functions.len() { + return Err(RunError::UnresolvedBody { call, target }); + } + self.run_function(idx) + } + + fn pop(&mut self, call: FnIndex) -> Result> { + self.stack.pop().ok_or(RunError::StackUnderflow { call }) + } + + /// Walk backwards from `end` over `want` operands' worth of calls. + /// + /// Each step consumes one needed operand and adds back that call's own + /// arity, so a nested expression is counted whole rather than by depth. + fn operand_span_start( + &self, + body: &FunctionBody, + end: usize, + want: u8, + ) -> Result> { + let table = self.vocab.table(); + let mut need = usize::from(want); + let mut i = end; + while need > 0 { + if i == 0 { + // The body does not contain the operands this call claims — + // a malformed program, refused rather than clamped to 0, + // which would silently re-run the whole body as a condition. + return Err(RunError::StackUnderflow { + call: body.call(end).map(|c| c.function).unwrap_or(FnIndex::NOP), + }); + } + i -= 1; + let c = body.call(i).expect("in bounds: walked forward over these"); + let arity = table + .stack_arity(c.function) + .ok_or(RunError::UncoveredArity { call: c.function })?; + need -= 1; + need += usize::from(arity); + } + Ok(i) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::vocabulary::conformance::validate; + use crate::{Call, LaneShape}; + + /// No domain bytes — the shared core alone, which is all these programs use. + struct CoreOnly; + impl Vocabulary for CoreOnly { + fn domain_stack_arity(&self, _f: FnIndex) -> Option { + None + } + fn domain_body_refs(&self, _f: FnIndex) -> u8 { + 0 + } + } + + /// The smallest dialect that can run an algorithm: `i64`, 256 variables. + /// + /// Deliberately a TEST dialect, not a shipped one. The engine's claim is + /// that it carries no semantics; proving that needs *a* dialect, not *the* + /// dialect, and shipping one here would be the engine learning a language. + struct I64Dialect { + vars: [i64; 256], + calls: u32, + } + + // `[i64; 256]` has no `Default` (arrays implement it only up to 32), so + // the derive cannot be used here. + impl Default for I64Dialect { + fn default() -> Self { + Self { + vars: [0; 256], + calls: 0, + } + } + } + + #[derive(Debug, PartialEq, Eq)] + enum I64Error { + Underflow(u8), + Unsupported(u8), + } + + impl Dialect for I64Dialect { + type Value = i64; + type Error = I64Error; + + fn truthy(&self, v: &i64) -> bool { + *v != 0 + } + + fn repeat_count(&self, v: &i64) -> u32 { + u32::try_from(*v).unwrap_or(0) + } + + fn call( + &mut self, + f: FnIndex, + values: [u8; 3], + stack: &mut Vec, + ) -> Result<(), I64Error> { + self.calls += 1; + let pop = |s: &mut Vec| s.pop().ok_or(I64Error::Underflow(f.0)); + match f { + FnIndex::NUMBER => stack.push(i64::from(values[0])), + FnIndex::ADD => { + let (b, a) = (pop(stack)?, pop(stack)?); + stack.push(a + b); + } + FnIndex::SUB => { + let (b, a) = (pop(stack)?, pop(stack)?); + stack.push(a - b); + } + FnIndex::MOD => { + let (b, a) = (pop(stack)?, pop(stack)?); + stack.push(if b == 0 { 0 } else { a % b }); + } + FnIndex::GT => { + let (b, a) = (pop(stack)?, pop(stack)?); + stack.push(i64::from(a > b)); + } + FnIndex::VAR_GET => stack.push(self.vars[usize::from(values[0])]), + FnIndex::VAR_SET => { + let v = pop(stack)?; + self.vars[usize::from(values[0])] = v; + } + other => return Err(I64Error::Unsupported(other.0)), + } + Ok(()) + } + } + + fn run(program: &Program) -> Result> { + let vocab = validate(CoreOnly).expect("core-only vocabulary conforms"); + let mut interp = Interpreter::new(&vocab, program, I64Dialect::default()); + interp.run()?; + Ok(interp.dialect) + } + + /// `sum 1..=n` with `REPEAT`: var0 = total, var1 = counter. + fn sum_program(n: u8) -> Program { + let entry = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::NUMBER, 0), + Call::with_value(FnIndex::VAR_SET, 0), + Call::with_value(FnIndex::NUMBER, 0), + Call::with_value(FnIndex::VAR_SET, 1), + Call::with_value(FnIndex::NUMBER, n), + Call::with_value(FnIndex::REPEAT, 1), + ], + ) + .unwrap(); + let body = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::VAR_GET, 1), + Call::with_value(FnIndex::NUMBER, 1), + Call::new(FnIndex::ADD), + Call::with_value(FnIndex::VAR_SET, 1), + Call::with_value(FnIndex::VAR_GET, 0), + Call::with_value(FnIndex::VAR_GET, 1), + Call::new(FnIndex::ADD), + Call::with_value(FnIndex::VAR_SET, 0), + ], + ) + .unwrap(); + Program { + functions: vec![entry, body], + } + } + + /// Euclid's GCD with `WHILE` — var0 = a, var1 = b, loop while b > 0. + /// + /// The condition is `VAR_GET 1, NUMBER 0, GT` — three calls sitting + /// immediately before the `WHILE`, in the SAME body. Re-testing means + /// re-running them, which is the whole point of this fixture. + fn gcd_program(a: u8, b: u8) -> Program { + let entry = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::NUMBER, a), + Call::with_value(FnIndex::VAR_SET, 0), + Call::with_value(FnIndex::NUMBER, b), + Call::with_value(FnIndex::VAR_SET, 1), + Call::with_value(FnIndex::VAR_GET, 1), + Call::with_value(FnIndex::NUMBER, 0), + Call::new(FnIndex::GT), + Call::with_value(FnIndex::WHILE, 1), + ], + ) + .unwrap(); + let body = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::VAR_GET, 1), + Call::with_value(FnIndex::VAR_GET, 0), + Call::with_value(FnIndex::VAR_GET, 1), + Call::new(FnIndex::MOD), + Call::with_value(FnIndex::VAR_SET, 1), + Call::with_value(FnIndex::VAR_SET, 0), + ], + ) + .unwrap(); + Program { + functions: vec![entry, body], + } + } + + fn gcd_ref(mut a: i64, mut b: i64) -> i64 { + while b > 0 { + let t = a % b; + a = b; + b = t; + } + a + } + + /// FAILS IF: `REPEAT` runs the wrong number of times, or the engine loses + /// the dialect's state across a branch. Checked against closed form, not + /// against a second run of the same engine. + #[test] + fn repeat_sums_to_the_closed_form() { + for n in [0u8, 1, 2, 7, 50] { + let d = run(&sum_program(n)).expect("runs"); + let want = i64::from(n) * (i64::from(n) + 1) / 2; + assert_eq!(d.vars[0], want, "sum 1..={n}"); + } + } + + /// FAILS IF: the condition's operand span is not re-run between + /// iterations — the loop then re-tests a stale stack top and either spins + /// to the iteration cap or exits after one pass. Both are visible here + /// because GCD needs several iterations and has an independent reference. + /// + /// The `(8, 0)` case is the silent half: a condition false on the FIRST + /// test, so the body must never run. + #[test] + fn while_reruns_its_condition_span_and_computes_gcd() { + for (a, b) in [(48u8, 18u8), (17, 5), (100, 75), (8, 0), (1, 1)] { + let d = run(&gcd_program(a, b)).expect("runs"); + assert_eq!( + d.vars[0], + gcd_ref(i64::from(a), i64::from(b)), + "gcd({a}, {b})" + ); + } + } + + /// FAILS IF: a runaway loop hangs instead of being refused. The dialect + /// here never falsifies the condition, which is exactly the shape a real + /// mis-written program has. + #[test] + fn a_loop_that_never_falsifies_hits_the_cap_rather_than_hanging() { + let entry = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::NUMBER, 1), + Call::with_value(FnIndex::WHILE, 1), + ], + ) + .unwrap(); + // The body pushes the same truthy constant every pass. + let body = + FunctionBody::from_calls(LaneShape::Pairs, &[Call::with_value(FnIndex::NUMBER, 1)]) + .unwrap(); + let p = Program { + functions: vec![entry, body], + }; + let vocab = validate(CoreOnly).expect("conforms"); + let mut interp = Interpreter::new(&vocab, &p, I64Dialect::default()).with_iteration_cap(64); + assert_eq!( + interp.run(), + Err(RunError::IterationCap { + call: FnIndex::WHILE, + cap: 64 + }) + ); + } + + /// FAILS IF: a branch to a function the program does not contain is + /// followed rather than refused — silently running nothing, which reads + /// as a program that simply did not do much. + #[test] + fn a_branch_to_a_missing_body_is_refused() { + let entry = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::NUMBER, 1), + Call::with_value(FnIndex::IF, 9), + ], + ) + .unwrap(); + let p = Program { + functions: vec![entry], + }; + assert_eq!( + run(&p).err(), + Some(RunError::UnresolvedBody { + call: FnIndex::IF, + target: 9 + }) + ); + // silent half: index 0 is the entry and is never a legal target + let entry0 = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::NUMBER, 1), + Call::with_value(FnIndex::IF, 0), + ], + ) + .unwrap(); + assert!(matches!( + run(&Program { + functions: vec![entry0] + }) + .err(), + Some(RunError::UnresolvedBody { target: 0, .. }) + )); + } + + /// FAILS IF: a control-flow function outside the proven five is executed + /// on a guess. `FOR_EACH` branches, so the engine reaches it — and must + /// say so rather than approximate it. + #[test] + fn an_unproven_control_flow_call_is_refused_not_approximated() { + let entry = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::NUMBER, 1), + Call::with_value(FnIndex::FOR_EACH, 1), + ], + ) + .unwrap(); + let body = + FunctionBody::from_calls(LaneShape::Pairs, &[Call::with_value(FnIndex::NUMBER, 1)]) + .unwrap(); + assert_eq!( + run(&Program { + functions: vec![entry, body] + }) + .err(), + Some(RunError::UnhandledControlFlow { + call: FnIndex::FOR_EACH + }) + ); + } + + /// FAILS IF: the engine executes a shared-core byte itself instead of + /// asking the dialect. A dialect that refuses EVERYTHING must make even + /// `NUMBER` fail — if it does not, the engine is carrying semantics. + #[test] + fn the_engine_carries_no_semantics_of_its_own() { + struct RefuseAll; + impl Dialect for RefuseAll { + type Value = i64; + type Error = (); + fn truthy(&self, _: &i64) -> bool { + true + } + fn repeat_count(&self, _: &i64) -> u32 { + 0 + } + fn call(&mut self, _: FnIndex, _: [u8; 3], _: &mut Vec) -> Result<(), ()> { + Err(()) + } + } + let entry = + FunctionBody::from_calls(LaneShape::Pairs, &[Call::with_value(FnIndex::NUMBER, 5)]) + .unwrap(); + let p = Program { + functions: vec![entry], + }; + let vocab = validate(CoreOnly).expect("conforms"); + let mut interp = Interpreter::new(&vocab, &p, RefuseAll); + assert_eq!(interp.run(), Err(RunError::Dialect(()))); + } +} diff --git a/crates/ogar-loco/src/lib.rs b/crates/ogar-loco/src/lib.rs index e460ab8..56b9176 100644 --- a/crates/ogar-loco/src/lib.rs +++ b/crates/ogar-loco/src/lib.rs @@ -120,6 +120,7 @@ use serde::{Deserialize, Serialize}; pub mod basin; +pub mod interpret; pub mod node; pub mod pool; pub mod program; @@ -128,6 +129,7 @@ pub mod statements; pub mod telemetry; pub mod vocabulary; +pub use interpret::{Dialect, Interpreter, RunError}; pub use node::FunctionNode; pub use pool::{Constant, ConstantPool, PoolError}; pub use program::{Program, branches_of}; From a0c961731ddfece278510bdc7956813b1a9e47e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 20:34:27 +0000 Subject: [PATCH 2/4] loco: pin the silent half of the engine/dialect split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `the_engine_carries_no_semantics_of_its_own` asserted only that the dialect SEES every non-branching call. The claim has two directions, and the other one — control flow never reaches the dialect — was unpinned. A suite with only the first half passes an engine that hands `IF` to the dialect and lets it improvise a branch, which is the failure that would make "loco speaks all dialects" false in the expensive direction: a dialect would have to implement control flow to be correct, and every dialect would implement it differently. `control_flow_never_reaches_the_dialect` runs a program whose entry really does branch (an IF that takes its body, a REPEAT that runs twice) and asserts the recorder saw the five NUMBERs and neither IF nor REPEAT. The NUMBER count is the anti-vacuity guard: without it the silence would hold just as well for a program that never branched at all. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X6y3drwKSE2zSgoexheLFX --- crates/ogar-loco/src/interpret.rs | 76 +++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/ogar-loco/src/interpret.rs b/crates/ogar-loco/src/interpret.rs index 6d52b51..f579968 100644 --- a/crates/ogar-loco/src/interpret.rs +++ b/crates/ogar-loco/src/interpret.rs @@ -649,4 +649,80 @@ mod tests { let mut interp = Interpreter::new(&vocab, &p, RefuseAll); assert_eq!(interp.run(), Err(RunError::Dialect(()))); } + + /// The silent twin of the test above, and the half that was missing. + /// + /// FAILS IF: the engine routes a control-flow call to the dialect. The + /// split is a claim in BOTH directions — the dialect sees every + /// non-branching call (above) and never sees a branching one (here) — and + /// a suite that only pins the first would pass an engine that handed + /// `IF` to the dialect and let it improvise a branch. + #[test] + fn control_flow_never_reaches_the_dialect() { + /// Records every call the engine delegates. + #[derive(Default)] + struct Recorder { + seen: Vec, + } + impl Dialect for Recorder { + type Value = i64; + type Error = (); + fn truthy(&self, v: &i64) -> bool { + *v != 0 + } + fn repeat_count(&self, v: &i64) -> u32 { + u32::try_from(*v).unwrap_or(0) + } + fn call( + &mut self, + f: FnIndex, + values: [u8; 3], + stack: &mut Vec, + ) -> Result<(), ()> { + self.seen.push(f.0); + if f == FnIndex::NUMBER { + stack.push(i64::from(values[0])); + } + Ok(()) + } + } + + // NUMBER 1, IF -> body(1); NUMBER 2, REPEAT -> body(1). + let entry = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::NUMBER, 1), + Call::with_value(FnIndex::IF, 1), + Call::with_value(FnIndex::NUMBER, 2), + Call::with_value(FnIndex::REPEAT, 1), + ], + ) + .unwrap(); + let body = + FunctionBody::from_calls(LaneShape::Pairs, &[Call::with_value(FnIndex::NUMBER, 9)]) + .unwrap(); + let p = Program { + functions: vec![entry, body], + }; + let vocab = validate(CoreOnly).expect("conforms"); + let mut interp = Interpreter::new(&vocab, &p, Recorder::default()); + interp.run().expect("runs"); + + let seen = &interp.dialect.seen; + // Anti-vacuity: the program really did execute control flow — the IF + // branched once and the REPEAT ran twice, so the body's NUMBER 9 ran + // three times in total. Without this the silence below would hold for + // a program that never branched at all. + assert_eq!( + seen.iter().filter(|b| **b == FnIndex::NUMBER.0).count(), + 5, + "2 in the entry + 3 from the branched body: {seen:?}" + ); + for cf in [FnIndex::IF, FnIndex::REPEAT] { + assert!( + !seen.contains(&cf.0), + "control flow {cf:?} was handed to the dialect: {seen:?}" + ); + } + } } From 135c079739f3184731fd7cfba7feaade9d695488 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 20:37:43 +0000 Subject: [PATCH 3/4] loco: the operand-span walk must not credit a void call with an operand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by reading the walk against the vocabulary it consults, not by a reviewer: `operand_span_start` stepped backwards crediting EVERY call it passed with having produced one operand (`need -= 1`), while the vocabulary declares `pushes_result` per byte and the non-pushing set is not hypothetical — it is every control-flow byte in the shared core (`pushes_result` returns `Some(false)` for IF / IF_ELSE / REPEAT / WHILE / REPEAT_UNTIL / FOREVER / FOR_EACH / FOR_RANGE / BREAK / CONTINUE) plus whatever void verbs a domain declares above the floor. The engine consulted that column nowhere. The consequence is not a slightly wrong span. Crediting a void call stops the walk one call early, so the span begins AFTER the call that actually produces the loop condition; the re-run then pushes one value and immediately voids it, and the next test pops an empty stack. It fails loudly rather than silently, which is the only mercy in it. Reachability is the part that matters: a thinking dialect's side-effecting verbs are exactly the shape that triggers this, so the bug would have surfaced the first time a dialect with a void op wrote a loop — after the engine had been trusted. `the_span_walk_does_not_credit_a_void_call_with_an_operand` builds the minimal program (a condition producer, then a void statement, then the WHILE) against a vocabulary declaring one VOID byte, and was verified RED against the previous commit before the fix went in. Its anti-vacuity half counts condition re-evaluations, so a span that silently stopped re-running would fail it too. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X6y3drwKSE2zSgoexheLFX --- crates/ogar-loco/src/interpret.rs | 146 +++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/crates/ogar-loco/src/interpret.rs b/crates/ogar-loco/src/interpret.rs index f579968..57b6169 100644 --- a/crates/ogar-loco/src/interpret.rs +++ b/crates/ogar-loco/src/interpret.rs @@ -304,7 +304,17 @@ impl<'a, V: Vocabulary, D: Dialect> Interpreter<'a, V, D> { let arity = table .stack_arity(c.function) .ok_or(RunError::UncoveredArity { call: c.function })?; - need -= 1; + // A call satisfies one operand only if it PRODUCES one. The + // non-pushing set is not hypothetical: it is every control-flow + // byte in the shared core, plus whatever void verbs a domain + // declares. Crediting one of those with an operand stops the walk + // early and drops the call that actually produces the condition. + if table + .pushes_result(c.function) + .ok_or(RunError::UncoveredArity { call: c.function })? + { + need -= 1; + } need += usize::from(arity); } Ok(i) @@ -385,10 +395,22 @@ mod tests { let (b, a) = (pop(stack)?, pop(stack)?); stack.push(a - b); } + FnIndex::MUL => { + let (b, a) = (pop(stack)?, pop(stack)?); + stack.push(a * b); + } + FnIndex::DIV => { + let (b, a) = (pop(stack)?, pop(stack)?); + stack.push(if b == 0 { 0 } else { a / b }); + } FnIndex::MOD => { let (b, a) = (pop(stack)?, pop(stack)?); stack.push(if b == 0 { 0 } else { a % b }); } + FnIndex::EQ => { + let (b, a) = (pop(stack)?, pop(stack)?); + stack.push(i64::from(a == b)); + } FnIndex::GT => { let (b, a) = (pop(stack)?, pop(stack)?); stack.push(i64::from(a > b)); @@ -725,4 +747,126 @@ mod tests { ); } } + + /// A vocabulary with one VOID domain byte: pops an operand, pushes + /// nothing. Legal, declared, and exactly what a thinking dialect's + /// side-effecting verbs look like. + struct WithVoidOp; + impl WithVoidOp { + const VOID: FnIndex = FnIndex(0x90); + } + impl Vocabulary for WithVoidOp { + fn domain_stack_arity(&self, f: FnIndex) -> Option { + (f == Self::VOID).then_some(1) + } + fn domain_body_refs(&self, _f: FnIndex) -> u8 { + 0 + } + fn domain_pushes_result(&self, f: FnIndex) -> Option { + (f == Self::VOID).then_some(false) + } + } + + /// FAILS IF: the backward operand-span walk assumes every call it steps + /// over produced a value. + /// + /// It does not. The vocabulary declares `pushes_result` per byte, and the + /// non-pushing set is not hypothetical — it is every control-flow byte in + /// the shared core, plus whatever void verbs a domain declares. Stepping + /// over one and crediting it with an operand stops the walk too early, so + /// the span misses the call that actually produces the condition, and the + /// re-run leaves the stack one short. + /// + /// The program: `counter = 3`, then the condition producer `VAR_GET 0`, + /// then a void statement (`NUMBER 0; VOID`) sitting between it and the + /// `WHILE`. The correct span starts at the `VAR_GET`; a walk that credits + /// `VOID` with a push starts at the `NUMBER` instead, and the re-run then + /// pushes one value and immediately voids it. + #[test] + fn the_span_walk_does_not_credit_a_void_call_with_an_operand() { + struct VoidDialect { + counter: i64, + counter_reads: u32, + } + impl Dialect for VoidDialect { + type Value = i64; + type Error = (); + fn truthy(&self, v: &i64) -> bool { + *v != 0 + } + fn repeat_count(&self, v: &i64) -> u32 { + u32::try_from(*v).unwrap_or(0) + } + fn call( + &mut self, + f: FnIndex, + values: [u8; 3], + stack: &mut Vec, + ) -> Result<(), ()> { + match f { + FnIndex::NUMBER => stack.push(i64::from(values[0])), + FnIndex::SUB => { + let b = stack.pop().ok_or(())?; + let a = stack.pop().ok_or(())?; + stack.push(a - b); + } + FnIndex::VAR_GET => { + self.counter_reads += 1; + stack.push(self.counter); + } + FnIndex::VAR_SET => self.counter = stack.pop().ok_or(())?, + WithVoidOp::VOID => { + stack.pop().ok_or(())?; + } + _ => return Err(()), + } + Ok(()) + } + } + + let entry = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::NUMBER, 3), + Call::with_value(FnIndex::VAR_SET, 0), + Call::with_value(FnIndex::VAR_GET, 0), + Call::with_value(FnIndex::NUMBER, 0), + Call::new(WithVoidOp::VOID), + Call::with_value(FnIndex::WHILE, 1), + ], + ) + .unwrap(); + let body = FunctionBody::from_calls( + LaneShape::Pairs, + &[ + Call::with_value(FnIndex::VAR_GET, 0), + Call::with_value(FnIndex::NUMBER, 1), + Call::new(FnIndex::SUB), + Call::with_value(FnIndex::VAR_SET, 0), + ], + ) + .unwrap(); + let p = Program { + functions: vec![entry, body], + }; + let vocab = validate(WithVoidOp).expect("conforms"); + let mut interp = Interpreter::new( + &vocab, + &p, + VoidDialect { + counter: 0, + counter_reads: 0, + }, + ); + interp.run().expect("the loop runs down to zero"); + assert_eq!(interp.dialect.counter, 0, "the loop ran to completion"); + // Anti-vacuity: the condition really was re-evaluated, three times in + // the span plus once per body pass. A span that never re-ran would + // read the counter far fewer times. + assert!( + interp.dialect.counter_reads >= 6, + "condition re-runs happened: {} reads", + interp.dialect.counter_reads + ); + } } From 5af73c0446c904f062941c8ea4f853315cd63c55 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 20:38:12 +0000 Subject: [PATCH 4/4] loco: mark the probe as the record, not the canonical engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `examples/interpret_probe.rs` carried the only interpreter in the workspace, which is why the library's engine exists and why its scope is exactly the five control-flow ops this probe validated against independent reference implementations. It is kept — it holds the pre-registration, the kill conditions, the four algorithms and the honest report, none of which survive a delete — but it is now a second implementation, and a second implementation with no stated precedence is a future session's coin flip. The header says which one wins. It is deliberately not rewritten on top of the library, for a reason worth recording rather than discovering twice: the probe TRACES every executed call, and the library's split puts control flow permanently out of a dialect's sight, so a dialect cannot observe a branch. Tracing branch decisions needs an observation seam on the engine — a capability question, not a refactor. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X6y3drwKSE2zSgoexheLFX --- crates/ogar-loco/examples/interpret_probe.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/ogar-loco/examples/interpret_probe.rs b/crates/ogar-loco/examples/interpret_probe.rs index be1b37d..13afb63 100644 --- a/crates/ogar-loco/examples/interpret_probe.rs +++ b/crates/ogar-loco/examples/interpret_probe.rs @@ -14,6 +14,26 @@ //! classic small algorithms, several real inputs each), and check the four //! pre-registered kill conditions honestly. //! +//! # ⊘ STATUS — the library now carries the engine (2026-09-14) +//! +//! This probe's own interpreter is no longer the only one: `ogar_loco:: +//! interpret::{Interpreter, Dialect}` is the canonical engine, and it +//! exists BECAUSE this probe ran. The five control-flow ops it executes are +//! exactly the ones validated here against the reference implementations +//! below; the library refuses the rest rather than guessing, on the reasoning +//! that behaviour nothing has executed should not ship. +//! +//! This file is kept as the RECORD — the pre-registration, the kill +//! conditions, the four algorithms and the honest report — not as the +//! canonical engine. It is deliberately NOT rewritten on top of the library +//! yet, for one concrete reason: it traces every executed call, and the +//! library's split puts control flow permanently out of a dialect's sight +//! (`control_flow_never_reaches_the_dialect`), so a dialect cannot observe a +//! branch. Tracing branch decisions needs an observation seam on the engine, +//! which is a capability question and not a refactor. Until that lands, do +//! not treat this interpreter as a second opinion on semantics: where the two +//! disagree, the library is canonical. +//! //! # Scope — what this probe does NOT attempt //! //! This interpreter executes only the **shared computational core**