HolyJS 2019: Debriefing from SEMrush (Part 1)



    At the regular conference for HolyJS JavaScript developers that was held on May 24-25 in St. Petersburg, our company booth offered everyone new tasks. This time there were 3 of them! Tasks were given in turn, and for the solution of each subsequent one an insignia was relied on (JS Brave> JS Adept> JS Master), which served as a good motivation not to stop. In total, we have collected about 900 answers and are in a hurry to share an analysis of the most popular and unique solutions.

    The proposed tests expect from the brave and understanding of the basic "features" of the language, and awareness of the new features of ECMAScript 2019 (in fact, the latter is not necessary). It is important that these tasks are not for interviews, are not practical, and are thought up just for the purpose of having fun.

    Task 1 ~ Countdown Expression


    What will return the expression? Rearrange any single character to

    1. expression returned 2
    2. expression returned 1

    +(_ => [,,~1])().length
    

    Additionally : is it possible to get 0 by permutations?

    What will return the expression?


    It’s not long to think here: the expression will return 3 . We have an anonymous function just returns an array of three elements, the first two of which are empty ( empty ). Function call gives us this array, we take from it the length ( the length ), and the unary plus does not solve anything.

    Expression returns 2


    A quick fix seems to reduce the number of elements in the returned array. To do this, just throw out one comma:

    [,~1].length // 2
    

    It’s impossible to throw out a symbol just like that: you need to rearrange it in another place of the original expression. But we know that the length property of the array takes into account the empty elements listed in the array literal, except for one case :

    If an element is elided at the end of an array, that element does not contribute to the length of the Array.

    That is, if an empty element is at the end of the array, then it is ignored:

    [,10,] // [empty, 10]
    

    Thus, the corrected expression looks like this:

    +(_ => [,~1,])().length // 2
    

    Is there another option to get rid of this comma? Or drove on.

    The expression returns 1


    It is no longer possible to get a unit by reducing the size of the array, since you will have to do at least two permutations. Have to look for another option.

    The function itself hints at the decision. Recall that a function in JavaScript is an object that has its own properties and methods. And one of the properties is also length , which determines the number of arguments expected by the function. In our case, the function has one single argument ( underscore ) - what you need!

    In order for length to be taken from a function, not from an array, you need to stop calling it through parentheses. It becomes obvious that one of these brackets is a contender for a permutation. And a couple of options come to mind:

    +((_ => [,,~1])).length // 1
    +(_ => ([,,~1])).length // 1
    

    Maybe there is something else? Or the next level.

    The expression returns 0


    In the additional task, the number of permutations is not limited. But we can try to fulfill it by making minimal movements.

    Developing the previous experience with length from the function object, you can quickly come to a solution:

    +(() => [,_,~1]).length // 0
    

    There are several derivatives, but the whole point is that we have reduced the number of function arguments to zero. To do this, we needed three permutations : two brackets and an underscore character , which became an element of the array.

    Well, but can it be time to stop ignoring the addition (+) and bitwise NOT (~) operators in our reasoning? It seems that arithmetic in this problem can play into our hands. In order not to flip to the beginning, here is the original expression:

    +(_ => [,,~1])().length
    

    To begin with, we calculate ~ 1 . The bitwise NOT of x will return - (x + 1) . That is, ~ 1 = -2 . And we also have an addition operator in the expression, which, as it were, hints that somewhere else we need to find 2 more and everything will work out.

    Most recently, we recalled the “non-effect” of the last empty element in an array literal on its size, which means our deuce is somewhere here:

    [,,].length // 2
    

    And it all adds up very well: we get the element ~ 1 from the array, reducing its length to 2, and add it as the first operand for our addition to the beginning of the expression:

    ~1+(_ => [,,])().length // 0
    

    Thus, we have achieved the goal already in two permutations !

    But what if this is not the only option? A little drum roll ...

    +(_ => [,,~1.])(),length // 0
    

    It also requires two permutations: the point after the unit (this is possible, because the numerical type is only number ) and the comma before length . It seems nonsense, but "sometimes" works. Why sometimes?

    In this case, the expression through the comma operator is divided into two expressions and the result will be the calculated value of the second. But the second expression is just length ! The fact is that here we are accessing the value of a variable in a global context. If the runtime is a browser, then window.length . And the window object really has a length property that returns the number of frames ( frame) On the page. If our document is empty, then length will return 0. Yes, an option with the assumption ... therefore, let us dwell on the previous one.

    And here are some more interesting options discovered (already for a different number of permutations):

    (_ => [,,].length+~1)() // 0
    +(~([,,].len_gth) >= 1) // 0
    ~(_ => 1)()+[,,].length // 0
    ~(_ => 1)().length,+[,] // 0
    ~[,,]+(_ => 1()).length // 0
    

    There are no comments. Does anyone find something even more fun?

    Motivation


    Good old-fashioned task about “rearrange one or two matches to make a square”. The well-known puzzle for the development of logic and creative thinking turns into obscurantism in such JavaScript variations. Is this useful? More likely no than yes. We touched on many features of the language, some even quite conceptual, to get to the bottom of the solutions. However, real projects are not about length from function and expression to steroids. This task was proposed at the conference as a kind of an addictive workout to remember what JavaScript is.

    Eval combinatorics


    Not all possible answers were considered, but in order not to miss anything at all, let us turn to real JavaScript ! If you are interested in trying to find them yourself, then there were enough tips above, and then it’s better not to read further.



    So, we have an expression of 23 characters, in the string record of which we will do permutations. In total, we need to perform n * (n - 1) = 506 permutations in the original record of the expression in order to get all variants with one permuted symbol (as required by the conditions of problem 1 and 2).

    Define the function combine, which takes an input expression as a string and a predicate to test the suitability of the value obtained as a result of the execution of this expression. The function will brute-force all possible options and evaluate the expression through eval , saving the result in an object: key - the received value, value - a list of mutations of our expression for this value. Something like this happened:

    const combine = (expr, cond) => {
      let res = {};
      let indices = [...Array(expr.length).keys()];
      indices.forEach(i => indices.forEach(j => {
        if (i !== j) {
          let perm = replace(expr, i, j);
          try {
            let val = eval(perm);
            if (cond(val)) {
              (res[val] = res[val] || []).push(perm);
            } 
          } catch (e) { /* do nothing */ }
        }
      }));
      return res;
    }
    

    Where the replace function from the passed string of the expression returns a new string with the character rearranged from position i to position j . And now, without much fear, we will do:

    console.dir(combine('+(_ => [,,~1])().length', val => typeof val === 'number' && !isNaN(val)));
    

    As a result, we got sets of solutions:

    {
    "1": [
      "+(_ => [,,~1]()).length",
      "+((_ => [,,~1])).length",
      "+(_ =>( [,,~1])).length", 
      "+(_ => ([,,~1])).length"
    ],
    "2": [
      "+(_ => [,~1,])().length"
    ]
    "3": [/* ... */]
    "-4": [/* ... */]
    }
    

    The solutions for 3 and -4 do not interest us, for the two we found the only solution, and for the unit there is an interesting new case with [,, ~ 1] () . Why not TypeError: bla-bla is not a function ? And everything is simple: this expression does not have an error for the syntactic parser, and in runtime it simply does not execute, since the function is not called.

    As you can see, in one permutation it is not possible to solve the problem for zero. Can we try in two? Solving the problem by such an exhaustive search, in this case we will have the complexity O (n ^ 4) of the length of the string and “evaluate” so many times pursued and punished, but curiosity prevails. It is not difficult to independently modify the combine function or write a better search that takes into account the particular expression.

    console.dir(combine2('+(_ => [,,~1])().length', val => val === 0));
    

    In the end, the set of solutions for zero would be:

    {
    "0": [
      "+(_ => [,~.1])(),length",
      "+(_ => [,~1.])(),length",
      "~1+(_ => [,,])().length"
    ]
    }
    

    It is curious that in the reasoning we rearranged the dot after the unit, but forgot that the dot in front of the unit is also possible: record 0.1 with zero omitted.

    If you perform all possible permutations of two characters each, you can find that there are quite a lot of answers for values ​​in the range from 3 to -4:

    { "3": 198, "2": 35, "1": 150, "0": 3, "-1": 129, "-2": 118, "-3": 15, "-4": 64 }
    

    Thus, the Countdown Expression solution path at two permutations can be longer than the proposed path from 3 to 0 on one.

    This was the first part of the analysis of our tasks at HolyJS 2019, and soon the second should appear, where we will consider the solutions of the second and third tests. We will be in touch!

    Also popular now: