|
| 1 | +//! JIT IR-optimization tests (JitContext::optimize). |
| 2 | +//! |
| 3 | +//! optimize() runs a curated scalar pass pipeline (mem2reg + instcombine/reassociate/ |
| 4 | +//! gvn/simplifycfg) via LLVM's new pass manager. The pipeline DELIBERATELY excludes Loop |
| 5 | +//! Strength Reduction — LSR crashed at OptimizationLevel::Default on the dual-band lowerer's |
| 6 | +//! LCSSA-form loops (why the engine runs at None). These tests prove the pipeline (a) runs |
| 7 | +//! without crashing on real lowered IR including a LOOP-with-locals (the danger zone), and |
| 8 | +//! (b) PRESERVES semantics — the optimized JIT'd function still returns the correct value. |
| 9 | +
|
| 10 | +#![cfg(feature = "llvm-jit")] |
| 11 | + |
| 12 | +use inkwell::context::Context; |
| 13 | +use omnimcode_codegen::JitContext; |
| 14 | +use omnimcode_core::ast::Pos; |
| 15 | +use omnimcode_core::bytecode::{CompiledFunction, Const, Op}; |
| 16 | +use omnimcode_core::parser::Parser; |
| 17 | + |
| 18 | +fn skeleton(name: &str, params: Vec<&str>, ops: Vec<Op>, constants: Vec<Const>) -> CompiledFunction { |
| 19 | + let n = ops.len(); |
| 20 | + let param_types = vec![None; params.len()]; |
| 21 | + CompiledFunction { |
| 22 | + name: name.to_string(), |
| 23 | + params: params.into_iter().map(String::from).collect(), |
| 24 | + param_types, |
| 25 | + return_type: None, |
| 26 | + op_positions: vec![Pos::unknown(); n], |
| 27 | + pragmas: Vec::new(), |
| 28 | + call_cache: (0..n).map(|_| std::cell::Cell::new(0)).collect(), |
| 29 | + ops, |
| 30 | + constants, |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +#[test] |
| 35 | +fn optimize_preserves_simple_arithmetic() { |
| 36 | + let f = skeleton( |
| 37 | + "double", |
| 38 | + vec!["x"], |
| 39 | + vec![Op::LoadParam(0), Op::LoadParam(0), Op::Add, Op::Return], |
| 40 | + vec![], |
| 41 | + ); |
| 42 | + let ctx = Context::create(); |
| 43 | + let jit = JitContext::new(&ctx).expect("jit ctx"); |
| 44 | + jit.lower_function(&f).expect("lower"); |
| 45 | + jit.optimize().expect("optimize ok"); |
| 46 | + unsafe { |
| 47 | + let native = jit.get_i64_i64("double").expect("jit fn"); |
| 48 | + assert_eq!(native.call(21), 42, "optimized fn must still double"); |
| 49 | + assert_eq!(native.call(-5), -10); |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +#[test] |
| 54 | +fn optimize_preserves_loop_with_locals() { |
| 55 | + // sum_to_n: allocas (StoreVar/LoadVar/AssignVar → mem2reg promotes) + a while LOOP |
| 56 | + // (the LSR danger zone). The scalar pipeline must survive it AND keep the result exact. |
| 57 | + let f = skeleton( |
| 58 | + "sum_to_n", |
| 59 | + vec!["n"], |
| 60 | + vec![ |
| 61 | + Op::LoadConst(0), |
| 62 | + Op::StoreVar("s".into()), |
| 63 | + Op::LoadConst(1), |
| 64 | + Op::StoreVar("k".into()), |
| 65 | + Op::LoadVar("k".into()), |
| 66 | + Op::LoadParam(0), |
| 67 | + Op::Le, |
| 68 | + Op::JumpIfFalse(10), |
| 69 | + Op::Pop, |
| 70 | + Op::LoadVar("s".into()), |
| 71 | + Op::LoadVar("k".into()), |
| 72 | + Op::Add, |
| 73 | + Op::AssignVar("s".into()), |
| 74 | + Op::LoadVar("k".into()), |
| 75 | + Op::LoadConst(1), |
| 76 | + Op::Add, |
| 77 | + Op::AssignVar("k".into()), |
| 78 | + Op::Jump(-14), |
| 79 | + Op::Pop, |
| 80 | + Op::LoadVar("s".into()), |
| 81 | + Op::Return, |
| 82 | + ], |
| 83 | + vec![Const::Int(0), Const::Int(1)], |
| 84 | + ); |
| 85 | + let ctx = Context::create(); |
| 86 | + let jit = JitContext::new(&ctx).expect("jit ctx"); |
| 87 | + jit.lower_function(&f).expect("lower"); |
| 88 | + jit.optimize().expect("optimize must not error on a loop+locals fn"); |
| 89 | + unsafe { |
| 90 | + let native = jit.get_i64_i64("sum_to_n").expect("jit fn"); |
| 91 | + assert_eq!(native.call(10), 55, "optimized loop must still sum 1..10"); |
| 92 | + assert_eq!(native.call(100), 5050); |
| 93 | + assert_eq!(native.call(0), 0); |
| 94 | + assert_eq!(native.call(1), 1); |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +#[test] |
| 99 | +fn jit_module_dispatch_with_opt_env_preserves_results() { |
| 100 | + // The DISPATCH path (jit_module) wires optimize() behind OMC_HBIT_JIT_OPT=1. With it on, |
| 101 | + // a cross-fn-call program must still build a correct dispatch table. Env leakage to other |
| 102 | + // parallel tests is benign — the passes preserve semantics, so any test still passes. |
| 103 | + std::env::set_var("OMC_HBIT_JIT_OPT", "1"); |
| 104 | + let source = r#" |
| 105 | + fn helper(x) { return x * 2; } |
| 106 | + fn caller(x) { return helper(x) + 1; } |
| 107 | + "#; |
| 108 | + let mut parser = Parser::new(source); |
| 109 | + let statements = parser.parse().expect("parse"); |
| 110 | + let module = omnimcode_core::compiler::compile_program(&statements).expect("compile"); |
| 111 | + let ctx = Context::create(); |
| 112 | + let jit = JitContext::new(&ctx).expect("jit"); |
| 113 | + let jitted = jit.jit_module(&module).expect("jit_module (opt path)"); |
| 114 | + let caller = jitted.get("caller").expect("caller fn"); |
| 115 | + assert_eq!(caller.call(&[10]).expect("call"), 21, "opt dispatch must preserve caller=helper(x)+1"); |
| 116 | + assert_eq!(caller.call(&[100]).expect("call"), 201); |
| 117 | + assert_eq!(caller.call(&[0]).expect("call"), 1); |
| 118 | + std::env::remove_var("OMC_HBIT_JIT_OPT"); |
| 119 | +} |
0 commit comments