Skip to content

Add this expandos #860

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 11 commits into from
May 15, 2025
Merged

Add this expandos #860

merged 11 commits into from
May 15, 2025

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented May 12, 2025

This PR adds this-property assignment declarations, aka this expandos. They currently only work in classes -- I haven't added any constructor function support yet, and Corsa no longer allows assignment to global this at the toplevel of a script.

The implementation rewrites a significant chunk of getWidenedTypeForAssignmentDeclaration. It should be easier to read and do less duplicative work -- but it's not perfect. And I know that I'll likely have to add handling for object-literal and class expandos later.

Because this change adds control-flow typing of this-property assignments in JS classes, it exposes the fact that Corsa's baselines always write auto types as their calculated type. Strada writes them as any most (all?) of the time.


I again faked type annotations on binary expressions as a cast. This works, but nowhere near everywhere it's supposed to. I'm going to send a separate PR to add a Type field to all the nodes that JS would like to reparse type annotations onto from JSDoc (BinaryExpression, PropertyAssignment, ExportAssignment). I'll have to change getWidenedTypeForAssignmentDeclaration's check for type annotations on BinaryExpressions when that PR goes in.

Edit: I tried this and found that it involves at least as much hacky code to add a Type field, and is likely slower because it grows the size of BinaryExpression.

sandersn added 8 commits May 9, 2025 09:02
Doesn't do control flow inference or respect type tags yet. Plus there
are couple of known bugs.
Just like the other places with no type annotation in Typescript.
I may eventually add a Type property to these, but only if the results
are unacceptable. For getting started they are fine.
Note that the baselines *still* show a difference for some auto-typed
results--Strada says `this.p: any` while Corsa says `this.p: string`,
even though both behave as that it's a string when used. I debugged a
test to convince myself that indeed both Strada and Corsa believe the
example property is a string, though.
@Copilot Copilot AI review requested due to automatic review settings May 12, 2025 20:45
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces support for this expandos (class-only this.property assignments) by adding a unified classification for JS assignment declarations, updating the parser reparser, binder, and checker, and refreshing test baselines to reflect the new behavior.

  • Added JSDeclarationKind and GetAssignmentDeclarationKind in utilities.go to classify assignment patterns.
  • Implemented bindThisPropertyAssignment in the binder and enriched the checker with expando tracking and widened-type logic.
  • Refactored the parser reparser to use the new declaration kind and updated numerous baselines for symbols, types, and errors.

Reviewed Changes

Copilot reviewed 521 out of 521 changed files in this pull request and generated no comments.

Show a summary per file
File Description
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor3_Js.symbols.diff Update symbols diff for this.foo and this.bar expandos.
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor3_Js.symbols Refresh symbols output for constructor 3 case.
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor3_Js.errors.txt Adjust error count after this-property assignment support.
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor2_Js.types Update types for this["arguments"] expando.
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor2_Js.symbols.diff Refresh symbols diff for bracket-access expandos.
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor2_Js.symbols Update symbols for constructor 2 expandos.
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor1_Js.types Update types for this.arguments expando.
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor1_Js.symbols.diff Refresh symbols diff for constructor 1 expandos.
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor1_Js.symbols Update symbols for constructor 1 expandos.
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor1_Js.errors.txt Remove now-obsolete error on this.arguments.
testdata/baselines/reference/submodule/compiler/ambientPropertyDeclarationInJs.symbols.diff Adjust baseline for ambient this.prop declarations.
testdata/baselines/reference/submodule/compiler/ambientPropertyDeclarationInJs.symbols Refresh ambient property symbols with dual declarations.
testdata/baselines/reference/submodule/compiler/allowJscheckJsTypeParameterNoCrash.types Update types baseline for method data2.
testdata/baselines/reference/submodule/compiler/allowJscheckJsTypeParameterNoCrash.symbols.diff Refresh symbols diff for data2 in vextend.
testdata/baselines/reference/submodule/compiler/allowJscheckJsTypeParameterNoCrash.symbols Update symbol locations for data2 declarations.
internal/parser/reparser.go Remove old jsDeclarationKind, use GetAssignmentDeclarationKind.
internal/checker/types.go Added isConstructorDeclaredProperty to TypeAliasLinks.
internal/checker/checker.go Extended checker with expando-kind caching and widened-type logic.
internal/binder/binder.go Introduced bindThisPropertyAssignment for this.property.
internal/ast/utilities.go Added JSDeclarationKind enum and GetAssignmentDeclarationKind.
Comments suppressed due to low confidence (3)

internal/parser/reparser.go:5

  • The import of core is unused in this file after removing getAssignmentDeclarationKind. Consider removing it to keep imports tidy.
"github.com/microsoft/typescript-go/internal/core"

