Code Monkey home page Code Monkey logo

js.metaret's Introduction

Goal

Write less code, that reads clear and runs fast [Budapest 2014 slides, video, both].

Contents

How?

Extend JavaScript with three new keyworkds: metafun, metaret and inline to write code that is short, expressive AND runs fast.

The extended JavaScript is automatically transformed back into fast, 100% standard JavaScript.

Fast functional calls: Tail metacomposition

For tail calls (quick definition), metafun and metaret can replace function and return.

This eliminates the call stack, which speeds up the code dramatically. Mutual tail recursion is supported.

Fast imperative calls: Inlining

inline speeds up imperative calls by eliminating function call overhead.

Convention

  • .jsm files may use the extra keywords metafun, metaret and inline.
  • .js files contain 100% standard JavaScript.

Functional example

./jsm2js.js transforms this clear, expressive code:

metafun gcd(self, a, b) {

  if (a > b)
    metaret self, a-b, b;

  if (b > a)
    metaret self, b-a, a;

  return a;
}

into 100% standard JavaScript code that runs fast (no function call):

function gcd(a, b) {
  _L_gcd_: while (true) {

    if (a > b) {
      var _a1_ = a - b;
      a = _a1_;
      continue _L_gcd_;
    }   
   
    if (b > a) {
      var _a_ = b - a;
      var _b_ = a;
      a = _a_;
      b = _b_;
      continue _L_gcd_;
    }
   
    return a;
  }
}

Imperative example

./inline_code.js transforms this clear, expressive code:

function doSomething() { alert("done!"); }

inline doSomething();

into (pretty ugly) 100% standard JavaScript code that runs fast (function call eliminated):

function doSomething(){alert('done');} 
  {
//#INLINE_BEGIN: inline doSomething()
var _undef_,_ret_;
//#INLINE_SET_INPUT_ARGS:
//#INLINE_IMPLEMENT:
do {
{alert('done');}
} while (false);
//#INLINE_END: inline doSomething()
};

Three variants are permitted:

inline doSomething();
inline x = getSomething();
inline var x = getSomething();

Node.js support

Thanks to Iain Ballard, there is a Node.js npm package (GitHub source).

Alternatively, you can develop using just a browser, or the V8 engine alone. See "Getting started".

Getting started: develop your app

Requirements:

Example

Getting started: test and build your app

Requirements:

jsm_build.py (1) transforms all .jsm files back to 100%-standard JavaScript .js files, (2) automatically runs the corresponding .test.js files and (3) collate them into a single, minified .js file.

Example

jsm_build.py jsm_dev/example_development.jsm

Bigger example

Under ./jsm_dev/:

Assuming that you have installed Python 3 and V8, you can build this example into one minified file:

jsm_build.py jsm_dev/expl.js

This takes the original jsm_dev/expl*.js[m] files and produces:

  • as many 100% JS-compatible files: jsm_out/expl*.js
    • ...and tests them against the corresponding .test.js files.
  • one build file: jsm_out_build/expl.js
    • ...and tests it against the corresponding .test.js files.
  • one minified file: jsm_out_mini/expl.js
    • ...and tests it against the corresponding .test.js files.

A test file .test.js declares one function test () { ... } which returns true if success. Any other behaviour is seen as a failure:

  • test() throws an eror,
  • or test() returns something else than true.

Background

If you do not know what a tail call is, you can have a look at this slide.

Earlier I implemented "mutual tail recursion optimization without trampoline" [1] for good performance, which transforms the clear, expressive but slow code (many function calls):

function gcd_rec(a, b) {

  if (a > b)
    return gcd_rec(a-b, b);

  if (b > a)
    return gcd_rec(b-a, a);

  return a;
}

into a very fast while loop (no call stack):

function gcd_loop(a, b) {
  while (a != b) {
    if (a > b) {
      a = a-b;
    }
    else if (b > a) {
      var c = a;
      a = b-a;
      b = c;
    }
  }
  return a;
}

But implementing this automatic transformation led me to write quite insane code [2].

