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

Added support for brackets in boolean expressions #165

Merged
merged 12 commits into from
Sep 21, 2018

Conversation

ilyapuchka
Copy link
Collaborator

@ilyapuchka ilyapuchka commented Dec 25, 2017

Currently there is no way to alter operations precedence in boolean expression, so it can force users to use cascades of ifs. With this it should become simpler.

Expression in brackets is parsed recursively as special case of IfToken that represents sub-expression which is evaluated similar to variable token.

@ilyapuchka ilyapuchka requested a review from kylef December 25, 2017 19:01
@ilyapuchka ilyapuchka force-pushed the if-expression-brackets branch 2 times, most recently from 943e064 to b269783 Compare December 26, 2017 14:09
Copy link
Collaborator

@AliSoftware AliSoftware left a comment

Choose a reason for hiding this comment

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

Didn't read all the code logic thoroughly yet but here are some early feedback

if bracketsBalance < 0 { throw TemplateSyntaxError("unbalanced brackets") }
return bracketsBalance
}) == 0 else {
throw TemplateSyntaxError("unbalanced brackets")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we give a more explicit error, like "unbalanced brackets: missing closing bracket" here and "unbalanced brackets: too many closing brackets" above when <0?

}
}

static func subExpression(from index: Int, components: [String], tokenParser: TokenParser) throws -> (Expression, [String]) {
Copy link
Collaborator

@AliSoftware AliSoftware May 10, 2018

Choose a reason for hiding this comment

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

Wouldn't it be better performance and memory-wise to use an ArraySlice here for components, to prevent rebuilding a new sub-array from the full array on every recursion level? After all, that's exactly what ArraySlices are designed for 😉

That would allow you to get rid of Array(…) wrapping 2 lines below too.
Of course you'd need to replace your components.enumerated() on line 133 by zip(components, components.indices) instead, which is generally better anyway (see Soroush's article about enumerated) because not all startIndices are 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.

Can't say anything about performance, but the only thing it will really change is move suffix to caller site, because IfExpressionParser accepts Array. But it allows to remove one function parameter, so I'll make the change still.

Speaking of enumeration I don't see what zip will improve in this particular case. I find enumerated use to be more clear here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's not about readability but about correctness (see Soroush's article), especially if we move to use slices (because they're the type to use when you recurse on subexpression, as they avoid a ton of memory reallocation, and that's exactly what we're doing here so that's a textbook use case) and enumerated will lead to wrong indexes with slices unlike zip (as explained by Soroush)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, if this type only accepts ArraySlice in constructor it makes sense 👍

@ilyapuchka
Copy link
Collaborator Author

@AliSoftware better now?

Copy link
Collaborator

@AliSoftware AliSoftware left a comment

Choose a reason for hiding this comment

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

LGTM overall, but just some nitpicks, and one idea/suggestion

var bracketsBalance = $0
if $1 == "(" { bracketsBalance += 1 }
else if $1 == ")" { bracketsBalance -= 1 }
if bracketsBalance < 0 { throw TemplateSyntaxError("unbalanced brackets: too many closing brackets") }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry to be nitpicking about code style, but if we soon add SwiftLint to harmonize the code style of all contributors it will raise a violation here probably: even for one-liners like that it'll suggest to go to the next line after opening brace and before closing brace. Besides just being strict with style (which I agree can be annoying sometimes) this has the advantage of allowing to debug step by step, entering inside the scope of needed during debug.

}
}

static func subExpression(from components: ArraySlice<String>, tokenParser: TokenParser) throws -> (Expression, Int) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason why this isn't private?

else if $0 == ")" { bracketsBalance -= 1 }
return bracketsBalance != 0
})

Copy link
Collaborator

@AliSoftware AliSoftware May 12, 2018

Choose a reason for hiding this comment

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

Not really sure it would be worth it, but what about a small class Balancer helper for every time we have to balance parentheses and/or brackets? Could serve here, in the code above too, and in the other PR from @djbe about indirect references…

class Balancer {
  var balance = 0
  let opening: Character
  let closing: Character
  init(pair: (Character, Character), initialBalance: Int = 0) {
    opening = pair.0
    closing = pair.1
    balance = initialBalance
  }
  // convenience constructors
  static func brackets(initial: Int = 0) -> Balancer {
    return Balancer(pair: ("[", "]"), initialBalance: initial)
  }
  // And same for parentheses

