This article was originally published on maurobringolf.ch.
Instead of starting with abstract questions and reading code to answer them, I will consider concrete program input and walk through its execution. This might be a more natural way for discovering a code base and hopefully a more narrative one as well. The plugin I consider is babel-plugin-minify-constant-folding. It is responsible for evaluating expressions of values that are known at compile-time. Just like last time I will link to code in the repository that I talk about. All links will reference a specific commit, such that even when the code is changed in the future these links should still make sense.
Our running example for this post is the input
3 + 4 which is transformed to
3 + 4 // input code 7 // output code
I admit, this is not a very impressive feat. However, walking through this will explain minification of any other arithmetic or logical expression on constant values as well. This is impressive to me, but we will see why that is later.
The visitor exported by the
constant-folding plugin visits
BinaryExpression, so it will work on
3 + 4 via this function. However, as explained in the functions header comment this code only works on a specific case of multiple string concatenations. Since neither
4 is a string, this function will just return without any effect.
Luckily we have more matching visitors such as Expression. There are quite a few things going on in this function. If you read my first post on the minifier, you know that
false are transformed into
And right here we have a check not to mess with these transformed values. I am not sure whether this is happening, but I guess it is either performance or correctness. But a bit further below we can see that the actual folding is done by the
evaluate function. This is a separate package altogether, so let us finish here first. Assuming we get
7 back from the magic evaluation function the plugin does not simply insert this value into the AST. Only under certain conditions the expression is replaced with its result.
It skips any numeric results that are not integers, which seems a bit rough to me.
A comment states two reasons: Numeric precision might be off and fractions can be longer than their original expressions. I buy the first one, but the second thing can also happen for integers:
2**20 is evaluated to
I am not sure what to think of this, so let's move on. In some cases the function substitutes the original expression with its result via
path.replaceWith(node) which will give us our
7 as result.
As we have seen the main task of expression evaluation is performed by another package called babel-helper-evaluate-path. It turns out that this is also a wrapper for the method
path.evaluate. So Babel gives the evaluation logic as an API to all Babel plugins and it is not something that the minifier itself implements. Looking around the Babel codebase I found the logic contained in a file titled as metainterpreter. Most of the work is done by the
_evaluate function. It takes a
BinaryExpression node so it will only enter the case checked by
What happens next is called recursion: Evaluation of a binary expression requires evaluation of both its argument nodes. So both argument nodes will run through the same type checks and end up matching the
isNumericLiteral case. This is the base case of the recursion because a literal is evaluated by simply returning its value.
Now the call stack collapses and arguments are given to the binary expression which computes
3 + 4. It should be clear that
(7 + (2 ** 6) - 1234) * 2 and any other arithmetic expression on constants works the same way by just doing more recursive function calls. This is the power of recursion.
In the code for evaluation we find a
confident flag which indicates if we can trust the result. This might not make a lot of sense in the case of arithmetic expressions on number literals, but more involved code can make it impossible to evaluate an expression statically. I touched upon this subject also in the context of Babel when I wrote about the limits of static code analysis in an older post. Whenever an expression or sub-expression cannot be evaluated with confidence, the plugin immediately returns without transforming anything.
Like any other program, the constants folding plugin of Babel's minifier is no magic. An expression node of the abstract syntax tree which only operates on values known at compile-time can be evaluated by recursion. This will replace the entire subtree of the expression with a single literal node containing its result.
It obviously is an optimization until you start thinking about it. Abstract syntax tree size does not directly translate into code size. Consider the exponential
2**20 whose value is
1048576. By replacing the expression with its result we make the code longer.
Fine, should we only replace results which are shorter than their computing expressions? This sounds good, but is not ideal either. Consider the expression
2**20 * 0 whose value is
0. If we do not replace
2**20 because its result is longer than its expression, we cannot compute the multiplication and get to the short
0. (Technically it might be possible in this case, because multiplication with zero is always zero.
But you can come up with other examples without zero but the same property.)