-
Notifications
You must be signed in to change notification settings - Fork 225
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
Conversation
943e064
to
b269783
Compare
b269783
to
c7059c9
Compare
There was a problem hiding this 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
Sources/IfTag.swift
Outdated
if bracketsBalance < 0 { throw TemplateSyntaxError("unbalanced brackets") } | ||
return bracketsBalance | ||
}) == 0 else { | ||
throw TemplateSyntaxError("unbalanced brackets") |
There was a problem hiding this comment.
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?
Sources/IfTag.swift
Outdated
} | ||
} | ||
|
||
static func subExpression(from index: Int, components: [String], tokenParser: TokenParser) throws -> (Expression, [String]) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 👍
@AliSoftware better now? |
There was a problem hiding this 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
Sources/IfTag.swift
Outdated
var bracketsBalance = $0 | ||
if $1 == "(" { bracketsBalance += 1 } | ||
else if $1 == ")" { bracketsBalance -= 1 } | ||
if bracketsBalance < 0 { throw TemplateSyntaxError("unbalanced brackets: too many closing brackets") } |
There was a problem hiding this comment.
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.
Sources/IfTag.swift
Outdated
} | ||
} | ||
|
||
static func subExpression(from components: ArraySlice<String>, tokenParser: TokenParser) throws -> (Expression, Int) { |
There was a problem hiding this comment.
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
?
Sources/IfTag.swift
Outdated
else if $0 == ")" { bracketsBalance -= 1 } | ||
return bracketsBalance != 0 | ||
}) | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Sources/IfTag.swift
Outdated
} | ||
} | ||
|
||
private init(components: ArraySlice<String>, tokenParser: TokenParser) throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
0eb11df
to
b489bf9
Compare
There was a problem hiding this 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 |
There was a problem hiding this comment.
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)
Sources/IfTag.swift
Outdated
parsedComponents.formUnion(Set(index...(index + parsedCount))) | ||
return .subExpression(expression) | ||
} | ||
else if component == ")" { |
There was a problem hiding this comment.
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 }
?
Sources/IfTag.swift
Outdated
var bracketsBalance = 1 | ||
let subComponents = components | ||
.prefix(while: { | ||
if $0 == "(" { bracketsBalance += 1 } |
There was a problem hiding this comment.
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.
Sources/IfTag.swift
Outdated
parsedComponents.insert(index) | ||
if let op = findOperator(name: component) { | ||
switch op { | ||
case .infix(let name, let bindingPower, let cls): |
There was a problem hiding this comment.
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")) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unneeded empty new line
Before I forget: This PR should document the changes in the |
@djbe comments addressed, can you look again? |
# Conflicts: # CHANGELOG.md # Sources/IfTag.swift
@djbe @kylef @yonaskolb can we move this forward? |
docs/builtins.rst
Outdated
@@ -149,6 +149,19 @@ Will be treated as: | |||
|
|||
one or (two and three) | |||
|
|||
You can use brackets to change operators precedence. For example: |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
@djbe fixed docs, good catch with brackets vs parentheses 👍 |
Does this still need a rebase? (github says it needs an update) |
@djbe I'll update and squash |
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.