Solving a fun puzzle in JavaScript

Our story begins with a tweet from Tomas Lakoma, in which he invites you to imagine that such a question met you in an interview.







It seems to me that the reaction to such a question in an interview depends on what exactly it is. If the question really is what the value is tree



, then the code can simply be inserted into the console and get the result.



However, if the question is how would you solve this problem, then everything becomes quite curious and leads to a test of knowledge of the intricacies of JavaScript and the compiler. In this article I will try to sort out all this confusion and get interesting conclusions.



I was streaming the process for solving this problem on Twitch . The broadcast is long, but it allows you to take another look at the process of step-by-step solving such problems.



General reasoning



First, let's convert the code to copyable format:



let b = 3, d = b, u = b;
const tree = ++d * d*b * b++ +
 + --d+ + +b-- +
 + +d*b+ +
 u
      
      





I immediately noticed some peculiarities and decided that some compiler tricks could be used here. You see, JavaScript usually adds semicolons at the end of each line, unless there is an expression that cannot be interrupted . In this case, +



at the end of each line it tells the compiler that there is no need to interrupt this construction.



The first line simply creates three variables and assigns them a value 3



. 3



Is a primitive value, so every time a copy is created, it is created by value , so all new variables are created with a value 3



... If JavaScript were to assign values ​​to these variables by reference , then each new variable would point to the previously used variable, but not create a value for itself.



Additional Information



Operator Precedence and Associativity



These are the key concepts for solving this daunting task. In short, they define the order in which a combination of JavaScript expressions is evaluated.



Operator priority



Q: what is the difference between these two expressions?



3 + 5 * 5
      
      





5 * 5 + 3
      
      





From the point of view of the result, there is no difference. Anyone who remembers school math lessons knows that multiplication is done before addition. In English, we remember the order as BODMAS (Brackets Off Divide Multiply Add Subtract - brackets, degree, division, multiplication, addition, subtraction). JavaScript has a similar concept called Operator Precedence: it means the order in which we evaluate expressions. If we wanted to force computation first 3 + 5



, we would do the following:



(3+5) * 5
      
      





The parentheses force this part of the expression to be evaluated first, because the operator has a ()



higher precedence than the operator *



.



Every JavaScript operator takes precedence, so with so many operators in there, tree



we need to figure out in what order they will be evaluated. It is especially important what --



will change the values b



and d



, so we need to know when these expressions are evaluated relative to the rest tree



.



Important: Operator Priority Table and Additional Information



Associativity



Associativity is used to determine in which order expressions are evaluated in operators with equal precedence. For example:



a + b + c
      
      





There is no operator precedence in this expression because there is only one operator. So how is it to be calculated - how (a + b) + c



or how a + (b + c)



?



I know the result will be the same, but the compiler needs to know this so that it can select one operation first and then continue the computation. In this case, the correct answer is (a + b) + c



because the operator is +



left associative, that is, it evaluates the expression on the left first.



β€œWhy not just make all operators left associative?” You might ask.



Well, let's take an example like this:



a = b + c
      
      





If we use the left associativity formula, we get



(a = b) + c
      
      





But wait, this looks weird, and that's not what I meant. If we wanted this expression to work using only left associativity, then we would have to do something like this:



a + b = c
      
      





This is converted to (a + b) = c



, that is, first a + b



, and then the value of this result is assigned to the variable c



.



If we had to think this way, JavaScript would be much more confusing, which is why we use different associativities for different operators - it makes the code more readable. When we read a = b + c



, the order of calculation seems natural to us, despite the fact that everything is more cleverly arranged inside and uses right- and left-associative operands.



You probably noticed the associativity problem in a = b + c



... If both operators have different associativity, how do you know which expression to evaluate first? Answer: the one with the higher operator precedence , as in the previous section! In this case, it +



has a higher priority, therefore it is calculated first.



I have added a more detailed explanation at the end of the article, or you can read more information .



Understanding how our tree expression is evaluated



Having understood these principles, we can begin to analyze our problem. It uses many operators, and the absence of parentheses makes it difficult to understand. So let's just add parentheses, listing all the operators used along with their precedence and associativity.



(operator with variable x): a priority associativity
x ++: 18 not
x--: 18 not
++ x: 17 right
--x: 17 right
+ x: 17 right
*: fifteen left
x + y: 14 left
=: 3 right


Parentheses



It's worth mentioning here that adding parentheses correctly is a tricky task. I checked that the answer is calculated correctly at each stage, but this does not guarantee that my parentheses are always placed correctly! If you know a tool for automatic brace placement, please email me.



