Skip to content

Commit 507e6bb

Browse files
hyperpolymathclaude
andcommitted
Integration tests + cached/speculative branch eval + conversation log
Integration tests (integration_tests.rs, 15 tests): - e2e parse→typecheck→evaluate pipeline for data bindings, functions, branches, type declarations, send-to syntax, @ref, agent API - Negative tests: linear handle leak, budget exceeded (both caught) - Regression guard: parse all 9 example files - Agent registration, record evaluation, variable chaining Cached branch evaluation: - branch_cache HashMap on Evaluator keyed by label:context - eval_branch_cached(): cache hit returns stored result, miss evaluates - eval_branch_speculative(): evaluates ALL arms, records all traces, strategy picks best - 3 new eval tests: cache hit, cache miss (different context), speculative Conversation log updated with Austin, Schlegel, security, Bruner sections. 194 tests. Zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 654dd77 commit 507e6bb

7 files changed

Lines changed: 874 additions & 12 deletions

File tree

crates/oo7-core/src/eval.rs

Lines changed: 210 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,15 @@ pub struct Evaluator {
260260

261261
/// The ID of the agent currently executing, if any.
262262
pub current_agent_id: Option<String>,
263+
264+
// -- Branch Cache (Section 18.2: Cached Branches) --
265+
// Key: "label:serialized_given_context"
266+
// Value: (chosen_arm_label, result_value)
267+
268+
/// Cache for `branch cached` — memoises decisions by branch
269+
/// label + given context. On cache hit, the strategy is NOT
270+
/// consulted and the arm body is NOT re-executed.
271+
pub branch_cache: HashMap<String, (String, RtValue)>,
263272
}
264273

265274
impl Evaluator {
@@ -278,6 +287,7 @@ impl Evaluator {
278287
return_signal: false,
279288
return_value: None,
280289
current_agent_id: None,
290+
branch_cache: HashMap::new(),
281291
}
282292
}
283293

@@ -296,6 +306,7 @@ impl Evaluator {
296306
return_signal: false,
297307
return_value: None,
298308
current_agent_id: None,
309+
branch_cache: HashMap::new(),
299310
}
300311
}
301312

@@ -692,6 +703,197 @@ impl Evaluator {
692703
self.eval_stmts(&chosen_arm.body, env)
693704
}
694705

