From 9e4e4638a7946fa65cdeacaaaf17d76b7233a356 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Wed, 24 Jun 2026 18:46:55 -0400 Subject: [PATCH 01/20] refactor: hoist ExprStructurallyEqual to ExprUtils Deep Expr structural equality existed only as a file-local helper in OrchestratorPasses.cpp. Move it to ExprUtils (declaration + definition) so other passes can reuse it instead of re-implementing deep comparison. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/cobra/core/ExprUtils.h | 4 ++++ lib/core/ExprUtils.cpp | 12 ++++++++++++ lib/core/OrchestratorPasses.cpp | 14 -------------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/include/cobra/core/ExprUtils.h b/include/cobra/core/ExprUtils.h index 9d3c5fc..e50cf82 100644 --- a/include/cobra/core/ExprUtils.h +++ b/include/cobra/core/ExprUtils.h @@ -18,6 +18,10 @@ namespace cobra { /// Rewrite every kVariable node's var_index through index_map. void RemapVarIndices(Expr &expr, const std::vector< uint32_t > &index_map); + /// Deep structural equality: same kind, constant, var_index, and children + /// (in order). Unlike std::hash this never reports false positives. + bool ExprStructurallyEqual(const Expr &lhs, const Expr &rhs); + /// Map a subset of variable names to their indices in the full variable list. std::vector< uint32_t > BuildVarSupport( const std::vector< std::string > &all_vars, diff --git a/lib/core/ExprUtils.cpp b/lib/core/ExprUtils.cpp index 03c179b..abe61e2 100644 --- a/lib/core/ExprUtils.cpp +++ b/lib/core/ExprUtils.cpp @@ -32,6 +32,18 @@ namespace cobra { for (auto &child : expr.children) { RemapVarIndices(*child, index_map); } } + bool ExprStructurallyEqual(const Expr &lhs, const Expr &rhs) { + if (lhs.kind != rhs.kind || lhs.constant_val != rhs.constant_val + || lhs.var_index != rhs.var_index || lhs.children.size() != rhs.children.size()) + { + return false; + } + for (size_t i = 0; i < lhs.children.size(); ++i) { + if (!ExprStructurallyEqual(*lhs.children[i], *rhs.children[i])) { return false; } + } + return true; + } + std::unique_ptr< Expr > BuildAndProduct(uint64_t mask) { std::unique_ptr< Expr > result; while (mask != 0) { diff --git a/lib/core/OrchestratorPasses.cpp b/lib/core/OrchestratorPasses.cpp index 80a2a67..7521519 100644 --- a/lib/core/OrchestratorPasses.cpp +++ b/lib/core/OrchestratorPasses.cpp @@ -282,20 +282,6 @@ namespace cobra { size_t r = 0; }; - bool ExprStructurallyEqual(const Expr &lhs, const Expr &rhs) { - if (lhs.kind != rhs.kind || lhs.constant_val != rhs.constant_val - || lhs.var_index != rhs.var_index || lhs.children.size() != rhs.children.size()) - { - return false; - } - for (size_t i = 0; i < lhs.children.size(); ++i) { - if (!ExprStructurallyEqual(*lhs.children[i], *rhs.children[i])) { - return false; - } - } - return true; - } - std::unique_ptr< Expr > ReconstructMaskedProductFactor(const Expr &inclusive_term, const Expr &exclusive_term) { if (inclusive_term.kind != Expr::Kind::kAnd From a69f9c2522c0e34ae9523a203cd77c03819a3937 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Wed, 24 Jun 2026 18:46:57 -0400 Subject: [PATCH 02/20] feat: recover A|B from A+B-(A&B) in pattern-subtree peeling GAMBA recon cases obfuscate x | y as the inclusion-exclusion form A + B - (A&B) with arbitrary (including arithmetic, non-disjoint) operands, which CoBRA previously left as a messier equivalent. Add a structural recovery to SimplifyPatternSubtrees that flattens the additive form and collapses +c*A +c*B -c*(A&B) to +c*(A|B), reading A and B from the AND node's children (B may span several additive terms). Gated by a strict coefficient match (so XOR's -2*(A&B) cannot misfire), a cost-reduction check, and a FullWidthCheck soundness backstop. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/PatternMatcher.cpp | 188 ++++++++++++++++++++++++++++- test/core/test_pattern_matcher.cpp | 61 ++++++++++ 2 files changed, 247 insertions(+), 2 deletions(-) diff --git a/lib/core/PatternMatcher.cpp b/lib/core/PatternMatcher.cpp index f5ecdad..374b0df 100644 --- a/lib/core/PatternMatcher.cpp +++ b/lib/core/PatternMatcher.cpp @@ -1266,6 +1266,183 @@ namespace cobra { return result; } + // ---- Inclusion-exclusion disjunction recovery ---------------------- + // + // Reverses the MBA obfuscation A + B - (A & B) -> A | B for arbitrary + // operands (including arithmetic ones such as 10*y+5), where A and B may + // be spread across several terms of a flattened additive sum. Operands + // are read verbatim from the AND node's children; the rewrite is the + // exact inclusion-exclusion identity and is verified with FullWidthCheck. + + struct AdditiveTerm + { + uint64_t coeff; + const Expr *atom; // non-owning; points into the source tree + }; + + // Flatten `e` (scaled by `coeff`) into signed additive terms plus an + // accumulated constant. Constant multipliers fold into the coefficient. + void FlattenAdditive( + const Expr &e, uint64_t coeff, uint32_t bitwidth, + std::vector< AdditiveTerm > &terms, uint64_t &constant + ) { + switch (e.kind) { + case Expr::Kind::kAdd: + for (const auto &child : e.children) { + FlattenAdditive(*child, coeff, bitwidth, terms, constant); + } + return; + case Expr::Kind::kNeg: + FlattenAdditive( + *e.children[0], ModNeg(coeff, bitwidth), bitwidth, terms, constant + ); + return; + case Expr::Kind::kConstant: + constant = + ModAdd(constant, ModMul(coeff, e.constant_val, bitwidth), bitwidth); + return; + case Expr::Kind::kMul: + if (e.children.size() == 2 && e.children[0]->kind == Expr::Kind::kConstant) + { + FlattenAdditive( + *e.children[1], + ModMul(coeff, e.children[0]->constant_val, bitwidth), bitwidth, + terms, constant + ); + return; + } + if (e.children.size() == 2 && e.children[1]->kind == Expr::Kind::kConstant) + { + FlattenAdditive( + *e.children[0], + ModMul(coeff, e.children[1]->constant_val, bitwidth), bitwidth, + terms, constant + ); + return; + } + terms.push_back({ coeff, &e }); + return; + default: + terms.push_back({ coeff, &e }); + return; + } + } + + // Mark pool terms matching each of `need` (exact coeff + structural + // atom) as consumed. Returns false (leaving `consumed` unchanged) when + // any needed term is absent. + bool ConsumeTerms( + const std::vector< AdditiveTerm > &pool, const std::vector< AdditiveTerm > &need, + std::vector< bool > &consumed + ) { + auto trial = consumed; + for (const auto &nt : need) { + bool found = false; + for (size_t i = 0; i < pool.size(); ++i) { + if (trial[i]) { continue; } + if (pool[i].coeff == nt.coeff + && ExprStructurallyEqual(*pool[i].atom, *nt.atom)) + { + trial[i] = true; + found = true; + break; + } + } + if (!found) { return false; } + } + consumed = std::move(trial); + return true; + } + + std::unique_ptr< Expr > RebuildAdditive( + const std::vector< AdditiveTerm > &pool, const std::vector< bool > &consumed, + uint64_t constant, std::unique_ptr< Expr > extra, uint32_t bitwidth + ) { + std::unique_ptr< Expr > result; + if (constant != 0) { result = Expr::Constant(constant); } + for (size_t i = 0; i < pool.size(); ++i) { + if (consumed[i]) { continue; } + auto term = ApplyCoefficient(CloneExpr(*pool[i].atom), pool[i].coeff, bitwidth); + result = + result ? Expr::Add(std::move(result), std::move(term)) : std::move(term); + } + if (extra) { + result = + result ? Expr::Add(std::move(result), std::move(extra)) : std::move(extra); + } + return result ? std::move(result) : Expr::Constant(0); + } + + // Confirm `candidate` matches `original` at random full-width inputs. + // candidate reuses original's variable indices, so an identity var_map + // over [0, max_index] is sufficient (no densification needed). + bool VerifyRecovery(const Expr &original, const Expr &candidate, uint32_t bitwidth) { + std::vector< uint32_t > support; + CollectSupport(original, support); + uint32_t num_vars = 0; + for (uint32_t v : support) { num_vars = std::max(num_vars, v + 1); } + return FullWidthCheck(original, num_vars, candidate, {}, bitwidth).passed; + } + + // Attempt to collapse the inclusion-exclusion pattern anchored at the + // AND term `terms[and_idx]`. Returns the rewritten expression on success. + std::optional< std::unique_ptr< Expr > > TryRecoverDisjunctionAt( + const std::vector< AdditiveTerm > &terms, uint64_t constant, size_t and_idx, + const Expr &expr, uint32_t bitwidth + ) { + const Expr *and_atom = terms[and_idx].atom; + if (and_atom->kind != Expr::Kind::kAnd || and_atom->children.size() != 2) { + return std::nullopt; + } + const uint64_t c = ModNeg(terms[and_idx].coeff, bitwidth); + if (c == 0) { return std::nullopt; } + + std::vector< AdditiveTerm > a_terms; + std::vector< AdditiveTerm > b_terms; + uint64_t a_const = 0; + uint64_t b_const = 0; + FlattenAdditive(*and_atom->children[0], c, bitwidth, a_terms, a_const); + FlattenAdditive(*and_atom->children[1], c, bitwidth, b_terms, b_const); + + std::vector< bool > consumed(terms.size(), false); + consumed[and_idx] = true; + if (!ConsumeTerms(terms, a_terms, consumed)) { return std::nullopt; } + if (!ConsumeTerms(terms, b_terms, consumed)) { return std::nullopt; } + + const uint64_t new_constant = + ModSub(ModSub(constant, a_const, bitwidth), b_const, bitwidth); + auto or_term = ApplyCoefficient( + Expr::BitwiseOr( + CloneExpr(*and_atom->children[0]), CloneExpr(*and_atom->children[1]) + ), + c, bitwidth + ); + auto candidate = + RebuildAdditive(terms, consumed, new_constant, std::move(or_term), bitwidth); + + if (!IsBetter(ComputeCost(*candidate).cost, ComputeCost(expr).cost)) { + return std::nullopt; + } + if (!VerifyRecovery(expr, *candidate, bitwidth)) { return std::nullopt; } + return candidate; + } + + std::optional< std::unique_ptr< Expr > > + TryRecoverInclusionExclusion(const Expr &expr, uint32_t bitwidth) { + if (expr.kind != Expr::Kind::kAdd && expr.kind != Expr::Kind::kNeg) { + return std::nullopt; + } + std::vector< AdditiveTerm > terms; + uint64_t constant = 0; + FlattenAdditive(expr, 1, bitwidth, terms, constant); + + for (size_t i = 0; i < terms.size(); ++i) { + auto recovered = TryRecoverDisjunctionAt(terms, constant, i, expr, bitwidth); + if (recovered.has_value()) { return recovered; } + } + return std::nullopt; + } + } // namespace std::optional< std::unique_ptr< Expr > > @@ -1314,8 +1491,15 @@ namespace cobra { } auto rewritten = TrySimplifyPatternSubtree(*expr, bitwidth); - if (!rewritten.has_value()) { return expr; } - return SimplifyPatternSubtrees(std::move(*rewritten), bitwidth); + if (rewritten.has_value()) { + return SimplifyPatternSubtrees(std::move(*rewritten), bitwidth); + } + + auto recovered = TryRecoverInclusionExclusion(*expr, bitwidth); + if (recovered.has_value()) { + return SimplifyPatternSubtrees(std::move(*recovered), bitwidth); + } + return expr; } } // namespace cobra diff --git a/test/core/test_pattern_matcher.cpp b/test/core/test_pattern_matcher.cpp index 7dd7cd4..99fde89 100644 --- a/test/core/test_pattern_matcher.cpp +++ b/test/core/test_pattern_matcher.cpp @@ -741,3 +741,64 @@ TEST(PatternMatcherTest, FiveVarSampledCorrectness) { verify(rng); } } + +namespace { + bool ContainsKind(const Expr &e, Expr::Kind k) { + if (e.kind == k) { return true; } + for (const auto &c : e.children) { + if (ContainsKind(*c, k)) { return true; } + } + return false; + } +} // namespace + +// Inclusion-exclusion OR recovery (GAMBA recon): A + B - (A & B) -> A | B, +// where B is arithmetic (10*y+5) and operands are spread across a flattened +// additive sum. Target: 10*y + 5 + x + (x^4) - ((x^4) & (10*y+5)) == x + ((x^4) | (10*y+5)). +TEST(PatternMatcherTest, RecoverDisjunctionArithmeticOperand) { + auto x = []() { return Expr::Variable(0); }; + auto y = []() { return Expr::Variable(1); }; + auto a = [&]() { return Expr::BitwiseXor(x(), Expr::Constant(4)); }; + auto b = [&]() { return Expr::Add(Expr::Mul(Expr::Constant(10), y()), Expr::Constant(5)); }; + auto input = Expr::Add( + Expr::Add(Expr::Add(b(), x()), a()), Expr::Negate(Expr::BitwiseAnd(a(), b())) + ); + + auto original = CloneExpr(*input); + auto result = SimplifyPatternSubtrees(std::move(input), 64); + + EXPECT_TRUE(ContainsKind(*result, Expr::Kind::kOr)); + EXPECT_TRUE(FullWidthCheck(*original, 2, *result, {}, 64).passed); + EXPECT_TRUE(IsBetter(ComputeCost(*result).cost, ComputeCost(*original).cost)); +} + +// Simpler 2-var instance of the same gap: (x^4) + y - ((x^4) & y) -> (x^4) | y. +TEST(PatternMatcherTest, RecoverDisjunctionTwoVar) { + auto x = []() { return Expr::Variable(0); }; + auto y = []() { return Expr::Variable(1); }; + auto a = [&]() { return Expr::BitwiseXor(x(), Expr::Constant(4)); }; + auto input = Expr::Add(Expr::Add(a(), y()), Expr::Negate(Expr::BitwiseAnd(a(), y()))); + + auto original = CloneExpr(*input); + auto result = SimplifyPatternSubtrees(std::move(input), 64); + + EXPECT_TRUE(ContainsKind(*result, Expr::Kind::kOr)); + EXPECT_TRUE(FullWidthCheck(*original, 2, *result, {}, 64).passed); +} + +// Regression guard: XOR lowering A + B - 2*(A & B) must stay A ^ B and must +// NOT be misrecovered as A | B (coefficient on the AND term is 2, not 1). +TEST(PatternMatcherTest, RecoverDisjunctionDoesNotMisfireOnXor) { + auto x = []() { return Expr::Variable(0); }; + auto y = []() { return Expr::Variable(1); }; + auto input = Expr::Add( + Expr::Add(x(), y()), + Expr::Negate(Expr::Mul(Expr::Constant(2), Expr::BitwiseAnd(x(), y()))) + ); + + auto original = CloneExpr(*input); + auto result = SimplifyPatternSubtrees(std::move(input), 64); + + EXPECT_FALSE(ContainsKind(*result, Expr::Kind::kOr)); + EXPECT_TRUE(FullWidthCheck(*original, 2, *result, {}, 64).passed); +} From af8828e6b02542c0323543bc8d9fa2daa00d2d95 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Wed, 24 Jun 2026 19:39:25 -0400 Subject: [PATCH 03/20] fix: decline no-op product-core short-circuit when reducible For a single-product input, ExtractProductCore returns a core equal to the whole input, whose direct full-width check trivially passes. Emitting it as a solved candidate halts the worklist before the decomposition/signature passes can recover a simpler form, so GAMBA recon cases like the parity sign-square (x1+x2)*(1-2*(x3&1))*(1-2*(x3&1)) (and its XOR-self-cancel full form) were left unchanged. Decline the short-circuit when the product core is the whole input (single_product) AND a full-width-confirmed spurious variable exists, so the worklist continues to the extractors that recover the reduced form. Genuine sum-of-product cores and cores with no spurious variable are unaffected, so product-inside-bitwise decomposition (QSynth, Multivariate) is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/cobra/core/DecompositionEngine.h | 3 ++ lib/core/DecompositionEngine.cpp | 5 +- lib/core/DecompositionPasses.cpp | 20 +++++++ test/core/test_simplifier.cpp | 66 +++++++++++++++++++++++- 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/include/cobra/core/DecompositionEngine.h b/include/cobra/core/DecompositionEngine.h index 20dd2e1..7dce6bb 100644 --- a/include/cobra/core/DecompositionEngine.h +++ b/include/cobra/core/DecompositionEngine.h @@ -40,6 +40,9 @@ namespace cobra { std::unique_ptr< Expr > expr; ExtractorKind kind; uint8_t degree_used = 0; + // kProductAST only: true when the whole input is a single product term + // (SplitAddTree found no additive split), i.e. the core equals the input. + bool single_product = false; }; struct DecompositionResult diff --git a/lib/core/DecompositionEngine.cpp b/lib/core/DecompositionEngine.cpp index a24c87b..7b309c9 100644 --- a/lib/core/DecompositionEngine.cpp +++ b/lib/core/DecompositionEngine.cpp @@ -157,8 +157,9 @@ namespace cobra { } CoreCandidate core; - core.expr = std::move(core_expr); - core.kind = ExtractorKind::kProductAST; + core.expr = std::move(core_expr); + core.kind = ExtractorKind::kProductAST; + core.single_product = (products.size() == 1); return SolverResult< CoreCandidate >::Success(std::move(core)); } diff --git a/lib/core/DecompositionPasses.cpp b/lib/core/DecompositionPasses.cpp index 662203a..dfaf8d9 100644 --- a/lib/core/DecompositionPasses.cpp +++ b/lib/core/DecompositionPasses.cpp @@ -166,6 +166,26 @@ namespace cobra { FullWidthCheckEval(*active_eval, num_vars, *core_payload.expr, ctx.bitwidth); if (direct_check.passed) { + // A degenerate product core -- the whole input as a single + // product term (single_product) -- "passes" the direct check + // only because it IS the input. Emitting it short-circuits the + // worklist as a no-op winner before the decomposition/signature + // passes can recover a simpler form. When such a core also has a + // (full-width-confirmed) spurious variable, a real reduction is + // reachable, so decline the short-circuit and let later passes + // run. Genuine sum-of-product cores (single_product == false) + // and cores with no spurious variable are unaffected. + if (expected_kind == ExtractorKind::kProductAST && core_payload.single_product + && !EliminateAuxVars(decomp_sig, active_vars, *active_eval, ctx.bitwidth) + .spurious_vars.empty()) + { + return Ok( + PassResult{ + .decision = PassDecision::kNotApplicable, + .disposition = ItemDisposition::kRetainCurrent, + } + ); + } auto cost_info = ComputeCost(*core_payload.expr); WorkItem cand_item; cand_item.payload = CandidatePayload{ diff --git a/test/core/test_simplifier.cpp b/test/core/test_simplifier.cpp index 4736e4e..cbf529b 100644 --- a/test/core/test_simplifier.cpp +++ b/test/core/test_simplifier.cpp @@ -1893,7 +1893,7 @@ TEST(SimplifierTest, RejectsExcessiveInputVarCount) { // any allocation. std::vector< std::string > vars; for (int i = 0; i < 25; ++i) { vars.push_back("x" + std::to_string(i)); } - std::vector< uint64_t > sig(2, 0); // size doesn't matter; rejected first + std::vector< uint64_t > sig(2, 0); // size doesn't matter; rejected first Options opts{ .bitwidth = 64, .max_vars = 16, .spot_check = false }; auto result = Simplify(sig, vars, nullptr, opts); @@ -1955,3 +1955,67 @@ TEST(SimplifierTest, AcceptsInputVarCountAtCeiling) { ASSERT_TRUE(result.has_value()); EXPECT_EQ(Render(*result.value().expr, result.value().real_vars), "7"); } + +namespace { + // s(v) = 1 - 2*(v & 1), a +-1 value (s^2 == 1). GAMBA parity sign-flip. + std::unique_ptr< Expr > SignFlip(uint32_t var_index) { + return Expr::Add( + Expr::Constant(1), + Expr::Mul( + Expr::BitwiseAnd(Expr::Variable(var_index), Expr::Constant(1)), + Expr::Constant(static_cast< uint64_t >(-2)) + ) + ); + } + + bool RendersToSum(const SimplifyOutcome &out, const char *a, const char *b) { + auto text = Render(*out.expr, out.real_vars); + return text == std::string(a) + " + " + b || text == std::string(b) + " + " + a; + } +} // namespace + +// GAMBA recon: parity sign-square E*s*s -> E for multi-variable E. The +// degenerate single-product core (the whole input) used to short-circuit the +// worklist before the recovery passes ran, leaving the input unchanged. +TEST(SimplifierTest, SignSquareCollapseMultiVar) { + // (x1 + x2) * s(x3) * s(x3) == x1 + x2 + auto ast = Expr::Mul( + Expr::Mul(Expr::Add(Expr::Variable(0), Expr::Variable(1)), SignFlip(2)), SignFlip(2) + ); + std::vector< std::string > vars = { "x1", "x2", "x3" }; + + auto sig = EvaluateBooleanSignature(*ast, 3, 64); + Options opts{ .bitwidth = 64, .max_vars = 16, .spot_check = true }; + opts.evaluator = Evaluator::FromExpr(*ast, 64); + + auto result = Simplify(sig, vars, ast.get(), opts); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->kind, SimplifyOutcome::Kind::kSimplified); + EXPECT_TRUE(RendersToSum(result.value(), "x1", "x2")); + auto check = FullWidthCheckEval(Evaluator::FromExpr(*ast, 64), 3, *result->expr, 64); + EXPECT_TRUE(check.passed); +} + +// Full Arnau target: XOR self-cancel of duplicate complex operands plus the +// sign-square collapse must together reduce to x1 + x2. +TEST(SimplifierTest, SignSquareWithXorSelfCancel) { + // ((((x1+x2)*s3) ^ ((x5+3)*s4) ^ ((x5+3)*s4)) * s3) == x1 + x2 + auto term_a = Expr::Mul(Expr::Add(Expr::Variable(0), Expr::Variable(1)), SignFlip(2)); + auto term_b = [&]() { + return Expr::Mul(Expr::Add(Expr::Variable(4), Expr::Constant(3)), SignFlip(3)); + }; + auto inner = Expr::BitwiseXor(Expr::BitwiseXor(std::move(term_a), term_b()), term_b()); + auto ast = Expr::Mul(std::move(inner), SignFlip(2)); + std::vector< std::string > vars = { "x1", "x2", "x3", "x4", "x5" }; + + auto sig = EvaluateBooleanSignature(*ast, 5, 64); + Options opts{ .bitwidth = 64, .max_vars = 16, .spot_check = true }; + opts.evaluator = Evaluator::FromExpr(*ast, 64); + + auto result = Simplify(sig, vars, ast.get(), opts); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->kind, SimplifyOutcome::Kind::kSimplified); + EXPECT_TRUE(RendersToSum(result.value(), "x1", "x2")); + auto check = FullWidthCheckEval(Evaluator::FromExpr(*ast, 64), 5, *result->expr, 64); + EXPECT_TRUE(check.passed); +} From c00042d5c9aed82e8cbd273934f59b41ec39b02f Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Thu, 25 Jun 2026 02:04:50 -0400 Subject: [PATCH 04/20] fix: seed mask constants in TemplateDecomposer for high-bit masking The {0,1} signature of a high-bit masking result like x & 128 is all-zero (bit 7 is invisible on {0,1}), so the boolean candidate is 0 and fails the full-width check. The case is solved by AuxVarEliminator + TemplateDecomposer, but its atom pool seeded only {0, 1, 2, mask}, so it could not synthesize the literal mask constant M for M >= 8 within the bounded layer depth. GAMBA recon cases like ((x & 128) + (y & 127)) & 128 (carry-free masked add: y&127 < 128 is disjoint from bit 7) were left unsupported. Seed data-derived constants into the pool after Populate: the OR-fold of the observed probe outputs (recovers the mask) plus a few distinct output values (cover split masks). Soundness is unchanged -- every candidate is FullWidthCheckEval-gated, so extra constants only widen the search, and a genuine carry (((x & 255) + (y & 255)) & 255) is not falsely collapsed to x & 255. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/TemplateDecomposer.cpp | 24 +++++++++++++ test/core/test_simplifier.cpp | 62 +++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/lib/core/TemplateDecomposer.cpp b/lib/core/TemplateDecomposer.cpp index f33117d..460f6b2 100644 --- a/lib/core/TemplateDecomposer.cpp +++ b/lib/core/TemplateDecomposer.cpp @@ -1038,6 +1038,30 @@ namespace cobra { } COBRA_PLOT("TemplatePoolSize", static_cast< int64_t >(pool.size())); + // Seed data-derived constants so masking results like x & M (whose set + // bit is invisible to the {0,1} signature) become synthesizable. The + // OR-fold of observed outputs recovers the mask; a few distinct output + // values cover split masks. Soundness is unchanged: every candidate is + // FullWidthCheckEval-gated, so extra constants only widen the search. + { + uint64_t or_fold = 0; + for (size_t i = 0; i < kNProbes; ++i) { or_fold |= target[i]; } + auto seed_const = [&](uint64_t c) { + if (c == 0) { return; } + auto e = Expr::Constant(c); + auto v = Probe(*e, pts, opts.bitwidth); + Push(pool, vmap, std::move(e), v); + }; + seed_const(or_fold); + uint32_t seeded = 0; + for (size_t i = 0; i < kNProbes && seeded < 7; ++i) { + if (target[i] != 0) { + seed_const(target[i]); + ++seeded; + } + } + } + // Direct atom match. { const auto *slot = vmap.find(target); diff --git a/test/core/test_simplifier.cpp b/test/core/test_simplifier.cpp index cbf529b..b0491d5 100644 --- a/test/core/test_simplifier.cpp +++ b/test/core/test_simplifier.cpp @@ -2019,3 +2019,65 @@ TEST(SimplifierTest, SignSquareWithXorSelfCancel) { auto check = FullWidthCheckEval(Evaluator::FromExpr(*ast, 64), 5, *result->expr, 64); EXPECT_TRUE(check.passed); } + +namespace { + bool RendersToAndConst(const SimplifyOutcome &out, const char *v, uint64_t c) { + auto text = Render(*out.expr, out.real_vars); + auto cs = std::to_string(c); + return text == std::string(v) + " & " + cs || text == cs + " & " + std::string(v); + } + + // ((x & m) + (y & ymask)) & outmask + std::unique_ptr< Expr > MaskedAdd(uint64_t m, uint64_t ymask, uint64_t outmask) { + return Expr::BitwiseAnd( + Expr::Add( + Expr::BitwiseAnd(Expr::Variable(0), Expr::Constant(m)), + Expr::BitwiseAnd(Expr::Variable(1), Expr::Constant(ymask)) + ), + Expr::Constant(outmask) + ); + } + + SimplifyOutcome RunAst(const Expr &ast, std::vector< std::string > vars) { + auto nv = static_cast< uint32_t >(vars.size()); + auto sig = EvaluateBooleanSignature(ast, nv, 64); + Options opts{ .bitwidth = 64, .max_vars = 16, .spot_check = true }; + opts.evaluator = Evaluator::FromExpr(ast, 64); + auto result = Simplify(sig, vars, &ast, opts); + return std::move(result.value()); + } +} // namespace + +// GAMBA recon: carry-free masked addition. ((x & 128) + (y & 127)) & 128 == x & 128 +// because y&127 < 128 is disjoint from bit 7, so the add can't carry into it. The +// {0,1} signature is all-zero (bit 7 invisible); recovered once TemplateDecomposer +// can synthesize the mask constant 128. +TEST(SimplifierTest, MaskedCarryFreeAddBit7) { + auto ast = MaskedAdd(128, 127, 128); + auto out = RunAst(*ast, { "x", "y" }); + EXPECT_EQ(out.kind, SimplifyOutcome::Kind::kSimplified); + EXPECT_TRUE(RendersToAndConst(out, "x", 128)); + EXPECT_TRUE(FullWidthCheckEval(Evaluator::FromExpr(*ast, 64), 2, *out.expr, 64).passed); +} + +// Whole family across the former bit>=3 threshold: ((x&2^k)+(y&(2^k-1)))&2^k -> x&2^k. +TEST(SimplifierTest, MaskedCarryFreeAddFamily) { + for (uint32_t bit : { 3u, 4u, 5u, 6u, 7u, 8u }) { + const uint64_t m = uint64_t{ 1 } << bit; + auto ast = MaskedAdd(m, m - 1, m); + auto out = RunAst(*ast, { "x", "y" }); + EXPECT_EQ(out.kind, SimplifyOutcome::Kind::kSimplified) << "bit " << bit; + EXPECT_TRUE(RendersToAndConst(out, "x", m)) << "bit " << bit; + EXPECT_TRUE(FullWidthCheckEval(Evaluator::FromExpr(*ast, 64), 2, *out.expr, 64).passed) + << "bit " << bit; + } +} + +// Soundness: a genuine carry ((x & 255) + (y & 255)) & 255 must NOT collapse to +// x & 255 (it is (x + y) & 255). Whatever is returned must stay full-width-correct. +TEST(SimplifierTest, MaskedAddWithCarryNotFalselyCollapsed) { + auto ast = MaskedAdd(255, 255, 255); + auto out = RunAst(*ast, { "x", "y" }); + EXPECT_TRUE(FullWidthCheckEval(Evaluator::FromExpr(*ast, 64), 2, *out.expr, 64).passed); + EXPECT_FALSE(RendersToAndConst(out, "x", 255)); +} From abd0abbefbe02476f45123c167e2debb6dcb9406 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Thu, 25 Jun 2026 22:10:57 -0400 Subject: [PATCH 05/20] feat: recover XOR self-cancellation at the exhaustion fallback (A ^ B) ^ B == A is exact at every bit width, but when the surviving operand A has a degenerate {0,1} signature (e.g. ((x+y)&z)|3, which is constant 3 on {0,1}), no signature or decomposition pass re-emits it, so the worklist exhausts and CoBRA echoes the obfuscated input unchanged. Add SimplifyXorChains (top-down: flatten each XOR chain and drop operands of even multiplicity by exact structural equality) and apply it as an exhaustion fallback in the orchestrator -- when the raw input collapses to a strictly cheaper form, return that instead of reporting unsupported. The cancellation is an exact algebraic identity (not a full-width approximation), so the result is provably equivalent; the full-width check is defense in depth and the fallback only fires on the otherwise-doomed exhaustion path. QSynthEA gains 2 cases (verify-failed 7->6, search-exhausted 21->20) whose inputs contained structurally-repeated XOR operands. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/cobra/core/PatternMatcher.h | 6 +++ lib/core/Orchestrator.cpp | 33 +++++++++++++ lib/core/PatternMatcher.cpp | 66 +++++++++++++++++++++++++ test/core/test_pattern_matcher.cpp | 44 +++++++++++++++++ test/core/test_simplifier.cpp | 28 +++++++++++ test/verify/test_dataset_benchmarks.cpp | 11 +++-- 6 files changed, 184 insertions(+), 4 deletions(-) diff --git a/include/cobra/core/PatternMatcher.h b/include/cobra/core/PatternMatcher.h index 849f326..c97bd1f 100644 --- a/include/cobra/core/PatternMatcher.h +++ b/include/cobra/core/PatternMatcher.h @@ -20,4 +20,10 @@ namespace cobra { std::unique_ptr< Expr > SimplifyPatternSubtrees(std::unique_ptr< Expr > expr, uint32_t bitwidth); + // Recursively cancel XOR operands that appear an even number of times + // (T ^ T == 0), flattening nested XOR chains. This is an exact algebraic + // identity (no full-width approximation), so the result is provably + // equivalent to the input at every bit width. + std::unique_ptr< Expr > SimplifyXorChains(std::unique_ptr< Expr > expr, uint32_t bitwidth); + } // namespace cobra diff --git a/lib/core/Orchestrator.cpp b/lib/core/Orchestrator.cpp index 73b746e..7538d87 100644 --- a/lib/core/Orchestrator.cpp +++ b/lib/core/Orchestrator.cpp @@ -1356,6 +1356,39 @@ namespace cobra { } telemetry.queue_high_water = static_cast< uint32_t >(worklist.HighWaterMark()); + + // Exhaustion fallback: cancel structurally-repeated XOR operands in the + // raw input (T ^ T == 0, an EXACT identity at every bit width). When the + // survivors have a degenerate {0,1} signature, no signature/decomposition + // pass re-emits them, so the worklist exhausts even though the input + // collapses. Because this is exact (not a full-width approximation) the + // result is provably equivalent and sound to return directly. Fire only + // when it actually cancelled something and the result is strictly + // cheaper; the full-width check is defense in depth, not the proof. + if (input_expr != nullptr && context.evaluator) { + auto xor_peel = SimplifyXorChains(CloneExpr(*input_expr), context.bitwidth); + if (!ExprStructurallyEqual(*xor_peel, *input_expr) + && IsBetter(ComputeCost(*xor_peel).cost, ComputeCost(*input_expr).cost)) + { + const auto num_vars = static_cast< uint32_t >(vars.size()); + auto check = FullWidthCheckEval( + *context.evaluator, num_vars, *xor_peel, context.bitwidth + ); + if (check.passed) { + return Ok(ToSimplifyOutcome( + OrchestratorResult{ + .outcome = PassOutcome::Success( + std::move(xor_peel), vars, VerificationState::kVerified + ), + .metadata = std::move(final_meta), + .run_metadata = context.run_metadata, + }, + input_expr, telemetry, context.bitwidth + )); + } + } + } + return Ok(ToSimplifyOutcome( OrchestratorResult{ .outcome = PassOutcome::Blocked(std::move(exhaustion_reason)), diff --git a/lib/core/PatternMatcher.cpp b/lib/core/PatternMatcher.cpp index 374b0df..95f5377 100644 --- a/lib/core/PatternMatcher.cpp +++ b/lib/core/PatternMatcher.cpp @@ -1443,6 +1443,57 @@ namespace cobra { return std::nullopt; } + void FlattenXor(const Expr &e, std::vector< const Expr * > &out) { + if (e.kind == Expr::Kind::kXor) { + for (const auto &child : e.children) { FlattenXor(*child, out); } + return; + } + out.push_back(&e); + } + + // Cancel XOR operands appearing an even number of times (T ^ T == 0). + // Flattens the XOR chain and keeps each distinct operand that occurs an + // odd number of times. Universally sound; a FullWidthCheck backstops the + // structural equality. Unlike the signature path, this fires even when + // the surviving operand has a degenerate {0,1} signature. + std::optional< std::unique_ptr< Expr > > + TryXorSelfCancel(const Expr &expr, uint32_t bitwidth) { + if (expr.kind != Expr::Kind::kXor) { return std::nullopt; } + + std::vector< const Expr * > operands; + FlattenXor(expr, operands); + + std::vector< bool > consumed(operands.size(), false); + std::vector< const Expr * > survivors; + for (size_t i = 0; i < operands.size(); ++i) { + if (consumed[i]) { continue; } + size_t count = 1; + consumed[i] = true; + for (size_t j = i + 1; j < operands.size(); ++j) { + if (!consumed[j] && ExprStructurallyEqual(*operands[i], *operands[j])) { + consumed[j] = true; + ++count; + } + } + if (count % 2 == 1) { survivors.push_back(operands[i]); } + } + + if (survivors.size() == operands.size()) { return std::nullopt; } + + std::unique_ptr< Expr > result; + if (survivors.empty()) { + result = Expr::Constant(0); + } else { + result = CloneExpr(*survivors[0]); + for (size_t i = 1; i < survivors.size(); ++i) { + result = Expr::BitwiseXor(std::move(result), CloneExpr(*survivors[i])); + } + } + + if (!VerifyRecovery(expr, *result, bitwidth)) { return std::nullopt; } + return result; + } + } // namespace std::optional< std::unique_ptr< Expr > > @@ -1502,4 +1553,19 @@ namespace cobra { return expr; } + std::unique_ptr< Expr > SimplifyXorChains(std::unique_ptr< Expr > expr, uint32_t bitwidth) { + // Cancel repeated operands across the full XOR chain rooted here first + // (top-down): flattening the whole chain before reducing children avoids + // a nested pair collapsing to 0 and stranding an odd third copy. + auto cancelled = TryXorSelfCancel(*expr, bitwidth); + if (cancelled.has_value()) { + return SimplifyXorChains(std::move(*cancelled), bitwidth); + } + // No cancellation here: recurse into children to handle nested chains. + for (auto &child : expr->children) { + child = SimplifyXorChains(std::move(child), bitwidth); + } + return expr; + } + } // namespace cobra diff --git a/test/core/test_pattern_matcher.cpp b/test/core/test_pattern_matcher.cpp index 99fde89..9f8a785 100644 --- a/test/core/test_pattern_matcher.cpp +++ b/test/core/test_pattern_matcher.cpp @@ -802,3 +802,47 @@ TEST(PatternMatcherTest, RecoverDisjunctionDoesNotMisfireOnXor) { EXPECT_FALSE(ContainsKind(*result, Expr::Kind::kOr)); EXPECT_TRUE(FullWidthCheck(*original, 2, *result, {}, 64).passed); } + +// Structural XOR self-cancellation: (A ^ B) ^ B -> A, even when A has a +// degenerate {0,1} signature (so the signature path can't recover it). +// A = ((x+y)&z)|3 (constant 3 on {0,1}); B = (x*z | y&85) + w (appears twice). +TEST(PatternMatcherTest, XorSelfCancelComplexOperand) { + auto a = []() { + return Expr::BitwiseOr( + Expr::BitwiseAnd( + Expr::Add(Expr::Variable(0), Expr::Variable(1)), Expr::Variable(2) + ), + Expr::Constant(3) + ); + }; + auto b = []() { + return Expr::Add( + Expr::BitwiseOr( + Expr::Mul(Expr::Variable(0), Expr::Variable(2)), + Expr::BitwiseAnd(Expr::Variable(1), Expr::Constant(85)) + ), + Expr::Variable(3) + ); + }; + auto input = Expr::BitwiseXor(Expr::BitwiseXor(a(), b()), b()); + + auto original = CloneExpr(*input); + auto result = SimplifyXorChains(std::move(input), 64); + + // B ^ B cancels; no XOR should remain, and the result equals A. + EXPECT_FALSE(ContainsKind(*result, Expr::Kind::kXor)); + EXPECT_TRUE(FullWidthCheck(*original, 4, *result, {}, 64).passed); + EXPECT_TRUE(IsBetter(ComputeCost(*result).cost, ComputeCost(*original).cost)); +} + +// Odd multiplicity: T ^ T ^ T -> T (a single copy survives). +TEST(PatternMatcherTest, XorSelfCancelOddMultiplicity) { + auto t = []() { return Expr::BitwiseAnd(Expr::Variable(0), Expr::Variable(1)); }; + auto input = Expr::BitwiseXor(Expr::BitwiseXor(t(), t()), t()); + auto original = CloneExpr(*input); + auto result = SimplifyXorChains(std::move(input), 64); + + EXPECT_FALSE(ContainsKind(*result, Expr::Kind::kXor)); + EXPECT_EQ(result->kind, Expr::Kind::kAnd); + EXPECT_TRUE(FullWidthCheck(*original, 2, *result, {}, 64).passed); +} diff --git a/test/core/test_simplifier.cpp b/test/core/test_simplifier.cpp index b0491d5..75bc9a6 100644 --- a/test/core/test_simplifier.cpp +++ b/test/core/test_simplifier.cpp @@ -2073,6 +2073,34 @@ TEST(SimplifierTest, MaskedCarryFreeAddFamily) { } } +// End-to-end XOR self-cancel: (A ^ B) ^ B -> A through the full Simplify pipeline, +// where A = ((x+y)&z)|3 (degenerate {0,1} signature) and B = (x*z | y&85) + w. +TEST(SimplifierTest, XorSelfCancelEndToEnd) { + auto a = []() { + return Expr::BitwiseOr( + Expr::BitwiseAnd( + Expr::Add(Expr::Variable(0), Expr::Variable(1)), Expr::Variable(2) + ), + Expr::Constant(3) + ); + }; + auto b = []() { + return Expr::Add( + Expr::BitwiseOr( + Expr::Mul(Expr::Variable(0), Expr::Variable(2)), + Expr::BitwiseAnd(Expr::Variable(1), Expr::Constant(85)) + ), + Expr::Variable(3) + ); + }; + auto ast = Expr::BitwiseXor(Expr::BitwiseXor(a(), b()), b()); + auto out = RunAst(*ast, { "x", "y", "z", "w" }); + EXPECT_EQ(out.kind, SimplifyOutcome::Kind::kSimplified); + // B ^ B cancels -> no XOR remains in the rendered result. + EXPECT_EQ(Render(*out.expr, out.real_vars).find('^'), std::string::npos); + EXPECT_TRUE(FullWidthCheckEval(Evaluator::FromExpr(*ast, 64), 4, *out.expr, 64).passed); +} + // Soundness: a genuine carry ((x & 255) + (y & 255)) & 255 must NOT collapse to // x & 255 (it is (x + y) & 255). Whatever is returned must stay full-width-correct. TEST(SimplifierTest, MaskedAddWithCarryNotFalselyCollapsed) { diff --git a/test/verify/test_dataset_benchmarks.cpp b/test/verify/test_dataset_benchmarks.cpp index 660d279..3fda5e5 100644 --- a/test/verify/test_dataset_benchmarks.cpp +++ b/test/verify/test_dataset_benchmarks.cpp @@ -358,15 +358,18 @@ TEST(GAMBADataset, QSynthEA) { EXPECT_EQ(stats.total, 503); EXPECT_EQ(stats.skipped_parse, 3); EXPECT_EQ(stats.parsed, 500); - EXPECT_EQ(stats.simplified, 466); - EXPECT_EQ(stats.unsupported, 34); + // +2 vs prior baseline: two cases with structurally-repeated XOR operands + // (one previously verify-failed, one search-exhausted) now collapse via the + // exact XOR self-cancellation exhaustion fallback (T^T==0). + EXPECT_EQ(stats.simplified, 468); + EXPECT_EQ(stats.unsupported, 32); EXPECT_EQ(stats.failed_simplify, 0); // Every unsupported result carries a structured reason code. EXPECT_EQ(stats.has_structured_reason, stats.unsupported); - EXPECT_EQ(stats.by_category[ReasonCategory::kVerifyFailed], 7); + EXPECT_EQ(stats.by_category[ReasonCategory::kVerifyFailed], 6); EXPECT_EQ(stats.by_category[ReasonCategory::kGuardFailed], 6); - EXPECT_EQ(stats.by_category[ReasonCategory::kSearchExhausted], 21); + EXPECT_EQ(stats.by_category[ReasonCategory::kSearchExhausted], 20); // Decomposition cause frames propagated into cause_chain. // MixedRewrite unsupported outcomes should carry delegated From 338d6edfa23e7369902ea00ba38c43873549faad Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Thu, 25 Jun 2026 23:13:21 -0400 Subject: [PATCH 06/20] fix: gate simplification output against input size to avoid blow-ups A CoB-linear function such as sum(xi) - xor(xi) is recovered by change-of-basis as a linear combination of AND-basis monomials whose size grows exponentially with the variable count (an 8-variable input of ~30 weighted nodes expanded to ~1000). The core Simplify path had no input-vs-output size check, so it returned the blow-up even though the input was already minimal. Guard in ToSimplifyOutcome: when a successful result both more than doubles the input AND exceeds an absolute floor (32 weighted nodes), keep the input instead and report it unsupported with a kCostRejected reason. The ratio spares genuine simplifications of large obfuscated inputs (cheaper output); the absolute floor spares the small expansions of legitimate canonicalization (e.g. NOT-over-arith lowering ~(b*b) -> -(b*b)-1, which roughly doubles a tiny expression). Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/Orchestrator.cpp | 30 +++++++++++++++++++++++++++++- test/core/test_simplifier.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/core/Orchestrator.cpp b/lib/core/Orchestrator.cpp index 7538d87..3f5522e 100644 --- a/lib/core/Orchestrator.cpp +++ b/lib/core/Orchestrator.cpp @@ -845,7 +845,6 @@ namespace cobra { SimplifyOutcome outcome; if (result.outcome.Succeeded()) { - outcome.kind = SimplifyOutcome::Kind::kSimplified; outcome.real_vars = result.outcome.RealVars(); // Read PassOutcome.Verification() instead of the parallel // ItemMetadata.verification field. The two were initialized @@ -858,6 +857,35 @@ namespace cobra { result.outcome.Verification() == VerificationState::kVerified; outcome.expr = CleanupFinalExpr(result.outcome.TakeExpr(), bitwidth); outcome.sig_vector = std::move(result.metadata.sig_vector); + outcome.kind = SimplifyOutcome::Kind::kSimplified; + + // Don't return a result that blows up relative to the input. A + // CoB-linear function (e.g. sum(xi) - xor(xi)) recovers an + // exponential AND-basis expansion that is correct but far larger + // than the already-minimal input; keep the input in that case. + // Gate only when the output BOTH more than doubles the input AND + // is large in absolute terms: the ratio spares genuine + // simplifications of large obfuscated inputs (cheaper output), and + // the absolute floor spares the small expansions of legitimate + // canonicalization (e.g. NOT-over-arith lowering ~(b*b) -> + // -(b*b)-1, which roughly doubles a tiny expression). + constexpr uint32_t kBlowupAbsFloor = 32; + if (original_expr != nullptr) { + const auto in_size = ComputeCost(*original_expr).cost.weighted_size; + const auto out_size = ComputeCost(*outcome.expr).cost.weighted_size; + if (out_size > 2 * in_size && out_size > kBlowupAbsFloor) { + outcome.kind = SimplifyOutcome::Kind::kUnchangedUnsupported; + outcome.expr = CloneExpr(*original_expr); + outcome.real_vars.clear(); + outcome.verified = false; + // Stamp a structured reason so the unsupported result + // still satisfies the has_structured_reason invariant + // (set on result.metadata so the copy below propagates). + result.metadata.reason_code = + ReasonCode{ ReasonCategory::kCostRejected, + ReasonDomain::kOrchestrator, 0 }; + } + } } else { outcome.kind = SimplifyOutcome::Kind::kUnchangedUnsupported; outcome.expr = original_expr != nullptr ? CloneExpr(*original_expr) : nullptr; diff --git a/test/core/test_simplifier.cpp b/test/core/test_simplifier.cpp index 75bc9a6..35110f8 100644 --- a/test/core/test_simplifier.cpp +++ b/test/core/test_simplifier.cpp @@ -2109,3 +2109,27 @@ TEST(SimplifierTest, MaskedAddWithCarryNotFalselyCollapsed) { EXPECT_TRUE(FullWidthCheckEval(Evaluator::FromExpr(*ast, 64), 2, *out.expr, 64).passed); EXPECT_FALSE(RendersToAndConst(out, "x", 255)); } + +// Never return a result more expensive than the input. sum(xi) - xor(xi) is +// CoB-linear, so change-of-basis recovers an exponential AND-basis blow-up; the +// input is already minimal, so the cost gate must keep it instead. +TEST(SimplifierTest, DoesNotExpandBeyondInput) { + auto sum = Expr::Variable(0); + auto xr = Expr::Variable(0); + for (uint32_t i = 1; i < 8; ++i) { + sum = Expr::Add(std::move(sum), Expr::Variable(i)); + xr = Expr::BitwiseXor(std::move(xr), Expr::Variable(i)); + } + auto ast = Expr::Add(std::move(sum), Expr::Negate(std::move(xr))); + std::vector< std::string > vars; + for (uint32_t i = 0; i < 8; ++i) { vars.push_back("x" + std::to_string(i)); } + + auto input_cost = ComputeCost(*ast).cost; + auto sig = EvaluateBooleanSignature(*ast, 8, 64); + Options opts{ .bitwidth = 64, .max_vars = 16, .spot_check = true }; + opts.evaluator = Evaluator::FromExpr(*ast, 64); + auto result = Simplify(sig, vars, ast.get(), opts); + ASSERT_TRUE(result.has_value()); + // The result must not be strictly more expensive than the input. + EXPECT_FALSE(IsBetter(input_cost, ComputeCost(*result->expr).cost)); +} From e422b552d0e0483439c3b64a2c2f97851c81e86b Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Fri, 26 Jun 2026 14:12:01 -0400 Subject: [PATCH 07/20] perf: gate full-width aux-var elimination behind cheap boolean check The product-core no-op short-circuit ran the 128-probe full-width EliminateAuxVars on every single-product core. Full-width spurious variables are always a subset of the boolean ones, so an empty boolean result is final: run the cheap 2-arg EliminateAuxVars first and only escalate to the full-width overload when it finds a candidate. The gate decision is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/DecompositionPasses.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/core/DecompositionPasses.cpp b/lib/core/DecompositionPasses.cpp index dfaf8d9..f98dd29 100644 --- a/lib/core/DecompositionPasses.cpp +++ b/lib/core/DecompositionPasses.cpp @@ -174,8 +174,12 @@ namespace cobra { // (full-width-confirmed) spurious variable, a real reduction is // reachable, so decline the short-circuit and let later passes // run. Genuine sum-of-product cores (single_product == false) - // and cores with no spurious variable are unaffected. + // and cores with no spurious variable are unaffected. The + // cheap boolean elimination gates the 128-probe full-width + // confirmation: full-width spurious vars are always a subset + // of the boolean ones, so an empty boolean result is final. if (expected_kind == ExtractorKind::kProductAST && core_payload.single_product + && !EliminateAuxVars(decomp_sig, active_vars).spurious_vars.empty() && !EliminateAuxVars(decomp_sig, active_vars, *active_eval, ctx.bitwidth) .spurious_vars.empty()) { From 6fa700bcf4576c4dde8f7325038971efde81fd17 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Fri, 26 Jun 2026 14:12:08 -0400 Subject: [PATCH 08/20] perf: skip inclusion-exclusion recovery when no AND term is present TryRecoverInclusionExclusion flattened every additive subtree during pattern-subtree peeling, even those with no AND term to recover. Add a no-allocation precondition (AdditiveHasBinaryAnd) that mirrors FlattenAdditive's descent and returns true only for the additive trees that contain a top-level 2-child AND term -- the only terms TryRecoverDisjunctionAt can act on. Additive nodes without an AND term now skip the flatten and per-term scan entirely. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/PatternMatcher.cpp | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/core/PatternMatcher.cpp b/lib/core/PatternMatcher.cpp index 95f5377..8a318fd 100644 --- a/lib/core/PatternMatcher.cpp +++ b/lib/core/PatternMatcher.cpp @@ -1427,11 +1427,45 @@ namespace cobra { return candidate; } + // Cheap precondition for TryRecoverInclusionExclusion: does the additive + // chain rooted at `e` contain a top-level 2-child AND term? Mirrors + // FlattenAdditive's descent (through kAdd / kNeg / constant-scaled kMul) + // but allocates nothing, so additive nodes with no AND term skip the + // FlattenAdditive + per-term scan entirely. + bool AdditiveHasBinaryAnd(const Expr &e) { + switch (e.kind) { + case Expr::Kind::kAdd: + for (const auto &child : e.children) { + if (AdditiveHasBinaryAnd(*child)) { return true; } + } + return false; + case Expr::Kind::kNeg: + return AdditiveHasBinaryAnd(*e.children[0]); + case Expr::Kind::kMul: + if (e.children.size() == 2 + && e.children[0]->kind == Expr::Kind::kConstant) + { + return AdditiveHasBinaryAnd(*e.children[1]); + } + if (e.children.size() == 2 + && e.children[1]->kind == Expr::Kind::kConstant) + { + return AdditiveHasBinaryAnd(*e.children[0]); + } + return false; + case Expr::Kind::kAnd: + return e.children.size() == 2; + default: + return false; + } + } + std::optional< std::unique_ptr< Expr > > TryRecoverInclusionExclusion(const Expr &expr, uint32_t bitwidth) { if (expr.kind != Expr::Kind::kAdd && expr.kind != Expr::Kind::kNeg) { return std::nullopt; } + if (!AdditiveHasBinaryAnd(expr)) { return std::nullopt; } std::vector< AdditiveTerm > terms; uint64_t constant = 0; FlattenAdditive(expr, 1, bitwidth, terms, constant); From 7c4d063d7fc0f76af51c92c29a9902b5d8706a0e Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Fri, 26 Jun 2026 14:12:16 -0400 Subject: [PATCH 09/20] perf: dedup TemplateDecomposer seed constants The data-derived constant seeding could Probe() and Push() the same constant value repeatedly (or_fold and several equal target values). Push already dedups the pool by probe-vals, so the repeats only wasted Probe() calls. Track seeded values in a set and skip duplicates; the resulting pool is identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/TemplateDecomposer.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/core/TemplateDecomposer.cpp b/lib/core/TemplateDecomposer.cpp index 460f6b2..dd8772d 100644 --- a/lib/core/TemplateDecomposer.cpp +++ b/lib/core/TemplateDecomposer.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -1046,8 +1047,11 @@ namespace cobra { { uint64_t or_fold = 0; for (size_t i = 0; i < kNProbes; ++i) { or_fold |= target[i]; } + // Skip values already seeded: Push dedups the pool by probe-vals, so + // re-seeding the same constant only wastes a Probe() call. + std::unordered_set< uint64_t > seeded_vals; auto seed_const = [&](uint64_t c) { - if (c == 0) { return; } + if (c == 0 || !seeded_vals.insert(c).second) { return; } auto e = Expr::Constant(c); auto v = Probe(*e, pts, opts.bitwidth); Push(pool, vmap, std::move(e), v); From 1fab91a04cdec8fed1a7b72794d7c48e840ac6e3 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Fri, 26 Jun 2026 14:12:23 -0400 Subject: [PATCH 10/20] refactor: ExprUtils cleanup -- FlattenAssoc, ContainsXor, exact factor match - Merge the duplicated owning FlattenAdd and FlattenMul into one FlattenAssoc(node, kind, out); update the three call sites. - Add ContainsXor (mirrors ContainsShr) for the orchestrator XOR guard. - Match common factors in ExtractCommonFactor by ExprStructurallyEqual instead of a hash-plus-kind filter. Structural equality never matches a non-equal factor, removing the hash-collision unsoundness the docstring previously accepted; update the docstring to match. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/cobra/core/ExprUtils.h | 13 ++++----- lib/core/ExprUtils.cpp | 52 ++++++++++++++------------------- test/core/test_dynamic_mask.cpp | 24 +++++++++++++++ 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/include/cobra/core/ExprUtils.h b/include/cobra/core/ExprUtils.h index e50cf82..4b15fc7 100644 --- a/include/cobra/core/ExprUtils.h +++ b/include/cobra/core/ExprUtils.h @@ -34,6 +34,9 @@ namespace cobra { /// Returns true if any node in the AST is kShr. bool ContainsShr(const Expr &expr); + /// Returns true if any node in the AST is kXor. + bool ContainsXor(const Expr &expr); + /// Append the var_index of every kVariable node (pre-order). Duplicates /// are preserved; callers that want a deduplicated support set must /// sort + unique the output themselves. @@ -50,13 +53,9 @@ namespace cobra { /// Chains: constant folding → negation refolding → ExtractCommonFactor /// → constant folding. /// - /// Semantics-preserving under the assumption that std::hash - /// does not collide on the input AST. ExtractCommonFactor uses - /// hash-based factor equality, so a hash collision could in - /// principle produce a non-equivalent rewrite. The hash-collision - /// tradeoff is accepted at the project level (see audit - /// 2026-05-04 lifting-passes-29 / -65). Callers requiring strict - /// semantics-preservation must re-verify the result. + /// Semantics-preserving: ExtractCommonFactor matches common factors by + /// exact structural equality (ExprStructurallyEqual), so the distributive + /// rewrite never fires on a non-equal factor. std::unique_ptr< Expr > CleanupFinalExpr(std::unique_ptr< Expr > expr, uint32_t bitwidth); /// Check if an Expr subtree depends on any variable. diff --git a/lib/core/ExprUtils.cpp b/lib/core/ExprUtils.cpp index abe61e2..eed13b1 100644 --- a/lib/core/ExprUtils.cpp +++ b/lib/core/ExprUtils.cpp @@ -79,6 +79,13 @@ namespace cobra { }); } + bool ContainsXor(const Expr &expr) { + if (expr.kind == Expr::Kind::kXor) { return true; } + return std::ranges::any_of(expr.children, [](const auto &child) { + return ContainsXor(*child); + }); + } + void CollectVariables(const Expr &expr, std::vector< uint32_t > &out) { if (expr.kind == Expr::Kind::kVariable) { out.push_back(expr.var_index); @@ -145,15 +152,17 @@ namespace cobra { namespace { - // Flatten a left-folded Add chain into a list of terms. - void FlattenAdd( - std::unique_ptr< Expr > node, std::vector< std::unique_ptr< Expr > > &terms + // Flatten a left-folded binary associative chain of `kind` (kAdd or + // kMul) into its operand list, moving each leaf out of the tree. + void FlattenAssoc( + std::unique_ptr< Expr > node, Expr::Kind kind, + std::vector< std::unique_ptr< Expr > > &out ) { - if (node->kind == Expr::Kind::kAdd) { - FlattenAdd(std::move(node->children[0]), terms); - FlattenAdd(std::move(node->children[1]), terms); + if (node->kind == kind) { + FlattenAssoc(std::move(node->children[0]), kind, out); + FlattenAssoc(std::move(node->children[1]), kind, out); } else { - terms.push_back(std::move(node)); + out.push_back(std::move(node)); } } @@ -173,7 +182,7 @@ namespace cobra { if (expr->kind != Expr::Kind::kAdd) { return expr; } std::vector< std::unique_ptr< Expr > > terms; - FlattenAdd(std::move(expr), terms); + FlattenAssoc(std::move(expr), Expr::Kind::kAdd, terms); uint64_t const_sum = 0; std::vector< std::unique_ptr< Expr > > non_const; @@ -226,18 +235,6 @@ namespace cobra { return expr; } - // Flatten binary Mul chain into factor list. - void FlattenMul( - std::unique_ptr< Expr > node, std::vector< std::unique_ptr< Expr > > &factors - ) { - if (node->kind == Expr::Kind::kMul) { - FlattenMul(std::move(node->children[0]), factors); - FlattenMul(std::move(node->children[1]), factors); - } else { - factors.push_back(std::move(node)); - } - } - // Rebuild a left-folded Mul chain from a factor list. std::unique_ptr< Expr > RebuildMul(std::vector< std::unique_ptr< Expr > > &factors) { auto result = std::move(factors[0]); @@ -260,7 +257,7 @@ namespace cobra { // Flatten the Add chain. std::vector< std::unique_ptr< Expr > > terms; - FlattenAdd(std::move(expr), terms); + FlattenAssoc(std::move(expr), Expr::Kind::kAdd, terms); if (terms.size() < 2) { if (terms.size() == 1) { return std::move(terms[0]); } @@ -278,7 +275,7 @@ namespace cobra { for (auto &t : terms) { TermFactors tf; if (t->kind == Expr::Kind::kMul) { - FlattenMul(std::move(t), tf.factors); + FlattenAssoc(std::move(t), Expr::Kind::kMul, tf.factors); } else { tf.factors.push_back(std::move(t)); } @@ -292,9 +289,8 @@ namespace cobra { if (candidate->kind == Expr::Kind::kConstant) { continue; } if (candidate->kind == Expr::Kind::kVariable) { continue; } - auto candidate_hash = std::hash< Expr >{}(*candidate); - - // Check all other terms for this factor. + // Check all other terms for this factor, matching on exact + // structural equality (never a false positive, unlike a hash). bool universal = true; std::vector< size_t > match_indices; match_indices.push_back(fi); @@ -303,11 +299,7 @@ namespace cobra { for (size_t fj = 0; fj < all_factors[ti].factors.size(); ++fj) { auto &f = all_factors[ti].factors[fj]; if (f->kind == Expr::Kind::kConstant) { continue; } - if (std::hash< Expr >{}(*f) == candidate_hash - && f->kind == candidate->kind) - { - // Deep equality check via CloneExpr comparison. - // Use the hash as strong filter — collisions are rare. + if (ExprStructurallyEqual(*f, *candidate)) { found = true; match_indices.push_back(fj); break; diff --git a/test/core/test_dynamic_mask.cpp b/test/core/test_dynamic_mask.cpp index e20c5f3..cef2eea 100644 --- a/test/core/test_dynamic_mask.cpp +++ b/test/core/test_dynamic_mask.cpp @@ -87,3 +87,27 @@ TEST(DynamicMaskTest, ContainsShr_Deep) { ); EXPECT_TRUE(ContainsShr(*expr)); } + +TEST(DynamicMaskTest, ContainsXorFalse) { + auto expr = Expr::Add( + Expr::BitwiseAnd(Expr::Variable(0), Expr::Variable(1)), + Expr::BitwiseOr(Expr::Variable(0), Expr::Constant(3)) + ); + EXPECT_FALSE(ContainsXor(*expr)); +} + +TEST(DynamicMaskTest, ContainsXorTrue) { + auto expr = Expr::BitwiseXor(Expr::Variable(0), Expr::Variable(1)); + EXPECT_TRUE(ContainsXor(*expr)); +} + +TEST(DynamicMaskTest, ContainsXorDeep) { + auto expr = Expr::BitwiseAnd( + Expr::Add( + Expr::Variable(0), + Expr::BitwiseOr(Expr::Variable(1), Expr::BitwiseXor(Expr::Variable(0), Expr::Variable(2))) + ), + Expr::Constant(0xFF) + ); + EXPECT_TRUE(ContainsXor(*expr)); +} From 63fa7589e08cc0001136097c9773075931413471 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Fri, 26 Jun 2026 14:12:33 -0400 Subject: [PATCH 11/20] refactor: extract IsCostBlowup gate, thread input cost, fix XOR diagnostics - Extract the output-size blow-up gate into IsCostBlowup(candidate, baseline) with named kBlowupRatio/kBlowupAbsFloor constants. Semantics are unchanged (output > 2x input and > 32 weighted size). - Compute the input expression cost once in Simplify() and thread it through ToSimplifyOutcome, reusing it in the XOR exhaustion fallback so the input cost is no longer computed twice on that path. - On the XOR-cancel success path, clear the exhaustion failure lineage (reason_code, cause_chain, candidate_failed_verification) so a kSimplified result no longer reports a stale search-exhausted reason. - Guard the XOR fallback with ContainsXor so the clone and walk are skipped when the input has no XOR. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/cobra/core/ExprCost.h | 12 +++++++++ lib/core/ExprCost.cpp | 10 ++++++++ lib/core/Orchestrator.cpp | 48 +++++++++++++++++++++-------------- test/core/test_expr_cost.cpp | 38 +++++++++++++++++++++++++++ test/core/test_simplifier.cpp | 20 +++++++++++---- 5 files changed, 104 insertions(+), 24 deletions(-) diff --git a/include/cobra/core/ExprCost.h b/include/cobra/core/ExprCost.h index d0433fc..8cbc9b9 100644 --- a/include/cobra/core/ExprCost.h +++ b/include/cobra/core/ExprCost.h @@ -22,4 +22,16 @@ namespace cobra { bool IsBetter(const ExprCost &candidate, const ExprCost &baseline); + // Returns true when `candidate` is a pathological size blow-up relative to + // `baseline`. A blow-up must clear two bars at once: it more than doubles + // the baseline's weighted size AND is large in absolute terms. The ratio + // catches change-of-basis expansions proportional to a large input (e.g. + // sum(xi) - xor(xi) recovers an exponential AND-basis form); the absolute + // floor spares the small ~2x expansions of legitimate canonicalization + // (NOT-over-arith lowering ~(b*b) -> -(b*b)-1 roughly doubles a tiny + // expression). This is deliberately keyed on weighted_size alone: it is a + // size-explosion guard, distinct from IsBetter's full lexicographic + // "is this an improvement" comparison. + bool IsCostBlowup(const ExprCost &candidate, const ExprCost &baseline); + } // namespace cobra diff --git a/lib/core/ExprCost.cpp b/lib/core/ExprCost.cpp index 83531f3..0871ab6 100644 --- a/lib/core/ExprCost.cpp +++ b/lib/core/ExprCost.cpp @@ -92,4 +92,14 @@ namespace cobra { ); } + bool IsCostBlowup(const ExprCost &candidate, const ExprCost &baseline) { + // Output must exceed this multiple of the input to count as a blow-up. + constexpr uint32_t kBlowupRatio = 2; + // ...and exceed this absolute weighted size, sparing the small ~2x + // expansions of legitimate canonicalization on tiny inputs. + constexpr uint32_t kBlowupAbsFloor = 32; + return candidate.weighted_size > kBlowupRatio * baseline.weighted_size + && candidate.weighted_size > kBlowupAbsFloor; + } + } // namespace cobra diff --git a/lib/core/Orchestrator.cpp b/lib/core/Orchestrator.cpp index 3f5522e..950b6b3 100644 --- a/lib/core/Orchestrator.cpp +++ b/lib/core/Orchestrator.cpp @@ -840,7 +840,8 @@ namespace cobra { SimplifyOutcome ToSimplifyOutcome( OrchestratorResult result, const Expr *original_expr, - const OrchestratorTelemetry &telemetry, uint32_t bitwidth + const OrchestratorTelemetry &telemetry, uint32_t bitwidth, + const std::optional< ExprCost > &input_cost ) { SimplifyOutcome outcome; @@ -863,17 +864,10 @@ namespace cobra { // CoB-linear function (e.g. sum(xi) - xor(xi)) recovers an // exponential AND-basis expansion that is correct but far larger // than the already-minimal input; keep the input in that case. - // Gate only when the output BOTH more than doubles the input AND - // is large in absolute terms: the ratio spares genuine - // simplifications of large obfuscated inputs (cheaper output), and - // the absolute floor spares the small expansions of legitimate - // canonicalization (e.g. NOT-over-arith lowering ~(b*b) -> - // -(b*b)-1, which roughly doubles a tiny expression). - constexpr uint32_t kBlowupAbsFloor = 32; - if (original_expr != nullptr) { - const auto in_size = ComputeCost(*original_expr).cost.weighted_size; - const auto out_size = ComputeCost(*outcome.expr).cost.weighted_size; - if (out_size > 2 * in_size && out_size > kBlowupAbsFloor) { + // See IsCostBlowup for the ratio/floor rationale. + if (original_expr != nullptr && input_cost.has_value()) { + const auto out_cost = ComputeCost(*outcome.expr).cost; + if (IsCostBlowup(out_cost, *input_cost)) { outcome.kind = SimplifyOutcome::Kind::kUnchangedUnsupported; outcome.expr = CloneExpr(*original_expr); outcome.real_vars.clear(); @@ -1032,13 +1026,21 @@ namespace cobra { Worklist worklist; OrchestratorTelemetry telemetry; + // Cost of the raw input, computed once and reused by every return path + // (the blow-up gate in ToSimplifyOutcome and the XOR exhaustion + // fallback). Absent when there is no input AST (sig-only path). + const std::optional< ExprCost > input_cost = + input_expr ? std::optional< ExprCost >(ComputeCost(*input_expr).cost) + : std::nullopt; + // Seeding if (input_expr == nullptr) { auto seed_result = SeedNoAst(sig, vars, context, worklist); if (!seed_result.has_value()) { return std::unexpected(seed_result.error()); } if (seed_result.value().has_value()) { return Ok(ToSimplifyOutcome( - std::move(*seed_result.value()), input_expr, telemetry, context.bitwidth + std::move(*seed_result.value()), input_expr, telemetry, context.bitwidth, + input_cost )); } } else { @@ -1046,7 +1048,8 @@ namespace cobra { if (!seed_result.has_value()) { return std::unexpected(seed_result.error()); } if (seed_result.value().has_value()) { return Ok(ToSimplifyOutcome( - std::move(*seed_result.value()), input_expr, telemetry, context.bitwidth + std::move(*seed_result.value()), input_expr, telemetry, context.bitwidth, + input_cost )); } } @@ -1163,7 +1166,7 @@ namespace cobra { .metadata = std::move(item.metadata), .run_metadata = context.run_metadata, }, - input_expr, telemetry, context.bitwidth + input_expr, telemetry, context.bitwidth, input_cost )); } } @@ -1393,16 +1396,23 @@ namespace cobra { // result is provably equivalent and sound to return directly. Fire only // when it actually cancelled something and the result is strictly // cheaper; the full-width check is defense in depth, not the proof. - if (input_expr != nullptr && context.evaluator) { + if (input_expr != nullptr && context.evaluator && ContainsXor(*input_expr)) { auto xor_peel = SimplifyXorChains(CloneExpr(*input_expr), context.bitwidth); if (!ExprStructurallyEqual(*xor_peel, *input_expr) - && IsBetter(ComputeCost(*xor_peel).cost, ComputeCost(*input_expr).cost)) + && IsBetter(ComputeCost(*xor_peel).cost, *input_cost)) { const auto num_vars = static_cast< uint32_t >(vars.size()); auto check = FullWidthCheckEval( *context.evaluator, num_vars, *xor_peel, context.bitwidth ); if (check.passed) { + // This is a genuine (exact) simplification, not the + // exhaustion failure final_meta was built to describe. Drop + // the stale failure lineage so the kSimplified result does + // not report a search-exhausted reason or cause chain. + final_meta.reason_code.reset(); + final_meta.cause_chain.clear(); + final_meta.candidate_failed_verification = false; return Ok(ToSimplifyOutcome( OrchestratorResult{ .outcome = PassOutcome::Success( @@ -1411,7 +1421,7 @@ namespace cobra { .metadata = std::move(final_meta), .run_metadata = context.run_metadata, }, - input_expr, telemetry, context.bitwidth + input_expr, telemetry, context.bitwidth, input_cost )); } } @@ -1423,7 +1433,7 @@ namespace cobra { .metadata = std::move(final_meta), .run_metadata = context.run_metadata, }, - input_expr, telemetry, context.bitwidth + input_expr, telemetry, context.bitwidth, input_cost )); } diff --git a/test/core/test_expr_cost.cpp b/test/core/test_expr_cost.cpp index 47227bb..3dccecb 100644 --- a/test/core/test_expr_cost.cpp +++ b/test/core/test_expr_cost.cpp @@ -100,3 +100,41 @@ TEST(ExprCostTest, IsBetterEqual) { ExprCost a{ 5, 1, 3 }; EXPECT_FALSE(IsBetter(a, a)); } + +TEST(ExprCostTest, IsCostBlowupExponentialExpansion) { + // 100 weighted vs 10: > 2x AND > 32 absolute -> blow-up. + ExprCost candidate{ 100, 0, 5 }; + ExprCost baseline{ 10, 0, 3 }; + EXPECT_TRUE(IsCostBlowup(candidate, baseline)); +} + +TEST(ExprCostTest, IsCostBlowupSparedByRatio) { + // 30 vs 20: not more than 2x (would need > 40), so not a blow-up + // even though it exceeds the absolute floor. + ExprCost candidate{ 30, 0, 4 }; + ExprCost baseline{ 20, 0, 4 }; + EXPECT_FALSE(IsCostBlowup(candidate, baseline)); +} + +TEST(ExprCostTest, IsCostBlowupSparedBySmallAbsoluteFloor) { + // 15 vs 7: more than 2x, but below the absolute floor (32). This is + // the legitimate NOT-over-arith canonicalization that must be kept. + ExprCost candidate{ 15, 0, 4 }; + ExprCost baseline{ 7, 0, 3 }; + EXPECT_FALSE(IsCostBlowup(candidate, baseline)); +} + +TEST(ExprCostTest, IsCostBlowupBoundary) { + // Exactly at both thresholds is NOT a blow-up (strict greater-than); + // one past both is. + EXPECT_FALSE(IsCostBlowup(ExprCost{ 32, 0, 1 }, ExprCost{ 16, 0, 1 })); + EXPECT_TRUE(IsCostBlowup(ExprCost{ 33, 0, 1 }, ExprCost{ 16, 0, 1 })); +} + +TEST(ExprCostTest, IsCostBlowupKeysOnSizeNotMulOrDepth) { + // The gate axis is weighted_size only: a candidate that is smaller in + // size is never a blow-up regardless of mul-count or depth. + ExprCost candidate{ 10, 50, 50 }; + ExprCost baseline{ 100, 0, 1 }; + EXPECT_FALSE(IsCostBlowup(candidate, baseline)); +} diff --git a/test/core/test_simplifier.cpp b/test/core/test_simplifier.cpp index 35110f8..9773a2a 100644 --- a/test/core/test_simplifier.cpp +++ b/test/core/test_simplifier.cpp @@ -2099,6 +2099,10 @@ TEST(SimplifierTest, XorSelfCancelEndToEnd) { // B ^ B cancels -> no XOR remains in the rendered result. EXPECT_EQ(Render(*out.expr, out.real_vars).find('^'), std::string::npos); EXPECT_TRUE(FullWidthCheckEval(Evaluator::FromExpr(*ast, 64), 4, *out.expr, 64).passed); + // The exact-XOR-cancel success must not carry the exhaustion failure + // lineage that the fallback path was built atop. + EXPECT_FALSE(out.diag.reason_code.has_value()); + EXPECT_TRUE(out.diag.cause_chain.empty()); } // Soundness: a genuine carry ((x & 255) + (y & 255)) & 255 must NOT collapse to @@ -2110,10 +2114,10 @@ TEST(SimplifierTest, MaskedAddWithCarryNotFalselyCollapsed) { EXPECT_FALSE(RendersToAndConst(out, "x", 255)); } -// Never return a result more expensive than the input. sum(xi) - xor(xi) is -// CoB-linear, so change-of-basis recovers an exponential AND-basis blow-up; the -// input is already minimal, so the cost gate must keep it instead. -TEST(SimplifierTest, DoesNotExpandBeyondInput) { +// A change-of-basis blow-up must be rejected. sum(xi) - xor(xi) is CoB-linear, +// so change-of-basis recovers an exponential AND-basis expansion; the input is +// already minimal, so the cost gate keeps the input and reports kCostRejected. +TEST(SimplifierTest, RejectsExponentialBlowup) { auto sum = Expr::Variable(0); auto xr = Expr::Variable(0); for (uint32_t i = 1; i < 8; ++i) { @@ -2130,6 +2134,12 @@ TEST(SimplifierTest, DoesNotExpandBeyondInput) { opts.evaluator = Evaluator::FromExpr(*ast, 64); auto result = Simplify(sig, vars, ast.get(), opts); ASSERT_TRUE(result.has_value()); - // The result must not be strictly more expensive than the input. + // The blow-up is rejected: the input is kept unchanged... + EXPECT_EQ(result->kind, SimplifyOutcome::Kind::kUnchangedUnsupported); + EXPECT_TRUE(ExprStructurallyEqual(*result->expr, *ast)); + // ...and the result must not be strictly more expensive than the input. EXPECT_FALSE(IsBetter(input_cost, ComputeCost(*result->expr).cost)); + // ...with a kCostRejected structured reason. + ASSERT_TRUE(result->diag.reason_code.has_value()); + EXPECT_EQ(result->diag.reason_code->category, ReasonCategory::kCostRejected); } From 0328f27bca3a82f21d2e2f9e9c9257ff5da1cfdc Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Fri, 26 Jun 2026 15:27:12 -0400 Subject: [PATCH 12/20] refactor: generalize ContainsShr/ContainsXor into ContainsType Both helpers were identical except for the node kind they matched. Replace them with one ContainsType(expr, kind) and update all call sites (Orchestrator shift/xor guards, SemilinearNormalizer, StructureRecovery, the mask-audit bench, and the dynamic-mask tests). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/cobra/core/ExprUtils.h | 7 ++----- lib/core/ExprUtils.cpp | 15 ++++----------- lib/core/Orchestrator.cpp | 6 ++++-- lib/core/SemilinearNormalizer.cpp | 2 +- lib/core/StructureRecovery.cpp | 4 +++- test/bench/bench_mask_audit.cpp | 2 +- test/core/test_dynamic_mask.cpp | 28 +++++++++++++++------------- test/core/test_simplifier.cpp | 2 +- 8 files changed, 31 insertions(+), 35 deletions(-) diff --git a/include/cobra/core/ExprUtils.h b/include/cobra/core/ExprUtils.h index 4b15fc7..cd0ab41 100644 --- a/include/cobra/core/ExprUtils.h +++ b/include/cobra/core/ExprUtils.h @@ -31,11 +31,8 @@ namespace cobra { /// Check if an Expr subtree contains only constants (no variables). bool IsConstantSubtree(const Expr &expr); - /// Returns true if any node in the AST is kShr. - bool ContainsShr(const Expr &expr); - - /// Returns true if any node in the AST is kXor. - bool ContainsXor(const Expr &expr); + /// Returns true if any node in the AST has the given kind. + bool ContainsType(const Expr &expr, Expr::Kind kind); /// Append the var_index of every kVariable node (pre-order). Duplicates /// are preserved; callers that want a deduplicated support set must diff --git a/lib/core/ExprUtils.cpp b/lib/core/ExprUtils.cpp index eed13b1..a831782 100644 --- a/lib/core/ExprUtils.cpp +++ b/lib/core/ExprUtils.cpp @@ -72,17 +72,10 @@ namespace cobra { }); } - bool ContainsShr(const Expr &expr) { - if (expr.kind == Expr::Kind::kShr) { return true; } - return std::ranges::any_of(expr.children, [](const auto &child) { - return ContainsShr(*child); - }); - } - - bool ContainsXor(const Expr &expr) { - if (expr.kind == Expr::Kind::kXor) { return true; } - return std::ranges::any_of(expr.children, [](const auto &child) { - return ContainsXor(*child); + bool ContainsType(const Expr &expr, Expr::Kind kind) { + if (expr.kind == kind) { return true; } + return std::ranges::any_of(expr.children, [kind](const auto &child) { + return ContainsType(*child, kind); }); } diff --git a/lib/core/Orchestrator.cpp b/lib/core/Orchestrator.cpp index 950b6b3..529e552 100644 --- a/lib/core/Orchestrator.cpp +++ b/lib/core/Orchestrator.cpp @@ -969,7 +969,7 @@ namespace cobra { // recurse only a handful of times before reaching 1 bit. if (input_expr != nullptr) { auto mask = DetectRootLowBitMask(*input_expr, opts.bitwidth); - if (mask.has_value() && !ContainsShr(*mask->inner)) { + if (mask.has_value() && !ContainsType(*mask->inner, Expr::Kind::kShr)) { auto inner = CloneExpr(*mask->inner); uint32_t eff_bw = mask->effective_width; auto inner_sig = EvaluateBooleanSignature( @@ -1396,7 +1396,9 @@ namespace cobra { // result is provably equivalent and sound to return directly. Fire only // when it actually cancelled something and the result is strictly // cheaper; the full-width check is defense in depth, not the proof. - if (input_expr != nullptr && context.evaluator && ContainsXor(*input_expr)) { + if (input_expr != nullptr && context.evaluator + && ContainsType(*input_expr, Expr::Kind::kXor)) + { auto xor_peel = SimplifyXorChains(CloneExpr(*input_expr), context.bitwidth); if (!ExprStructurallyEqual(*xor_peel, *input_expr) && IsBetter(ComputeCost(*xor_peel).cost, *input_cost)) diff --git a/lib/core/SemilinearNormalizer.cpp b/lib/core/SemilinearNormalizer.cpp index e324529..9cd6937 100644 --- a/lib/core/SemilinearNormalizer.cpp +++ b/lib/core/SemilinearNormalizer.cpp @@ -203,7 +203,7 @@ namespace cobra { // identical Boolean truth tables but differ on full-width inputs // because variables evaluate to {0,1} while constants keep their // full value. - const bool kPure = !HasConstant(expr) && !ContainsShr(expr); + const bool kPure = !HasConstant(expr) && !ContainsType(expr, Expr::Kind::kShr); if (kPure && !kKey.truth_table.empty()) { auto it = ctx.atom_map.find(kKey); if (it != ctx.atom_map.end()) { return it->second; } diff --git a/lib/core/StructureRecovery.cpp b/lib/core/StructureRecovery.cpp index 52a9200..05cfdf4 100644 --- a/lib/core/StructureRecovery.cpp +++ b/lib/core/StructureRecovery.cpp @@ -360,7 +360,9 @@ namespace cobra { // Only flatten single-variable atoms without shifts. // Shifts mix bits across positions, breaking the per-bit // decomposition f(x) = f(0) + (x & pass) - (x & invert). - if (info.key.support.size() != 1 || ContainsShr(*info.original_subtree)) { + if (info.key.support.size() != 1 + || ContainsType(*info.original_subtree, Expr::Kind::kShr)) + { new_terms.push_back(term); continue; } diff --git a/test/bench/bench_mask_audit.cpp b/test/bench/bench_mask_audit.cpp index 0e6c6e1..327bd36 100644 --- a/test/bench/bench_mask_audit.cpp +++ b/test/bench/bench_mask_audit.cpp @@ -101,7 +101,7 @@ namespace { MaskAuditResult r; r.line = lineno; r.node_count = CountNodes(expr); - r.has_shr = ContainsShr(expr); + r.has_shr = ContainsType(expr, Expr::Kind::kShr); // Root mask auto root_mask = DetectRootLowBitMask(expr, 64); diff --git a/test/core/test_dynamic_mask.cpp b/test/core/test_dynamic_mask.cpp index cef2eea..bb75ade 100644 --- a/test/core/test_dynamic_mask.cpp +++ b/test/core/test_dynamic_mask.cpp @@ -64,20 +64,20 @@ TEST(DynamicMaskTest, DetectRootLowBitMask_NotAndNode) { EXPECT_FALSE(mask.has_value()); } -TEST(DynamicMaskTest, ContainsShr_False) { +TEST(DynamicMaskTest, ContainsTypeShrFalse) { auto expr = Expr::Add( Expr::BitwiseAnd(Expr::Variable(0), Expr::Variable(1)), Expr::BitwiseNot(Expr::Variable(0)) ); - EXPECT_FALSE(ContainsShr(*expr)); + EXPECT_FALSE(ContainsType(*expr, Expr::Kind::kShr)); } -TEST(DynamicMaskTest, ContainsShr_True) { +TEST(DynamicMaskTest, ContainsTypeShrTrue) { auto expr = Expr::Add(Expr::LogicalShr(Expr::Variable(0), 3), Expr::Variable(1)); - EXPECT_TRUE(ContainsShr(*expr)); + EXPECT_TRUE(ContainsType(*expr, Expr::Kind::kShr)); } -TEST(DynamicMaskTest, ContainsShr_Deep) { +TEST(DynamicMaskTest, ContainsTypeShrDeep) { auto expr = Expr::BitwiseAnd( Expr::Add( Expr::Variable(0), @@ -85,29 +85,31 @@ TEST(DynamicMaskTest, ContainsShr_Deep) { ), Expr::Constant(0xFF) ); - EXPECT_TRUE(ContainsShr(*expr)); + EXPECT_TRUE(ContainsType(*expr, Expr::Kind::kShr)); } -TEST(DynamicMaskTest, ContainsXorFalse) { +TEST(DynamicMaskTest, ContainsTypeXorFalse) { auto expr = Expr::Add( Expr::BitwiseAnd(Expr::Variable(0), Expr::Variable(1)), Expr::BitwiseOr(Expr::Variable(0), Expr::Constant(3)) ); - EXPECT_FALSE(ContainsXor(*expr)); + EXPECT_FALSE(ContainsType(*expr, Expr::Kind::kXor)); } -TEST(DynamicMaskTest, ContainsXorTrue) { +TEST(DynamicMaskTest, ContainsTypeXorTrue) { auto expr = Expr::BitwiseXor(Expr::Variable(0), Expr::Variable(1)); - EXPECT_TRUE(ContainsXor(*expr)); + EXPECT_TRUE(ContainsType(*expr, Expr::Kind::kXor)); } -TEST(DynamicMaskTest, ContainsXorDeep) { +TEST(DynamicMaskTest, ContainsTypeXorDeep) { auto expr = Expr::BitwiseAnd( Expr::Add( Expr::Variable(0), - Expr::BitwiseOr(Expr::Variable(1), Expr::BitwiseXor(Expr::Variable(0), Expr::Variable(2))) + Expr::BitwiseOr( + Expr::Variable(1), Expr::BitwiseXor(Expr::Variable(0), Expr::Variable(2)) + ) ), Expr::Constant(0xFF) ); - EXPECT_TRUE(ContainsXor(*expr)); + EXPECT_TRUE(ContainsType(*expr, Expr::Kind::kXor)); } diff --git a/test/core/test_simplifier.cpp b/test/core/test_simplifier.cpp index 9773a2a..50ad1e5 100644 --- a/test/core/test_simplifier.cpp +++ b/test/core/test_simplifier.cpp @@ -1122,7 +1122,7 @@ TEST(SimplifierTest, RepairProductShadow_CompoundChildSkipped) { // --- Dynamic masking fallback tests --- TEST(SimplifierTest, DynamicMask_ShrRejectsOptimization) { - // 0xFF & (x >> 1): ContainsShr rejects dynamic masking, + // 0xFF & (x >> 1): the shift check rejects dynamic masking, // falls through to normal pipeline. auto inner = Expr::LogicalShr(Expr::Variable(0), 1); auto masked = Expr::BitwiseAnd(Expr::Constant(0xFF), std::move(inner)); From 336f6f9894d1a52e0f55b884383251dc1163027f Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Mon, 29 Jun 2026 11:42:09 -0400 Subject: [PATCH 13/20] perf: use a stronger Newton seed in ModInverseOdd Seed the Hensel inverse with ((3*x) ^ 2), which is correct to 5 bits (vs 3 for the bare x), so the doubling loop runs one fewer iteration at every width (4 instead of 5 at 64-bit). Output is the unique inverse mod 2^bits, so it is bit-identical to the previous seed. Add exhaustive small-width and random 64-bit correctness tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/cobra/core/MathUtils.h | 12 +++++++----- test/core/test_math_utils.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/include/cobra/core/MathUtils.h b/include/cobra/core/MathUtils.h index c6c1ea6..51b5d75 100644 --- a/include/cobra/core/MathUtils.h +++ b/include/cobra/core/MathUtils.h @@ -37,15 +37,17 @@ namespace cobra { } // Modular inverse of an odd number x modulo 2^bits. - // Hensel lifting: x*x = 1 mod 8 for all odd x, so 3 bits are - // correct initially. Each iteration doubles the number of - // correct bits. + // Newton/Hensel lifting: the seed ((3 * x) ^ 2) -- i.e. (3*x) XOR 2, not a + // square -- satisfies x * seed = 1 mod 32 for all odd x, so 5 bits are + // correct initially. Each iteration doubles the correct bits + // (5 -> 10 -> 20 -> 40 -> 80), so 64-bit inverses need 4 iterations. (The + // bare seed x is correct to only 3 bits and would need a 5th iteration.) inline uint64_t ModInverseOdd(uint64_t x, uint32_t bits) { assert(bits >= 1); assert(x & 1); const uint64_t kModMask = Bitmask(bits); - uint64_t inv = x & kModMask; - for (uint32_t b = 3; b < bits; b *= 2) { inv = (inv * (2 - (x * inv))) & kModMask; } + uint64_t inv = ((3 * x) ^ 2) & kModMask; + for (uint32_t b = 5; b < bits; b *= 2) { inv = (inv * (2 - (x * inv))) & kModMask; } return inv & kModMask; } diff --git a/test/core/test_math_utils.cpp b/test/core/test_math_utils.cpp index 0c5a6ec..715c130 100644 --- a/test/core/test_math_utils.cpp +++ b/test/core/test_math_utils.cpp @@ -98,3 +98,29 @@ TEST(ModInverseOddTest, SingleBit) { EXPECT_EQ(ModInverseOdd(1, 1), 1u); EXPECT_EQ(ModInverseOdd(3, 1), 1u); // 3 & 1 == 1 } + +// Exhaustive: every odd residue is its own inverse's partner mod 2^bits, and +// the result is reduced to [0, 2^bits). Covers the (3x)^2 seed at small widths +// where the doubling loop runs 0-2 times. +TEST(ModInverseOddTest, ExhaustiveSmallWidths) { + for (uint32_t bits : { 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 12u, 16u }) { + uint64_t mask = Bitmask(bits); + for (uint64_t x = 1; x <= mask; x += 2) { + uint64_t inv = ModInverseOdd(x, bits); + EXPECT_EQ((x * inv) & mask, 1u) << "bits=" << bits << " x=" << x; + EXPECT_EQ(inv & mask, inv) << "bits=" << bits << " x=" << x; + } + } +} + +TEST(ModInverseOddTest, Random64Bit) { + uint64_t state = 0x123456789abcdef0ULL; + for (int i = 0; i < 100000; ++i) { + state += 0x9E3779B97F4A7C15ULL; + uint64_t z = state; + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + uint64_t x = (z ^ (z >> 31)) | 1ULL; + EXPECT_EQ(x * ModInverseOdd(x, 64), 1u) << "x=" << x; + } +} From f141eb4135aeb8d63d34e025722a5f4f520652dc Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Mon, 29 Jun 2026 11:42:10 -0400 Subject: [PATCH 14/20] refactor: delegate ModInverseOddHalf to ModInverseOdd ModInverseOddHalf(x, w) is exactly the odd inverse at width w-1, so replace its duplicated Hensel loop with a call to ModInverseOdd(x, w-1). It keeps its name and tests and inherits the stronger seed. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/CoefficientSplitter.cpp | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/core/CoefficientSplitter.cpp b/lib/core/CoefficientSplitter.cpp index 1907f65..b04822f 100644 --- a/lib/core/CoefficientSplitter.cpp +++ b/lib/core/CoefficientSplitter.cpp @@ -1,5 +1,6 @@ #include "cobra/core/CoefficientSplitter.h" #include "cobra/core/BitWidth.h" +#include "cobra/core/MathUtils.h" #include "cobra/core/Trace.h" #include #include @@ -12,20 +13,8 @@ namespace cobra { uint64_t ModInverseOddHalf(uint64_t x, uint32_t w) { assert(w >= 2); - assert(x & 1); - - // Target: x^{-1} mod 2^{w-1}. - // Hensel lifting: inv = x is correct mod 2^3 (since x^2 = 1 mod 8 - // for all odd x). Each iteration doubles correct bits. - const uint32_t kTargetBits = w - 1; - const uint64_t kModMask = (kTargetBits >= 64) ? UINT64_MAX : (1ULL << kTargetBits) - 1; - - uint64_t inv = x & kModMask; - // ceil(log2(kTargetBits)) iterations, starting from 3 correct bits - for (uint32_t bits = 3; bits < kTargetBits; bits *= 2) { - inv = (inv * (2 - (x * inv))) & kModMask; - } - return inv & kModMask; + // x^{-1} mod 2^{w-1} is exactly the general odd inverse at width w-1. + return ModInverseOdd(x, w - 1); } namespace { From b1b78619f7b480970fe8e7a957bacf8ff2e104cf Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Mon, 29 Jun 2026 11:42:11 -0400 Subject: [PATCH 15/20] perf: use the canonical block butterfly in InterpolateCoefficients Replace the scan-all-indices-with-branch Mobius loop with the canonical block butterfly: each variable pairs index i with i+half over half-blocks, touching exactly len/2 elements per variable with no per-element bit test and contiguous access. Same subset-Mobius transform. Add a randomized property test against a naive Mobius reference. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/CoeffInterpolator.cpp | 14 ++++++---- test/core/test_coeff_interpolator.cpp | 39 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/core/CoeffInterpolator.cpp b/lib/core/CoeffInterpolator.cpp index 8256a4b..4663c12 100644 --- a/lib/core/CoeffInterpolator.cpp +++ b/lib/core/CoeffInterpolator.cpp @@ -28,12 +28,16 @@ namespace cobra { // "without this variable" entry from the "with" entry to isolate its // contribution. Equivalent to evaluating directly in the (1, x, y, x&y) // basis rather than computing a change-of-basis matrix. + // + // Canonical block butterfly: each var pairs index i (bit clear) with + // i + half (bit set), so we touch exactly len/2 elements per var with + // no per-element bit test and contiguous access within each half-block. for (uint32_t var = 0; var < num_vars; ++var) { - const uint32_t stride = 1U << var; - for (size_t i = 0; i < len; ++i) { - if ((i & stride) != 0u) { - const size_t i0 = i & ~static_cast< size_t >(stride); - sig[i] = (sig[i] - sig[i0]) & mask; + const size_t half = size_t{ 1 } << var; + const size_t block = half << 1; + for (size_t base = 0; base < len; base += block) { + for (size_t i = base; i < base + half; ++i) { + sig[i + half] = (sig[i + half] - sig[i]) & mask; } } } diff --git a/test/core/test_coeff_interpolator.cpp b/test/core/test_coeff_interpolator.cpp index e1fa802..7bb82bb 100644 --- a/test/core/test_coeff_interpolator.cpp +++ b/test/core/test_coeff_interpolator.cpp @@ -221,6 +221,45 @@ TEST(CoeffInterpolatorTest, AllZeroSig) { // Interaction terms are zero — affine result } +// Property: the butterfly equals the naive subset Mobius transform, +// coeff[i] = sum_{j subset of i} (-1)^(popcount(i)-popcount(j)) * sig[j], +// across random signatures, variable counts, and bitwidths. +TEST(CoeffInterpolatorTest, MatchesNaiveMobius) { + uint64_t state = 0xC0B7A1234ULL; + auto next = [&state]() { + state += 0x9E3779B97F4A7C15ULL; + uint64_t z = state; + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); + }; + for (uint32_t num_vars : { 0u, 1u, 2u, 3u, 4u, 5u, 6u }) { + for (uint32_t bitwidth : { 1u, 8u, 16u, 32u, 64u }) { + const uint64_t mask = Bitmask(bitwidth); + const size_t len = size_t{ 1 } << num_vars; + std::vector< uint64_t > sig(len); + for (auto &s : sig) { s = next() & mask; } + + std::vector< uint64_t > expected(len, 0); + for (size_t i = 0; i < len; ++i) { + // Enumerate submasks j of i; sign by parity of |i| - |j|. + for (size_t j = i;; j = (j - 1) & i) { + uint64_t term = sig[j] & mask; + if (((std::popcount(i) - std::popcount(j)) & 1) != 0) { + term = (0 - term) & mask; + } + expected[i] = (expected[i] + term) & mask; + if (j == 0) { break; } + } + } + + auto got = InterpolateCoefficients(sig, num_vars, bitwidth); + EXPECT_EQ(got, expected) + << "num_vars=" << num_vars << " bitwidth=" << bitwidth; + } + } +} + // Large constant with wrapping at 8-bit TEST(CoeffInterpolatorTest, Bitwidth8LargeCoeffs) { // 200*x + 100*y at 8-bit: sig = [0, 200, 100, 44] From 0629d17587b2a07ec6b304a95b599ff1eda8cc84 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Mon, 29 Jun 2026 11:42:11 -0400 Subject: [PATCH 16/20] perf: reuse the interpolation grid across degrees in RecoverAndVerifyPoly Extract the forward-difference + factorial-coefficient conversion into RecoverFromGrid. RecoverAndVerifyPoly now keeps an incrementally-grown evaluation grid across degrees min..cap: each higher degree extends the grid by its new shell instead of re-evaluating the whole grid from scratch, so every support point is evaluated at most once. Reuse is exact because the evaluator is pure; per-degree results, early-exit, and reason codes are unchanged. RecoverMultivarPoly evaluates its grid then delegates to RecoverFromGrid, so its behavior is identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/MultivarPolyRecovery.cpp | 278 ++++++++++++++++++++---------- 1 file changed, 186 insertions(+), 92 deletions(-) diff --git a/lib/core/MultivarPolyRecovery.cpp b/lib/core/MultivarPolyRecovery.cpp index ef11b20..73d6595 100644 --- a/lib/core/MultivarPolyRecovery.cpp +++ b/lib/core/MultivarPolyRecovery.cpp @@ -27,6 +27,106 @@ namespace cobra { }; } // namespace multivar_poly + namespace { + + // Convert an evaluated {0..max_degree}^k grid (mixed-radix base + // max_degree+1, little-endian over support_vars) into factorial-basis + // coefficients via tensor-product forward differences. The grid is + // consumed in place. Validation is the caller's responsibility. + SolverResult< NormalizedPoly > RecoverFromGrid( + std::vector< uint64_t > table, const std::vector< uint32_t > &support_vars, + uint32_t total_num_vars, uint32_t bitwidth, uint8_t max_degree + ) { + const auto kK = static_cast< uint32_t >(support_vars.size()); + const uint64_t kMask = Bitmask(bitwidth); + const auto kBase = static_cast< size_t >(max_degree) + 1; + const size_t table_size = table.size(); + + // Tensor-product forward differences: max_degree passes per dimension + for (uint32_t dim = 0; dim < kK; ++dim) { + size_t stride = 1; + for (uint32_t i = 0; i < dim; ++i) { stride *= kBase; } + + for (uint32_t pass = 1; pass <= max_degree; ++pass) { + for (size_t idx = table_size; idx-- > 0;) { + const auto kCoord = static_cast< uint32_t >((idx / stride) % kBase); + if (kCoord < pass) { continue; } + table[idx] = (table[idx] - table[idx - stride]) & kMask; + } + } + } + + // Convert forward differences to factorial-basis coefficients + auto nv = static_cast< uint8_t >(total_num_vars); + NormalizedPoly result; + result.num_vars = nv; + result.bitwidth = bitwidth; + + std::array< uint8_t, kMaxPolyVars > exps{}; + + for (size_t idx = 0; idx < table_size; ++idx) { + const uint64_t kAlpha = table[idx]; + if (kAlpha == 0) { continue; } + + // Decode mixed-radix to per-variable exponents + exps.fill(0); + size_t tmp = idx; + uint32_t q = 0; + for (uint32_t i = 0; i < kK; ++i) { + const auto kE = static_cast< uint8_t >(tmp % kBase); + exps[support_vars[i]] = kE; + q += TwosInFactorial(kE); + tmp /= kBase; + } + + // Null-space term: 2^q >= bitwidth means coefficient vanishes + if (q >= bitwidth) { continue; } + + // Divisibility gate: alpha must be divisible by 2^q + if (q > 0) { + const uint64_t kLowBits = kAlpha & ((1ULL << q) - 1); + if (kLowBits != 0) { + ReasonDetail reason{ + .top = { .code = { ReasonCategory::kNoSolution, + ReasonDomain::kMultivarPoly, + multivar_poly::kDivisibilityFail }, + .message = "falling-factorial coefficient fails divisibility " + "gate" } + }; + return SolverResult< NormalizedPoly >::Blocked(std::move(reason)); + } + } + + const uint32_t kPrecBits = bitwidth - q; + + // Compute product of odd parts mod 2^kPrecBits + uint64_t odd_product = 1; + const uint64_t kPrecMask = + (kPrecBits >= 64) ? UINT64_MAX : ((1ULL << kPrecBits) - 1); + for (uint32_t i = 0; i < kK; ++i) { + uint8_t e = exps[support_vars[i]]; + if (e >= 2) { + odd_product = (odd_product * OddPartFactorial(e, kPrecBits)) & kPrecMask; + } + } + + // h = (alpha >> q) * ModInverseOdd(odd_product) mod 2^kPrecBits + uint64_t h = (kAlpha >> q) & kPrecMask; + if (odd_product != 1) { + h = (h * ModInverseOdd(odd_product, kPrecBits)) & kPrecMask; + } + + if (h == 0) { continue; } + + auto key = MonomialKey::FromExponents(exps.data(), nv); + result.coeffs[key] = h; + } + + return SolverResult< NormalizedPoly >::Success(std::move(result)); + } + + } // namespace + SolverResult< NormalizedPoly > RecoverMultivarPoly( const Evaluator &eval, const std::vector< uint32_t > &support_vars, uint32_t total_num_vars, uint32_t bitwidth, uint8_t max_degree @@ -97,87 +197,9 @@ namespace cobra { } for (uint32_t i = 0; i < kK; ++i) { point[support_vars[i]] = 0; } - // Tensor-product forward differences: max_degree passes per dimension - for (uint32_t dim = 0; dim < kK; ++dim) { - size_t stride = 1; - for (uint32_t i = 0; i < dim; ++i) { stride *= kBase; } - - for (uint32_t pass = 1; pass <= max_degree; ++pass) { - for (size_t idx = table_size; idx-- > 0;) { - const auto kCoord = static_cast< uint32_t >((idx / stride) % kBase); - if (kCoord < pass) { continue; } - table[idx] = (table[idx] - table[idx - stride]) & kMask; - } - } - } - - // Convert forward differences to factorial-basis coefficients - auto nv = static_cast< uint8_t >(total_num_vars); - NormalizedPoly result; - result.num_vars = nv; - result.bitwidth = bitwidth; - - std::array< uint8_t, kMaxPolyVars > exps{}; - - for (size_t idx = 0; idx < table_size; ++idx) { - const uint64_t kAlpha = table[idx]; - if (kAlpha == 0) { continue; } - - // Decode mixed-radix to per-variable exponents - exps.fill(0); - size_t tmp = idx; - uint32_t q = 0; - for (uint32_t i = 0; i < kK; ++i) { - const auto kE = static_cast< uint8_t >(tmp % kBase); - exps[support_vars[i]] = kE; - q += TwosInFactorial(kE); - tmp /= kBase; - } - - // Null-space term: 2^q >= bitwidth means coefficient vanishes - if (q >= bitwidth) { continue; } - - // Divisibility gate: alpha must be divisible by 2^q - if (q > 0) { - const uint64_t kLowBits = kAlpha & ((1ULL << q) - 1); - if (kLowBits != 0) { - ReasonDetail reason{ - .top = { .code = { ReasonCategory::kNoSolution, - ReasonDomain::kMultivarPoly, - multivar_poly::kDivisibilityFail }, - .message = "falling-factorial coefficient fails divisibility " - "gate" } - }; - return SolverResult< NormalizedPoly >::Blocked(std::move(reason)); - } - } - - const uint32_t kPrecBits = bitwidth - q; - - // Compute product of odd parts mod 2^kPrecBits - uint64_t odd_product = 1; - const uint64_t kPrecMask = - (kPrecBits >= 64) ? UINT64_MAX : ((1ULL << kPrecBits) - 1); - for (uint32_t i = 0; i < kK; ++i) { - uint8_t e = exps[support_vars[i]]; - if (e >= 2) { - odd_product = (odd_product * OddPartFactorial(e, kPrecBits)) & kPrecMask; - } - } - - // h = (alpha >> q) * ModInverseOdd(odd_product) mod 2^kPrecBits - uint64_t h = (kAlpha >> q) & kPrecMask; - if (odd_product != 1) { - h = (h * ModInverseOdd(odd_product, kPrecBits)) & kPrecMask; - } - - if (h == 0) { continue; } - - auto key = MonomialKey::FromExponents(exps.data(), nv); - result.coeffs[key] = h; - } - - return SolverResult< NormalizedPoly >::Success(std::move(result)); + return RecoverFromGrid( + std::move(table), support_vars, total_num_vars, bitwidth, max_degree + ); } SolverResult< PolyRecoveryResult > RecoverAndVerifyPoly( @@ -194,20 +216,92 @@ namespace cobra { return SolverResult< PolyRecoveryResult >::Inapplicable(std::move(reason)); } - // Each degree is recovered independently (no incremental reuse). - for (uint8_t d = min_degree; d <= max_degree_cap; ++d) { - auto poly = RecoverMultivarPoly(eval, support_vars, total_num_vars, bitwidth, d); - if (!poly.Succeeded()) { continue; } + // Validate the degree-independent preconditions once. RecoverMultivarPoly + // re-checks these per call; when they fail the old loop swallowed the + // Inapplicable into kNoVerifiedDegree, so mirror that by skipping the + // grid work and falling through to the exhaustion result. + bool inputs_ok = !support_vars.empty() && total_num_vars <= kMaxPolyVars + && bitwidth >= 2 && bitwidth <= 64; + if (inputs_ok) { + for (auto idx : support_vars) { + if (idx >= total_num_vars) { + inputs_ok = false; + break; + } + } + } + + if (inputs_ok) { + const auto kK = static_cast< uint32_t >(support_vars.size()); + const uint64_t kMask = Bitmask(bitwidth); + + // Incrementally-grown {0..d}^k evaluation grid (mixed-radix base + // d+1). Each distinct support point is evaluated once across all + // degrees: a higher degree extends the grid by its new shell rather + // than re-evaluating from scratch (evaluator calls dominate the + // per-degree cost). Reuse is exact because the evaluator is pure. + std::vector< uint64_t > grid; + uint32_t grid_base = 0; // 0 = empty; otherwise current degree + 1 + std::vector< uint64_t > point(total_num_vars, 0); + + auto eval_at = [&](size_t idx, uint32_t base) -> uint64_t { + size_t tmp = idx; + for (uint32_t i = 0; i < kK; ++i) { + point[support_vars[i]] = tmp % base; + tmp /= base; + } + const uint64_t v = eval(point) & kMask; + for (uint32_t i = 0; i < kK; ++i) { point[support_vars[i]] = 0; } + return v; + }; + + for (uint8_t d = min_degree; d <= max_degree_cap; ++d) { + if (d < 1) { continue; } // RecoverMultivarPoly rejects max_degree < 1 + + const uint32_t kBase = static_cast< uint32_t >(d) + 1; + size_t table_size = 1; + for (uint32_t i = 0; i < kK; ++i) { table_size *= kBase; } - auto expr = BuildPolyExpr(poly.TakePayload()); - if (!expr.has_value()) { continue; } + // Extend the grid to base kBase, evaluating only the new points. + if (grid_base == 0) { + grid.assign(table_size, 0); + for (size_t idx = 0; idx < table_size; ++idx) { + grid[idx] = eval_at(idx, kBase); + } + } else { + std::vector< uint64_t > grown(table_size, 0); + for (size_t idx = 0; idx < table_size; ++idx) { + size_t tmp = idx; + size_t old_idx = 0; + size_t place = 1; + bool is_old = true; + for (uint32_t i = 0; i < kK; ++i) { + const size_t kCoord = tmp % kBase; + tmp /= kBase; + if (kCoord >= grid_base) { is_old = false; } + old_idx += kCoord * place; + place *= grid_base; + } + grown[idx] = is_old ? grid[old_idx] : eval_at(idx, kBase); + } + grid = std::move(grown); + } + grid_base = kBase; + + // RecoverFromGrid consumes its grid in place, so pass a copy. + auto poly = RecoverFromGrid(grid, support_vars, total_num_vars, bitwidth, d); + if (!poly.Succeeded()) { continue; } - auto check = FullWidthCheckEval(eval, total_num_vars, *expr.value(), bitwidth); - if (!check.passed) { continue; } + auto expr = BuildPolyExpr(poly.TakePayload()); + if (!expr.has_value()) { continue; } - return SolverResult< PolyRecoveryResult >::Success( - PolyRecoveryResult{ std::move(expr.value()), d } - ); + auto check = FullWidthCheckEval(eval, total_num_vars, *expr.value(), bitwidth); + if (!check.passed) { continue; } + + return SolverResult< PolyRecoveryResult >::Success( + PolyRecoveryResult{ std::move(expr.value()), d } + ); + } } ReasonDetail reason{ .top = { .code = { ReasonCategory::kSearchExhausted, ReasonDomain::kMultivarPoly, From 6477ef951f63a72c449e55d2aed94c12ff657200 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Mon, 29 Jun 2026 13:22:04 -0400 Subject: [PATCH 17/20] perf: recycle per-node buffers in EvalSigRecursive The bottom-up boolean-signature evaluator allocated a fresh 2^n result vector per tree node. Thread a free-list buffer pool through the recursion: binary nodes release their right child's buffer, which leaf nodes then reuse. This bounds live buffers (and allocations) to ~tree depth instead of one per leaf. Output is bit-identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/SignatureEval.cpp | 76 ++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/lib/core/SignatureEval.cpp b/lib/core/SignatureEval.cpp index 253704c..f9d3c88 100644 --- a/lib/core/SignatureEval.cpp +++ b/lib/core/SignatureEval.cpp @@ -17,38 +17,62 @@ namespace cobra { namespace { - // Bottom-up evaluation: walk the expression tree once, - // computing all 2^n outputs per node instead of calling - // eval_expr 2^n times. Complexity: O(tree_size * 2^n) - // element-wise ops in a single tree walk, vs O(tree_size) - // recursive calls * 2^n invocations in the naive approach. - // The key win is eliminating function call/dispatch overhead. + // Reusable per-node buffers for EvalSigRecursive. Each node produces a + // length-2^n result; binary nodes free their right child's buffer back + // here so leaf acquisitions recycle it. This bounds live buffers (and + // thus allocations) to ~tree depth instead of one per leaf. + struct SigBufferPool + { + size_t len = 0; + std::vector< std::vector< uint64_t > > free; + + std::vector< uint64_t > Acquire() { + if (!free.empty()) { + std::vector< uint64_t > buf = std::move(free.back()); + free.pop_back(); + buf.resize(len); // no-op: every pooled buffer is already len + return buf; + } + return std::vector< uint64_t >(len); + } + + void Release(std::vector< uint64_t > &&buf) { free.push_back(std::move(buf)); } + }; + + // Bottom-up evaluation: walk the expression tree once, computing all + // 2^n outputs per node instead of calling eval_expr 2^n times. + // Complexity: O(tree_size * 2^n) element-wise ops in a single tree + // walk. Per-node result buffers are recycled through `pool` so the + // 2^n vectors are not malloc'd once per leaf. std::vector< uint64_t > - EvalSigRecursive(const Expr &expr, size_t len, uint32_t bitwidth) { + EvalSigRecursive(const Expr &expr, SigBufferPool &pool, uint32_t bitwidth) { const uint64_t kMask = Bitmask(bitwidth); + const size_t len = pool.len; switch (expr.kind) { case Expr::Kind::kConstant: { - return std::vector< uint64_t >(len, expr.constant_val & kMask); + auto r = pool.Acquire(); + std::fill(r.begin(), r.end(), expr.constant_val & kMask); + return r; } case Expr::Kind::kVariable: { - std::vector< uint64_t > r(len); + auto r = pool.Acquire(); const uint32_t kIdx = expr.var_index; for (size_t i = 0; i < len; ++i) { r[i] = (i >> kIdx) & 1; } return r; } case Expr::Kind::kNot: { - auto child = EvalSigRecursive(*expr.children[0], len, bitwidth); + auto child = EvalSigRecursive(*expr.children[0], pool, bitwidth); for (size_t i = 0; i < len; ++i) { child[i] = (~child[i]) & kMask; } return child; } case Expr::Kind::kNeg: { - auto child = EvalSigRecursive(*expr.children[0], len, bitwidth); + auto child = EvalSigRecursive(*expr.children[0], pool, bitwidth); for (size_t i = 0; i < len; ++i) { child[i] = (-child[i]) & kMask; } return child; } case Expr::Kind::kShr: { - auto child = EvalSigRecursive(*expr.children[0], len, bitwidth); + auto child = EvalSigRecursive(*expr.children[0], pool, bitwidth); const uint64_t kK = expr.constant_val; if (kK >= 64) { std::fill(child.begin(), child.end(), 0); @@ -60,33 +84,38 @@ namespace cobra { return child; } case Expr::Kind::kAdd: { - auto left = EvalSigRecursive(*expr.children[0], len, bitwidth); - auto right = EvalSigRecursive(*expr.children[1], len, bitwidth); + auto left = EvalSigRecursive(*expr.children[0], pool, bitwidth); + auto right = EvalSigRecursive(*expr.children[1], pool, bitwidth); for (size_t i = 0; i < len; ++i) { left[i] = (left[i] + right[i]) & kMask; } + pool.Release(std::move(right)); return left; } case Expr::Kind::kMul: { - auto left = EvalSigRecursive(*expr.children[0], len, bitwidth); - auto right = EvalSigRecursive(*expr.children[1], len, bitwidth); + auto left = EvalSigRecursive(*expr.children[0], pool, bitwidth); + auto right = EvalSigRecursive(*expr.children[1], pool, bitwidth); for (size_t i = 0; i < len; ++i) { left[i] = (left[i] * right[i]) & kMask; } + pool.Release(std::move(right)); return left; } case Expr::Kind::kAnd: { - auto left = EvalSigRecursive(*expr.children[0], len, bitwidth); - auto right = EvalSigRecursive(*expr.children[1], len, bitwidth); + auto left = EvalSigRecursive(*expr.children[0], pool, bitwidth); + auto right = EvalSigRecursive(*expr.children[1], pool, bitwidth); for (size_t i = 0; i < len; ++i) { left[i] = left[i] & right[i]; } + pool.Release(std::move(right)); return left; } case Expr::Kind::kOr: { - auto left = EvalSigRecursive(*expr.children[0], len, bitwidth); - auto right = EvalSigRecursive(*expr.children[1], len, bitwidth); + auto left = EvalSigRecursive(*expr.children[0], pool, bitwidth); + auto right = EvalSigRecursive(*expr.children[1], pool, bitwidth); for (size_t i = 0; i < len; ++i) { left[i] = (left[i] | right[i]) & kMask; } + pool.Release(std::move(right)); return left; } case Expr::Kind::kXor: { - auto left = EvalSigRecursive(*expr.children[0], len, bitwidth); - auto right = EvalSigRecursive(*expr.children[1], len, bitwidth); + auto left = EvalSigRecursive(*expr.children[0], pool, bitwidth); + auto right = EvalSigRecursive(*expr.children[1], pool, bitwidth); for (size_t i = 0; i < len; ++i) { left[i] = (left[i] ^ right[i]) & kMask; } + pool.Release(std::move(right)); return left; } } @@ -116,7 +145,8 @@ namespace cobra { auto t0 = std::chrono::high_resolution_clock::now(); #endif const size_t kLen = size_t{ 1 } << num_vars; - auto result = EvalSigRecursive(expr, kLen, bitwidth); + SigBufferPool pool{ .len = kLen, .free = {} }; + auto result = EvalSigRecursive(expr, pool, bitwidth); #ifdef COBRA_SIG_STATS auto t1 = std::chrono::high_resolution_clock::now(); double us = std::chrono::duration< double, std::micro >(t1 - t0).count(); From 554407e76c6ac1cce130675680e29dd199b7e036 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Mon, 29 Jun 2026 13:22:05 -0400 Subject: [PATCH 18/20] perf: precompute falling-factorial table in WeightedPolyFit The matrix build recomputed FallingFactorial(coord, exp) once per (row, col, dim) for only (grid_base) x (per_var_cap+1) distinct pairs. Precompute the table once per solve and look it up. Bit-identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/WeightedPolyFit.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/core/WeightedPolyFit.cpp b/lib/core/WeightedPolyFit.cpp index 3541f36..13c708c 100644 --- a/lib/core/WeightedPolyFit.cpp +++ b/lib/core/WeightedPolyFit.cpp @@ -166,6 +166,20 @@ namespace cobra { std::vector< uint64_t > full_point(total_num_vars, 0); std::vector< uint64_t > local_point(kK, 0); + // Precompute falling factorials over the grid. The matrix build + // below would otherwise recompute FallingFactorial(coord, exp) once + // per (row, col, dim) for only kGridBase x (kPerVarCap+1) distinct + // (coord, exp) pairs. + std::vector< std::vector< uint64_t > > falling( + kGridBase, std::vector< uint64_t >(static_cast< size_t >(kPerVarCap) + 1, 0) + ); + for (size_t coord = 0; coord < kGridBase; ++coord) { + for (uint32_t e = 0; e <= kPerVarCap; ++e) { + falling[coord][e] = + FallingFactorial(coord, static_cast< uint8_t >(e), kMask); + } + } + for (size_t row = 0; row < num_rows; ++row) { size_t tmp = row; for (uint32_t i = 0; i < kK; ++i) { @@ -178,9 +192,7 @@ namespace cobra { for (size_t col = 0; col < kNumCols; ++col) { uint64_t phi = 1; for (uint32_t i = 0; i < kK; ++i) { - phi = - (phi * FallingFactorial(local_point[i], basis_exps[col][i], kMask)) - & kMask; + phi = (phi * falling[local_point[i]][basis_exps[col][i]]) & kMask; } mat[row][col] = (kWVal * phi) & kMask; } From e05d15432a72a333b86a0f33e3fa42154e5a8638 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Mon, 29 Jun 2026 13:22:05 -0400 Subject: [PATCH 19/20] perf: precompute odd-part factorials in RecoverFromGrid OddPartFactorial(e, bw) equals the 64-bit odd-part product masked to bw, so build a per-degree table once (incrementally) and use it under the existing kPrecMask instead of recomputing the odd-part product per monomial. Bit-identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/MultivarPolyRecovery.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/core/MultivarPolyRecovery.cpp b/lib/core/MultivarPolyRecovery.cpp index 73d6595..cb338ac 100644 --- a/lib/core/MultivarPolyRecovery.cpp +++ b/lib/core/MultivarPolyRecovery.cpp @@ -64,6 +64,17 @@ namespace cobra { std::array< uint8_t, kMaxPolyVars > exps{}; + // Per-degree odd-part factorials at full 64-bit width, built once. + // OddPartFactorial(e, bw) == odd_fact[e] & Bitmask(bw), so the + // per-monomial product below uses the table directly under the + // existing kPrecMask instead of recomputing the odd-part product. + std::vector< uint64_t > odd_fact(static_cast< size_t >(max_degree) + 1, 1); + for (uint32_t e = 2; e <= max_degree; ++e) { + uint32_t odd = e; + while ((odd & 1) == 0) { odd >>= 1; } + odd_fact[e] = odd_fact[e - 1] * odd; + } + for (size_t idx = 0; idx < table_size; ++idx) { const uint64_t kAlpha = table[idx]; if (kAlpha == 0) { continue; } @@ -99,15 +110,15 @@ namespace cobra { const uint32_t kPrecBits = bitwidth - q; - // Compute product of odd parts mod 2^kPrecBits + // Product of odd parts mod 2^kPrecBits, via the precomputed + // table (masking by kPrecMask is equivalent to computing the + // odd-part product at width kPrecBits). uint64_t odd_product = 1; const uint64_t kPrecMask = (kPrecBits >= 64) ? UINT64_MAX : ((1ULL << kPrecBits) - 1); for (uint32_t i = 0; i < kK; ++i) { uint8_t e = exps[support_vars[i]]; - if (e >= 2) { - odd_product = (odd_product * OddPartFactorial(e, kPrecBits)) & kPrecMask; - } + if (e >= 2) { odd_product = (odd_product * odd_fact[e]) & kPrecMask; } } // h = (alpha >> q) * ModInverseOdd(odd_product) mod 2^kPrecBits From c7886dc840fc7cbb6536d4fcf82ccdc0c118d081 Mon Sep 17 00:00:00 2001 From: kyle-elliott-tob Date: Tue, 30 Jun 2026 10:18:02 -0400 Subject: [PATCH 20/20] style: apply clang-format to satisfy the lint CI check Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/core/ExprCost.cpp | 2 +- lib/core/MultivarPolyRecovery.cpp | 11 ++++++----- lib/core/Orchestrator.cpp | 6 +++--- lib/core/PatternMatcher.cpp | 6 ++---- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/core/ExprCost.cpp b/lib/core/ExprCost.cpp index 0871ab6..b6107f0 100644 --- a/lib/core/ExprCost.cpp +++ b/lib/core/ExprCost.cpp @@ -94,7 +94,7 @@ namespace cobra { bool IsCostBlowup(const ExprCost &candidate, const ExprCost &baseline) { // Output must exceed this multiple of the input to count as a blow-up. - constexpr uint32_t kBlowupRatio = 2; + constexpr uint32_t kBlowupRatio = 2; // ...and exceed this absolute weighted size, sparing the small ~2x // expansions of legitimate canonicalization on tiny inputs. constexpr uint32_t kBlowupAbsFloor = 32; diff --git a/lib/core/MultivarPolyRecovery.cpp b/lib/core/MultivarPolyRecovery.cpp index cb338ac..7aded93 100644 --- a/lib/core/MultivarPolyRecovery.cpp +++ b/lib/core/MultivarPolyRecovery.cpp @@ -98,11 +98,12 @@ namespace cobra { const uint64_t kLowBits = kAlpha & ((1ULL << q) - 1); if (kLowBits != 0) { ReasonDetail reason{ - .top = { .code = { ReasonCategory::kNoSolution, - ReasonDomain::kMultivarPoly, - multivar_poly::kDivisibilityFail }, - .message = "falling-factorial coefficient fails divisibility " - "gate" } + .top = { .code = { ReasonCategory::kNoSolution, + ReasonDomain::kMultivarPoly, + multivar_poly::kDivisibilityFail }, + .message = + "falling-factorial coefficient fails divisibility " + "gate" } }; return SolverResult< NormalizedPoly >::Blocked(std::move(reason)); } diff --git a/lib/core/Orchestrator.cpp b/lib/core/Orchestrator.cpp index 529e552..37ccae3 100644 --- a/lib/core/Orchestrator.cpp +++ b/lib/core/Orchestrator.cpp @@ -1029,9 +1029,9 @@ namespace cobra { // Cost of the raw input, computed once and reused by every return path // (the blow-up gate in ToSimplifyOutcome and the XOR exhaustion // fallback). Absent when there is no input AST (sig-only path). - const std::optional< ExprCost > input_cost = - input_expr ? std::optional< ExprCost >(ComputeCost(*input_expr).cost) - : std::nullopt; + const std::optional< ExprCost > input_cost = input_expr + ? std::optional< ExprCost >(ComputeCost(*input_expr).cost) + : std::nullopt; // Seeding if (input_expr == nullptr) { diff --git a/lib/core/PatternMatcher.cpp b/lib/core/PatternMatcher.cpp index 8a318fd..716d708 100644 --- a/lib/core/PatternMatcher.cpp +++ b/lib/core/PatternMatcher.cpp @@ -1442,13 +1442,11 @@ namespace cobra { case Expr::Kind::kNeg: return AdditiveHasBinaryAnd(*e.children[0]); case Expr::Kind::kMul: - if (e.children.size() == 2 - && e.children[0]->kind == Expr::Kind::kConstant) + if (e.children.size() == 2 && e.children[0]->kind == Expr::Kind::kConstant) { return AdditiveHasBinaryAnd(*e.children[1]); } - if (e.children.size() == 2 - && e.children[1]->kind == Expr::Kind::kConstant) + if (e.children.size() == 2 && e.children[1]->kind == Expr::Kind::kConstant) { return AdditiveHasBinaryAnd(*e.children[0]); }