Ahead-of-time JavaScript compiler
github.com
github.com
For the compiler un-initiated such as myself, is there a simplified explanation for why static compilation is inferior to JIT for languages like javascript?
E.g.: "Every time this function has been called so far, the first argument was a small int. Since it's a hot function, I'll recompile it and optimize for that assumption, and wrap the result in a type check. If the type check ever fails I'll fall back to the unoptimized function, and update my assumptions about the function's types."
In other words, a general compiler has to assume all JS types are dynamic. V8 does the same at first, but if it sees code behaving like it was statically typed, it can take advantage of that.
function myFunc() {
console.log(this);
}
var myObj = {};
// this = window
myFunc();
// this = window
myFunc.apply(null, []);
// this = myObj
myFunc.apply(myObj, []);
myObj.myFunc = myFunc;
// this = myObj
myObj.myFunc();
var tmpFunc = myObj.myFunc;
// this = window
tmpFunc();
Another issues is closures, where variables and properties available to nested functions are dependent on the outer functions. Combined with the floating "this", leads to highly dynamic behaviour which can only be assessed at runtime.The tricky part would be consolidating every structural subtype into cohesive classes.
With JIT you're only interested in the type of a variable when you're reading it, whereas ahead of time you'd have to account for many cases - I think this would be a big deal with JavaScript's inheritance model.
The first and most obvious problem in terms of performance is that for sufficiently dynamic languages it is exceedingly hard to know what types will look like at runtime. Both in JS and Ruby you can define types at runtime. In Ruby by mutating classes at runtime, and in JS by mutating prototypes.
This means that it's much harder to e.g. lay out objects so that you can cheaply access attributes just through indexed reads for example. It also makes e.g. inlining very hard, as it may be hard to impossible to even determine the types of objects you'll get passed. There are lots of workarounds, but many of them bring you progressively closer to including a full JIT anyway.
For a JIT like V8, you can infer/guess at types because you have the entire infrastructure to correct your guess. E.g. if you come across a "Point" class that appears to have the attributes/instance variables "x" and "y", you can allocate those objects, and if a "z" is introduced later, you can either create a hidden subclass dynamically or you can re-allocate all existing Point instances with an extra field, and re-compile the code that accesses it accordingly. In an ahead of time compiler, this is harder without either embedding a JIT anyway, or embedding specialized code that carries out specialised code generation tasks etc., or trying to infer a single unified type (but this is problematic; code that is validly executed for the Point class when the instances doesn't have "z" could very well fail or behave entirely differently once it is mutated to add it)
Ultimately I believe that a lot of dynamic code is ultimately de-facto static, but even for code that in effect is 100% static at runtime, you sometimes may need extra hints to guarantee that. E.g. any code that loads a library could potentially be intended to be static, but for dynamic languages you may need extra hints beyond the semantics of the language to know whether the library is meant to always be static, or e.g. is intended to be a plugin mechanism. (A typical Ruby idiom is to iterate over a directory listing and "require" every .rb file found in it; that maybe to avoid having to explicitly list all of them, or it may be to let people drop in plugins)
Once you know where that delineation is, there's often nothing particular preventing type inference across the board, but it can be a lot* of effort. E.g. in both Javascript and Ruby, you face the situation that an analysis of the types that is valid in one instance can be invalid the next, so must be at least in theory prepared to run type-inference several times and potentially be forced to generate different code based on when in the program fow a method is called.
E.g. even in my compiler implementation, I'm likely to, for the ease of bootstrapping, end up replacing methods with more capable versions during the bootstrap of the runtime library, and it may very well end up altering which types are safe inputs to a given methods.
Dynamic languages also often make it tricky to determine conclusively what should be compile errors. E.g. a lot of constructs in Ruby will trigger exceptions that seems like they should be compile time errors. For example there's a LoadError if "require" fails to find a file. But most of them can also be caught, and then for an AOT compiler you face the challenge of deciding whether to try to process that at compile time (e.g. conditionally including an optional dependency can be done this way) or generate code to try the load at runtime (and thus losing a lot of the ability to statically infer the precise types at compile time, as for what you know the runtime version of this file may redefine everything).
For my part I intend to resolve a lot of this with pragma's and compiler flags to let you signal whether to favour precise compliance with the interpreted Ruby, or making it as static as possible (if I ever get that far...)
You always want to compile to the easiest target language that solves the problem well and allows you to do the optimizations you want to do. By compiling to C++, they can focus on static typing and closures etc., but don't have to worry about generating machine code, and register allocation and whatnot.
Back in school, I worked on a project to compile Matlab. We picked Fortran as the target, because it was very similar in terms of operating on matrices. But it's typed, and had a few very fast compilers available.
Python compiles to its own bytecode, but only when a script is first interpreted.
Haxe compiles to many different source code languages ahead of time, including C++, Python, Java, JavaScript, and C#. It also compiles to a custom "Neko VM" bytecode.
So, "compiling" isn't necessarily ahead of time, depending on what level of "compilation" you're talking about.
Well, not that implicit, seeing that we also say "JIT compiler".
Transition the runtime to C [from C++]
Wondering why? I thought C++ and C are comparable in speed.