Skip to content

Conversation

Ladicek
Copy link
Contributor

@Ladicek Ladicek commented Mar 27, 2025

We need constants of types byte, short and char because without them, we cannot statically make a difference between them and int, which may lead to all kinds of issues. For example, we could emit an invocation of a different overload of a method than expected, if the target class has an overload of the method for both byte (or short or char) and int.

In addition to that, this commit also adds Constant.of(Boolean), because java.lang.Boolean constants were not of the primitive type, unlike all others. This might be a bad idea, but all other constants of wrapper types are in fact constants of the corresponding primitive type, and consistency is warranted. We might later want to rethink that.

@Ladicek Ladicek requested review from dmlloyd and mkouba March 27, 2025 10:27
@Ladicek Ladicek force-pushed the fix-primitive-constants-2.x branch from cd4fbe4 to 673db7f Compare March 27, 2025 14:51
@dmlloyd
Copy link
Member

dmlloyd commented Mar 27, 2025

TL;DR I agree with the basic idea. There are some details to work out though.

Longer explanation & background:

As you guessed, I left off these types initially because they are all represented as int when loaded into bytecode, and I hoped to avoid the complexity which comes with these values. But you're right, we need a way to represent constants with different static types for easy method resolution at least (the user could always give an explicit signature and use int constants, but that's pretty unwieldy).

What complexity you may ask? Well...

Relying on Constable means that any time a constant of this type shows up in bytecode, we're doing a ldc with a dynamic constant whose initializer is basically taking an int constant and casting it to byte or short or whatever. However, ideally when we generate code for a constant byte (for example) we'd want to just load the int constant and use it directly (since we generate the constant, we will know it's in range). This will prevent our constant table from getting too bloated in the common case.

So, to do this right, we need to do a couple of things:

  • Create ByteConstant, ShortConstant, and CharConstant classes whose writeCode method is the same as IntConstant (maybe extract a common superclass called IntLoadableConstant or something)
    • BooleanConstant is already basically doing this correctly
    • CharConstant wouldn't have negative values but this might not matter, depending on how you do it
  • Modify io.quarkus.gizmo2.impl.constant.ConstantImpl#of(java.lang.constant.Constable) to detect Byte, Short, and Character (and Boolean, oops I forgot that one!), and call a type-specific of method much like io.quarkus.gizmo2.impl.constant.ConstantImpl#of(java.lang.constant.ConstantDesc) does when it gets an Integer or Long etc.
  • Now the ugly one: modify io.quarkus.gizmo2.impl.constant.ConstantImpl#of(java.lang.constant.DynamicConstantDesc<?>) to detect a DynamicConstantDesc which matches the following described constants from the wrapper classes, and redirects them to the correct type-specific of method:
    • For these three, check the component constantName(), constantType(), and bootstrapMethod():
      • DynamicConstantDesc.ofNamed(BSM_EXPLICIT_CAST, DEFAULT_NAME, CD_byte, (int) value)
      • DynamicConstantDesc.ofNamed(BSM_EXPLICIT_CAST, DEFAULT_NAME, CD_char, (int) value)
      • DynamicConstantDesc.ofNamed(BSM_EXPLICIT_CAST, DEFAULT_NAME, CD_short, (int) value)
    • For these two, just use equals:
      • java.lang.constant.ConstantDescs#TRUE
      • java.lang.constant.ConstantDescs#FALSE

This way, no matter what weird way the user sends in a Byte, Short, etc., we always get the same correctly-typed constant out of it.

I hope this makes sense!

@Ladicek
Copy link
Contributor Author

Ladicek commented Mar 27, 2025

Yeah, I originally started doing something like that, but @mkouba's comment that Byte, Short and Character are Constable made me lazy :-) I'll fix this properly, thanks!

@Ladicek Ladicek force-pushed the fix-primitive-constants-2.x branch from 673db7f to a8913c7 Compare March 27, 2025 17:01
@Ladicek
Copy link
Contributor Author

Ladicek commented Mar 27, 2025

Done!

Copy link
Member

@dmlloyd dmlloyd left a comment

Choose a reason for hiding this comment

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

Looks good other than two minor questions. Thanks!

* @param value the value to create a constant from
*/
static Constant of(byte value) {
return ConstantImpl.of((Byte) value);
Copy link
Member

Choose a reason for hiding this comment

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

These methods could just be ConstantImpl.of(value) right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, will fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -98,6 +101,18 @@ public StringConstant stringConstant(String value) {
return (StringConstant) constants.computeIfAbsent(value, StringConstant::new);
}

public ByteConstant byteConstant(Byte value) {
return (ByteConstant) funnyHashCodeConstants.computeIfAbsent(new ByteConstant(value), Function.identity());
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we try to find a better name for funnyHashCodeConstants? 🤔

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 would personally merge constants and funnyHashCodeConstants together (using the funnyHashCodeConstants approach), not sure why they're separate (except perhaps a little less allocations in the constants case?).

Copy link
Contributor

@mkouba mkouba left a comment

Choose a reason for hiding this comment

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

We could also extend the BoxUnboxTest now...

@Ladicek
Copy link
Contributor Author

Ladicek commented Mar 28, 2025

We could also extend the BoxUnboxTest now...

Let me take a look.

@Ladicek Ladicek force-pushed the fix-primitive-constants-2.x branch from a8913c7 to 8aae32a Compare March 28, 2025 08:45
@Ladicek
Copy link
Contributor Author

Ladicek commented Mar 28, 2025

Done. I also adjusted BoxUnboxTest, which prompted a small fix in Rel.

We need constants of types `byte`, `short` and `char` because without them,
we cannot statically make a difference between them and `int`, which may
lead to all kinds of issues. For example, we could emit an invocation of
a different overload of a method than expected, if the target class has
an overload of the method for both `byte` (or `short` or `char`) and `int`.

In addition to that, this commit also adds `Constant.of(Boolean)`, because
`java.lang.Boolean` constants were not of the primitive type, unlike all
others. This might be a bad idea, but all other constants of wrapper types
are in fact constants of the corresponding primitive type, and consistency
is warranted. We might later want to rethink that.
@Ladicek Ladicek force-pushed the fix-primitive-constants-2.x branch from 8aae32a to 879cafa Compare March 28, 2025 12:30
@dmlloyd dmlloyd merged commit 80e7448 into quarkusio:2.x Mar 28, 2025
2 checks passed
@dmlloyd
Copy link
Member

dmlloyd commented Mar 28, 2025

Awesome, thanks!

@Ladicek Ladicek deleted the fix-primitive-constants-2.x branch March 28, 2025 12:32
@Ladicek Ladicek added the 2.x Issue applies to Gizmo 2.x label Mar 28, 2025
@Ladicek Ladicek moved this to Done in WG - Gizmo 2 Apr 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2.x Issue applies to Gizmo 2.x
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

3 participants