Skip to content

Conversation

baierd
Copy link
Contributor

@baierd baierd commented Sep 2, 2025

While implementing PR 512 i found a problem in our implementation of FloatingPointType and number. Our returned significant precision is off by one, see SMTLib2 standard page for Floating-Points. E.g. for single precision FP, we return significant size 23. But the standard defines exponent and significant as:

eb defines the number of bits in the exponent;
sb defines the number of bits in the significand, *including* the hidden bit.

Hence it should be 24.
As a consequence, we also return this wrongly for FormulaType method toSMTLIBString().
This is fixed in this PR.
We also encode the significant without the hidden bit in toString(), and subsequently in fromString.
While this is in general not as bad, i changed it to include the hidden bit now (in both), so that it is in-line with the standard.
These behavior changes should be communicated clearly to users!

The solvers generally expect the hidden bit to be part of the significant, hence why we had 4 solvers with the code type.getMantissaSize() + 1, and only one solver without the + 1 (MathSAT5).

The general behavior of FP creation/solving is correct IFF you know that the mantissa does not include the sign bit.

Also, we don't communicate any information about our interpretation of FP precisions, hence the users might think that by building FormulaType.getFloatingPointType(8, 24) (first input is exponent, second is significant) they get a single precision FP-type, but in reality they get a total size of 33 bits currently. Our static implementation of the single/double precisions is fine however.

This PR adds/changes the following:

  • Add new constructors for FP type that specify the sign bit inclusion in the method name. Also, update the API for getMantissaSize() to getMantissaSizeWithHiddenBit() and getMantissaSizeWithoutHiddenBit().
  • Use the less ambiguous API in our internal implementation, reducing confusion about magic +-1
  • Including the hidden bit in the documentation of relevant API, as well as the parameter names.
  • Deprecate the old FP type constructor and mantissa size getters.
  • Return the correct SMTLIB2 interpretation for FloatingPointType.toSMTLIBString().
  • Change behavior of FloatingPointType.toString() and FloatingPointType.fromString() to include the hidden bit in the significant.
  • Deprecated public constants for FP single/double precisions, as they do not specify the in/exclusion of the hidden bit and will be moved to internal API. Also added new constants that are not public for mantissas that specify the hidden bit exclusion.
  • Adds tests for FP precision sizes, as well as FloatingPointType.toString() etc.

I feel like the API changes reduce the magic + 1 and - 1 by a large amount, increasing readability of the code. My guess is that users, especially new users, will benefit from that. The deprecation warning needs to be discussed/done.

@baierd baierd self-assigned this Sep 2, 2025
@PhilippWendler
Copy link
Member

What is the actual effect of this MR (supposed to be)? Is it purely an API improvement that adds new methods and deprecates some old ones, or does it actually change the behavior in some cases? From your previous off-line explanations and from the title ("Fix") I would assume that there was actually broken behavior that is now fixed. But in the description of the MR I do not find anything related to this and it reads as if there is just new stuff and no behavior changes. Please make clear which is the case, and if no existing behavior is actually broken and needs to be changed, reflect this in the MR title.

Fix FP type etc. to include the sign bit, and switch to what the standard defines in our internal implementation. Only MathSAT5 expects the mantissa to not include the sign bit.

This wording doesn't really make sense. The FloatingPointType class itself (I assume this is what you mean) is not broken and does not need to be fixed, and the MR also does not attempt to fix it nor changes anything in it (except for toString()). What is broken and fixed is only that there are existing methods with unclear names and missing documentation.

@baierd
Copy link
Contributor Author

baierd commented Sep 2, 2025

What is the actual effect of this MR (supposed to be)? Is it purely an API improvement that adds new methods and deprecates some old ones, or does it actually change the behavior in some cases?

The behavior is unchanged. It only improves the API, adds new methods, and improves code readability/maintainability.

The main problem is that the standard defines the mantissa to include the sign bit, but our API does not. 4/5 SMT solvers with FP support in JavaSMT handle it the same way. Only some parts of the related API are documented, hence users might assume that our API works like the standard or the majority of the solvers describe it. This PR aims to fix this ambiguity. I improved the wording above.

@baierd baierd requested a review from kfriedberger September 2, 2025 09:00
@PhilippWendler
Copy link
Member

For the record: I find it good that you decided to not add getMantissaSizeWithSignBit() and getExponentSize() to FloatingPointFormulaManager as it was done in #512. These methods are not necessary, encourage bad code (users should pass around FP size information as FloatingPointType instances instead of two ints), and their implementations contained redundant code that duplicated already existing code in JavaSMT (making it more error prone).

Copy link
Member

@PhilippWendler PhilippWendler left a comment

