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 #4544: postfix conditional on first line of implicit object #4554

Conversation

helixbass
Copy link
Collaborator

Fixes #4544. Also includes some little rewriter.coffee cleanups

The reason for the behavior @couchand describes in #4544 is that postfix ifs are currently treated as implicit-object-literal-closing if they're on the first line of the object literal so that things like x = a: b if c work correctly. But it's assuming that it's a single-line object literal, so if the expected compilation described by @couchand is desired (which I think it is, ie you should be able to swap two lines of an object literal without anything weird happening) then this fixes it by sniffing ahead to see if we appear to be on the first line of a multi-line object literal

Added the examples given by @couchand as well as a few more cases to test/objects.coffee

@GeoffreyBooth I figured you'd want this targeted against 2?

@GeoffreyBooth @lydell let me know if I should justify the correctness of any/all of the little cleanup refactorings

@GeoffreyBooth
Copy link
Collaborator

2 is great, thanks. This looks quite good. Yes thanks for the cleanups, rewriter.coffee is a beast and can use all the cleanup it can get 😄

Does this fix any of the other lexer or related open issues? Thanks for tackling this!

@lydell
Copy link
Collaborator

lydell commented May 24, 2017

Wow! Just glancing over the diff I have no idea what's going on, but it all looks sensible. Good job! (I guess 😉 )

@GeoffreyBooth
Copy link
Collaborator

Yes, @lydell echoes my thoughts exactly ;)

Just merge this?

@helixbass
Copy link
Collaborator Author

@GeoffreyBooth yup figured this was a good excuse to jump into the belly of the beast and understand how addImplicitBracesAndParens() works. Took a brief look through open issues, this also closes #2051. Thanks for organizing and tagging them "Lexer:", I'll start going through those more closely as the lexer is what I'm most familiar with at this point. On a somewhat related note, I'd like to start following all activity in the Coffeescript repo (and perhaps coffeescript6/discuss), is Github notification emails the best way to get notified of stuff that happens? I've found the Slack Github integration feed easy to browse/scroll but looks like you need to own the repos you want to follow

@lydell @GeoffreyBooth I'll go through now and leave specific comments in case you do want to vet anything more closely but I appreciate your trust that I'm not doing anything nonsensical

@GeoffreyBooth
Copy link
Collaborator

@helixbass thanks! Yes, that was the idea behind the tagging. You might also want to skim through the “Invalid JS”-tagged ones, they might be related to the lexer/rewriter stuff.

I don’t know how to get better notifications. I watch both repos but I only get emails when I’m mentioned or I’ve already participated in a thread. If you find a better way please let me know. On a related note, the plan is to merge discuss into this repo when 2.0.0 is released.

I’ve never worked in the rewriter, so you know it far better than I do. Reading your code there’s nothing sloppy and nothing obviously wrong, so if you say it fixes the bug and doesn’t break any tests, I’m kind of taking your word for it 😄

@@ -559,7 +560,9 @@ exports.Lexer = class Lexer
else if tok[0] is '('
tok[0] = 'PARAM_START'
return this
else return this
else
paramEndToken[0] = 'CALL_END'
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

clean up after ourselves here rather than force the rewriter to (non-obviously) adjust a mismatched CALL_START/PARAM_END pair into CALL_START/CALL_END (in closeOpenCalls()). This is for cases like f() -> where here in tagParameters() we start trying to back-label the parens as PARAM_START/PARAM_END but realize we're not actually looking at params but rather an immediately preceding call

if token[0] in EXPRESSION_START
levels += 1
else if token[0] in EXPRESSION_END
levels -= 1
if levels < 0
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is combining a cleanup of the existing logic with the addition of a new option

Cleanup: you can see that the if not token in the line removed above could never be true, so you can move that line down here (simplifying the condition to just if levels < 0) and not have to do the awkward i - 1 call at the beginning of the next iteration

New option: then just additionally check for the new returnOnNegativeLevel option param, which allows you to override detectEnd()'s default behavior which is to also perform the specified action if levels < 0 (ie we hit an EXPRESSION_END we didn't see the start of)

