Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement ac_nf', an alternative normalizer that takes terms shared across both sides of an equality into account #43

Draft
wants to merge 91 commits into
base: master
Choose a base branch
from

Conversation

alexkeizer
Copy link

@alexkeizer alexkeizer commented Jan 22, 2025

This PR adds ac_nf', a tactic that normalizes terms up to commutativity and associativity, much like ac_nf, but does it in a way that exposes common terms among both sides of an equality. For example, x₁ * (y₁ * z) = x₂ * (y₂ * z) is normalized to z * (x₁ * y₁) = z * (x₂ * y₂), pulling the shared variable z to the front on both sides. The PR also replaces the use of ac_nf in the normalization pass of bv_decide.

This tactic reuses the existing ac_nf implementation to (a) normalize compound atomic expression (e.g., x + y and y + x in x * (y + x) * (x + y)) and (b) produce an equivalence proof once a sharing-aware normal form has been determined. The new ac_nf' tactic is thus not a replacement for ac_nf.

Furthermore, we only run the normalization on terms of the form (a * b) = (c * d) for a, b, c, d bitvectors. This mirrors Bitwuzla's PassNormalize::process's PassNormalize::normalize_eq_add_mul. We leave the implementation of PassNormalize::normalize_comm_assoc, which is called when the toplevel terms are different for a subsequent patch.

For posterity, we record the precise location in Bitwuzla where the codepath occurs:

-- https://github.com/bitwuzla/bitwuzla/blob/d1f1bc2ad3963936d3167afb879867364554cee7/src/preprocess/pass/normalize.cpp#L1550-L1554
        Kind k = cur.kind();
        if (k == Kind::EQUAL && children[0].kind() == children[1].kind()
            && (children[0].kind() == Kind::BV_ADD
                || children[0].kind() == Kind::BV_MUL))
        {
          auto [res, norm] = normalize_eq_add_mul(children[0], children[1]);
          ...

@alexkeizer
Copy link
Author

Currently, I've deliberately implemented the new ac_nf! without touching the implementation of ac_nf at all -- I do use ac_nf, but simply by calling the tactic to construct a proof, I don't share any datastructures or computation in the implementation.

This was deliberate, as I'd expect changes to ac_nf to invite additional scrutiny in review, but it does mean we're wasting some time by computing similar things twice. Then again, as the benchmark showed, tactic runtime is not a bottleneck at all, so this might be entirely fine. We might be able to also generate a more optimized proof-term by sharing implementation, which would be slightly more significant, but I'm not a 100% sure how much benefit there is on that side.

@bollu @hargoniX, thoughts?

@tobiasgrosser
Copy link

I think this was mostly fine. There was one change to ac_nf that deliberately changes the proof terms it generates to avoid overly eager kernel reduction. We should make sure these fixes also work in our case (change ac_nf history for details).

@alexkeizer alexkeizer changed the title feat: implement ac_nf!, an alternative normalizer that takes terms shared across both sides of an equality into account feat: implement ac_nf', an alternative normalizer that takes terms shared across both sides of an equality into account Jan 24, 2025
src/Lean/Elab/Tactic/BVDecide/Frontend/Normalize/AC.lean Outdated Show resolved Hide resolved
@@ -1,39 +1,362 @@
/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Copyright (c) 2024 Lean FRO, LLC. All rights reserved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time travelling copyright is interesting :P Also an interesting question whether this file's copyright should be FRO if neither of the authors are associated with the FRO.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The year is indeed wrong. Not sure what to do with the copyright: a tiny part of the original file (the declaration of the pass) remains, not sure what the copyright implications of that are

src/Lean/Elab/Tactic/BVDecide/Frontend/Normalize/AC.lean Outdated Show resolved Hide resolved
src/Lean/Elab/Tactic/BVDecide/Frontend/Normalize/AC.lean Outdated Show resolved Hide resolved
src/Lean/Elab/Tactic/BVDecide/Frontend/Normalize/AC.lean Outdated Show resolved Hide resolved

example (x y : BitVec 256) : x * y = y * x := by
bv_decide (config := { acNf := true })

example {x y z : BitVec 64} : ~~~(x &&& (y * z)) = (~~~x ||| ~~~(z * y)) := by

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we have to change this test case? Does it not work quickly anymore? That would be quite the shame.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bollu, I believe you made this change?

src/Lean/Elab/Tactic/BVDecide/Frontend/Normalize/AC.lean Outdated Show resolved Hide resolved
src/Lean/Elab/Tactic/BVDecide/Frontend/Normalize/AC.lean Outdated Show resolved Hide resolved
src/Lean/Elab/Tactic/BVDecide/Frontend/Normalize/AC.lean Outdated Show resolved Hide resolved
let simpCtx ← Simp.mkContext
(simpTheorems := {})
(congrTheorems := (← getSimpCongrTheorems))
(config := Simp.neutralConfig)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to thread the max rewrite steps from PreProcessM through to this simp call in order to make sure that on SMTLIB we do not run out of rewrite steps for large expressions.

@bollu
Copy link

bollu commented Feb 14, 2025

(Sid's notes on the algorithmic details are at: https://gist.github.com/bollu/e204a1f4d5a042695eee9cbaebce214a)

alexkeizer and others added 16 commits February 14, 2025 15:57
This removes the need to carry it around in the `Op`. This is technically less general, as it only works with the default instance, but the rest of the code assumes the instance is the default one regardless (e.g., when determining the neutral element), so in practice there is no difference
Earlier passes in bv_normalize should have already rewritten all `Eq`s to Beq
This removes the need to infer the universe
This def lives in the `BVDecide.Frontend.Normalize` namespace, so it deserves a name that ties it to bvAcNf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants