Skip to content

Fix #4947: Retain the structure of PreTransRecordTree. #5091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 27, 2024

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Dec 18, 2024

Instead of only retaining the RecordType.

The structure contains the complete information required to map FieldNames to the record's SimpleFieldNames.


Other than the newly added test, this PR causes 0 changes to the generated JavaScript code.

@sjrd sjrd requested a review from gzm0 December 18, 2024 10:19
@gzm0
Copy link
Contributor

gzm0 commented Dec 22, 2024

A higher level comment here, before I dive into the specifics: It seems there are two overall approaches to take to class field / record field mapping:

  • Dynamic: allocate record field names dynamically when we need them; track them. this is the status quo approach which was broken before and this PR fixes.
  • Static: fixed mapping between class field name / record field names (based on mangling of names, potentially even later, e.g. use field names in record types).

I was wondering if you have explored the static approach. It seems at least at face value, it would keep the optimizer simpler.

@sjrd
Copy link
Member Author

sjrd commented Dec 22, 2024

It's a good question. I have thought a bit about it. I don't think there's a strong case either way.

One thing going for the dynamic approach is that it produces less code size in fastLinkJS. I am particularly concerned about expanded Longs, which already often "catch my eye" when I skim through test-suite/main.js.

Regarding complexity, I'm not sure the static approach would really be simpler. Overall, I find that keeping the structure is simpler, regardless of the static/dynamic approach. Or at least it's clearer to me. It is more obvious that it does the right thing too, especially when merging the results of If/Match/Labeled.

@sjrd sjrd force-pushed the fix-record-simple-names branch from b76258f to 540550e Compare December 22, 2024 16:06
@sjrd
Copy link
Member Author

sjrd commented Dec 22, 2024

Rebased to fix conflicts after #5092 was merged. No other changes.

Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

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

Or at least it's clearer to me. It is more obvious that it does the right thing too, especially when merging the results of If/Match/Labeled.

After review, I strongly agree with this. The joining points are much easier to understand with the approach in this PR than the approach before.

@@ -5660,7 +5671,10 @@ private[optimizer] object OptimizerCore {
private val ClassTagApplyMethodName =
MethodName("apply", List(ClassRef(ClassClass)), ClassRef(ClassName("scala.reflect.ClassTag")))

final class InlineableClassStructure(private val allFields: List[FieldDef]) {
final class InlineableClassStructure(val className: ClassName, private val allFields: List[FieldDef]) {
private[OptimizerCore] val refinedType: RefinedType =
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this useful for? IIUC this is only used in tpe of PreTransRecordTree. And that usage feels very odd (I have commented there).

@@ -6286,9 +6309,13 @@ private[optimizer] object OptimizerCore {
* whereas `tree.tpe` is always the lowered `RecordType`.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

This specification of tpe feels odd. From the doc of PreTransform:

A PreTransform has a tpe as precisely refined as if a full transformExpr() had been performed.

So this is technically correct: a full transformExpr will roll-back any PreTransRecord tree (with the exception of RT long).

Do you know when this is useful in practice?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, right now I'm not sure. I can check later. For now I've addressed all the other comments.

Copy link
Contributor

Choose a reason for hiding this comment

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

How would you like to proceed with this? It seems this is the last open comment on this PR.

From my second look, it seems like before this PR, we needed the tpe field to validate if two PreTransRecordTrees correspond to the same class. In hindsight, it is not much less obvious to me in the original code that the optimizer was correct in that regard, we're just moving form rhsOrigType != lhsOrigType to lhsStructure.sameClassAs(rhsStructure).

This PR introduces a clear distinction between RefinedTypes and (transient) allocated structures. I think this is an improvement we should keep; it makes many decisions the optimizer makes more explicit (notably in label processing this becomes clear).

However, this feels like a strong hint, that requiring a tpe: RefinedType on an arbitrary pre-transform might not be ideal. So maybe we should try an move it up the hierarchy?

Copy link
Contributor

Choose a reason for hiding this comment

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

I've had a look at usages of PreTransform#tpe and IMO it's fairly quickly clear why it is useful. For example, take IsInstanceOf:

case IsInstanceOf(expr, testType) =>
trampoline {
pretransformExpr(expr) { texpr =>
val result = {
if (isSubtype(texpr.tpe.base.toNonNullable, testType)) {
if (texpr.tpe.isNullable)
BinaryOp(BinaryOp.!==, finishTransformExpr(texpr), Null())
else
Block(finishTransformStat(texpr), BooleanLiteral(true))
} else {
if (texpr.tpe.isExact)
Block(finishTransformStat(texpr), BooleanLiteral(false))
else
IsInstanceOf(finishTransformExpr(texpr), testType)
}
}
TailCalls.done(result)
}
}

PreTransform#tpe allows the transformation of IsInstanceOf to fold the operation if the type is statically know. We want this to happen, if the PreTransform ends up as a record.

Another usage I've found is to accurately type bindings: I suspect without the tpe, we'd have to roll-back the binding itself in case it needs var replacement (like this, we only need to roll-back the pretransform).

IMO this is sufficient to move forward with this PR, but PTAL as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah indeed, the IsInstanceOf example is pretty obvious. Another one where it is clearly the right thing is in pretansformApply to resolve targets and decide whether and what to inline:

val className = boxedClassForType(treceiver.tpe.base)

Instead of only retaining the `RecordType`.

The `structure` contains the complete information required to map
`FieldName`s to the record's `SimpleFieldName`s.
@sjrd sjrd force-pushed the fix-record-simple-names branch from 540550e to 24c79c2 Compare December 25, 2024 16:29
Copy link
Member Author

@sjrd sjrd left a comment

Choose a reason for hiding this comment

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

Updated. I haven't addressed the question about the spec of PreTransRecordTree.tpe.

@@ -6286,9 +6309,13 @@ private[optimizer] object OptimizerCore {
* whereas `tree.tpe` is always the lowered `RecordType`.
*/
Copy link
Member Author

Choose a reason for hiding this comment

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

No, right now I'm not sure. I can check later. For now I've addressed all the other comments.

Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

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

Just the open comment about tpe. I might try and fiddle with it a bit today if I find the time.

@@ -6286,9 +6309,13 @@ private[optimizer] object OptimizerCore {
* whereas `tree.tpe` is always the lowered `RecordType`.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

How would you like to proceed with this? It seems this is the last open comment on this PR.

From my second look, it seems like before this PR, we needed the tpe field to validate if two PreTransRecordTrees correspond to the same class. In hindsight, it is not much less obvious to me in the original code that the optimizer was correct in that regard, we're just moving form rhsOrigType != lhsOrigType to lhsStructure.sameClassAs(rhsStructure).

This PR introduces a clear distinction between RefinedTypes and (transient) allocated structures. I think this is an improvement we should keep; it makes many decisions the optimizer makes more explicit (notably in label processing this becomes clear).

However, this feels like a strong hint, that requiring a tpe: RefinedType on an arbitrary pre-transform might not be ideal. So maybe we should try an move it up the hierarchy?

@sjrd sjrd merged commit 7da555a into scala-js:main Dec 27, 2024
3 checks passed
@sjrd sjrd deleted the fix-record-simple-names branch December 27, 2024 10:40
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