  func parse(_ char: Character) throws -> Int {
    // … parse the char, increment/decrement the balance if it matches the opening/closing chars, throws if balance < 0, return new balance
  }
}

For example here you could then create a balancer then use components.prefix(while: { balancer.parse($0) != 0)

(all that code typed from my phone so no guarantee of being correct or typo-free)

I'll let you discuss if it's worth adding it? And if so adapt the code (what I typed is just a draft, maybe you'll need to adjust it to fit all situations where it could be used) but could be nice for various places where we do that kind of logic, in this PR and others

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea but I don't think it will be useful in #215 as validation there is done during parsing, not as a separate step. We can do the same here and remove validation step.

}
}

private init(components: ArraySlice<String>, tokenParser: TokenParser) throws {
Copy link
Collaborator

Choose a reason for hiding this comment

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

👌

@ilyapuchka ilyapuchka force-pushed the if-expression-brackets branch from 0eb11df to b489bf9 Compare May 13, 2018 00:59
Copy link
Contributor

@djbe djbe left a comment

Choose a reason for hiding this comment

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

Only some tiny style changes, otherwise this LGTM.

CHANGELOG.md Outdated
@@ -7,6 +7,7 @@
- Added support for resolving superclass properties for not-NSObject subclasses
- The `{% for %}` tag can now iterate over tuples, structures and classes via
their stored properties.
- You can now use brackets in boolean expressions to change operators precedence
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you update the entry format to match the rest? (., 2 spaces, credit yourself and ref. this PR)

parsedComponents.formUnion(Set(index...(index + parsedCount)))
return .subExpression(expression)
}
else if component == ")" {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why isn't this on the same line as the }?

var bracketsBalance = 1
let subComponents = components
.prefix(while: {
if $0 == "(" { bracketsBalance += 1 }
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a big fan of having the content of the if on the same line, and pretty sure swiftlint'll complain about this too.

parsedComponents.insert(index)
if let op = findOperator(name: component) {
switch op {
case .infix(let name, let bindingPower, let cls):
Copy link
Contributor

Choose a reason for hiding this comment

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

I had to look up what the cls value was in the Operator enum. Maybe ..., let operator woud be clearer?

.toThrow(TemplateSyntaxError("'if' expression error: infix operator 'and' doesn't have a left hand side"))
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

unneeded empty new line

@djbe
Copy link
Contributor

djbe commented Jul 11, 2018

Before I forget: This PR should document the changes in the docs.

@djbe djbe added this to the 0.12.0 milestone Jul 11, 2018
@ilyapuchka
Copy link
Collaborator Author

@djbe comments addressed, can you look again?

@ilyapuchka
Copy link
Collaborator Author

@djbe @kylef @yonaskolb can we move this forward?

@@ -149,6 +149,19 @@ Will be treated as:

one or (two and three)

You can use brackets to change operators precedence. For example:
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use parentheses to change operator precedence. For example:

CHANGELOG.md Outdated
@@ -11,6 +11,9 @@
- Now requires Swift 4.1 or newer.
[Yonas Kolb](https://github.com/yonaskolb)
[#228](https://github.com/stencilproject/Stencil/pull/228)
- You can now use brackets in boolean expressions to change operators precedence.
Copy link
Contributor

Choose a reason for hiding this comment

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

Weird that I didn't catch this before. Generally "brackets" (in en-US) is used to refer to "[ ]", whereas "parentheses" is used for "( )". Also pretty sure that in this situation it's "operator" (without an s).

You can now use parentheses in boolean expressions to change operator precedence.

@ilyapuchka
Copy link
Collaborator Author

@djbe fixed docs, good catch with brackets vs parentheses 👍

@djbe
Copy link
Contributor

djbe commented Sep 21, 2018

Does this still need a rebase? (github says it needs an update)

@ilyapuchka
Copy link
Collaborator Author

ilyapuchka commented Sep 21, 2018

@djbe I'll update and squash

@ilyapuchka ilyapuchka merged commit 2c3962a into master Sep 21, 2018
@ilyapuchka ilyapuchka deleted the if-expression-brackets branch September 21, 2018 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants