linkedin/dustjs-helpers — pull request #55 by rragan (2013-10-14 01:32:59+00:00)
Title: Add option to @if that does not use eval

Here it is at last folks. The existing @if depends on eval which is undesirable. This change provides an option to @if that will not use eval. As a result of the changes, @if can be used with either test="expr" or cond="expr" (the eval way). This ensures backward compatibility.

The expression for the test param is a relational expression using:

* names, dust paths including subscripts in the path. Anchored paths are the same as normal dust rules.
* constants (strings, integer, float, and hex)
* relational operators ==, !=, <=, >=, <, >, ! (these are an abstraction of the JavaScript operators and == maps onto === and != onto !==).
* && and || for combining relational expressions
* parentheses for grouping. Default operator precedence is the same as JavaScript
* dust references using braces are allowed and are evaluated in determining the final string expression. This allows a simple conversion by changing cond="expr" to test="expr" for most if's

The implementation tokenizes the @if expression and then interpretively executes the token stream. The rumor was that eval is slow. It's not all that slow when you have to do roughly the same work in JavaScript that is built in to eval in c++. After several iterations, I've managed to get my version fairly speedy but it is typically still a bit slower than eval. See below for timings:

Things to bear in mind regarding the change:
* A simple cache mechanism saves re-tokenizing the expression for @if's in loops for a nice speedup
* Simple @if test (var == constant) takes 1.4 usec with eval and 2.1 usec without eval. Short of an incredibly tight long running loop in a template, this increase is unlikely to be noticeable
* Uncached timings are quite a bit longer as the tokenizing cost must be paid. Still if the @if is only executed once, it is unlikely to be noticeable. It is in loops that things matter and this is where caching helps.
* Expressions are easier for the user to write because names and paths do not have to be wrapped in braces or braces within quotes. E.g. {@if cond="\'{state}\' == 'CA' "} versus {@if test="state == 'CA'}
* One can argue that slightly slower @if test="xxx" is another argument to encourage users to keep the logic simple in the template. Removing all the things eval allows, like .length, parseInt, etc. further eliminates the "too much logic" tempation.
* Lots of tests are included (78 new ones at last count including a number of negative tests)

Time for 1 million iterations: node on a 3.06GHz Intel Core Duo iMac.
You can think of the timing number as a time in microseconds to do one @if.
============================================================

Timings for if cond | test="2>1"
if cond: 0.859237944
if test cached: 1.723681162
if test: 5.232863085

Timings for if cond | test="2>1 || 3>=3"
if cond: 0.940688659
if test cached: 2.150172574
if test: 6.853805585

Timings for if cond | test="2>1 && 3>=3 && 4==4 && 5!=6 && 8>7 && 9>=9 && 10>=9 && 9<=10"
if cond: 1.01484539
if test cached: 5.669977481
if test: 16.265120939

Timings for if cond="{two}>{one} && {three}>={three} && {four}=={four} && {five}!={six} && {eight}>{seven} && {nine}>={nine} && {ten}>={nine} && {nine}<={ten}"
if test=two>one && three>=three && four==four && five!=six && eight>seven && nine>=nine && ten>=nine && nine<=ten
if cond: 6.218082587
if test cached: 9.84698457
if test: 16.932856676

Timings for expression: if cond={arr}[{nums}[1]]==3 if test=arr[nums[1]]==3
if cond: 6.239112676
if test cached: 2.762390068
if test: 8.861937934

Timings for if cond="{one}==1" if test="one==1"
if cond: 1.432628352
if test cached: 2.186059046
if test: 6.487652594

PayPal is eager to have this capability as we move towards using CSP headers to eliminate XSS problems.