internal/checker/types.go:158

  • The isConstructorDeclaredProperty field in TypeAliasLinks is never referenced. Either remove it or implement its intended use to avoid dead code.
isConstructorDeclaredProperty bool

internal/binder/binder.go:1058

  • The handler for KindFunctionDeclaration/KindFunctionExpression is left empty (commented // !!! constructor functions). This likely skips binding for constructor functions. Add proper handling or clarify why it's omitted.
case ast.KindFunctionDeclaration, ast.KindFunctionExpression:

@jakebailey
Copy link
Member

I'm going to send a separate PR to add a Type field to all the nodes that JS would like to reparse type annotations onto from JSDoc (BinaryExpression, PropertyAssignment, ExportAssignment).

Hopefully this isn't very expensive, because BinaryExpression is one of the top nodes we allocate.

@jakebailey
Copy link
Member

it exposes the fact that Corsa's baselines always write auto types as their calculated type

This seems odd; I guess it's a problem in our type printer that it doesn't back out on auto types?

@sandersn
Copy link
Member Author

sandersn commented May 13, 2025

I guess we'll find out, but also that's one reason why I'm going to send it as a separate PR.

Edit: It bumps the size from 88 to 96, and hovering over *TypeNode, it says that size=8, offset=72. Are pointers normally 8 bytes? Or is that telling me the total space taken? If they're less than 8 bytes, I would expect the subsequent node pointers to pack better, but they're all size=8, offsets +8 for each.

Later edit: Oh, of course, 64 bits=8 bytes. Pointers are just huge these days.

@sandersn
Copy link
Member Author

sandersn commented May 13, 2025

An alternate idea: have the reparser emit JSExpando nodes for assignments. Then BinaryExpression doesn't have to be a declaration either.

The problem with this is that, while this.p assignments are context-free, f.p assignments require binder knowledge to restrict f to variables initialised with function expressions, class expressions and empty object literals. The reparser could emit JSExpando nodes that the binder later ignores if they don't apply. With respect to efficiency, that means trading off 8 bytes for every BinaryExpression versus duping only assignment BinaryExpressions. Probably the latter is cheaper.

The more I talk about it, the more I like this idea. It lets me delete a lot of BinaryExpression special-casing in the checker too.

@sandersn
Copy link
Member Author

After some noodling, I, uh, just asked o4-mini to do the math for me. Since the 16 extra bits for the current approach are 1/5 the size of the trimmed down BinaryExpression and JSExpando, as long as assignments are less than 1/5 of BinaryExpressions, the dupe approach is cheaper.

Here's the entire conversation:

sandersn

If I have a BinaryExpression with size=80, I have to emit a duplicate JSExpando, also size=80, whenever the BinaryExpression is an assignment. Alternatively, I could make all BinaryExpressions have size=96, with no duplicates. What's the ratio of assignments that would make the duplicate JSExpando approach cheaper?

GitHub Copilot

The math is pretty straightforward. Let α = fraction of Binaries that are assignments.

• With the “duplicate‐JSExpando” approach everyone stays at 80 bytes, but assignments pay +80:
avg = (1–α)·80 + α·(80+80) = 80 + 80α

• With the 96 byte “blow‐up‐everyone” approach you pay 96 always.

You want
80 + 80α < 96
⇔ α < (96–80)/80 = 16/80 = 0.2

So if fewer than 20% of your BinaryExpressions are assignments, the duplicate‐JSExpando (80+80) approach is smaller on average. At ≥20% assignments it’s cheaper to just make all of them 96 bytes.

@sandersn
Copy link
Member Author

I'm going to send a separate PR to add a Type field to all the nodes that JS would like to reparse type annotations onto from JSDoc (BinaryExpression, PropertyAssignment, ExportAssignment).

I got most of the way through implementing this and realised that ExportAssignment is quite hacky to add a type annotation to; normally we resolve all the way to the target of the alias for type checking, skipping the annotation entirely. Since there's only one place so far that I have to check for a cast-as-proxy-for-annotation, I think a surface-level hack in a JS-related location is better than an obscure hack in a common code path.

}
case ast.KindSourceFile, ast.KindModuleDeclaration:
// top-level this.property as assignment to globals is no longer supported
break
Copy link
Member

Choose a reason for hiding this comment

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

This break isn't needed.

sandersn added 2 commits May 15, 2025 14:30
@sandersn sandersn enabled auto-merge May 15, 2025 21:44
@sandersn sandersn added this pull request to the merge queue May 15, 2025
Merged via the queue into microsoft:main with commit 7f3ffe1 May 15, 2025
23 checks passed
@sandersn sandersn deleted the add-this-expandos branch May 22, 2025 18:57
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.

None yet

2 participants