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

Optional chaining #6973

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

ShortDevelopment
Copy link
Contributor

@ShortDevelopment ShortDevelopment commented Apr 15, 2024

This PR aims to add support for the stage-4 proposal optional-chaining.
It's inspired by the work of @rhuanjl but uses a more hacky approach to parsing.

Goals

  • Minimize amount of changes
  • (Hopefully) Performance

ToDo

  • Add tests
    • Unused expression result
    • Return expression
    • Root optional-call (e.g. eval?.('a'))
    • Scoped method load (e.g. inside eval)
  • the delete operator is currently not working
    We might want to split this into a separat PR
  • optional call invoked on eval function should be indirect eval
  • Simple function calls
  • Preserve this
    (a.b)().c should be equivalent to a.b().c
  • What about the apply call optimization?
  • short circuit embedded expressions like a?.[++x]
    ++x should not be evaluated if a is null or undefined
  • Don't tokenize a?.3:0 (ternary) as tkOptChain (?.)
  • Tagged templates are not allowed in optional chain

Out of scope

  • Remove EmitInvoke (seems unused)
  • Better error message for a call on a non-function

Spec: Optional Chains
Fixes #6349

Copy link
Collaborator

@rhuanjl rhuanjl left a comment

Choose a reason for hiding this comment

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

I think this looks neater than my version was, though struggling to follow how it handles every branch. What happens if you do a function call in an optional chain does it crash?

(Not going to merge until we have some significant test coverage)

lib/Runtime/ByteCode/ByteCodeEmitter.cpp Outdated Show resolved Hide resolved
lib/Runtime/ByteCode/ByteCodeEmitter.cpp Outdated Show resolved Hide resolved
lib/Runtime/ByteCode/FuncInfo.h Outdated Show resolved Hide resolved
@ShortDevelopment
Copy link
Contributor Author

ShortDevelopment commented Apr 18, 2024

I'm currently working on function calls.
At the moment it would just be handled as if no optional-chaining would be used
⇾ crash in js land

Edit: Function call should work now but this is not always propagated correctly (yet)
Edit: Function calls should now work

Comment on lines +105 to +108
// ToDo: How can I run async tests?
// simpleObj.nothing?.[await Promise.reject()];
// simpleObj.null?.[await Promise.reject()];
// simpleObj.undefined?.[await Promise.reject()];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

How can I run async tests?

Copy link
Collaborator

Choose a reason for hiding this comment

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

There are a few ways, have a look at test/es7/async-generator-functionality.js for one example.

Note, I'd recommend putting the async tests in a separate file - I think it's easier/better to manage them separately.

@ShortDevelopment
Copy link
Contributor Author

The jitted code currently crashes at this assertion (causing the test-failures)

Assert(block->globOptData.IsTypeSpecialized(varSym));

@ShortDevelopment ShortDevelopment force-pushed the feat-optional-chaining branch 2 times, most recently from aae4d9f to a9a1c70 Compare May 1, 2024 13:46
@ShortDevelopment ShortDevelopment force-pushed the feat-optional-chaining branch 2 times, most recently from b132685 to 9904051 Compare May 3, 2024 21:15
@@ -70,7 +70,7 @@ static void EmitOptionalChainWrapper(ParseNodeUni *pnodeOptChain, ByteCodeGenera
// Acquire slot for the result value
Js::RegSlot resultSlot = funcInfo->AcquireLoc(pnodeOptChain);
// Copy chain result
byteCodeGenerator->Writer()->Reg2(Js::OpCode::Ld_A_ReuseLoc, resultSlot, innerNode->location);
byteCodeGenerator->Writer()->Reg2(Js::OpCode::Ld_A, resultSlot, innerNode->location);
Copy link
Collaborator

@rhuanjl rhuanjl May 4, 2024

Choose a reason for hiding this comment

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

Minor but I think you can delete this line.

resultSlot should be guaranteed to be the same register as innerNode->location here; I tested offline removing this line and your tests all still pass - except for the existing fail for the Simple method Calls test in the Jit.

May be good to replace with a comment noting this or an: Assert (innerNode->location == resultSlot);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's an edge case:
ByteCodeGenerator::ReturnRegister == resultSlot => innerNode->location != resultSlot

Copy link
Collaborator

@rhuanjl rhuanjl May 4, 2024

Choose a reason for hiding this comment

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

Ugh didn't think of that. I suppose for optimal could do: if (innerNode->location != resultSlot) and add the LdA or even a specific check for resultSlot == ByteCodeGenerator::ReturnRegister

Should include a test of this.

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'd just go back to sth like

innerNode->location = funcInfo->AcquireLoc(pnodeOptChain);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Question: Is it bad to reserve a register BEFORE emitting the whole chain-expression?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought in the version before you did funcInfo->ReleaseLoc(innerNode) but didn't actually do AcquireLoc for it as it happened inside the emitting of the chain.

I'm sorry this has brought in quite a bit of confusion, and I'm sorry I didn't think of the case where the resultSlot was already allocated (e.g. as the return reg) - I think it may be cleanest to return to the logic before but simply include an if(resultSlot != innerNode->location) around the LdA.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ooh, no need to say sorry; I just did not understand 😅
After some testing I actually think we need

innerNode->location = funcInfo->AcquireLoc(pnodeOptChain);

though, because the stack of temp-registers will be asymmetrical released for optional-chains as call-target (another edge-case)

(a?.b)().c

Copy link
Contributor Author

Choose a reason for hiding this comment

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

During testing, test262 sometimes failed while my own tests passed -> I need more tests!
(See ToDo in PR description)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thinking of extra tests, do you have a cases for an optional call where the property exists but is not callable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now I have 😉

@ShortDevelopment ShortDevelopment force-pushed the feat-optional-chaining branch 2 times, most recently from 5958fb4 to 8c4eddf Compare May 10, 2024 11:03
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.

Implement optional chaining
2 participants