Skip to main content

sochdb_vector/
jit_ir.rs

1// Copyright 2025 SochDB Authors
2//
3// Licensed under the Apache License, Version 2.0
4
5//! JIT Compilation for Small IR
6//!
7//! This module provides a minimal SSA-like IR for filter expressions
8//! that can be JIT-compiled for hot paths.
9//!
10//! # Problem
11//!
12//! Interpreted filter evaluation has overhead:
13//! - Function call per operation
14//! - Branch misprediction from expression dispatch
15//! - No optimization across operations
16//!
17//! # Solution
18//!
19//! JIT compilation for hot filters:
20//! 1. Define simple SSA-like IR
21//! 2. Compile to native code (via Cranelift when available)
22//! 3. Cache compiled functions
23//! 4. Fall back to interpreter for cold paths
24//!
25//! # IR Design
26//!
27//! - Three-address code style
28//! - Explicit types (i64, f64, bool, ptr)
29//! - No control flow (single basic block)
30//! - Used for simple filter expressions
31//!
32//! # Example
33//!
34//! Filter: age >= 18 AND score > 50.0
35//!
36//! ```text
37//! v0 = load_i64 field:0     ; load age
38//! v1 = const_i64 18
39//! v2 = gte_i64 v0, v1       ; age >= 18
40//! v3 = load_f64 field:1     ; load score
41//! v4 = const_f64 50.0
42//! v5 = gt_f64 v3, v4        ; score > 50.0
43//! v6 = and v2, v5           ; AND
44//! ret v6
45//! ```
46
47use std::collections::HashMap;
48use std::sync::Arc;
49
50/// Value type in the IR.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum IrType {
53    Bool,
54    I64,
55    F64,
56    Ptr,
57}
58
59impl IrType {
60    /// Size in bytes.
61    pub fn size(&self) -> usize {
62        match self {
63            IrType::Bool => 1,
64            IrType::I64 | IrType::F64 | IrType::Ptr => 8,
65        }
66    }
67}
68
69/// Virtual register.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub struct Reg(pub u32);
72
73impl Reg {
74    pub fn new(id: u32) -> Self {
75        Reg(id)
76    }
77}
78
79/// IR instruction.
80#[derive(Debug, Clone)]
81pub enum IrInst {
82    // Constants
83    ConstBool(Reg, bool),
84    ConstI64(Reg, i64),
85    ConstF64(Reg, f64),
86
87    // Load from field (field index)
88    LoadI64(Reg, u32),
89    LoadF64(Reg, u32),
90    LoadBool(Reg, u32),
91
92    // Integer comparisons
93    EqI64(Reg, Reg, Reg),
94    NeI64(Reg, Reg, Reg),
95    LtI64(Reg, Reg, Reg),
96    LeI64(Reg, Reg, Reg),
97    GtI64(Reg, Reg, Reg),
98    GeI64(Reg, Reg, Reg),
99
100    // Float comparisons
101    EqF64(Reg, Reg, Reg),
102    NeF64(Reg, Reg, Reg),
103    LtF64(Reg, Reg, Reg),
104    LeF64(Reg, Reg, Reg),
105    GtF64(Reg, Reg, Reg),
106    GeF64(Reg, Reg, Reg),
107
108    // Logical
109    And(Reg, Reg, Reg),
110    Or(Reg, Reg, Reg),
111    Not(Reg, Reg),
112
113    // Arithmetic (for expression evaluation)
114    AddI64(Reg, Reg, Reg),
115    SubI64(Reg, Reg, Reg),
116    MulI64(Reg, Reg, Reg),
117    AddF64(Reg, Reg, Reg),
118    SubF64(Reg, Reg, Reg),
119    MulF64(Reg, Reg, Reg),
120    DivF64(Reg, Reg, Reg),
121
122    // Type conversions
123    I64ToF64(Reg, Reg),
124    F64ToI64(Reg, Reg),
125
126    // Return result
127    Ret(Reg),
128}
129
130impl IrInst {
131    /// Get destination register if any.
132    pub fn dest(&self) -> Option<Reg> {
133        match self {
134            IrInst::ConstBool(r, _)
135            | IrInst::ConstI64(r, _)
136            | IrInst::ConstF64(r, _)
137            | IrInst::LoadI64(r, _)
138            | IrInst::LoadF64(r, _)
139            | IrInst::LoadBool(r, _)
140            | IrInst::EqI64(r, _, _)
141            | IrInst::NeI64(r, _, _)
142            | IrInst::LtI64(r, _, _)
143            | IrInst::LeI64(r, _, _)
144            | IrInst::GtI64(r, _, _)
145            | IrInst::GeI64(r, _, _)
146            | IrInst::EqF64(r, _, _)
147            | IrInst::NeF64(r, _, _)
148            | IrInst::LtF64(r, _, _)
149            | IrInst::LeF64(r, _, _)
150            | IrInst::GtF64(r, _, _)
151            | IrInst::GeF64(r, _, _)
152            | IrInst::And(r, _, _)
153            | IrInst::Or(r, _, _)
154            | IrInst::Not(r, _)
155            | IrInst::AddI64(r, _, _)
156            | IrInst::SubI64(r, _, _)
157            | IrInst::MulI64(r, _, _)
158            | IrInst::AddF64(r, _, _)
159            | IrInst::SubF64(r, _, _)
160            | IrInst::MulF64(r, _, _)
161            | IrInst::DivF64(r, _, _)
162            | IrInst::I64ToF64(r, _)
163            | IrInst::F64ToI64(r, _) => Some(*r),
164            IrInst::Ret(_) => None,
165        }
166    }
167
168    /// Get source registers.
169    pub fn sources(&self) -> Vec<Reg> {
170        match self {
171            IrInst::ConstBool(_, _)
172            | IrInst::ConstI64(_, _)
173            | IrInst::ConstF64(_, _)
174            | IrInst::LoadI64(_, _)
175            | IrInst::LoadF64(_, _)
176            | IrInst::LoadBool(_, _) => vec![],
177
178            IrInst::Not(_, src)
179            | IrInst::I64ToF64(_, src)
180            | IrInst::F64ToI64(_, src)
181            | IrInst::Ret(src) => vec![*src],
182
183            IrInst::EqI64(_, a, b)
184            | IrInst::NeI64(_, a, b)
185            | IrInst::LtI64(_, a, b)
186            | IrInst::LeI64(_, a, b)
187            | IrInst::GtI64(_, a, b)
188            | IrInst::GeI64(_, a, b)
189            | IrInst::EqF64(_, a, b)
190            | IrInst::NeF64(_, a, b)
191            | IrInst::LtF64(_, a, b)
192            | IrInst::LeF64(_, a, b)
193            | IrInst::GtF64(_, a, b)
194            | IrInst::GeF64(_, a, b)
195            | IrInst::And(_, a, b)
196            | IrInst::Or(_, a, b)
197            | IrInst::AddI64(_, a, b)
198            | IrInst::SubI64(_, a, b)
199            | IrInst::MulI64(_, a, b)
200            | IrInst::AddF64(_, a, b)
201            | IrInst::SubF64(_, a, b)
202            | IrInst::MulF64(_, a, b)
203            | IrInst::DivF64(_, a, b) => vec![*a, *b],
204        }
205    }
206}
207
208/// IR function (single basic block).
209#[derive(Debug, Clone)]
210pub struct IrFunction {
211    /// Instructions.
212    pub instructions: Vec<IrInst>,
213    /// Register types.
214    pub reg_types: HashMap<Reg, IrType>,
215    /// Field types (for load instructions).
216    pub field_types: Vec<IrType>,
217    /// Number of registers used.
218    pub num_regs: u32,
219}
220
221impl IrFunction {
222    /// Create a new empty function.
223    pub fn new() -> Self {
224        Self {
225            instructions: Vec::new(),
226            reg_types: HashMap::new(),
227            field_types: Vec::new(),
228            num_regs: 0,
229        }
230    }
231
232    /// Allocate a new register.
233    pub fn alloc_reg(&mut self, ty: IrType) -> Reg {
234        let reg = Reg(self.num_regs);
235        self.num_regs += 1;
236        self.reg_types.insert(reg, ty);
237        reg
238    }
239
240    /// Add a field type.
241    pub fn add_field(&mut self, ty: IrType) -> u32 {
242        let idx = self.field_types.len() as u32;
243        self.field_types.push(ty);
244        idx
245    }
246
247    /// Add an instruction.
248    pub fn push(&mut self, inst: IrInst) {
249        self.instructions.push(inst);
250    }
251
252    /// Get instruction count.
253    pub fn len(&self) -> usize {
254        self.instructions.len()
255    }
256
257    /// Check if empty.
258    pub fn is_empty(&self) -> bool {
259        self.instructions.is_empty()
260    }
261
262    /// Validate the function.
263    pub fn validate(&self) -> Result<(), String> {
264        // Check that all used registers are defined
265        let mut defined: std::collections::HashSet<Reg> = std::collections::HashSet::new();
266
267        for inst in &self.instructions {
268            // Check sources are defined
269            for src in inst.sources() {
270                if !defined.contains(&src) {
271                    return Err(format!("register {:?} used before definition", src));
272                }
273            }
274
275            // Mark destination as defined
276            if let Some(dest) = inst.dest() {
277                defined.insert(dest);
278            }
279        }
280
281        // Check that function ends with Ret
282        if let Some(last) = self.instructions.last() {
283            if !matches!(last, IrInst::Ret(_)) {
284                return Err("function must end with Ret".to_string());
285            }
286        } else {
287            return Err("function is empty".to_string());
288        }
289
290        Ok(())
291    }
292}
293
294impl Default for IrFunction {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300/// Runtime value for interpreter.
301#[derive(Debug, Clone, Copy)]
302pub enum RtValue {
303    Bool(bool),
304    I64(i64),
305    F64(f64),
306}
307
308impl RtValue {
309    pub fn as_bool(&self) -> bool {
310        match self {
311            RtValue::Bool(b) => *b,
312            RtValue::I64(i) => *i != 0,
313            RtValue::F64(f) => *f != 0.0,
314        }
315    }
316
317    pub fn as_i64(&self) -> i64 {
318        match self {
319            RtValue::Bool(b) => {
320                if *b {
321                    1
322                } else {
323                    0
324                }
325            }
326            RtValue::I64(i) => *i,
327            RtValue::F64(f) => *f as i64,
328        }
329    }
330
331    pub fn as_f64(&self) -> f64 {
332        match self {
333            RtValue::Bool(b) => {
334                if *b {
335                    1.0
336                } else {
337                    0.0
338                }
339            }
340            RtValue::I64(i) => *i as f64,
341            RtValue::F64(f) => *f,
342        }
343    }
344}
345
346/// Field accessor for runtime.
347pub trait FieldAccess {
348    fn get_i64(&self, field: u32) -> i64;
349    fn get_f64(&self, field: u32) -> f64;
350    fn get_bool(&self, field: u32) -> bool;
351}
352
353/// Simple array-based field storage.
354pub struct ArrayFields {
355    i64_fields: Vec<i64>,
356    f64_fields: Vec<f64>,
357    bool_fields: Vec<bool>,
358}
359
360impl ArrayFields {
361    pub fn new() -> Self {
362        Self {
363            i64_fields: Vec::new(),
364            f64_fields: Vec::new(),
365            bool_fields: Vec::new(),
366        }
367    }
368
369    pub fn set_i64(&mut self, idx: usize, value: i64) {
370        if idx >= self.i64_fields.len() {
371            self.i64_fields.resize(idx + 1, 0);
372        }
373        self.i64_fields[idx] = value;
374    }
375
376    pub fn set_f64(&mut self, idx: usize, value: f64) {
377        if idx >= self.f64_fields.len() {
378            self.f64_fields.resize(idx + 1, 0.0);
379        }
380        self.f64_fields[idx] = value;
381    }
382
383    pub fn set_bool(&mut self, idx: usize, value: bool) {
384        if idx >= self.bool_fields.len() {
385            self.bool_fields.resize(idx + 1, false);
386        }
387        self.bool_fields[idx] = value;
388    }
389}
390
391impl Default for ArrayFields {
392    fn default() -> Self {
393        Self::new()
394    }
395}
396
397impl FieldAccess for ArrayFields {
398    fn get_i64(&self, field: u32) -> i64 {
399        self.i64_fields.get(field as usize).copied().unwrap_or(0)
400    }
401
402    fn get_f64(&self, field: u32) -> f64 {
403        self.f64_fields.get(field as usize).copied().unwrap_or(0.0)
404    }
405
406    fn get_bool(&self, field: u32) -> bool {
407        self.bool_fields
408            .get(field as usize)
409            .copied()
410            .unwrap_or(false)
411    }
412}
413
414/// IR interpreter (fallback when JIT not available).
415pub struct IrInterpreter {
416    /// The function to interpret.
417    function: IrFunction,
418}
419
420impl IrInterpreter {
421    /// Create a new interpreter.
422    pub fn new(function: IrFunction) -> Result<Self, String> {
423        function.validate()?;
424        Ok(Self { function })
425    }
426
427    /// Execute the function.
428    pub fn execute<F: FieldAccess>(&self, fields: &F) -> bool {
429        let mut regs: Vec<RtValue> = vec![RtValue::Bool(false); self.function.num_regs as usize];
430
431        for inst in &self.function.instructions {
432            match inst {
433                IrInst::ConstBool(dest, val) => {
434                    regs[dest.0 as usize] = RtValue::Bool(*val);
435                }
436                IrInst::ConstI64(dest, val) => {
437                    regs[dest.0 as usize] = RtValue::I64(*val);
438                }
439                IrInst::ConstF64(dest, val) => {
440                    regs[dest.0 as usize] = RtValue::F64(*val);
441                }
442                IrInst::LoadI64(dest, field) => {
443                    regs[dest.0 as usize] = RtValue::I64(fields.get_i64(*field));
444                }
445                IrInst::LoadF64(dest, field) => {
446                    regs[dest.0 as usize] = RtValue::F64(fields.get_f64(*field));
447                }
448                IrInst::LoadBool(dest, field) => {
449                    regs[dest.0 as usize] = RtValue::Bool(fields.get_bool(*field));
450                }
451                IrInst::EqI64(dest, a, b) => {
452                    let result = regs[a.0 as usize].as_i64() == regs[b.0 as usize].as_i64();
453                    regs[dest.0 as usize] = RtValue::Bool(result);
454                }
455                IrInst::NeI64(dest, a, b) => {
456                    let result = regs[a.0 as usize].as_i64() != regs[b.0 as usize].as_i64();
457                    regs[dest.0 as usize] = RtValue::Bool(result);
458                }
459                IrInst::LtI64(dest, a, b) => {
460                    let result = regs[a.0 as usize].as_i64() < regs[b.0 as usize].as_i64();
461                    regs[dest.0 as usize] = RtValue::Bool(result);
462                }
463                IrInst::LeI64(dest, a, b) => {
464                    let result = regs[a.0 as usize].as_i64() <= regs[b.0 as usize].as_i64();
465                    regs[dest.0 as usize] = RtValue::Bool(result);
466                }
467                IrInst::GtI64(dest, a, b) => {
468                    let result = regs[a.0 as usize].as_i64() > regs[b.0 as usize].as_i64();
469                    regs[dest.0 as usize] = RtValue::Bool(result);
470                }
471                IrInst::GeI64(dest, a, b) => {
472                    let result = regs[a.0 as usize].as_i64() >= regs[b.0 as usize].as_i64();
473                    regs[dest.0 as usize] = RtValue::Bool(result);
474                }
475                IrInst::EqF64(dest, a, b) => {
476                    let result = regs[a.0 as usize].as_f64() == regs[b.0 as usize].as_f64();
477                    regs[dest.0 as usize] = RtValue::Bool(result);
478                }
479                IrInst::NeF64(dest, a, b) => {
480                    let result = regs[a.0 as usize].as_f64() != regs[b.0 as usize].as_f64();
481                    regs[dest.0 as usize] = RtValue::Bool(result);
482                }
483                IrInst::LtF64(dest, a, b) => {
484                    let result = regs[a.0 as usize].as_f64() < regs[b.0 as usize].as_f64();
485                    regs[dest.0 as usize] = RtValue::Bool(result);
486                }
487                IrInst::LeF64(dest, a, b) => {
488                    let result = regs[a.0 as usize].as_f64() <= regs[b.0 as usize].as_f64();
489                    regs[dest.0 as usize] = RtValue::Bool(result);
490                }
491                IrInst::GtF64(dest, a, b) => {
492                    let result = regs[a.0 as usize].as_f64() > regs[b.0 as usize].as_f64();
493                    regs[dest.0 as usize] = RtValue::Bool(result);
494                }
495                IrInst::GeF64(dest, a, b) => {
496                    let result = regs[a.0 as usize].as_f64() >= regs[b.0 as usize].as_f64();
497                    regs[dest.0 as usize] = RtValue::Bool(result);
498                }
499                IrInst::And(dest, a, b) => {
500                    let result = regs[a.0 as usize].as_bool() && regs[b.0 as usize].as_bool();
501                    regs[dest.0 as usize] = RtValue::Bool(result);
502                }
503                IrInst::Or(dest, a, b) => {
504                    let result = regs[a.0 as usize].as_bool() || regs[b.0 as usize].as_bool();
505                    regs[dest.0 as usize] = RtValue::Bool(result);
506                }
507                IrInst::Not(dest, src) => {
508                    let result = !regs[src.0 as usize].as_bool();
509                    regs[dest.0 as usize] = RtValue::Bool(result);
510                }
511                IrInst::AddI64(dest, a, b) => {
512                    let result = regs[a.0 as usize]
513                        .as_i64()
514                        .wrapping_add(regs[b.0 as usize].as_i64());
515                    regs[dest.0 as usize] = RtValue::I64(result);
516                }
517                IrInst::SubI64(dest, a, b) => {
518                    let result = regs[a.0 as usize]
519                        .as_i64()
520                        .wrapping_sub(regs[b.0 as usize].as_i64());
521                    regs[dest.0 as usize] = RtValue::I64(result);
522                }
523                IrInst::MulI64(dest, a, b) => {
524                    let result = regs[a.0 as usize]
525                        .as_i64()
526                        .wrapping_mul(regs[b.0 as usize].as_i64());
527                    regs[dest.0 as usize] = RtValue::I64(result);
528                }
529                IrInst::AddF64(dest, a, b) => {
530                    let result = regs[a.0 as usize].as_f64() + regs[b.0 as usize].as_f64();
531                    regs[dest.0 as usize] = RtValue::F64(result);
532                }
533                IrInst::SubF64(dest, a, b) => {
534                    let result = regs[a.0 as usize].as_f64() - regs[b.0 as usize].as_f64();
535                    regs[dest.0 as usize] = RtValue::F64(result);
536                }
537                IrInst::MulF64(dest, a, b) => {
538                    let result = regs[a.0 as usize].as_f64() * regs[b.0 as usize].as_f64();
539                    regs[dest.0 as usize] = RtValue::F64(result);
540                }
541                IrInst::DivF64(dest, a, b) => {
542                    let result = regs[a.0 as usize].as_f64() / regs[b.0 as usize].as_f64();
543                    regs[dest.0 as usize] = RtValue::F64(result);
544                }
545                IrInst::I64ToF64(dest, src) => {
546                    regs[dest.0 as usize] = RtValue::F64(regs[src.0 as usize].as_i64() as f64);
547                }
548                IrInst::F64ToI64(dest, src) => {
549                    regs[dest.0 as usize] = RtValue::I64(regs[src.0 as usize].as_f64() as i64);
550                }
551                IrInst::Ret(src) => {
552                    return regs[src.0 as usize].as_bool();
553                }
554            }
555        }
556
557        false
558    }
559}
560
561/// IR builder helper.
562pub struct IrBuilder {
563    function: IrFunction,
564}
565
566impl IrBuilder {
567    /// Create a new builder.
568    pub fn new() -> Self {
569        Self {
570            function: IrFunction::new(),
571        }
572    }
573
574    /// Define a field.
575    pub fn define_field(&mut self, ty: IrType) -> u32 {
576        self.function.add_field(ty)
577    }
578
579    /// Load an i64 field.
580    pub fn load_i64(&mut self, field: u32) -> Reg {
581        let reg = self.function.alloc_reg(IrType::I64);
582        self.function.push(IrInst::LoadI64(reg, field));
583        reg
584    }
585
586    /// Load an f64 field.
587    pub fn load_f64(&mut self, field: u32) -> Reg {
588        let reg = self.function.alloc_reg(IrType::F64);
589        self.function.push(IrInst::LoadF64(reg, field));
590        reg
591    }
592
593    /// Create i64 constant.
594    pub fn const_i64(&mut self, val: i64) -> Reg {
595        let reg = self.function.alloc_reg(IrType::I64);
596        self.function.push(IrInst::ConstI64(reg, val));
597        reg
598    }
599
600    /// Create f64 constant.
601    pub fn const_f64(&mut self, val: f64) -> Reg {
602        let reg = self.function.alloc_reg(IrType::F64);
603        self.function.push(IrInst::ConstF64(reg, val));
604        reg
605    }
606
607    /// Create bool constant.
608    pub fn const_bool(&mut self, val: bool) -> Reg {
609        let reg = self.function.alloc_reg(IrType::Bool);
610        self.function.push(IrInst::ConstBool(reg, val));
611        reg
612    }
613
614    /// Greater-than-or-equal for i64.
615    pub fn ge_i64(&mut self, a: Reg, b: Reg) -> Reg {
616        let reg = self.function.alloc_reg(IrType::Bool);
617        self.function.push(IrInst::GeI64(reg, a, b));
618        reg
619    }
620
621    /// Greater-than for i64.
622    pub fn gt_i64(&mut self, a: Reg, b: Reg) -> Reg {
623        let reg = self.function.alloc_reg(IrType::Bool);
624        self.function.push(IrInst::GtI64(reg, a, b));
625        reg
626    }
627
628    /// Less-than for i64.
629    pub fn lt_i64(&mut self, a: Reg, b: Reg) -> Reg {
630        let reg = self.function.alloc_reg(IrType::Bool);
631        self.function.push(IrInst::LtI64(reg, a, b));
632        reg
633    }
634
635    /// Greater-than for f64.
636    pub fn gt_f64(&mut self, a: Reg, b: Reg) -> Reg {
637        let reg = self.function.alloc_reg(IrType::Bool);
638        self.function.push(IrInst::GtF64(reg, a, b));
639        reg
640    }
641
642    /// Logical AND.
643    pub fn and(&mut self, a: Reg, b: Reg) -> Reg {
644        let reg = self.function.alloc_reg(IrType::Bool);
645        self.function.push(IrInst::And(reg, a, b));
646        reg
647    }
648
649    /// Logical OR.
650    pub fn or(&mut self, a: Reg, b: Reg) -> Reg {
651        let reg = self.function.alloc_reg(IrType::Bool);
652        self.function.push(IrInst::Or(reg, a, b));
653        reg
654    }
655
656    /// Logical NOT.
657    pub fn not(&mut self, a: Reg) -> Reg {
658        let reg = self.function.alloc_reg(IrType::Bool);
659        self.function.push(IrInst::Not(reg, a));
660        reg
661    }
662
663    /// Return result.
664    pub fn ret(&mut self, reg: Reg) {
665        self.function.push(IrInst::Ret(reg));
666    }
667
668    /// Build the function.
669    pub fn build(self) -> IrFunction {
670        self.function
671    }
672}
673
674impl Default for IrBuilder {
675    fn default() -> Self {
676        Self::new()
677    }
678}
679
680/// Compiled filter that can be executed.
681pub struct CompiledFilter {
682    /// The underlying interpreter (or JIT code).
683    interpreter: IrInterpreter,
684    /// Execution count for hot-path detection.
685    exec_count: std::sync::atomic::AtomicU64,
686}
687
688impl CompiledFilter {
689    /// Compile a filter function.
690    pub fn compile(function: IrFunction) -> Result<Self, String> {
691        let interpreter = IrInterpreter::new(function)?;
692        Ok(Self {
693            interpreter,
694            exec_count: std::sync::atomic::AtomicU64::new(0),
695        })
696    }
697
698    /// Execute the filter.
699    pub fn execute<F: FieldAccess>(&self, fields: &F) -> bool {
700        self.exec_count
701            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
702        self.interpreter.execute(fields)
703    }
704
705    /// Get execution count.
706    pub fn exec_count(&self) -> u64 {
707        self.exec_count.load(std::sync::atomic::Ordering::Relaxed)
708    }
709
710    /// Check if hot (should be JIT compiled).
711    pub fn is_hot(&self) -> bool {
712        self.exec_count() > 1000
713    }
714}
715
716/// Filter cache for compiled filters.
717pub struct FilterCache {
718    cache: std::sync::RwLock<HashMap<String, Arc<CompiledFilter>>>,
719}
720
721impl FilterCache {
722    /// Create a new cache.
723    pub fn new() -> Self {
724        Self {
725            cache: std::sync::RwLock::new(HashMap::new()),
726        }
727    }
728
729    /// Get or compile a filter.
730    pub fn get_or_compile(
731        &self,
732        key: &str,
733        build_fn: impl FnOnce() -> IrFunction,
734    ) -> Result<Arc<CompiledFilter>, String> {
735        // Check cache first
736        {
737            let cache = self.cache.read().unwrap();
738            if let Some(filter) = cache.get(key) {
739                return Ok(Arc::clone(filter));
740            }
741        }
742
743        // Compile new filter
744        let function = build_fn();
745        let filter = Arc::new(CompiledFilter::compile(function)?);
746
747        // Insert into cache
748        {
749            let mut cache = self.cache.write().unwrap();
750            cache.insert(key.to_string(), Arc::clone(&filter));
751        }
752
753        Ok(filter)
754    }
755
756    /// Get cached filter count.
757    pub fn len(&self) -> usize {
758        self.cache.read().unwrap().len()
759    }
760
761    /// Check if empty.
762    pub fn is_empty(&self) -> bool {
763        self.cache.read().unwrap().is_empty()
764    }
765
766    /// Clear cache.
767    pub fn clear(&self) {
768        self.cache.write().unwrap().clear();
769    }
770}
771
772impl Default for FilterCache {
773    fn default() -> Self {
774        Self::new()
775    }
776}
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781
782    #[test]
783    fn test_simple_filter() {
784        // Filter: age >= 18
785        let mut builder = IrBuilder::new();
786        let age_field = builder.define_field(IrType::I64);
787
788        let age = builder.load_i64(age_field);
789        let threshold = builder.const_i64(18);
790        let result = builder.ge_i64(age, threshold);
791        builder.ret(result);
792
793        let func = builder.build();
794        let interp = IrInterpreter::new(func).unwrap();
795
796        let mut fields = ArrayFields::new();
797        fields.set_i64(0, 21);
798        assert!(interp.execute(&fields));
799
800        fields.set_i64(0, 16);
801        assert!(!interp.execute(&fields));
802    }
803
804    #[test]
805    fn test_compound_filter() {
806        // Filter: age >= 18 AND score > 50.0
807        let mut builder = IrBuilder::new();
808        let age_field = builder.define_field(IrType::I64);
809        let score_field = builder.define_field(IrType::F64);
810
811        let age = builder.load_i64(age_field);
812        let age_threshold = builder.const_i64(18);
813        let age_ok = builder.ge_i64(age, age_threshold);
814
815        let score = builder.load_f64(score_field);
816        let score_threshold = builder.const_f64(50.0);
817        let score_ok = builder.gt_f64(score, score_threshold);
818
819        let result = builder.and(age_ok, score_ok);
820        builder.ret(result);
821
822        let func = builder.build();
823        let interp = IrInterpreter::new(func).unwrap();
824
825        let mut fields = ArrayFields::new();
826
827        // Both pass - age at field 0, score at field 1
828        fields.set_i64(age_field as usize, 21);
829        fields.set_f64(score_field as usize, 75.0);
830        assert!(interp.execute(&fields));
831
832        // Age fails
833        fields.set_i64(age_field as usize, 16);
834        fields.set_f64(score_field as usize, 75.0);
835        assert!(!interp.execute(&fields));
836
837        // Score fails
838        fields.set_i64(age_field as usize, 21);
839        fields.set_f64(score_field as usize, 30.0);
840        assert!(!interp.execute(&fields));
841    }
842
843    #[test]
844    fn test_or_filter() {
845        // Filter: age < 18 OR age > 65
846        let mut builder = IrBuilder::new();
847        let age_field = builder.define_field(IrType::I64);
848
849        let age = builder.load_i64(age_field);
850        let low = builder.const_i64(18);
851        let high = builder.const_i64(65);
852
853        let too_young = builder.lt_i64(age, low);
854        let too_old = builder.gt_i64(age, high);
855        let result = builder.or(too_young, too_old);
856        builder.ret(result);
857
858        let func = builder.build();
859        let interp = IrInterpreter::new(func).unwrap();
860
861        let mut fields = ArrayFields::new();
862
863        fields.set_i64(0, 10);
864        assert!(interp.execute(&fields)); // Too young
865
866        fields.set_i64(0, 70);
867        assert!(interp.execute(&fields)); // Too old
868
869        fields.set_i64(0, 30);
870        assert!(!interp.execute(&fields)); // In range
871    }
872
873    #[test]
874    fn test_not_filter() {
875        // Filter: NOT (age < 18)
876        let mut builder = IrBuilder::new();
877        let age_field = builder.define_field(IrType::I64);
878
879        let age = builder.load_i64(age_field);
880        let threshold = builder.const_i64(18);
881        let too_young = builder.lt_i64(age, threshold);
882        let result = builder.not(too_young);
883        builder.ret(result);
884
885        let func = builder.build();
886        let interp = IrInterpreter::new(func).unwrap();
887
888        let mut fields = ArrayFields::new();
889
890        fields.set_i64(0, 10);
891        assert!(!interp.execute(&fields)); // Too young, NOT fails
892
893        fields.set_i64(0, 21);
894        assert!(interp.execute(&fields)); // Not too young, NOT passes
895    }
896
897    #[test]
898    fn test_validation() {
899        // Empty function
900        let func = IrFunction::new();
901        assert!(func.validate().is_err());
902
903        // Missing Ret
904        let mut func = IrFunction::new();
905        func.push(IrInst::ConstBool(Reg(0), true));
906        assert!(func.validate().is_err());
907
908        // Undefined register
909        let mut func = IrFunction::new();
910        func.push(IrInst::Ret(Reg(99)));
911        assert!(func.validate().is_err());
912
913        // Valid function
914        let mut func = IrFunction::new();
915        func.alloc_reg(IrType::Bool);
916        func.push(IrInst::ConstBool(Reg(0), true));
917        func.push(IrInst::Ret(Reg(0)));
918        assert!(func.validate().is_ok());
919    }
920
921    #[test]
922    fn test_compiled_filter() {
923        let mut builder = IrBuilder::new();
924        let field = builder.define_field(IrType::I64);
925        let val = builder.load_i64(field);
926        let threshold = builder.const_i64(10);
927        let result = builder.ge_i64(val, threshold);
928        builder.ret(result);
929
930        let filter = CompiledFilter::compile(builder.build()).unwrap();
931
932        let mut fields = ArrayFields::new();
933        fields.set_i64(0, 15);
934
935        assert!(filter.execute(&fields));
936        assert_eq!(filter.exec_count(), 1);
937
938        filter.execute(&fields);
939        filter.execute(&fields);
940        assert_eq!(filter.exec_count(), 3);
941    }
942
943    #[test]
944    fn test_filter_cache() {
945        let cache = FilterCache::new();
946
947        let filter1 = cache
948            .get_or_compile("age_filter", || {
949                let mut builder = IrBuilder::new();
950                let field = builder.define_field(IrType::I64);
951                let val = builder.load_i64(field);
952                let threshold = builder.const_i64(18);
953                let result = builder.ge_i64(val, threshold);
954                builder.ret(result);
955                builder.build()
956            })
957            .unwrap();
958
959        let filter2 = cache
960            .get_or_compile("age_filter", || {
961                panic!("should not be called - filter should be cached");
962            })
963            .unwrap();
964
965        assert!(Arc::ptr_eq(&filter1, &filter2));
966        assert_eq!(cache.len(), 1);
967    }
968
969    #[test]
970    fn test_ir_type_size() {
971        assert_eq!(IrType::Bool.size(), 1);
972        assert_eq!(IrType::I64.size(), 8);
973        assert_eq!(IrType::F64.size(), 8);
974        assert_eq!(IrType::Ptr.size(), 8);
975    }
976
977    #[test]
978    fn test_instruction_sources() {
979        let inst = IrInst::And(Reg(2), Reg(0), Reg(1));
980        assert_eq!(inst.sources(), vec![Reg(0), Reg(1)]);
981        assert_eq!(inst.dest(), Some(Reg(2)));
982
983        let inst = IrInst::ConstI64(Reg(0), 42);
984        assert!(inst.sources().is_empty());
985        assert_eq!(inst.dest(), Some(Reg(0)));
986
987        let inst = IrInst::Ret(Reg(0));
988        assert_eq!(inst.sources(), vec![Reg(0)]);
989        assert_eq!(inst.dest(), None);
990    }
991
992    #[test]
993    #[allow(clippy::approx_constant)] // 3.14 is arbitrary test data, not PI
994    fn test_rt_value_conversions() {
995        assert!(RtValue::Bool(true).as_bool());
996        assert!(!RtValue::Bool(false).as_bool());
997        assert!(RtValue::I64(1).as_bool());
998        assert!(!RtValue::I64(0).as_bool());
999
1000        assert_eq!(RtValue::I64(42).as_i64(), 42);
1001        assert_eq!(RtValue::F64(3.14).as_f64(), 3.14);
1002        assert_eq!(RtValue::I64(10).as_f64(), 10.0);
1003    }
1004}