Read a float symbol back as a float - #80
Open
HowardvanRooijen wants to merge 1 commit into
Open
HowardvanRooijen wants to merge 1 commit into
HowardvanRooijen wants to merge 1 commit into
Conversation
TypeCode.Single parsed the model value with double.Parse, so a solved float symbol was boxed as a double and reflection refused to write it to a float member. Translation, solving and the model were all correct; only the last step lost the answer. Parsing straight to float rounds once rather than parsing to double and narrowing. The array element arm carried the same defect and is corrected with it, though no test can reach it while #64 stands. Fixes #54. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes marshalling of solved float symbols so TypeCode.Single values are read back as float (not double), preventing reflection assignment failures when populating the solution object.
Changes:
- Updated
TypeCode.Singleresult parsing inTheorem.ConvertZ3Expression(both scalar and array-element paths) fromdouble.Parse(...)tofloat.Parse(...). - Added/updated tests to validate
floatround-tripping (including alongsidedouble), and adjusted culture-invariance coverage to assert correctfloatresults now that marshalling succeeds. - Added characterization tests to explicitly pin known defects (#6 non-terminating decimal expansions; #64 float-array translation failure) while documenting why some code paths remain unreachable.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| solutions/Z3.Linq/Theorem.cs | Fixes float model-value parsing to produce boxed float values for TypeCode.Single (scalar + array element loop). |
| solutions/Z3.Linq.Tests/SymbolTypeMarshallingTests.cs | Adds float round-trip coverage, float/double cohabitation coverage, and new pins for #6-related parse failures. |
| solutions/Z3.Linq.Tests/CultureInvarianceTests.cs | Updates the float culture test to round-trip assert (no longer only asserting an exception). |
| solutions/Z3.Linq.Tests/CollectionSymbolTests.cs | Adds a pinned test for float[] still failing under #64 and documents why the array-element fix is currently unobservable. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #54.
The defect
TypeCode.Singleread the model value withdouble.Parse:so a solved
floatsymbol came back boxed as adouble, and the reflection write that followsrefused it:
Nothing else about the solve was wrong. Declaring the symbol (
MkRealConst), translating theconstant and solving all succeeded; only the last step lost the answer. Measured across the shapes
a float symbol can take:
X1 == 1.5fArgumentException1.5X1 == 0.1f- not exactly representable in binaryArgumentException0.1X1 == 1.2345678f- eight significant digitsArgumentException1.2345678X1 == -2.75fArgumentException-2.75X1 == float.MaxValueArgumentException3.4028235E+382.5f < X1 < 2.75fArgumentException2.625ArgumentException0OrderByDescendingArgumentException4.5ArgumentException1.5doublesymbolArgumentException1.5/2.5float[]Z3Exception(#64)Z3Exception(#64)X1 * 3 == 1FormatException(#6)FormatException(#6)X1 == float.EpsilonFormatException(#6)FormatException(#6)The change
Two characters at each of two sites:
Why
float.Parserather than(float)double.Parse. The issue notes a design choice aboutprecision here. Parsing to
doubleand narrowing rounds twice - to the nearest double, then to thenearest float - and the two roundings can disagree with a single rounding to float. Parsing
straight to float rounds once, which is the answer the model actually names.
Being straight about the evidence: I could not make that difference observable. Both forms were
tried against the strings this path produces, including the classic double-rounding candidates
16777217and1.00000005960464477539062, and every result was bit-identical; the whole suitepasses either way. So the choice rests on the argument rather than on a failing test, and the PR
should not pretend otherwise.
The array element arm at
Theorem.cs:471carried the same defect and is corrected in the samecommit, on the issue's own reading. It cannot be verified: a
float[]declares a floating-pointrange against a real-valued constraint and dies during translation under #64, so reverting that
site alone fails nothing at all. Stated plainly rather than counted as covered - the same position
PR #73 took on the decimal element site.
What is deliberately not changed is the
32inToDecimalString(32). It reads as a nod tofloat's 32 bits but is a count of decimal places, and it is too coarse for float's range - which is
why
float.Epsiloncomes back as0.00000000000000000000000000000000?and fails to parse. That is#6, not #54: the
Doublearm has exactly the same shape at 64 places against a range reaching5e-324, and
X1 * 3 == 1over adoublefails identically today. Fixing the type withoutdisturbing the precision keeps the two arms parallel and leaves #6 as one defect rather than half of
one.
Tests
166 -> 177. The
#54pin becomes a round-trip test, and the#52culture test that could onlyassert an
ArgumentExceptionnow says what it always wanted to.Solve_FloatSymbol_RoundTripsTheValue(6 rows)Single.MaxValueSolve_FloatAndDoubleSymbolsTogether_MarshalEachToItsOwnTypeSolve_UnconstrainedFloatSymbol_ReturnsAResultSolve_FloatConstantUnderANonInvariantCulture_RoundTripsTheValue_FailsInMarshallingNotTranslationSolve_FloatSymbolWithANonTerminatingValue_ThrowsFormatExceptionSolve_DoubleSymbolWithANonTerminatingValue_ThrowsFormatExceptionSolve_FloatSymbolAtItsSmallestPositiveValue_ThrowsFormatExceptionSingle.Epsilonat allSolve_FloatArraySymbol_ThrowsZ3ExceptionThe three #6 pins are the first coverage that defect has had. It was found by measuring rather than
inferred:
float.Epsilonand a third both fail before ever reaching the write this PR fixes.Mutation results
double.Parsedouble.Parse(float)double.Parse(...)- the alternative fixvalue = 0fVerification
dotnet build solutions/Z3.Linq.slnx -c Release- clean,TreatWarningsAsErrorson./build.ps1 -Configuration Release- 46 tasks, 0 errors, 0 warningspreviously ran into a dead end, so these are lines that had never completed before
Release note
Releases remain on hold under #60 until Microsoft.Z3 5.x reaches nuget.org, so this reaches
mainbut not consumers. Nothing about the hold changes.