matrei commented on PR #16322:
URL: https://github.com/apache/grails-core/pull/16322#issuecomment-5695272004

   ## Review Findings, round 3
   
   Head `7a31802892`, base `8.0.x` at `37f8ca6377`. The branch contains the 
base head, so there is nothing to merge. The round-2 findings are all addressed 
in `fc1941a96e`: `Float` widened to `Double` now passes, every `castToType` 
failure is wrapped with the field name (the `NaN`/`BigDecimal` case has its own 
spec), and the staging `Sync` mirrors the plugin's `EXCLUDE` strategy.
   
   What I ran on the head, all green, with `cleanTest` and `--no-build-cache` 
so the result XML is from this run:
   
   - `:grails-gsp-core:test`, the whole module: 227 tests, 0 failures. 
`GspCompileStaticSpec` is 66 of them.
   - `:grails-scaffolding:test`, the whole module: 31 tests, 0 failures.
   - `:grails-test-examples-gsp-compile-static:integrationTest`: 7 tests, 0 
failures. The welcome page is compiled and listed in `views.properties` as 
before.
   - `:grails-gsp-core:codeStyle` and `:grails-scaffolding:codeStyle`: no 
violations.
   - Duplicate check: with a throwaway root `index.gsp` in the example app, 
`stageViewsWithWelcomePage` succeeds and the staged file is the app's, not the 
profile's. Removed afterwards.
   
   One finding, in the rule the new commit chose for the numeric guard.
   
   ### [P2] The exact-binary comparison rejects almost every real value 
crossing between `BigDecimal` and `Double`/`Float`
   
   References:
   
   - `grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java:209-228` 
(`sameNumericValue`, `exactDecimalValue`)
   - 
`grails-gsp/core/src/test/groovy/org/grails/gsp/GspCompileStaticSpec.groovy:313-314`
 (the `'Double' | 0.1G` and `'BigDecimal' | 0.1d` rows)
   - `grails-doc/src/en/guide/theWebLayer/gsp/gspStaticCompilation.adoc:58`
   
   Round 2 asked for the `Float` to `Double` widening to be compared at binary 
precision, which the new floating-point branch does. The commit also switched 
the cross-family comparison, `BigDecimal` against `Double` or `Float`, from 
decimal strings to the exact binary expansion, and that goes the other way: a 
`double` is only ever equal to a decimal fraction when the fraction is dyadic, 
so `0.5` and `100.00` pass and everything else fails. I rendered `@{ 
model="<declared> v"}${v}` on the head:
   
   | declared | supplied | head |
   |---|---|---|
   | `Double` | `19.99G` | fails, "cannot hold the java.math.BigDecimal 19.99 
... without changing it" |
   | `double` | `19.99G` | fails |
   | `Double` | `1234.56G` | fails |
   | `Float` | `19.99G` | fails |
   | `Double` | `0.1G` | fails, as the doc now says |
   | `BigDecimal` | `19.99d` | fails |
   | `BigDecimal` | `0.1f` | fails |
   | `BigDecimal` | `19.99f` | fails |
   | `Double` | `1.5G` | renders `1.5` |
   | `Double` | `100.00G` | renders `100.0` |
   
   These are the two families a Grails application actually mixes: GORM money 
properties are `BigDecimal`, measurements are `Double` or `Float`, and a 
`Double price` page fed a `BigDecimal` domain property, or the reverse, now 
fails at render on almost every row of real data. Neither direction loses 
anything a user can observe. Groovy's own conversion agrees: `castToType(0.1d, 
BigDecimal)` is `0.1`, `castToType(0.1G, Double)` is `0.1`, `castToType(19.99f, 
BigDecimal)` is `19.99`, and plain `BigDecimal b = 0.1d` and `double d = 
19.99G` assign without complaint. So the guide's first sentence, "converted the 
way a Groovy assignment converts it", is contradicted by the paragraph two 
lines below it.
   
   The rule that gives the guide's promise is: a conversion is accepted when 
the converted value converts back to the original. For the floating-point 
family that is the `Double.compare` branch the commit added. For everything 
else it is what round 2 already had, the decimal strings, because a `double` 
prints as the shortest decimal that reads back to it and that is also the 
decimal Groovy converts it to. Concretely, `sameNumericValue` keeps the new 
first branch and goes back to `new 
BigDecimal(original.toString()).compareTo(new BigDecimal(converted.toString())) 
== 0` for the rest, with the existing `NumberFormatException` catch returning 
`false`; `exactDecimalValue` goes away. I ran the PR's spec and the table above 
with that in place:
   
   - `GspCompileStaticSpec`: 66 tests, 2 failures, and they are exactly the 
`'Double' | 0.1G` and `'BigDecimal' | 0.1d` rows in "a conversion that would 
change the value", which encode the exact-binary rule. Moving them to the 
"converted the way a Groovy assignment converts it" table with expected `0.1` 
is the whole spec change.
   - Every other rejection stays rejected: `3_000_000_000L` for `Integer`, 
`42.9G` for `Integer`, `70_000` for `Short`, `0.1d` for `Float`, 
`Long.MAX_VALUE` for `Double`, `16_777_217` for `Float`, 
`9_007_199_254_740_993G` for `Double`, `123456789012345678L` for `Double`, 
`NaN` for `Integer`, `1E400` for `Double`, `42.5d` for `Integer`, `1.0E+23G` 
for `Long`.
   - Every row in the table above renders, `0.1f` for `Double` still renders 
`0.10000000149011612`, and `0.30000000000000004d` for `BigDecimal` renders 
`0.30000000000000004`.
   - The one visible consequence of the rule: the `BigInteger` `10^23` becomes 
a `Double` and renders `1.0E23`, where the head rejects it because the nearest 
`double` is `99999999999999991611392`. That is the round-trip semantics, the 
same one that lets `19.99G` through, and it prints what the user supplied.
   
   The doc paragraph at line 58 then reads along the lines of: "A `BigDecimal` 
and a `Double` or `Float` are compared through the decimal the floating-point 
value prints as, so `19.99G` satisfies a `Double` declaration and `19.99d` a 
`BigDecimal` one, while a value with more digits than the type can carry, 
`9007199254740993G` for a `Double`, is rejected. Widening a finite `Float` to 
`Double` is accepted; narrowing a `Double` to `Float` is rejected if it loses 
precision." The `NaN`/infinity sentence stays. The prototype is not in the 
working tree.
   
   ### Nit: two rows in the "would change the value" table are Groovy refusals, 
not guard rejections
   
   References:
   
   - 
`grails-gsp/core/src/test/groovy/org/grails/gsp/GspCompileStaticSpec.groovy:317-318`
   
   `'Double' | new BigDecimal('1E400')` and `'Double' | 