Moreover, since the automatic transformation worked on 100% normal JavaScript, the difference remained implicit between tail calls (optimized):

// tail call: return + single function call
return gcd_rec(a-b, b);

and the other function calls (not optimized):

// not a tail call: no return statement
t.children.forEach(doSomething);

// not a single function call
return sumtree(t.left) + sumtree(t.right);

You cannot expect you whole team to know about this implicit difference, which makes it somewhat unfit to develop large-scale applications.

Instead, here we make the difference explicit using metafun and metaret instead of function and return:

// WITH function calls (call stack)
...
var v = f(x);
...
return f(x); 
...
return f(x)+g(x);
...
o.doSomething();
...

// WITHOUT function calls (no call stack)
metafun gcd(self, a, b) { // metafunction: contains metaret

  if (a > b)
    metaret self, a-b, b; // NOT a call, rather a sanitized goto

  if (b > a) {
    metaret self, b-a, a; // NOT a call, rather a sanitized goto

  return a;
}

metaret simply change the parameter values and jumps to the beginning of the metafunction. This runs fast (no call stack), implements one tiny bit of Backus' metacomposition [3], and can be seen as a sanitized sort of goto:

  • you cannot just put a label anywhere and jump to it (spaghetti code).

  • metaret can only jump to the beginning of a metafunction.

  • therefore, you still have to think in term of clean encapsulations using metafun and metaret, just as you would using function and return.

Addition

metaret only permits a tail call, which excludes an imperative call (calling without returning).

Thus, an extra keyword inline was also added that triggers hygienic inlining, see issue #3 and expl_longer.jsm for examples.

inline can be useful to speedup imperative code. Three variants are permitted:

inline doSomething();
inline x = getSomething();
inline var x = getSomething();

Fun fact

./metaret_standalone.js was produced by building the one-liner ./jsm_dev/metaret_standalone.js:

need$( 'need$.js' );

For more details: ./build_standalone.sh

Core tests

The command-line ./test.py and its browser pendant ./test.html (live).

js.metaret's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

i-e-b

js.metaret's Issues

error messages for beginner

Based on one comment on my talk at mloc.js 2014.

There is already a warning if the user defines a metafunction without any metaret, which is valid but does not bring any optimization.

Now if the user uses metaret from within a normal javascript function

function f() { ... metaret ... }

then the code is not transformed, so the metaret is still present in the resulting code, and an error is triggered when evaluating the code.

Task: in this case give a more meaningful error message to the developer, before the eval.

inline

Add support for convenience inlining of normal functions:

inline var result = f(x,y,z);

inline result = f(x,y,z);

inline f(x,y,z);

Requires hygenic renaming of local variables within the calling scope.

lightparse: improve the detection of regexp

When re-parsing minified code false positive detections of regexps are triggered by a division sign because the current strategy is still too naive. This is a problem if we want to reparse the minified code - where most newlines have been removed - to e.g. further shorten it.

blah = x / y; ...... line goes further ...... ; foo = bar / bin

-> false positive with the current implementation: / y ..... bar / detected as a regexp because the identifier x is not taken into account.

Task: reject such false positive regexps.

Difficulty: we want to try to keep lightparse "light" i.e. without a full tokenizer (mmm maybe I won't avoid it in the long term, but for now this way if it goes). So we should not only reject the simple false positives but also prevent introducing false negatives:

for (...) /true regexp/.exec();    // Here the regexp is correct!
while(...) /true regexp/.exec();    // Here the regexp is correct!
do (...) /true regexp/.exec();  while ( )  /another true regexp/ ...  // Here both regexps are correct!
similarly with if(...) /true regexp/
similarly with else /true regexp/

See also the remarks at the beginning of section 7 of the ECMAscript 5 spec:

NOTE There are no syntactic grammar contexts where both a leading division or division-assignment, and a leading RegularExpressionLiteral are permitted. This is not affected by semicolon insertion (see 7.9); in examples such as the following:
14 © Ecma International 2011
a = b
 /hi/g.exec(c).map(d);
where the first non-whitespace, non-comment character after a LineTerminator is slash (/) and the syntactic context allows division or division-assignment, no semicolon is inserted at the LineTerminator. That is, the above example is interpreted in the same way as:
a = b / hi / g.exec(c).map(d);

autotest while building

At each step of the build, if there is a corresponding .test.js file, it should be executed in a sort of sandbox. If the test fails, then the build should break down ((over)write "empty" files with some error message).

mini

finish a first version of jsm_out_mini

build or jsm2js: remove unused function declarations

Background: in cases where the original metafunction (resp. function) is only used within metafun (resp. inline) then after converting the code to normal JS we may end up producing unused function declaration (basically code duplicates).

Unused means: not in the global namespace, not called or passed around anywhere at all.

Task: remove such a function. Not sure yet at which stage:

  • right away during jsm2js (could help debugging: reduce confusion).
  • at the next step (build).

support inline across files

function declarations only + function name must be unique else error (we only parse and change the code, we do not execute it at this point).

About closures:

  • forbid inlining of a function from another file when the function body uses some external variables (closure).
  • permit closure when inlining a function within the same file as long as there is no ambiguity on the external variables. Else forbid (error).

instead of .jsm, permit .js

Reason: Switching file names and corresponding dependency calls is not practical. Looking for metafun/metaret (and inline) keywords should be simpler.

Add npm support?

Hi.

I've hand-made a little npm package for js.metaret (https://github.com/i-e-b/metaret-npm)

If the generated js files included shims like this at the top

global = global || exports;
if (exports) {
    lightparse = require('./lightparse').lightparse;
    lp2fmtree = require('./lp2fmtree').lp2fmtree;
}

Then packaging for Node.js could be done automatically.

beginner help: detect `arguments` usage

We cannot inline the body of a function that uses arguments, because the arguments would then (wrongly) point to the containing function. An explicative error should be thrown in such a case.

local namespaces

Add support for local namespaces e.g.

(function (global) {
  global.myFunc = myFunc;

  metafun myFunc ...

})(this);

beginner help for metafun: throw closure-related errors

Similarly to issue #7 but now for metafun in the mutual recursion case ("other" dependencies):

  • when the "other" metafun is in another file... well we do not allow that at all, because most of the time mutual recursion involves tightly coupled metafunctions == on a similar or close order of abstraction. (Contrast this to the inline case, where one "mother" function needs to inline small tool functions possibly from another file == much lower level of abstaction == the programmer does not want to even think about the inlined body at the target place).
    • so all metafun must be in the same file.
  • when all involved metafun are in the same file they are allowed to have bound local variables as long as they are share all of them == it is forbidden to have a bound variable in one metafun that has the same name as a bound variable in another metafun, but are actually different == homonyms declared in two different scopes.

Some (stupid) example to clarify the issues involving bound variables.

This is okay:

var a; // shared bound variable
metafun f(self) { ... var x = a + 1; ... metaret g;  }
metafun  g(self) { ...var x = a + 1; ... metaret f; }

This is also okay:

var a; // shared bound variable
metafun f(self) { 
... var x = a  + 1;... metaret g;  
    metafun  g(self) { ... var x = a + 1;... metaret f; } 
}

This is NOT okay:

var a; // bound by f
metafun f(self) { .... var x = a + 1;... metaret g; }
metafun g(self) { 
    var a;  // homonym: redeclared by g, same name, but different variable
    var x = a + 1;
    metaret f;
}

...because it leads to ambiguities when resolving the metafunctions into normal JavaScript function with while loop and the inlined bodies of f and g.

support imbricated inline + detect and forbid inline cycles

for convenience

This one is permitted (no cycle):

function a() { inline var x = b(); return x+1; }
function b() { inline var y = c(); return y+1; }
function c() { return 1; }

This one is forbidden (cycle, throw Error suggesting using metafun/metaret instead):

function a() { inline var x = b(); return x+1; }
function b() { inline var y = a(); return y+1; }

(stupid examples but clear)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.