passes: add mba scheme to Constants (and+or+add identity)

(a&b)+(a|b) = a+b, documented honestly: falls to the same symbolic
evaluator as xor/add/shift (every sub-expression is a foldable
LDC,LDC,BINOP triple), but defeats a pattern matcher hardcoded to one
opcode ("ldc,ldc,ixor = split constant"). Added to ConstantsConfig
default schemes.

Tests: mba_form_uses_and_or_add_not_a_single_binop, round-trip coverage
in int_forms_round_trip/long_forms_keep_shape.
This commit is contained in:
Loki 2026-10-14 01:30:10 +02:00
parent 550f62e473
commit 2b19ed6096
Signed by: boba
GPG key ID: 253067914055423B
4 changed files with 133 additions and 10 deletions

View file

@ -124,7 +124,8 @@ impl Default for FlowConfig {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ConstantsConfig {
/// Splitting schemes (`xor`, `add`, `shift`), picked by RNG per constant.
/// Splitting schemes (`xor`, `add`, `shift`, `mba`), picked by RNG per
/// constant.
pub schemes: Vec<String>,
/// Insert opaque predicates (at most this many per method).
pub max_predicates: usize,
@ -135,7 +136,12 @@ pub struct ConstantsConfig {
impl Default for ConstantsConfig {
fn default() -> Self {
Self {
schemes: vec!["xor".to_string(), "add".to_string(), "shift".to_string()],
schemes: vec![
"xor".to_string(),
"add".to_string(),
"shift".to_string(),
"mba".to_string(),
],
max_predicates: 1,
skip: Vec::new(),
}

View file

@ -1,8 +1,16 @@
//! Constant-splitting schemes and opaque predicates.
//!
//! Integer constants outside the `iconst` range are replaced by triples
//! (`x^k`, `a+b`, shift); an opaque predicate is an always-true condition with
//! a one-`nop` dead branch (valid, never executed, breaks linear disassembly).
//! (`x^k`, `a+b`, shift) or the `mba` form below; an opaque predicate is an
//! always-true condition with a one-`nop` dead branch (valid, never
//! executed, breaks linear disassembly).
//!
//! None of this survives a real constant-propagation pass (every operand is
//! a literal, so a general evaluator folds the whole expression regardless
//! of depth) - that is a known, documented limit, not a claim otherwise.
//! `mba` raises the bar against a *pattern* matcher instead: `xor`/`add`
//! hardcode one opcode triple as "this is a split constant", `mba` needs the
//! `(a&b)+(a|b) = a+b` identity specifically.
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, Prim};
use noxis_core::Rng;
@ -13,6 +21,7 @@ pub(super) enum Scheme {
Xor,
Add,
Shift,
Mba,
}
impl Scheme {
@ -21,6 +30,7 @@ impl Scheme {
"xor" => Some(Scheme::Xor),
"add" => Some(Scheme::Add),
"shift" => Some(Scheme::Shift),
"mba" => Some(Scheme::Mba),
_ => None,
}
}
@ -36,7 +46,7 @@ pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec<Insn
}
(value ^ key, key)
}
Scheme::Add => {
Scheme::Add | Scheme::Mba => {
let part = rng.next_u64() as i32;
(part, value.wrapping_sub(part))
}
@ -46,6 +56,7 @@ pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec<Insn
Scheme::Xor => binary(first, second, Arith::Xor, Prim::Int),
Scheme::Add => binary(first, second, Arith::Add, Prim::Int),
Scheme::Shift => shift_form(value),
Scheme::Mba => mba_form(first, second),
}
}
@ -59,7 +70,7 @@ pub(super) fn encrypt_long(value: i64, scheme: Scheme, rng: &mut Rng) -> Vec<Ins
}
(value ^ key, key)
}
Scheme::Add => {
Scheme::Add | Scheme::Mba => {
let part = rng.next_u64() as i64;
(part, value.wrapping_sub(part))
}
@ -69,6 +80,7 @@ pub(super) fn encrypt_long(value: i64, scheme: Scheme, rng: &mut Rng) -> Vec<Ins
Scheme::Xor => binary_long(first, second, Arith::Xor),
Scheme::Add => binary_long(first, second, Arith::Add),
Scheme::Shift => shift_form_long(value),
Scheme::Mba => mba_form_long(first, second),
}
}
@ -92,6 +104,53 @@ fn binary_long(first: i64, second: i64, op: Arith) -> Vec<Insn> {
]
}
/// MBA identity: `a+b == (a&b) + (a|b)` (exact, bit-exact under wrapping).
/// `a`/`b` are pushed twice (no stack juggling needed) so each sub-expression
/// stays a plain, correct `and`/`or` - the shape, not a single opcode, is
/// what a matcher has to recognize.
fn mba_form(a: i32, b: i32) -> Vec<Insn> {
vec![
Insn::Const(Const::Int(a)),
Insn::Const(Const::Int(b)),
Insn::Binary {
prim: Prim::Int,
op: Arith::And,
},
Insn::Const(Const::Int(a)),
Insn::Const(Const::Int(b)),
Insn::Binary {
prim: Prim::Int,
op: Arith::Or,
},
Insn::Binary {
prim: Prim::Int,
op: Arith::Add,
},
]
}
/// The same for `long`.
fn mba_form_long(a: i64, b: i64) -> Vec<Insn> {
vec![
Insn::Const(Const::Long(a)),
Insn::Const(Const::Long(b)),
Insn::Binary {
prim: Prim::Long,
op: Arith::And,
},
Insn::Const(Const::Long(a)),
Insn::Const(Const::Long(b)),
Insn::Binary {
prim: Prim::Long,
op: Arith::Or,
},
Insn::Binary {
prim: Prim::Long,
op: Arith::Add,
},
]
}
/// `v = ((v >> 1) << 1) | (v & 1)` - works for any value, including negatives.
fn shift_form(value: i32) -> Vec<Insn> {
vec![

View file

@ -30,6 +30,30 @@ fn eval_int(insns: &[Insn]) -> i32 {
Insn::Const(Const::Int(b)),
Insn::Binary { .. },
] => (a << 1) | (b & 1),
// MBA: `(a&b) + (a|b) == a+b`. Each sub-expression is still a
// foldable `LDC,LDC,BINOP` triple - a generic constant evaluator
// still resolves this instantly. What it defeats is a *pattern*
// matcher hardcoded to one opcode (`ldc,ldc,ixor` = "this is a
// split constant"): it would need to recognize the `and+or+add`
// identity specifically, not just fold a literal pair.
[
Insn::Const(Const::Int(a1)),
Insn::Const(Const::Int(b1)),
Insn::Binary {
op: noxis_classfile::Arith::And,
..
},
Insn::Const(Const::Int(a2)),
Insn::Const(Const::Int(b2)),
Insn::Binary {
op: noxis_classfile::Arith::Or,
..
},
Insn::Binary {
op: noxis_classfile::Arith::Add,
..
},
] if a1 == a2 && b1 == b2 => (a1 & b1).wrapping_add(a1 | b1),
_ => panic!("not a form: {insns:?}"),
}
}
@ -38,7 +62,7 @@ fn eval_int(insns: &[Insn]) -> i32 {
fn int_forms_round_trip() {
let mut rng = noxis_core::Rng::from_seed(11);
for value in [-1000, -7, -6, 6, 7, 42, 65536, i32::MAX, i32::MIN] {
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] {
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift, Scheme::Mba] {
assert_eq!(eval_int(&encrypt_int(value, scheme, &mut rng)), value);
}
}
@ -47,7 +71,7 @@ fn int_forms_round_trip() {
#[test]
fn long_forms_keep_shape() {
let mut rng = noxis_core::Rng::from_seed(12);
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] {
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift, Scheme::Mba] {
let insns = encrypt_long(1_000_000_000_000, scheme, &mut rng);
assert!(matches!(
insns.last(),
@ -59,6 +83,34 @@ fn long_forms_keep_shape() {
}
}
/// MBA shape: `and`/`or`/`add` on two reused operands, not `xor`/`add` on a
/// single pair - a matcher hardcoded to one opcode triple (`ldc,ldc,ixor`)
/// doesn't recognize this as "a split constant" without the identity.
#[test]
fn mba_form_uses_and_or_add_not_a_single_binop() {
let mut rng = noxis_core::Rng::from_seed(13);
for value in [-1000, -6, 6, 42, 65536, i32::MAX, i32::MIN] {
let insns = encrypt_int(value, Scheme::Mba, &mut rng);
let ops: Vec<_> = insns
.iter()
.filter_map(|i| match i {
Insn::Binary { op, .. } => Some(*op),
_ => None,
})
.collect();
assert_eq!(
ops,
[
noxis_classfile::Arith::And,
noxis_classfile::Arith::Or,
noxis_classfile::Arith::Add,
],
"unexpected mba shape: {insns:?}"
);
assert_eq!(insns.len(), 7, "unexpected mba insn count: {insns:?}");
}
}
fn sample() -> Program {
let code = Code {
max_stack: 1,