Let's take a look at the order in which the expressions are evaluated and add parentheses to show it. I'll show you step by step how I arrived at the end result, just moving from the highest priority operators down.



Postfix ++ and postfix -



const tree = ++d * d*b * (b++) +
 + --d+ + +(b--) +
 + +d*b+ +
 u
      
      





Unary +, prefix ++ and prefix -



We have a small problem here, but I'll start by evaluating the unary operator +



, and then we'll get to the problem point.



const tree = ++d * d*b * (b++) +
 + --d+ (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





And this is where difficulties arise.



+ --d+
      
      





--



and +()



have the same priority. How do we know in what order to calculate them? Let's formulate the problem in a simpler way:



let d = 10
const answer = + --d
      
      





Remember, +



this is not addition, but unary plus, or positivity. You can perceive it as -1



, only here it is +1



.



The solution is that we evaluate from right to left, because the operators of this precedence are right associative .



So, our expression is converted to + (--d)



.



To understand this, try to imagine that all operators are the same. In this case, it + +1



will be equivalent (+ (+1))



according to the logic, which is 1 β€” 1 β€” 1



equivalent to ((1 β€” 1) β€” 1)



... Notice that the result of right-hand associative operators in the notation with parentheses is the opposite of the case with left-hand operators?



If we apply the same logic to the problem point, then we get the following:



const tree = ++d * d*b * (b++) +
 (+ (--d)) + (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





And finally, by inserting parentheses for the latter ++



, we get:



const tree = (++d) * d*b * (b++) +
 (+ (--d)) + (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





Multiplication (*)



Again we have to deal with associativity, but this time with the same operator, which is left associative. Compared to the previous step, this should be easy!



const tree = ((((++d) * d) * b) * (b++)) +
 (+ (--d)) + (+(+(b--))) +
 (+(+((d*b) + (+u))))
      
      





We have reached the stage at which it is already possible to begin calculations. It would be possible to add parentheses for the assignment operator, but I think it will be more confusing than easier to read, so we will not do that. Note that the above expression is just a little more complicated x = a + b + c



.



We can shorten some of the unary operators, but I'll save them in case they are important.



By dividing the expression into several parts, we can understand the individual stages of the calculations and build on them.



let b = 3, d = b, u = b;
 
const treeA = ((((++d) * d) * b) * (b++))
const treeB = (+ (--d)) + (+(+(b--)))
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





With that done, we can begin to explore the computation of different values. Let's start with treeA.



TreeA



let b = 3, d = b, u = b;
const treeA = (((++d) * d) * b) * (b++)
      
      





The first thing that will be evaluated here is an expression ++d



that will return 4



and increment d



.



// b = 3
// d = 4
((4 * d) * b) * (b++)
      
      





Then it is executed 4*d



: we know that at this stage d is 4, therefore it 4*4



is 16.



// b = 3
// d = 4
(16 * b) * (b++)
      
      





The interesting thing about this step is that we're going to multiply by b before incrementing b, so the calculation is done from left to right. 16 * 3 = 48



...



// b = 3
// d = 4
48 * (b++)
      
      





Above, we talked about what ++



has a higher priority than *



, so this can be written as 48 * b++



, but there are other tricks here - the return value b++



is the value before the increment, not after. So even though b eventually becomes 4, the multiplied value will be 3.



// b = 3
// d = 4
48 * 3
// b = 4
// d = 4
      
      





48 * 3



is equal 144



, so after calculating the first part b and d are equal to 4, and the result of the expression is 144







let b = 4, d = 4, u = 3;
 
const treeA = 144
const treeB = (+ (--d)) + (+(+(b--)))
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





TreeB



const treeB = (+ (--d)) + (+(+(b--)))
      
      





At this point, we can see that unary operators don't actually do anything. If we shorten them, we will greatly simplify the expression.



// b = 4
// d = 4
const treeB = (--d) + (b--)
      
      





We have already seen this trick above. --d



returns 3



, but b--



returns 4



, but by the time the expression is evaluated, both will be assigned the value 3.



const treeB = 3 + 4
// b = 3
// d = 3
      
      





So now our task looks something like this:



let b = 3, d= 3, u = 3;
 
const treeA = 144
const treeB = 7
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





TreeC



And we're almost done!



// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + (+u))))
      
      





Let's get rid of those annoying unary operators first.



// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + u)))
      
      





We got rid of it, but here you need to be careful with brackets, etc.



// b = 3
// d = 3
// u = 3
const treeC = (d*b) + u
      
      





It's pretty simple now. 3 * 3



equal 9



, 9 + 3



equal 12



, and finally, we have ...



Answer!



let b = 3, d= 3, u = 3;
 
const treeA = 144
const treeB = 7
const treeC = 12
const tree = treeA + treeB + treeC
      
      





144 + 7 + 12



equal 163



. The answer to the problem: 163



.



Conclusion



JavaScript can puzzle you in a bunch of weird and delightful ways. But by understanding how language works, you can get to the most fundamental reason for this.



Generally speaking, the path to a solution can be more informative than the answer, and the mini-solutions found along the way can in themselves teach us something.



It is worth saying that I checked my work using the browser console and it was more interesting for me to reverse engineer the solution than to solve the problem based on the basic principles.



Even if you know how to solve a problem, there are many syntactic ambiguities that need to be dealt with along the way. And I'm sure many of you noticed when you looked at our tree expression. I've listed some of them below, but each one is worth a separate article!



I would also like to thank https://twitter.com/AnthonyPAlicea, without which I would never have been able to figure it all out, and https://twitter.com/tlakomy for this question.



Notes and oddities



I have highlighted the mini-riddles that I encountered along the way in a separate section so that the process of finding a solution remains transparent.



How changing the order of variables affects



Watch this video



let x = 10
console.log(x++ + x)
      
      





Several questions can be asked here. What will be printed to the console and what is the value x



on the second line?



If you think this is the same number, then excuse me, I outwitted you. The trick is what is x++ + x



calculated as (x++) + x



, and when the JavaScript engine calculates the left side (x++)



, it does the increment x



, so when it comes to + x



, the value of x is equal 11



, not 10



.



Another tricky question - what value x++



does it return ?



I've given a pretty obvious clue as to what the answer actually is 10



.



This is the difference between x++



and ++x



. Looking at the underlying functions of the operators, they look something like this:



function ++x(x) {
  const oldValue = x;
  x = x + 1;
  return oldValue;
}
function x++(x) {
  x = x + 1;
  return x
}
      
      





Looking at them in this way, we can understand that



let x = 10
console.log(x++ + x)
      
      





will mean what it x++



returns 10



, and at the time of evaluation, + x



its value is 11



. Therefore, it will be printed to the console 21



, and the value x will be equal to 11



.



This relatively simple task points to a common anti-pattern used throughout code - jumbled expressions and side effects . More details.



Could there be two operators with the same precedence but different associativities?



Let's move in order and forget about the word "associativity" for now.



Let's take the operators +



and =



, and summarize the situation.



It was shown above what is a + b + c



calculated as (a + b) + c



, because it is +



left associative.



a = b = c



calculated as a = (b = c)



because it is =



right-associative. Note that it =



returns the value assigned to the variable, so it a



will be equal to what it is b



after evaluating the expression.



Let's replace the operands with their priority:



a left b left c = (a left b) left c
a right b right c = a right (b right c)

  

a left b right c = ?
a right b left c = ?
      
      





See that the second examples are logically impossible? a + b = c



is only possible because it +



takes precedence over =



, so the parser knows what to do. If two operators have the same precedence, but different associativity, then the syntax parser will not be able to determine in what order to perform actions!



So, to summarize: no, operators with the same precedence cannot have different associativity!



It's curious that in F # you can change the associativity of functions on the fly, which is why I was able to talk about associativity without going crazy! More details.



Unary Operators



An interesting point discovered when parsing the order of calculation +n



and ++n



.



Cannot be executed -- -i



because it -



returns a number, and numbers cannot be incremented or decremented, and cannot be done ---i



because the meaning is ---



ambiguous (is this -- -



or - --



? See comments below.), But you can do this:



let i = 10
console.log(-+-+-+-+-+--i)
      
      





Confused positivity



One of the most problematic issues was the ambiguity +



in JavaScript. The same symbol, as seen below, is used in four different functions:



let i = 10
console.log(i++ + + ++i)
      
      





Each operand has its own meaning, priority and associativity. It reminds me of the famous word puzzle:



Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo .



Unary operators or assignment?



+



can mean either a unary operator or an assignment. What is it in the case of the u



problem from the beginning of the article?



... +
u
      
      





Ultimately the answer depends on ... what is. If we wrote everything on one line



... + u
      
      





then the answer would be different for x + u



and x - + u



. In the first case, the symbol means addition, and in the second - unary +



. The only way to figure out what it means is to parse the rest of the expression until there is only one operator left to represent!






Advertising



VDS for programmers with the latest hardware, attack protection and a huge selection of operating systems. The maximum configuration is 128 CPU cores, 512 GB RAM, 4000 GB NVMe.






All Articles