Skip to main content

crue_engine/
vm.rs

1//! Bytecode VM for compiled CRUE DSL rules (Phase 2 bootstrap).
2
3use crate::context::{EvaluationContext, FieldValue};
4use crate::decision::{ActionResult, Decision};
5use crate::error::EngineError;
6use crate::ir::ActionInstruction;
7use crue_dsl::compiler::{Bytecode, Constant, Opcode};
8
9#[derive(Debug, Clone, PartialEq)]
10enum VmValue {
11    Bool(bool),
12    Number(i64),
13    String(String),
14}
15
16/// Explicit VM instruction set used by the decoded execution path.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Instruction {
19    LoadField(u16),
20    LoadConst(u32),
21    LoadTrue,
22    LoadFalse,
23    Gt,
24    Lt,
25    Gte,
26    Lte,
27    Eq,
28    Neq,
29    And,
30    Or,
31    Not,
32    JumpIfFalse(usize),
33    Jump(usize),
34    Ret,
35    EmitDecision(Decision),
36}
37
38/// VM exit value, allowing either a boolean gate or an emitted decision.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum VmExit {
41    Bool(bool),
42    Decision(Decision),
43}
44
45pub struct BytecodeVm;
46pub struct ActionVm;
47
48impl BytecodeVm {
49    /// Evaluate a compiled CRUE bytecode condition to a boolean decision gate.
50    pub fn eval(bytecode: &Bytecode, ctx: &EvaluationContext) -> Result<bool, EngineError> {
51        let program = Self::decode(bytecode)?;
52        match Self::eval_program(&program, bytecode, ctx)? {
53            VmExit::Bool(v) => Ok(v),
54            VmExit::Decision(_) => Err(EngineError::EvaluationError(
55                "VM emitted decision in boolean eval path".to_string(),
56            )),
57        }
58    }
59
60    /// Decode raw CRUE bytecode into an explicit instruction sequence.
61    pub fn decode(bytecode: &Bytecode) -> Result<Vec<Instruction>, EngineError> {
62        let mut pc = 0usize;
63        let code = &bytecode.instructions;
64        let mut program = Vec::new();
65
66        while pc < code.len() {
67            let op = decode_opcode(code[pc])?;
68            pc += 1;
69            match op {
70                Opcode::LoadField => {
71                    program.push(Instruction::LoadField(read_u16(code, &mut pc)?));
72                }
73                Opcode::LoadConst => {
74                    program.push(Instruction::LoadConst(read_u32(code, &mut pc)?));
75                }
76                Opcode::LoadTrue => program.push(Instruction::LoadTrue),
77                Opcode::LoadFalse => program.push(Instruction::LoadFalse),
78                Opcode::Gt => program.push(Instruction::Gt),
79                Opcode::Lt => program.push(Instruction::Lt),
80                Opcode::Gte => program.push(Instruction::Gte),
81                Opcode::Lte => program.push(Instruction::Lte),
82                Opcode::Eq => program.push(Instruction::Eq),
83                Opcode::Neq => program.push(Instruction::Neq),
84                Opcode::And => program.push(Instruction::And),
85                Opcode::Or => program.push(Instruction::Or),
86                Opcode::Not => program.push(Instruction::Not),
87                Opcode::Ret => program.push(Instruction::Ret),
88                Opcode::Jmp | Opcode::JmpF => {
89                    return Err(EngineError::EvaluationError(
90                        "Raw jump opcodes not supported in decoded VM yet".to_string(),
91                    ));
92                }
93            }
94        }
95
96        Ok(program)
97    }
98
99    /// Evaluate a decoded VM program against bytecode metadata/context.
100    pub fn eval_program(
101        program: &[Instruction],
102        bytecode: &Bytecode,
103        ctx: &EvaluationContext,
104    ) -> Result<VmExit, EngineError> {
105        let mut pc = 0usize;
106        let mut stack: Vec<VmValue> = Vec::new();
107
108        while pc < program.len() {
109            match &program[pc] {
110                Instruction::LoadField(idx) => {
111                    let field = bytecode.fields.get(*idx as usize).ok_or_else(|| {
112                        EngineError::EvaluationError("Invalid field index".to_string())
113                    })?;
114                    let value = ctx
115                        .get_field(field)
116                        .ok_or_else(|| EngineError::FieldNotFound(field.clone()))?;
117                    stack.push(field_to_vm(value)?);
118                    pc += 1;
119                }
120                Instruction::LoadConst(idx) => {
121                    let c = bytecode.constants.get(*idx as usize).ok_or_else(|| {
122                        EngineError::EvaluationError("Invalid constant index".to_string())
123                    })?;
124                    stack.push(constant_to_vm(c));
125                    pc += 1;
126                }
127                Instruction::LoadTrue => {
128                    stack.push(VmValue::Bool(true));
129                    pc += 1;
130                }
131                Instruction::LoadFalse => {
132                    stack.push(VmValue::Bool(false));
133                    pc += 1;
134                }
135                Instruction::Gt => {
136                    binary_compare(&mut stack, |a, b| a > b)?;
137                    pc += 1;
138                }
139                Instruction::Lt => {
140                    binary_compare(&mut stack, |a, b| a < b)?;
141                    pc += 1;
142                }
143                Instruction::Gte => {
144                    binary_compare(&mut stack, |a, b| a >= b)?;
145                    pc += 1;
146                }
147                Instruction::Lte => {
148                    binary_compare(&mut stack, |a, b| a <= b)?;
149                    pc += 1;
150                }
151                Instruction::Eq => {
152                    binary_eq(&mut stack, true)?;
153                    pc += 1;
154                }
155                Instruction::Neq => {
156                    binary_eq(&mut stack, false)?;
157                    pc += 1;
158                }
159                Instruction::And => {
160                    binary_bool(&mut stack, |a, b| a && b)?;
161                    pc += 1;
162                }
163                Instruction::Or => {
164                    binary_bool(&mut stack, |a, b| a || b)?;
165                    pc += 1;
166                }
167                Instruction::Not => {
168                    let v = pop_bool(&mut stack)?;
169                    stack.push(VmValue::Bool(!v));
170                    pc += 1;
171                }
172                Instruction::JumpIfFalse(target) => {
173                    let cond = pop_bool(&mut stack)?;
174                    if !cond {
175                        ensure_target(*target, program.len())?;
176                        pc = *target;
177                    } else {
178                        pc += 1;
179                    }
180                }
181                Instruction::Jump(target) => {
182                    ensure_target(*target, program.len())?;
183                    pc = *target;
184                }
185                Instruction::Ret => {
186                    return Ok(VmExit::Bool(pop_bool(&mut stack)?));
187                }
188                Instruction::EmitDecision(decision) => {
189                    return Ok(VmExit::Decision(*decision));
190                }
191            }
192        }
193
194        Err(EngineError::EvaluationError(
195            "VM program terminated without RET/EmitDecision".to_string(),
196        ))
197    }
198
199    /// Evaluate a compiled rule condition and emit a typed decision via VM instructions.
200    pub fn eval_decision(
201        bytecode: &Bytecode,
202        ctx: &EvaluationContext,
203        on_true: Decision,
204        on_false: Decision,
205    ) -> Result<Decision, EngineError> {
206        let mut program = Self::decode(bytecode)?;
207        if !matches!(program.last(), Some(Instruction::Ret)) {
208            return Err(EngineError::EvaluationError(
209                "Bytecode terminated without RET".to_string(),
210            ));
211        }
212        program.pop();
213
214        let false_target = program.len() + 2;
215        program.push(Instruction::JumpIfFalse(false_target));
216        program.push(Instruction::EmitDecision(on_true));
217        program.push(Instruction::EmitDecision(on_false));
218
219        match Self::eval_program(&program, bytecode, ctx)? {
220            VmExit::Decision(d) => Ok(d),
221            VmExit::Bool(_) => Err(EngineError::EvaluationError(
222                "VM returned bool in decision eval path".to_string(),
223            )),
224        }
225    }
226
227    /// Build a decoded VM program that emits `on_match` when the bytecode condition matches,
228    /// and returns `false` (boolean) when it does not match.
229    pub fn build_match_program(
230        bytecode: &Bytecode,
231        on_match: Decision,
232    ) -> Result<Vec<Instruction>, EngineError> {
233        let mut program = Self::decode(bytecode)?;
234        if !matches!(program.last(), Some(Instruction::Ret)) {
235            return Err(EngineError::EvaluationError(
236                "Bytecode terminated without RET".to_string(),
237            ));
238        }
239        program.pop();
240
241        // Stack holds the condition boolean at this point.
242        // false -> push false + RET (signals "no match")
243        // true  -> EmitDecision(on_match)
244        let false_target = program.len() + 2;
245        program.push(Instruction::JumpIfFalse(false_target));
246        program.push(Instruction::EmitDecision(on_match));
247        program.push(Instruction::LoadFalse);
248        program.push(Instruction::Ret);
249        Ok(program)
250    }
251
252    /// Evaluate a prebuilt match program and return:
253    /// - `Some(decision)` when rule matched and emitted a decision
254    /// - `None` when rule condition evaluated to false
255    pub fn eval_match_program(
256        program: &[Instruction],
257        bytecode: &Bytecode,
258        ctx: &EvaluationContext,
259    ) -> Result<Option<Decision>, EngineError> {
260        match Self::eval_program(program, bytecode, ctx)? {
261            VmExit::Decision(d) => Ok(Some(d)),
262            VmExit::Bool(false) => Ok(None),
263            VmExit::Bool(true) => Err(EngineError::EvaluationError(
264                "VM match program returned unexpected true boolean".to_string(),
265            )),
266        }
267    }
268}
269
270impl ActionVm {
271    /// Execute a compiled action program into a deterministic `ActionResult`.
272    pub fn execute(program: &[ActionInstruction]) -> Result<ActionResult, EngineError> {
273        let mut decision = Decision::Allow;
274        let mut error_code: Option<String> = None;
275        let mut message: Option<String> = None;
276        let mut approval_timeout: Option<u32> = None;
277        let mut alert_soc = false;
278
279        for insn in program {
280            match insn {
281                ActionInstruction::SetDecision(d) => decision = *d,
282                ActionInstruction::SetErrorCode(code) => error_code = Some(code.clone()),
283                ActionInstruction::SetMessage(msg) => message = Some(msg.clone()),
284                ActionInstruction::SetApprovalTimeout(timeout) => approval_timeout = Some(*timeout),
285                ActionInstruction::SetAlertSoc(v) => alert_soc = *v,
286                ActionInstruction::Halt => break,
287            }
288        }
289
290        let final_message = match decision {
291            Decision::ApprovalRequired => {
292                if let Some(m) = message {
293                    Some(m)
294                } else {
295                    Some(format!(
296                        "Approval required within {} minutes",
297                        approval_timeout.unwrap_or(30)
298                    ))
299                }
300            }
301            _ => message,
302        };
303
304        Ok(ActionResult {
305            decision,
306            error_code,
307            message: final_message,
308            alert_soc,
309        })
310    }
311}
312
313fn ensure_target(target: usize, len: usize) -> Result<(), EngineError> {
314    if target >= len {
315        return Err(EngineError::EvaluationError(format!(
316            "Invalid jump target {} (program len {})",
317            target, len
318        )));
319    }
320    Ok(())
321}
322
323fn decode_opcode(byte: u8) -> Result<Opcode, EngineError> {
324    let op = match byte {
325        0x01 => Opcode::LoadField,
326        0x02 => Opcode::LoadConst,
327        0x03 => Opcode::LoadTrue,
328        0x04 => Opcode::LoadFalse,
329        0x10 => Opcode::Gt,
330        0x11 => Opcode::Lt,
331        0x12 => Opcode::Gte,
332        0x13 => Opcode::Lte,
333        0x14 => Opcode::Eq,
334        0x15 => Opcode::Neq,
335        0x20 => Opcode::And,
336        0x21 => Opcode::Or,
337        0x22 => Opcode::Not,
338        0x30 => Opcode::JmpF,
339        0x31 => Opcode::Jmp,
340        0xFF => Opcode::Ret,
341        _ => {
342            return Err(EngineError::EvaluationError(format!(
343                "Unknown opcode 0x{byte:02x}"
344            )))
345        }
346    };
347    Ok(op)
348}
349
350fn read_u16(code: &[u8], pc: &mut usize) -> Result<u16, EngineError> {
351    if *pc + 2 > code.len() {
352        return Err(EngineError::EvaluationError(
353            "Truncated u16 operand".to_string(),
354        ));
355    }
356    let v = u16::from_be_bytes([code[*pc], code[*pc + 1]]);
357    *pc += 2;
358    Ok(v)
359}
360
361fn read_u32(code: &[u8], pc: &mut usize) -> Result<u32, EngineError> {
362    if *pc + 4 > code.len() {
363        return Err(EngineError::EvaluationError(
364            "Truncated u32 operand".to_string(),
365        ));
366    }
367    let v = u32::from_be_bytes([code[*pc], code[*pc + 1], code[*pc + 2], code[*pc + 3]]);
368    *pc += 4;
369    Ok(v)
370}
371
372fn constant_to_vm(c: &Constant) -> VmValue {
373    match c {
374        Constant::Number(n) => VmValue::Number(*n),
375        Constant::String(s) => VmValue::String(s.clone()),
376        Constant::Boolean(b) => VmValue::Bool(*b),
377    }
378}
379
380fn field_to_vm(v: &FieldValue) -> Result<VmValue, EngineError> {
381    match v {
382        FieldValue::Number(n) => Ok(VmValue::Number(*n)),
383        FieldValue::String(s) => Ok(VmValue::String(s.clone())),
384        FieldValue::Boolean(b) => Ok(VmValue::Bool(*b)),
385        FieldValue::Float(_) => Err(EngineError::TypeMismatch(
386            "float field unsupported in VM".into(),
387        )),
388    }
389}
390
391fn pop(stack: &mut Vec<VmValue>) -> Result<VmValue, EngineError> {
392    stack
393        .pop()
394        .ok_or_else(|| EngineError::EvaluationError("VM stack underflow".to_string()))
395}
396
397fn pop_bool(stack: &mut Vec<VmValue>) -> Result<bool, EngineError> {
398    match pop(stack)? {
399        VmValue::Bool(v) => Ok(v),
400        _ => Err(EngineError::TypeMismatch("Expected bool".to_string())),
401    }
402}
403
404fn pop_number(stack: &mut Vec<VmValue>) -> Result<i64, EngineError> {
405    match pop(stack)? {
406        VmValue::Number(v) => Ok(v),
407        _ => Err(EngineError::TypeMismatch("Expected number".to_string())),
408    }
409}
410
411fn binary_compare(
412    stack: &mut Vec<VmValue>,
413    cmp: impl Fn(i64, i64) -> bool,
414) -> Result<(), EngineError> {
415    let right = pop_number(stack)?;
416    let left = pop_number(stack)?;
417    stack.push(VmValue::Bool(cmp(left, right)));
418    Ok(())
419}
420
421fn binary_bool(
422    stack: &mut Vec<VmValue>,
423    op: impl Fn(bool, bool) -> bool,
424) -> Result<(), EngineError> {
425    let right = pop_bool(stack)?;
426    let left = pop_bool(stack)?;
427    stack.push(VmValue::Bool(op(left, right)));
428    Ok(())
429}
430
431fn binary_eq(stack: &mut Vec<VmValue>, eq: bool) -> Result<(), EngineError> {
432    let right = pop(stack)?;
433    let left = pop(stack)?;
434    let result = match (left, right) {
435        (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
436        (VmValue::Number(a), VmValue::Number(b)) => a == b,
437        (VmValue::String(a), VmValue::String(b)) => a == b,
438        _ => {
439            return Err(EngineError::TypeMismatch(
440                "Incompatible equality operands".to_string(),
441            ))
442        }
443    };
444    stack.push(VmValue::Bool(if eq { result } else { !result }));
445    Ok(())
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use crate::ir::ActionInstruction;
452    use crate::EvaluationRequest;
453    use crue_dsl::ast::{ActionNode, Expression, MetadataNode, RuleAst, Value};
454
455    #[test]
456    fn test_vm_eval_compiled_rule() {
457        let src = r#"
458RULE CRUE_001 VERSION 1.0
459WHEN
460    agent.requests_last_hour >= 50
461THEN
462    BLOCK WITH CODE "VOLUME_EXCEEDED"
463"#;
464        let ast = crue_dsl::parser::parse(src).unwrap();
465        let bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
466
467        let req = EvaluationRequest {
468            request_id: "req".into(),
469            agent_id: "a".into(),
470            agent_org: "o".into(),
471            agent_level: "standard".into(),
472            mission_id: None,
473            mission_type: None,
474            query_type: None,
475            justification: Some("demo justification".into()),
476            export_format: None,
477            result_limit: Some(1),
478            requests_last_hour: 60,
479            requests_last_24h: 100,
480            results_last_query: 1,
481            account_department: None,
482            allowed_departments: vec![],
483            request_hour: 10,
484            is_within_mission_hours: true,
485        };
486        let ctx = EvaluationContext::from_request(&req);
487        assert!(BytecodeVm::eval(&bytecode, &ctx).unwrap());
488    }
489
490    #[test]
491    fn test_vm_eval_decision_emits_decision() {
492        let src = r#"
493RULE CRUE_001 VERSION 1.0
494WHEN
495    agent.requests_last_hour >= 50
496THEN
497    BLOCK WITH CODE "VOLUME_EXCEEDED"
498"#;
499        let ast = crue_dsl::parser::parse(src).unwrap();
500        let bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
501        let mut req = EvaluationRequest {
502            request_id: "req".into(),
503            agent_id: "a".into(),
504            agent_org: "o".into(),
505            agent_level: "standard".into(),
506            mission_id: None,
507            mission_type: None,
508            query_type: None,
509            justification: Some("demo justification".into()),
510            export_format: None,
511            result_limit: Some(1),
512            requests_last_hour: 60,
513            requests_last_24h: 100,
514            results_last_query: 1,
515            account_department: None,
516            allowed_departments: vec![],
517            request_hour: 10,
518            is_within_mission_hours: true,
519        };
520        let ctx = EvaluationContext::from_request(&req);
521        assert_eq!(
522            BytecodeVm::eval_decision(&bytecode, &ctx, Decision::Block, Decision::Allow).unwrap(),
523            Decision::Block
524        );
525
526        req.requests_last_hour = 1;
527        let ctx2 = EvaluationContext::from_request(&req);
528        assert_eq!(
529            BytecodeVm::eval_decision(&bytecode, &ctx2, Decision::Block, Decision::Allow).unwrap(),
530            Decision::Allow
531        );
532    }
533
534    #[test]
535    fn test_vm_explicit_jump_and_emit_program() {
536        let bytecode = Bytecode {
537            instructions: vec![],
538            constants: vec![],
539            fields: vec![],
540            action_instructions: vec![],
541        };
542        let req = EvaluationRequest {
543            request_id: "req".into(),
544            agent_id: "a".into(),
545            agent_org: "o".into(),
546            agent_level: "standard".into(),
547            mission_id: None,
548            mission_type: None,
549            query_type: None,
550            justification: None,
551            export_format: None,
552            result_limit: None,
553            requests_last_hour: 0,
554            requests_last_24h: 0,
555            results_last_query: 0,
556            account_department: None,
557            allowed_departments: vec![],
558            request_hour: 0,
559            is_within_mission_hours: true,
560        };
561        let ctx = EvaluationContext::from_request(&req);
562        let program = vec![
563            Instruction::LoadFalse,
564            Instruction::JumpIfFalse(3),
565            Instruction::EmitDecision(Decision::Block),
566            Instruction::EmitDecision(Decision::Allow),
567        ];
568        assert_eq!(
569            BytecodeVm::eval_program(&program, &bytecode, &ctx).unwrap(),
570            VmExit::Decision(Decision::Allow)
571        );
572    }
573
574    #[test]
575    fn test_action_vm_exec_block_with_soc_alert() {
576        let program = vec![
577            ActionInstruction::SetDecision(Decision::Block),
578            ActionInstruction::SetErrorCode("VOLUME_EXCEEDED".into()),
579            ActionInstruction::SetMessage("Demo policy matched".into()),
580            ActionInstruction::SetAlertSoc(true),
581            ActionInstruction::Halt,
582        ];
583        let result = ActionVm::execute(&program).unwrap();
584        assert_eq!(result.decision, Decision::Block);
585        assert_eq!(result.error_code.as_deref(), Some("VOLUME_EXCEEDED"));
586        assert_eq!(result.message.as_deref(), Some("Demo policy matched"));
587        assert!(result.alert_soc);
588    }
589
590    #[test]
591    fn test_action_vm_exec_approval_default_message() {
592        let program = vec![
593            ActionInstruction::SetDecision(Decision::ApprovalRequired),
594            ActionInstruction::SetErrorCode("APPROVAL_REQUIRED".into()),
595            ActionInstruction::SetApprovalTimeout(15),
596            ActionInstruction::Halt,
597        ];
598        let result = ActionVm::execute(&program).unwrap();
599        assert_eq!(result.decision, Decision::ApprovalRequired);
600        assert_eq!(
601            result.message.as_deref(),
602            Some("Approval required within 15 minutes")
603        );
604    }
605
606    #[test]
607    fn test_vm_eval_in_operator() {
608        let ast = RuleAst {
609            id: "CRUE_IN_VM".to_string(),
610            version: "1.0.0".to_string(),
611            signed: false,
612            when_clause: Expression::In(
613                Box::new(Expression::field("request.export_format")),
614                vec![
615                    Value::String("PDF".to_string()),
616                    Value::String("CSV".to_string()),
617                ],
618            ),
619            then_clause: vec![ActionNode::Log],
620            metadata: MetadataNode {
621                name: "IN".to_string(),
622                description: "IN".to_string(),
623                severity: "LOW".to_string(),
624                category: "TEST".to_string(),
625                author: "system".to_string(),
626                created_at: "2026-01-01".to_string(),
627                validated_by: None,
628            },
629        };
630        let bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
631
632        let mut req = EvaluationRequest {
633            request_id: "req".into(),
634            agent_id: "a".into(),
635            agent_org: "o".into(),
636            agent_level: "standard".into(),
637            mission_id: None,
638            mission_type: None,
639            query_type: None,
640            justification: None,
641            export_format: Some("PDF".into()),
642            result_limit: None,
643            requests_last_hour: 0,
644            requests_last_24h: 0,
645            results_last_query: 0,
646            account_department: None,
647            allowed_departments: vec![],
648            request_hour: 12,
649            is_within_mission_hours: true,
650        };
651        let ctx = EvaluationContext::from_request(&req);
652        assert!(BytecodeVm::eval(&bytecode, &ctx).unwrap());
653
654        req.export_format = Some("XML".into());
655        let ctx = EvaluationContext::from_request(&req);
656        assert!(!BytecodeVm::eval(&bytecode, &ctx).unwrap());
657    }
658
659    #[test]
660    fn test_vm_eval_between_operator() {
661        let ast = RuleAst {
662            id: "CRUE_BETWEEN_VM".to_string(),
663            version: "1.0.0".to_string(),
664            signed: false,
665            when_clause: Expression::Between(
666                Box::new(Expression::field("context.request_hour")),
667                Box::new(Expression::number(8)),
668                Box::new(Expression::number(18)),
669            ),
670            then_clause: vec![ActionNode::Log],
671            metadata: MetadataNode {
672                name: "BETWEEN".to_string(),
673                description: "BETWEEN".to_string(),
674                severity: "LOW".to_string(),
675                category: "TEST".to_string(),
676                author: "system".to_string(),
677                created_at: "2026-01-01".to_string(),
678                validated_by: None,
679            },
680        };
681        let bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
682
683        let mut req = EvaluationRequest {
684            request_id: "req".into(),
685            agent_id: "a".into(),
686            agent_org: "o".into(),
687            agent_level: "standard".into(),
688            mission_id: None,
689            mission_type: None,
690            query_type: None,
691            justification: None,
692            export_format: None,
693            result_limit: None,
694            requests_last_hour: 0,
695            requests_last_24h: 0,
696            results_last_query: 0,
697            account_department: None,
698            allowed_departments: vec![],
699            request_hour: 9,
700            is_within_mission_hours: true,
701        };
702        let ctx = EvaluationContext::from_request(&req);
703        assert!(BytecodeVm::eval(&bytecode, &ctx).unwrap());
704
705        req.request_hour = 22;
706        let ctx = EvaluationContext::from_request(&req);
707        assert!(!BytecodeVm::eval(&bytecode, &ctx).unwrap());
708    }
709}