Float.NEGATIVE_INFINITY` never reach `sameNumericValue`: `castToType` throws 
`GroovyRuntimeException` ("Automatic coercion of java.lang.Float value 
-Infinity to double failed. Value is out of range.") and the page reports the 
"cannot be converted" message with that cause. The feature's assertions check 
only the field name and the two type names, which both messages contain, so the 
rows pass under the wrong heading. They belong next to the `Date` case in "a 
model value that cannot be converted", or that feature's `e.cause` assertion 
should become the discriminator. Cosmetic.
   
   ### Confirmed
   
   - The guide's "widening a finite `Float` to `Double`" is precise: Groovy 
itself refuses an infinite `Float` for a `Double` (plain `Double d = 
Float.NEGATIVE_INFINITY` throws the same `GroovyRuntimeException`), while 
`Float.NaN` for `Double` and either infinity for `Float` or `float` pass. The 
page inherits Groovy's asymmetry rather than adding one.
   - `catch (RuntimeException e)` around `castToType` is right: 
`GroovyCastException`, `GroovyRuntimeException` and `NumberFormatException` are 
all what it throws, none checked, and the new "non-finite value" feature pins 
the `NumberFormatException` cause with the field name in the message.
   - `Double.compare` in the floating-point branch handles `-0.0d` for `float` 
(spec row 9) and `NaN` for `Double` (row 10) as intended, and the `AtomicLong` 
row shows a `Number` subclass with no Groovy cast of its own still goes through 
the decimal comparison.
   - `duplicatesStrategy = DuplicatesStrategy.EXCLUDE` resolves without an 
import in a Groovy build script (Gradle's default imports cover 
`org.gradle.api.file`), and the app's views being listed first is what makes 
the app's page win, verified above.
   - The scaffold template, welcome pages, `ScaffoldedIndexViewModelSpec` and 
the staging task are unchanged since round 2, and the full `grails-scaffolding` 
and `grails-gsp-core` test tasks pass on the head.
   
   ## Verification
   
   - `./gradlew :grails-gsp-core:cleanTest :grails-gsp-core:test 
:grails-scaffolding:cleanTest :grails-scaffolding:test 
:grails-test-examples-gsp-compile-static:cleanIntegrationTest 
:grails-test-examples-gsp-compile-static:integrationTest 
:grails-gsp-core:codeStyle :grails-scaffolding:codeStyle --no-build-cache 
--continue`: BUILD SUCCESSFUL; 227 + 31 + 7 tests, 0 failures; result XML 
timestamps from this run.
   - `stageViewsWithWelcomePage --rerun-tasks` twice, once with a throwaway 
app-level `index.gsp` (staged copy is the app's) and once without (staged copy 
is the profile's). The throwaway page was removed.
   - A throwaway `ZzProbeSpec` in `grails-gsp-core` rendered the 33 
declared/supplied pairs above through `GroovyPagesTemplateEngine`, once on the 
head and once with the round-trip prototype in `GroovyPage.java`; the prototype 
run also executed `GspCompileStaticSpec` (66 tests, the 2 expected failures). 
Both files restored, `git status` shows no tracked changes.
   - Stale `book/index.gsp` classes from the round-one hand test were still in 
the example app's `build/gsp-classes` and `views.properties`; removed. Build 
output only.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to