TSL: Add type promotion in operators and math functions by shotamatsuda · Pull Request #33695 · mrdoob/three.js · GitHub
Skip to content

TSL: Add type promotion in operators and math functions#33695

Draft
shotamatsuda wants to merge 9 commits into
mrdoob:devfrom
shotamatsuda:feat/tsl-type-promotion
Draft

TSL: Add type promotion in operators and math functions#33695
shotamatsuda wants to merge 9 commits into
mrdoob:devfrom
shotamatsuda:feat/tsl-type-promotion

Conversation

@shotamatsuda

@shotamatsuda shotamatsuda commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Related: three-types/three-ts-types#2116 (comment)

Description

This PR adds type promotion in TSL's operators and math functions.

Many programming languages that I know have implicit type promotion in operators and function overloads in math functions, like C and Java, while more modern languages (with a strict type system) tend not to allow the use of operators and functions with different types, including WGSL and GLSL. TSL behaves like the former, but its behavior is rather odd, I would say.

For instance, the result of commutative operators differs by operand order, and the result of functions differs by parameter order:

float(0.5).mul(int(1)) // 0.5
int(1).mul(float(0.5)) // 1

max(float(1.5), int(1)) // 1
max(int(1), float(1.5)) // 1.5

With this PR, operands and parameters of math functions are implicitly coerced to broader common types, instead of being coerced to the narrower types in some cases. Those are breaking changes, and I am not entirely confident about this, but I do not think the above behavior is ideal either.

The E2E tests will utterly fail and need to be fixed if these changes (or the direction of this PR) make sense.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

@shotamatsuda

shotamatsuda commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

This can be a different issue (because it can be fixed separately), but equality operators on vectors have odd behavior as well. This PR also addresses it.

vec3(1.5).equal(ivec3(1)) // false
ivec3(1).equal(vec3(1.5)) // true!

@mrdoob mrdoob requested a review from sunag June 1, 2026 09:20
@shotamatsuda

shotamatsuda commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

It is arguable whether the conversion from u32 to i32 to f32 is actually "promotion". In most cases it is, but it does drop information if the value exceeds safe integer limits... Though the same applies to the current behavior.

@sunag

sunag commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

@shotamatsuda I still need to analyze your PR but regarding your comment on the issue:

I expected the above code to work because I'm too used to C-like types, where operand types are promoted to broader types (i.e. int × float -> float and float × int -> float), but TSL doesn't work that way.

I just want to say that your comment reflects the same point of view as mine. We should prioritize more common and broader types, even to make this more similar to JS. I’d love to support any improvement in that direction.

@shotamatsuda

shotamatsuda commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

After fixing the failed examples, though fewer than I thought, it revealed the problem I anticipated: operations on JS numbers. Because JS numbers are considered f32 (and rightly so), all operations with integer types will result in float types, and that will be the source of most breakages, I presume.

@sunag sunag Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When we have an uint * number, the number could be handled as a weak type instead of always handling it as float(). I think we can handle this in this PR.

In this case, 64 should automatically be converted to uint, prioritizing the type of output defined by the user. This should also be consistent with the use of array indexes.

Explicit definitions like float() and uint() should be handled differently as a strong type, maintaining the current logic.

instanceIndex.div( 64 ) // uint x uint ( number <-> weak type )
instanceIndex.div( uint( 64 ) ) // uint x uint
instanceIndex.div( float( 64 ) ) // uint x float

Getting it from the PR description, we should still keep the logic of prioritizing broader types:

float( 0.5 ).mul( int( 1 ) ) // 0.5
int( 1 ).mul( float( 0.5 ) ) // 0.5

I think we can add a boolean flag to ConstNode when this is converted from a nodeObject() to define that it is a weak type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It took some time to consider, but I agree that your idea is good. It could be formalized into a couple of rules. The potentially non-intuitive parts would be:

  • weak × uint -> uint holds, while weak × weak -> float should as well.
  • uint(1).mul(0.5) yields 1, but this is just a matter of how we see the node type of ConstNode; at least it is less confusing.

I will make the changes within this PR.

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.

2 participants