706+
// ========================================================
707+
// Branch Modifier Dispatch (Section 18.2)
708+
// ========================================================
709+
710+
/// Dispatch branch evaluation based on modifier.
711+
///
712+
/// - `None` → standard `eval_branch` (strategy chooses one arm)
713+
/// - `Cached` → memoised branch: cache key is label + serialized given context.
714+
/// On cache hit, returns cached (chosen_arm, result) WITHOUT running
715+
/// strategy or arm body. On miss, evaluates normally and stores result.
716+
/// - `Speculative` → evaluates ALL arms, uses strategy to pick best,
717+
/// records traces for ALL arms (not just chosen).
718+
pub fn eval_branch_with_modifier(
719+
&mut self,
720+
modifier: &Option<BranchModifier>,
721+
traced: &Option<String>,
722+
arms: &[BranchArm],
723+
env: &mut Env,
724+
) -> RtValue {
725+
match modifier {
726+
None => self.eval_branch(traced, arms, env),
727+
Some(BranchModifier::Cached) => self.eval_branch_cached(traced, arms, env),
728+
Some(BranchModifier::Speculative) => self.eval_branch_speculative(traced, arms, env),
729+
}
730+
}
731+
732+
/// Evaluate a cached branch (Section 18.2).
733+
///
734+
/// Cache key = "label:serialized_given_context".
735+
/// On cache hit: return cached result, emit a trace record noting
736+
/// "cache_hit" in the reason field.
737+
/// On cache miss: evaluate normally via `eval_branch`, store result.
738+
fn eval_branch_cached(
739+
&mut self,
740+
traced: &Option<String>,
741+
arms: &[BranchArm],
742+
env: &mut Env,
743+
) -> RtValue {
744+
let label = traced.as_deref().unwrap_or("__unlabelled__");
745+
746+
// Serialize given contexts to form the cache key.
747+
let mut given_key_parts: Vec<String> = Vec::new();
748+
for arm in arms {
749+
if let Some(given) = &arm.given {
750+
let (named, _preds) = self.eval_given(given, env);
751+
let json_vals: Vec<String> = named
752+
.iter()
753+
.map(|(k, v)| format!("{}={}", k, v.to_json()))
754+
.collect();
755+
given_key_parts.push(format!("{}:{}", arm.label, json_vals.join(",")));
756+
}
757+
}
758+
let cache_key = format!("{}:{}", label, given_key_parts.join("|"));
759+
760+
// Check cache.
761+
if let Some((cached_chosen, cached_result)) = self.branch_cache.get(&cache_key) {
762+
// Cache hit — emit trace record noting the hit.
763+
if let Some(trace_label) = traced {
764+
let record = TraceRecord {
765+
branch_id: trace_label.clone(),
766+
branch_options: arms.iter().map(|a| a.label.clone()).collect(),
767+
given_contexts: JsonValue::Null,
768+
chosen: cached_chosen.clone(),
769+
reason: format!("cache_hit(key={})", cache_key),
770+
trace_report: JsonValue::Null,
771+
timestamp: Utc::now().to_rfc3339(),
772+
agent_id: "evaluator".to_string(),
773+
hermeneutic_context: env.snapshot(),
774+
};
775+
self.trace.append(record);
776+
}
777+
return cached_result.clone();
778+
}
779+
780+
// Cache miss — evaluate normally.
781+
let result = self.eval_branch(traced, arms, env);
782+
783+
// Store in cache: look up chosen arm from the last trace record.
784+
let chosen_label = self
785+
.trace
786+
.records
787+
.last()
788+
.map(|r| r.chosen.clone())
789+
.unwrap_or_else(|| arms.first().map(|a| a.label.clone()).unwrap_or_default());
790+
self.branch_cache
791+
.insert(cache_key, (chosen_label, result.clone()));
792+
793+
result
794+
}
795+
796+
/// Evaluate a speculative branch (Section 18.2).
797+
///
798+
/// Evaluates ALL arms (not just the strategy-selected one).
799+
/// Uses the strategy to pick the best result.
800+
/// Records traces for ALL arms, marking the chosen one.
801+
fn eval_branch_speculative(
802+
&mut self,
803+
traced: &Option<String>,
804+
arms: &[BranchArm],
805+
env: &mut Env,
806+
) -> RtValue {
807+
let label = traced.as_deref().unwrap_or("__unlabelled__");
808+
809+
// Step 1: Evaluate given clauses for strategy selection.
810+
let mut evaluated_arms: Vec<EvaluatedArm> = Vec::new();
811+
let mut given_contexts_json: HashMap<String, JsonValue> = HashMap::new();
812+
813+
for arm in arms {
814+
let (named, preds) = if let Some(given) = &arm.given {
815+
let (n, p) = self.eval_given(given, env);
816+
given_contexts_json.insert(
817+
arm.label.clone(),
818+
JsonValue::Object(n.iter().map(|(k, v)| (k.clone(), v.to_json())).collect()),
819+
);
820+
(Some(n), Some(p))
821+
} else {
822+
(None, None)
823+
};
824+
evaluated_arms.push((arm.label.clone(), named, preds));
825+
}
826+
827+
// Step 2: Evaluate ALL arms and collect results.
828+
let mut arm_results: Vec<(String, RtValue)> = Vec::new();
829+
for arm in arms {
830+
let mut arm_env = env.clone();
831+
arm_env.push_scope();
832+
let result = self.eval_stmts(&arm.body, &mut arm_env);
833+
arm_results.push((arm.label.clone(), result));
834+
835+
// Record a trace for each arm.
836+
if let Some(trace_label) = traced {
837+
let trace_report = if let Some(tc) = &arm.trace {
838+
let fields: HashMap<String, JsonValue> = tc
839+
.fields
840+
.iter()
841+
.map(|(k, v)| (k.clone(), self.eval_data(v, env).to_json()))
842+
.collect();
843+
JsonValue::Object(fields.into_iter().collect())
844+
} else {
845+
JsonValue::Null
846+
};
847+
848+
let record = TraceRecord {
849+
branch_id: format!("{}/speculative/{}", trace_label, arm.label),
850+
branch_options: arms.iter().map(|a| a.label.clone()).collect(),
851+
given_contexts: JsonValue::Object(
852+
given_contexts_json.clone().into_iter().collect(),
853+
),
854+
chosen: arm.label.clone(),
855+
reason: "speculative: evaluating all arms".to_string(),
856+
trace_report,
857+
timestamp: Utc::now().to_rfc3339(),
858+
agent_id: "evaluator".to_string(),
859+
hermeneutic_context: env.snapshot(),
860+
};
861+
self.trace.append(record);
862+
}
863+
}
864+
865+
// Step 3: Use strategy to pick the best arm.
866+
let (chosen_idx, reason) = self.strategy.choose(&evaluated_arms, env);
867+
let chosen_idx = chosen_idx.min(arms.len() - 1);
868+
let chosen_arm = &arms[chosen_idx];
869+
870+
// Step 4: Record the final selection trace.
871+
if traced.is_some() {
872+
let record = TraceRecord {
873+
branch_id: label.to_string(),
874+
branch_options: arms.iter().map(|a| a.label.clone()).collect(),
875+
given_contexts: JsonValue::Object(given_contexts_json.into_iter().collect()),
876+
chosen: chosen_arm.label.clone(),
877+
reason: format!(
878+
"speculative_selected: {}",
879+
reason.unwrap_or_else(|| "default".to_string())
880+
),
881+
trace_report: JsonValue::Null,
882+
timestamp: Utc::now().to_rfc3339(),
883+
agent_id: "evaluator".to_string(),
884+
hermeneutic_context: env.snapshot(),
885+
};
886+
self.trace.append(record);
887+
}
888+
889+
// Return the result of the chosen arm.
890+
arm_results
891+
.into_iter()
892+
.nth(chosen_idx)
893+
.map(|(_, v)| v)
894+
.unwrap_or(RtValue::Unit)
895+
}
896+
695897
// ========================================================
696898
// Control Statement Evaluation
697899
// ========================================================
@@ -749,17 +951,18 @@ impl Evaluator {
749951
}
750952

751953
ControlStmt::Branch {
752-
modifier: _modifier,
954+
modifier,
753955
traced,
754956
arms,
755957
} => {
756-
// Section 18.2: modifier (speculative/cached) is ignored in
757-
// the reference evaluator — it affects scheduling strategy,
758-
// not observable semantics. A production runtime would fork
759-
// evaluation streams for speculative and check trace cache
760-
// for cached.
958+
// Section 18.2: Branch modifiers affect evaluation strategy.
959+
// - Cached: memoises decisions by label + given context.
960+
// On cache hit, the strategy is NOT consulted and the
961+
// arm body is NOT re-executed.
962+
// - Speculative: evaluates ALL arms, picks best result,
963+
// records traces for ALL arms (not just chosen).
761964
env.push_scope();
762-
let r = self.eval_branch(traced, arms, env);
965+
let r = self.eval_branch_with_modifier(modifier, traced, arms, env);
763966
env.pop_scope();
764967
r
765968
}

0 commit comments

Comments
 (0)