Skip to content

feat: Binary runtime helper. #255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 23, 2019
Merged

feat: Binary runtime helper. #255

merged 10 commits into from
May 23, 2019

Conversation

amiller-gh
Copy link
Contributor

@amiller-gh amiller-gh commented May 7, 2019

Working binary runtime package. Currently clocks in at under 681 bytes minified and will greatly reduce the possibility for template bloat on rewrite. See #47 for implementation details. To complete:

  • Add support for nested binary expressions.
  • Flesh out runtime test suite.
  • Finalize expression constructor API.
  • Integrate expression constructor API with analyzer.
  • Integrate runtime generation with rewriter.

I expect that the base ElementAnalysis object delivered in core will proxy some version of the expression constructor to analyzers. The core rewrite transport object will then expose the three generated array parts – expression shapes array, classes array, and expressions array – to the rewriters to apply as the wish.

Edit: this is ready to land. I'd like to tackle tasks 3, 4 and 5 in a separate PR now that this is fully functional and tested 👍

@amiller-gh amiller-gh requested a review from chriseppstein May 7, 2019 05:29
@amiller-gh amiller-gh changed the title [WIP] feat: Binary runtime POC. feat: Binary runtime helper. May 9, 2019
Copy link
Contributor

@chriseppstein chriseppstein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, after reviewing this I have some nontrivial feedback that buckets into a few main categories:

  1. The need for expressions to reference the result of another expression.
  2. Making references to runtime values more transparent and adding explicit support for boolean/truthy values.
  3. Having an encoding preamble to make the expression evaluation at runtime more robust.
  4. Embracing stack order evaluation.

@chriseppstein
Copy link
Contributor

@amiller-gh you requested a code example explaining expression references. here it is:

Consider the following classes:

.foo {
  color: red;
  width: 50%
}

.bar {
  color: blue;
  width: 100%;
}

.baz {
  color: red;
  width: 100%;
}

Which optimizes to:

.a {
  color: red;
}
.b {
  width: 50%;
}
.c {
  color: blue;
}
.d {
  width: 100%;
}

Template logic applies these source classes using something like:

<div class="${isFooOrBar ? 'foo' : 'bar'} ${bazEnabled ? 'baz'}">

Ideally the template rewriter (in dev mode) would rewrite to something like:

<div class="${runtime('devshape', ['foo', 'bar', 'baz'], [isFooOrBar, bazEnabled])}">

and the opcodes would be (with a stack-based representation):

foo => [VAL, 0]
bar => [VAL, 0, NOT]
baz => [VAL, 1]

and when the optimizer is enabled it would become:

<div class="${runtime('optimizedshape', ['a', 'b', 'c', 'd'], [isFooOrBar, bazEnabled])}">

Now the opcodes would be (with a stack-based representation and expression references):

0 => [VAL, 0]
1 => [VAL, 0, NOT]
2 => [VAL, 1]

a => [REF, 2, REF, 1, NOT, REF, 0, AND, OR] // or(and('foo', not('bar')), 'baz')
b => [REF, 0, REF, 1, REF, 2, OR, NOT, AND]
c => [REF, 1, REF, 2, NOT, AND]
d => [REF, 1, REF, 2, OR]

In this example the logic for the values is very trivial so it seems like
we could just inline them, but for more complex logic this gets unwieldy
and it also doesn't let the optimizer expression logic remain decoupled
from the template logic.

@amiller-gh
Copy link
Contributor Author

@chriseppstein, this is ready for review again 🎉

Fun fact, entire runtime minifies to 397 bytes:

function r(r,e,n){for(var f,t,i,a=n.slice(),l=n.length,o="",g=0,h=-1,u=0,c=2,s=0;s<r.length;s++)for(var v=parseInt(r[s],36),b=32;b--;){if(3==t&&(g=h=0),!c){for(;l-1>>c++;);c=g?c:2}if(u+=v%2*(1<<--c||1),v>>>=1,!c){if(i=!!(g&&+!!a[u>>>1]^u%2),g||(t=u),1==g&&(f=i),2==g&&(0==t&&(i=f||i),1==t&&(i=f&&i),2==t&&(i=f===i),a[l++]=i,~h&&(o+=i?(o?" ":"")+e[h]:"",++h==e.length)))break;u=0,g=++g%3}}return o}

Before I land this I'd like to update the runtime readme with a detailed description of our opcode packing scheme.

I updated it a little more from our past conversation. Instead of a preamble counting the number of expected args and expressions, I've used our spare opcode for a SEP (separator) operator. This allows us to differentiate between shared expressions, and the final triples that describe class application. I still believe in production we don't want the extra code to track expected input sizes, when we can do this statically at build time. It would bloat both the runtime and binary shapes with unnecessary data.

Take a look and let me know what you think – theres a lot of binary operators packed in there for terseness. Its well commented, but happy to answer questions 👍

val = !!(state && (+!!exprs[working >>> 1] ^ (working % 2))); // tslint:disable-line

// If discovering as opcode, save as an opcode.
if (state == STATE.OP) { op = working; }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm intentionally avoiding switch statements here to keep the minified runtime tiny. These static if statements should be just as performant for such a small option set. I also left out the else if expressions to keep byte size to a minimum. Its a tradeoff between size and couple extra comparison operations at runtime (that for all we know the compiler optimizes away for us). Let me know if you think a different tradeoff makes sense.

@chriseppstein
Copy link
Contributor

@amiller-gh Instead of comments, I've pushed changes for the things I wanted changed. I've pushed them as discreet commits so you can review each one and so they can be easily backed out.

@chriseppstein
Copy link
Contributor

Approved: Feel free to merge this when you're ready.

@amiller-gh amiller-gh merged commit 959fb8b into master May 23, 2019
@amiller-gh amiller-gh deleted the binary-runtime branch May 23, 2019 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants