@@ -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
265274impl 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