Skip to content
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

Fix complex property globs #1610

Merged
merged 1 commit into from
Nov 19, 2024
Merged

Fix complex property globs #1610

merged 1 commit into from
Nov 19, 2024

Conversation

edemaine
Copy link
Collaborator

@edemaine edemaine commented Nov 18, 2024

The following examples weren't working before:

  • <Component {f()?.{nx,ny}}> (ref caused trouble — I encountered this in the real world which is what sent me down this path)
  • {x().{a,b}, y()?.{c,d}} (didn't properly merge with multiple refs)

Also improved globs to not use refs if we only need one property, which occurs in a bunch of dynamic import examples.

Copy link
Contributor

@STRd6 STRd6 left a comment

Choose a reason for hiding this comment

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

A good improvement!

I added some notes that could be addressed in a follow-up about better resolution of ?. chaining before the glob and exactly what we want the behavior to be in those cases.

---
<Component {a, f()?.{nx,ny}, b}>
---
let ref;<Component a={a} b={b} {...{ nx:(ref = f())?.nx,ny:ref?.ny,}} />
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably want to preserve the order here if possible. I believe it can have an impact if the same key also appears in the spread.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. This is an issue "inherited" from my old JSX attribute code, which e.g. compiles <Foo {a, [b]: x, c}> to <Foo a={a} c={c} {...{ [b]: x }} />. But it shouldn't be too hard to fix, by accumulating maximal chunks that need {...{ treatment. I'll leave it for a future PR for easier review.

@@ -1285,7 +1285,15 @@ describe "object", ->
---
x.y()?.z.{a,b}
---
let ref;(ref = x.y()?.z,{a:ref.a,b:ref.b})
let ref;({a:(ref = x.y()?.z).a,b:ref.b})
Copy link
Contributor

Choose a reason for hiding this comment

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

This may be out of scope for this particular fix but if x.y() ends up undefined then .a and .b on the ref will throw instead of being short circuited.

Also related to that we should decide what to return in the case of the ref not resolving. I think undefined is correct but in the previous example with the ?. directly before the glob we're returing {nx: undefined, ny: undefined}

I think the check will end up being something like ((ref = x.y()) != null ? ((ref=ref.z), { a:ref.a, b:ref.b }) : ref

Sorry it's dense but note the use of the comma operator inside the first ternary branch to manually update the ref. We may need to do it for each time ?. appears before the glob.

Essentially manually short circuiting every ?. along the chain.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, good point: this is probably unexpected behavior that x?.y.{a,b} crashes when x is undefined. The behavior is not changed by this PR, but this PR's compilation does make it potentially more difficult

One challenge with your compilation is that globs can appear inside object expressions, e.g. {c, x?.y.{a,b}}. That's why I switched to setting the ref here to get set in the first element instead of outside in a parenthesized expression; the latter wasn't properly mixable. But of course it's possible to combine the two strategies as you say.

A simpler workaround, but with different behavior, would be to compile x()?.y.z.{a,b} into {a: (ref = x()?.y.z)?.a, b: ref?.b}. In other words, add a ? after ref if there was any ? earlier in the chain. But this would return {a: undefined, b: undefined} instead of undefined. I'm not totally sure which behavior is better... I could see preferring {a: undefined, b: undefined} if e.g. doing the following:

{a, b} := object.{a,b}

Although, in that case, we could just write {a, b} := object. Maybe a better example is returning a projected object? But again it's unclear which is better/more intuitive: returning undefined or an object containing undefineds... What do you think?

Copy link
Contributor

@STRd6 STRd6 Nov 19, 2024

Choose a reason for hiding this comment

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

I think ultimately we'll need to short circuit at exactly each ?. The ones outside the glob prevent adding any keys, the ones inside the glob add the key but its value may be undefined.

Checking later in aggregate wouldn't quite be semantically correct. In this case x?.y when x is defined but y is undefined should throw.

Spacing for clarity:

{c, x?.y.{a,b}}
---
let ref;
({ 
  c, 
  ...( (ref = x) != null && (    // short circuit at `x?.`
    ref = ref.y,    // update ref
    {a: ref.a, b: ref.b}    // object for spread
  ) )
})

For the case of projecting I think we'd want to explicitly show that we'd prefer to ignore the missing object rather than to absorb the error. TS will also warn us if the value may potentially be undefined.

{a, b} := {...object?.{a,b}}

Though a case could be made that the ?. immediately preceding the glob {...} may prefer constructing the projection anyway.

Copy link
Collaborator Author

@edemaine edemaine Nov 19, 2024

Choose a reason for hiding this comment

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

Interesting. The {...glob} splat perspective is clarifying: if the glob is undefined (because of an earlier failure), it doesn't add anything to the object. I guess this is somewhat in line with our recent object comprehensions too (which are kinda like implicit ... splats).

I'm still not totally sure which interpretation is correct, but I'm coming around to and you're probably right that undefined is more natural behavior than {a: undefined, b: undefined}.

Note that if we want the latter, we could avoid ... I think. (I do agree that we need to check each possible failure separately though; my ?. attempt is indeed broken.)

{c, x()?.y.{a,b}}
---
let xref, yref;
({
  c,
  a: (xref = x()) == null ? undefined :
    (yref = xref.y).a,
  b: xref == null ? undefined : yref.b
})

@edemaine edemaine merged commit 7a30fd4 into main Nov 19, 2024
4 checks passed
@edemaine edemaine deleted the glob-fixes branch November 19, 2024 15:25
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