Choose a reason for hiding this comment

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

It think I found several bugs. It seems this strongly highlights the need for more tests covering this.

@Override
public String toSMTLIBString() {
return "(_ FloatingPoint " + exponentSize + " " + mantissaSize + ")";
return "(_ FloatingPoint " + exponentSize + " " + getMantissaSizeWithSignBit() + ")";
Copy link
Member

Choose a reason for hiding this comment

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

Hm, this seems to be a desired behavior change of this MR, isn't it?

So it would mean that there are not just API improvements but also an actual bug fix. Hopefully this method is rarely used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The SMTLIB2 standard says that our previous implementation is wrong here, as statet above As a consequence, we also return this wrongly for toSMTLIBString() etc.. Example from the standard: - Float32 is a synonym for (_ FloatingPoint 8 24). As a consequence i would change it and communicate this to the users.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I updated the initial PR text accordingly.

Copy link
Member

Choose a reason for hiding this comment

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

This one-line-fix could have been part of a separate PR, which might have been accepted much easier.

Copy link
Member

Choose a reason for hiding this comment

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

Where can I find (new or updated) tests for that method?

@baierd
Copy link
Contributor Author

baierd commented Sep 7, 2025

too many open questions.

The whole PR never ever mentions once that the SMTLIB definition could be found online: https://smt-lib.org/theories-FloatingPoint.shtml

Sorry 😅, i assumed you guys know. My bad.

@baierd
Copy link
Contributor Author

baierd commented Sep 12, 2025

Just for clarification: we're returning 23 because we don't include the hidden bit in the significand size. Here hidden bit refers to the first digit of the significand, which is dropped from the floating point representation. So, for instance, instead of 1.100111*2^-3 only .100111*2^-3 will be written out. This saves precious space and the hidden bit can always be restored by looking at the exponent.
Some of the new names here suggest to me that the sign bit was missing from the old size. This is a bit unfortunate, as the sign is never included in the size of the significand. It just happends to have the same size (= 1) as the hidden bit, so the math works out. However, these are really different concepts, and I think we should be careful to not add any more confusion for the users
Maybe we could still find some better names?

Ha, this is true. One should have really looked into the SMTLIB standard before doing all this work:

 :notes
 "eb defines the number of bits in the exponent;
  sb defines the number of bits in the significand, *including* the hidden bit.
 " 

So all cases of "with(out)SignBit" need to be renamed to "with(out)HiddenBit". And for getTotalSize() we might need to rediscuss the name.

I renamed all API referencing the sign bit to reference the hidden bit instead.

The total size is easier in my opinion, as the total size always includes the hidden bit.

@baierd
Copy link
Contributor Author

baierd commented Sep 12, 2025

too many open questions.

The whole PR never ever mentions once that the SMTLIB definition could be found online: https://smt-lib.org/theories-FloatingPoint.shtml

I updated the PR text with all the changes and added the reference.

@PhilippWendler
Copy link
Member

And for getTotalSize() we might need to rediscuss the name.

The total size is easier in my opinion, as the total size always includes the hidden bit.

No, it doesn't. If we want getTotalSize() to return 32 for single precision, then the total size includes exponent size + mantissa size without hidden bit + sign bit (the current code for this is wrong because it misses the sign bit, it just happens to compute the same result).

@baierd
Copy link
Contributor Author

baierd commented Sep 12, 2025

And for getTotalSize() we might need to rediscuss the name.

The total size is easier in my opinion, as the total size always includes the hidden bit.

No, it doesn't. If we want getTotalSize() to return 32 for single precision, then the total size includes exponent size + mantissa size without hidden bit + sign bit (the current code for this is wrong because it misses the sign bit, it just happens to compute the same result).

Fair point, the description was wrong! I fixed the JavaDoc in both getTotalSize() calls and made it more explicit in the code. I need to check whether we have more incorrect references to the components of the total size though.

Still, i would argue that we need to rename getTotalSize() because of this?

@PhilippWendler
Copy link
Member

Still, i would argue that we need to rename getTotalSize() because of this?

If all other methods that do not include the hidden bit are called ...WithoutHiddenBit, and then we have a method called getTotalSize(), wouldn't you assume that "total" includes everything, i.e., also the hidden bit? At least I thought it should be considered.

@baierd
Copy link
Contributor Author

baierd commented Sep 12, 2025

Still, i would argue that we need to rename getTotalSize() because of this?

If all other methods that do not include the hidden bit are called ...WithoutHiddenBit, and then we have a method called getTotalSize(), wouldn't you assume that "total" includes everything, i.e., also the hidden bit? At least I thought it should be considered.

That is a fair point!
@kfriedberger what do you think?

@kfriedberger
Copy link
Member

I agree with Philipp and would also assume that totalSize covers all three components, i.e., exponent, mantissa, and sign bit.

@baierd
Copy link
Contributor Author

baierd commented Sep 12, 2025

I agree with Philipp and would also assume that totalSize covers all three components, i.e., exponent, mantissa, and sign bit.

I see it the same way, but would you rename getTotalSize()?

@PhilippWendler
Copy link
Member

I agree with Philipp and would also assume that totalSize covers all three components, i.e., exponent, mantissa, and sign bit.

Nobody has argued against this. The current question is what should the relation between getTotalSize() and the hidden mantissa bit be, in particular whether the method should be renamed because it does count in the hidden bit.

Copy link
Member

@PhilippWendler PhilippWendler left a comment

Choose a reason for hiding this comment

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

It seems there were some mistakes introduced in the change from "sign bit" to "hidden bit". I am not sure whether I found all, somebody else should also review everything in detail.

int mantissaSizeWithoutHiddenBit = pTargetType.getMantissaSizeWithoutHiddenBit();
int size = pTargetType.getTotalSize();
assert size == mantissaSize + exponentSize + 1;
// total size = mantissa without hidden bit + hidden bit + exponent
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// total size = mantissa without hidden bit + hidden bit + exponent
// total size = mantissa without hidden bit + size bit + exponent

assertThat(msat_get_bv_type_size(env, msat_term_get_type(bvNumber))).isEqualTo(totalBVSize);

int exponent = 8;
int mantissaWithoutSign = 23; // excluding hidden bit
Copy link
Member

Choose a reason for hiding this comment

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

Outdated name.

int expWidth = Integer.parseInt(matcher.group(2));
int mantWidth = Integer.parseInt(matcher.group(3));
// The term representation in MathSAT5 does not include the hidden bit
int mantWidthWithoutSignBit = Integer.parseInt(matcher.group(3));
Copy link
Member

Choose a reason for hiding this comment

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

Outdated name.

final long signSort = getFormulaCreator().getBitvectorType(1);
final long expoSort = getFormulaCreator().getBitvectorType(type.getExponentSize());
final long mantSort = getFormulaCreator().getBitvectorType(type.getMantissaSize());
final long mantSort =
Copy link
Member

Choose a reason for hiding this comment

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

Does Z3's mkFpaFp expect the mantissa to be passed with our without hidden bit? If the former, the bitvector below is also created incorrectly.

mant,
pType.getExponentSize(),
pType.getMantissaSize());
pType.getMantissaSizeWithSignBit());
Copy link
Member

