Hello, my name is Monica Kozbial and I'm going to be talking about the use of operator precedence
within Blockly. In this talk I'll cover the role of operator precedence in code generation;
then I'll talk about how to report the correct operator precedence within your code; after that
I'll talk about what sorts of things can go wrong and how to spot them; and finally I'll talk about
best practices in regards to operator precedence within generator code. You may be familiar with
the term operator precedence within the context of mathematical expressions. The same concept is used
in computer programming with there just being more operators to account for. Operator precedence is
what determines the grouping of terms and affects how the expression is evaluated. For example
in the expression two plus three times five the multiplication operator has a higher precedence
so we know the expression is evaluated by first multiplying three by five and then adding two.
Let's say we have the following Blockly blocks representing the expression two plus three times
five. However in this example the two plus three expression is grouped together and thus should
be evaluated first before multiplying by 5. When Blockly generates the code for this block it needs
to add parentheses around 2 plus 3 in order for the expression to be evaluated correctly.
Blockly uses the reported operator precedence to determine whether parentheses need to be added.
Operator precedence is specified in two types of places. First in valueToCode calls
which is used to get generated code for an input block. The strongest operator that could split the
return code is passed in as an enum. Second in the generator code for value blocks the second item
in the list returned by the block generator code is an enum representing the weakest operator that
the final block of code could split at. Developers are responsible for accurately reporting operator
precedence in these places in order for Blockly to determine whether wrapping code with parentheses
is needed. Going back to the previous example, let's talk about how Blockly determines whether
to add parentheses. In the code generator for the outermost math_arithmetic block
valueToCode is called to get the first input blocks code and is passed ORDER_MULTIPLICATION to
represent that the multiplication operator is the operator with the strongest precedence that could
split the code returned by the valueToCode call. When valueToCode gets the generated code for
the inner math_arithmetic block it receives the information that the addition operator
is the operator with the weakest precedence that the return code could split at
through the ORDER_ADDITION enum. Since ORDER_ADDITION is weaker than ORDER_MULTIPLICATION,
the parentheses are added around the return of valueToCode and the resulting output is two
plus three wrapped in parentheses times five. Each programming language generator in Blockly
provides an ordered list of operator precedences in the form of enums prefixed with ORDER.
In Blockly the value of the ORDER operator enums have an inverse relationship with strength. A lower
number is a stronger operator. Keep this in mind when you're referencing these enums to determine
which to use in your block generator code. Remember that ORDER_ATOMIC is always the strongest operator
and ORDER_NONE is the weakest. Historically the reason for the operator enums having an
inverse relationship with strength is due to older versions of documentation on javascript
operator precedence which followed that rule. Another point of clarification for terminology
is that a stronger operator is also the operator with a higher precedence and a weaker operator is
an operator with a lower precedence. We use these terms interchangeably within our documentation.
We covered a fairly simple example with the math blocks so I'd like to cover a few other examples
to enhance understanding. In this table the first column represents the generated code of a value
block that has a single input represented as X. The second column lists the strongest operator
which might split X and would also correspond to the order you would be passing to the valueToCode
call. The third column lists the weakest operator that could split the generated code
listed in the first column and would also be the order included in the block generator's return.
Starting with the first row with the generated code “math dot square root of x”,
the strongest operator against X is ORDER_NONE because there are no operators acting on X.
it's important to note when calling valueToCode, you're passing the max strength expected against
the return code for that input - not of the entire block of code. The operators in the generated code
are ORDER_MEMBER for the dot square root and ORDER_FUNCTION_CALL for the square root function
call. It doesn't matter what operators are within the return of X because if they would be weaker,
then valueToCode would have wrapped it in parentheses and it would no longer be
a place that the code could split at. ORDER_FUNCTION_CALL is the weaker operator based on
the operator precedence table enums, so that's the weakest operator that could split the code
Now I encourage you to open a reference to the javascript operator precedence table linked below
this video. Pause the video now and try to fill out the table on your own to check your understanding.
In the next example “foo x modulus ten” the modulus operator is the only operator acting on X.
Therefore the strongest operator against X is ORDER_MODULUS.
Although both the modulus and the member operators are contained in this block of code,
the block of code could only split at the member operator since the modulus operator is contained
within square brackets. Therefore the weakest operator that could split the code is ORDER_MEMBER.
For "!X && true" there are two operators acting on X: the logical NOT and the logical AND operators
The logical NOT is the stronger operator so the strongest operator against x is ORDER_LOGICAL_NOT.
The weakest operator that could split the block of code is ORDER_LOGICAL_AND.
In the final example the strongest operator against X
and the weakest operator that could split the code are both ORDER_MEMBER.
Now let's get into what can go wrong.
If you're not choosing the right operator precedence you're going to run into either
too many parentheses or too little parentheses. These issues can be really tricky because there
may only be too many parentheses or too little parentheses in very specific block combinations.
Too many parentheses is less of an issue because the generated code is still valid. However it may
be hard to read with the extra parentheses and it can sometimes hide issues with other blocks
which might have an issue with order that cause it to not generate parentheses when it should. Too
little parentheses is the bigger concern because it can cause syntax errors in the generated code.
Mistakes in order that cause extra parentheses are: passing in is stronger than the strongest
operator acting on the input or returning a weaker than the weakest operator at which the return code
could split at. This is why using ORDER_ATOMIC for every valueToCode call and ORDER_NONE as
a return for every value block will cause extra parentheses and also happen to generate valid code,
although it is riddled with parentheses. ORDER_ATOMIC is the strongest operator so
it's guaranteed to be stronger than or equal to the actual strongest operator acting on the input.
ORDER_NONE is the weakest operator so it's guaranteed to be weaker
than or equal to the actual weakest operator that could split the code.
As we reviewed our generated code we've noted some similar mistakes in our repo and we fix them
accordingly. We consider these mistakes in core to be especially bad because it makes it harder for
contributors looking at our code base as a source of truth and how to correctly write generator code.
Here I have some real-life examples of errors in javascript generation that were identified
and are now fixed in core. The first issue I have here is for the multi-line text block
which returns string pre-wrapped in parentheses. You can see that the code generated for the
print block with a multi-line input block had extra parentheses in the generated code.
The multi-line block was always returning ORDER_ATOMIC and pre-wrapping the code with parentheses
when there was a string concatenation. To fix it, it was updated to return ORDER_ADDITION when
string concatenation was used so that the output was only wrapped with parentheses when necessary.
Looking at the text_count block you can see that there are extra parentheses in the generated
code for the first parameter of textCount. The valueToCode call in text_count was passing in
a stronger operator precedence ORDER_MEMBER when it should have been ORDER_NONE because there are
no operators acting on that input. In the last example the code generator for the text_count
block was returning a weaker operator precedence ORDER_SUBTRACTION when it should have been
returning ORDER_FUNCTION_CALL. This caused the code to be unnecessarily wrapped in extra parentheses.
Mistakes in order that can cause not enough parentheses are: passing in a weaker than
the strongest operator acting on the input; or returning a stronger than the weakest
operator at which the return code could split at. These are mistakes you want to
avoid because they could cause a resulting code to evaluate incorrectly or cause syntax errors.
Here I have an example of another now-fixed issue where the incorrect order caused invalid code.
The text multi-line block used a stronger operator precedence
ORDER_ATOMIC when it should have returned ORDER_ADDITIVE which prevented
it from being wrapped in parentheses when passed as an input in the text_count block.
Some common mistakes and sources of mistakes are:
Mixing up whether you're supposed to find the strongest or weakest operator. As a reminder
you want to return the strongest operator acting on the input for valueToCode calls
and the weakest operator that the code could split at for the return of value block generator code.
Often people use the wrong order for parameters of a function call that have no operator acting
on them. ORDER_NONE should be used if there aren't any operators acting on the parameter,
not ORDER_FUNCTION_CALL or ORDER_COMMA. The comma operator is a rarely used operator in javascript
and comma separating parameters in a function call are not instances of a comma operator.
ORDER_MEMBER and ORDER_FUNCTION_CALL is also often mixed up using the wrong one here shouldn't cause
issues with code generation but because you can't have both a function call and a member operator
acting on the same X however it's best if you use the correct one because it makes it easier
to understand the code. When writing generator code use the most accurate operator precedence
for min and max strength. Some complicated blocks may need additional logic to determine
which operator precedence to use with valueToCode. text_getSubstring is a good example of a
block that does some extra checks to determine which operator precedence to use. Although extra
parentheses don't cause invalid code they can hide issues with other blocks that might not
be adding parentheses when they're supposed to and your generated code will be harder to read.
Another best practice is to not pre-wrap generated code in parentheses. Not only will
this cause unavoidable extra parentheses, it can hide bugs in other blocks making it hard to catch.
Seeing parentheses in a return generator code can be a tell-tale sign of issues with operator order
elsewhere. When we fixed the multi-line text block to no longer return code pre-wrapped
in parentheses and returned the correct order it revealed that there were issues in the order
of other text blocks that then needed to be fixed. Thanks for watching this talk if you have
any questions you can come to our Q&A session during our virtual summit, or if you're watching
this after our virtual summit has concluded you can post your question on our developer forum.