closeOpenCalls: ->
condition = (token, i) ->
token[0] in [')', 'CALL_END'] or
token[0] is 'OUTDENT' and @tag(i - 1) is ')'
token[0] in [')', 'CALL_END']
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this "mis-nested outdent" case seems to predate the behavior of eg pair() in the lexer, which prevents mis-nested outdents from making it this far, so we can safely get rid of it. Also, as mentioned in the above lexer.coffee comment, this method (closeOpenCalls()) also now doesn't have to clean up mismatched CALL_START/PARAM_END pairs, which is nice b/c the way it was cleaning them up was totally non-obvious (relied on the detectEnd() behavior described above of calling the action when levels < 0, in this case when it hit the incorrectly labeled PARAM_END)

@@ -159,31 +159,36 @@ exports.Rewriter = class Rewriter
# class declaration or if-conditionals)
inImplicitControl = -> inImplicit() and stackTop()?[0] is 'CONTROL'

startImplicitCall = (j) ->
idx = j ? i
startImplicitCall = (idx) ->
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

can safely remove all this j?-related logic here and in startImplicitObject() b/c all calls of these methods will clearly pass a numeric first argument

@@ -197,7 +202,7 @@ exports.Rewriter = class Rewriter
# 1. We have seen a `CONTROL` argument on the line.
# 2. The last token before the indent is part of the list below
#
if prevTag not in ['=>', '->', '[', '(', ',', '{', 'TRY', 'ELSE', '=']
if prevTag not in ['=>', '->', '[', '(', ',', '{', 'ELSE', '=']
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

safe to remove TRY from this list b/c an implicit call could never be at the top of the stack (ie inImplicitCall() could never be true) when we've just seen TRY b/c the preceding clause would've caused a CONTROL to get pushed onto the stack (in the previous scanTokens() iteration). Effectively, this list can safely be mutually exclusive from the list above (['IF', 'TRY', ...])

@@ -236,13 +241,6 @@ exports.Rewriter = class Rewriter
# a: b
# c: d
#
# and
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this comment seems to reference an old syntax that no longer compiles (now the first arg has to be an implicit object in order for indentation to imply an implicit call?)

@@ -316,7 +314,8 @@ exports.Rewriter = class Rewriter
# Close implicit objects such as:
# return a: 1, b: 2 unless true
else if inImplicitObject() and not @insideForDeclaration and sameLine and
tag isnt 'TERMINATOR' and prevTag isnt ':'
tag isnt 'TERMINATOR' and prevTag isnt ':' and
not (tag is 'POST_IF' and startsLine and implicitObjectContinues(i + 1))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the actual fix. If we see a POST_IF on the first line of an implicit object, check if it looks like that implicit object is actually multi-line rather than assuming it's an inline-implicit-object-ending POST_IF


endImplicitObject = (j) ->
j = j ? i
stack.pop()
tokens.splice j, 0, generate '}', '}', token
i += 1

implicitObjectContinues = (j) =>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

looks ahead (from the POST_IF in question) for a following line that should be treated as part of the same implicit object (ie is at the same indentation level, is the next line (after any properly balanced intervening nested stuff), and is itself looking like an object literal)

@@ -439,6 +439,12 @@ test "#3216: For loop declaration as a value of an implicit object", ->
arrayEq ob.b, test
arrayEq ob.c, test
arrayEq ob.d, test
byFirstKey =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added a couple tests since the @insideForDeclaration/#3216 stuff really only kicks in on the first line of an implicit object

@lydell
Copy link
Collaborator

lydell commented May 25, 2017

Well, I’m convinced! I’d say let’s just merge it!


endImplicitObject = (j) ->
j = j ? i
stack.pop()
tokens.splice j, 0, generate '}', '}', token
i += 1

implicitObjectContinues = (j) =>
nextTerminatorIdx = null
@detectEnd j,
Copy link
Collaborator

Choose a reason for hiding this comment

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

How costly is that lookahead here? It's called every single time we rewrite an implicit object literal, right? Is there a perf. impact?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@vendethiel nope only gets called when it sees a POST_IF on the first line of an implicit object literal

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thank you for the explanation.

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.

4 participants