Choose a reason for hiding this comment

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

Even with the current changes, this is still a behavioral change, and it is unclear whether it is a bug fix or breaks something.

I think all changes in Z3FormulaCreator need to be checked carefully against the Z3 docs to make sure that they really match.

Comment on lines +278 to +279
* format, according to the given type. The sum of the sizes of exponent and mantissa (including
* the hidden bit) of the target type needs to be equal to the size of the bitvector.
Copy link
Member

Choose a reason for hiding this comment

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

This is wrong, right? It needs to refer to the sign bit, but exclude the hidden bit.

* Create a formula that produces a representation of the given floating-point value as a
* bitvector conforming to the IEEE format. The size of the resulting bitvector is the sum of the
* sizes of the exponent and mantissa of the input formula plus 1 (for the sign bit).
* sizes of the exponent and mantissa (including the hidden bit) of the input formula.
Copy link
Member

Choose a reason for hiding this comment

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

Same as above.

/**
* Returns true if this floating-point number is an IEEE-754-2008 single precision type with 32
* bits length consisting of an 8 bit exponent, a 23 bit mantissa and a single sign bit.
* bits length consisting of an 8 bit exponent, a 24 bit mantissa (including the hidden bit).
Copy link
Member

Choose a reason for hiding this comment

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

The sentence is grammatically wrong now, and it misses the sign bit.

/**
* Returns true if this floating-point number is an IEEE-754-2008 double precision type with 64
* bits length consisting of an 11 bit exponent, a 52 bit mantissa and a single sign bit.
* bits length consisting of an 11 bit exponent, a 53 bit mantissa (including the hidden bit).
Copy link
Member

Choose a reason for hiding this comment

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

Same as above.

@kfriedberger
Copy link
Member

I agree with Philipp and would also assume that totalSize covers all three components, i.e., exponent, mantissa, and sign bit.

Nobody has argued against this. The current question is what should the relation between getTotalSize() and the hidden mantissa bit be, in particular whether the method should be renamed because it does count in the hidden bit.

A user might prefer shorter significant names, ... so getTotalSize() is sufficient and does not need a renaming.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants