Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Can prepack run on itself? #1307

Closed
jlongster opened this issue Jan 2, 2018 · 21 comments
Closed

Can prepack run on itself? #1307

jlongster opened this issue Jan 2, 2018 · 21 comments
Labels

Comments

@jlongster
Copy link

jlongster commented Jan 2, 2018

This is 100% not an issue but a question, so sorry for posting this here, but I don't see any other way to talk to the prepack community (any gitter/slack/etc channels?).

I'm having too much fun using prepack with interpreters & compilers. Today I was able to turn my interpreter, which interprets high-level instructions, almost into a compiler by running prepack on it with a given source code: https://twitter.com/jlongster/status/948247424023322624

I say "almost" because I don't really have a compiler yet. Prepack is running my VM and outputting the JS code itself. Evidently this is called Futamura projection. Since prepack isn't very fast though, this isn't extremely practical. I want just the raw compiler itself.

What I learned from that wiki page is we can take this even further. Supposedly I can generate an actual compiler by "specializing the specializer for the interpreter" - meaning run prepack on a program that runs prepack on the interpreter.

That would require prepack being able to run on itself. Is that possible? I'm going to try it tonight but given this is all blowing my mind, and I'm sure I'll run into problems, I'd like to know if anyone has tried that yet.

@cblappert
Copy link
Contributor

@sebmarkbage made a yarn prepack-prepack command, although I am not sure it still works and it was pinned to a specific node version.

@jlongster
Copy link
Author

That's super cool. Even if it ever worked at one point, that's encouraging and means it's achievable. I'll check it out and see if it still works.

@sebmarkbage
Copy link
Contributor

I believe it is broken on other versions of Node since it provides some shims for node internals that change between version and even then some of it is a bit shaky. Ideally we'd do this in a membrane where we simulate what would've happened if we ran it in Node's runtime but don't actually snapshot the whole of Node's internals too.

It definitely works though. Prepacking Prepack itself isn't hard. It's Node's file system handles, module system etc. that gets tricky but also doable.

@jlongster
Copy link
Author

Do you happen to know off-hand which version of node is the last one that worked?

Does prepack even need to use any of the filesystem if all I want to do is give it a source string to partially evaluate? Basically, I don't want any of the CLI. It's probably not as shaky if I ignore that part of the system, as you said prepacking prepack itself is more straight-forward.

@sebmarkbage
Copy link
Contributor

sebmarkbage commented Jan 2, 2018

If you don't want any of the node specific things you can use https://github.com/facebook/prepack/blob/master/src/prepack-standalone.js

Other than that it is basically just for the module system. You can use rollup or something on that file first, and then Prepack the result. (Compile with Babel first just like the normal build those since we may not support all features that we use.)

@jlongster
Copy link
Author

FWIW I tried to compile it tonight. Here's the webpack config to generate a bundle of prepack standalone:

module.exports = {
  entry: './src/prepack-standalone.js',
  output: {
    path: __dirname + '/dist',
    filename: 'output.js'
  },
  module: {
    rules: [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }]
  }
};

This generates a dist/output.js folder. Running prepare throws an obscure invariant violation though:

% prepack ./dist/output.js --out ./dist/processed.js --mathRandomSeed rnd --compatibility node-cli
Invariant Violation
    at invariant (/usr/local/lib/node_modules/prepack/lib/invariant.js:19:15)
    at Realm.onDestroyScope (/usr/local/lib/node_modules/prepack/lib/realm.js:295:31)
    at evalMachine (/usr/local/lib/node_modules/prepack/lib/intrinsics/node/contextify.js:299:13)
    at NativeFunctionValue.runInThisContext [as callback] (/usr/local/lib/node_modules/prepack/lib/intrinsics/node/contextify.js:115:12)
    at NativeFunctionValue.callCallback (/usr/local/lib/node_modules/prepack/lib/values/NativeFunctionValue.js:101:53)
    at OrdinaryCallEvaluateBody (/usr/local/lib/node_modules/prepack/lib/methods/call.js:314:16)
    at InternalCall (/usr/local/lib/node_modules/prepack/lib/methods/function.js:136:49)
    at FunctionImplementation.$Call (/usr/local/lib/node_modules/prepack/lib/methods/function.js:1227:14)
    at NativeFunctionValue._this.$Call (/usr/local/lib/node_modules/prepack/lib/values/NativeFunctionValue.js:44:36)
    at Call (/usr/local/lib/node_modules/prepack/lib/methods/call.js:495:12)

I copied the same flags as the prepack-prepack command in package.json. Not sure what to look for since there's not much info about the invariant violation. Based on the stack it looks like it's this invariant.

@sebmarkbage
Copy link
Contributor

Don't use the node-cli mode. That compiles the entire node runtime into the file. You don't need that if you webpack. Just leave it at the default compatibility mode.

@sebmarkbage
Copy link
Contributor

I don't have high hopes of this working well for your use case though. I assume that what you want is to run Prepack, not just on itself, but on itself combined with your interpreter while leaving abstract paths intact. There are many limitations of Prepack that you are likely to hit in this case.

There's a reason that not all compilers are as magical as Prepack and you have to be aware of the current limitations so you can apply it to scopes where the limitations of the scope unlocks some new capabilities.

E.g. module initialization has the benefit that most input is already known AOT and that the start of the program is known. React components has the benefit that you can assume that render functions are pure as well as that the programming model uses lazy evaluation so that if you need to bail out you have safe places to do that.

If you can't make those assumptions then it is not safe.

Prepack can run its whole initialization phase and export the API without running the API through some input data. Prepack can also run itself and produce the result given static input. It cannot be given arbitrary abstract input and partially compile itself in arbitrary ways.

@jlongster
Copy link
Author

Ok, that mostly works. Looks like it pulls in the debug module which has this code:

return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||

inside a useColors function, which prepack fails on:

In input file ./dist/output.js(95990:18) FatalError PP0001: This operation is not yet supported on abstract value document  (https://github.com/facebook/prepack/wiki/PP0001)

If I just removed all the code within useColors and return false, it prepacks!

If I add this code at the bottom of prepack-standalone.js:

console.log(prepack('var x = 0; for(var i=0; i<5; i++) { x += i }; console.log(x)'))

Then creating the new bundle and prepacking it generates a file that I can run in node (I need to go to the bottom of the file and replace }).call(this) with }).call(global) to run in node, is there a flag to automatically do that?). Running the prepacked JS output what you'd expect!

% node ./dist/processed.js
{ code: 'x = 0;\ni = 0;\ni = 1;\nx = 1;\ni = 2;\nx = 3;\ni = 3;\nx = 6;\ni = 4;\nx = 10;\ni = 5;\nconsole.log(10);',
  map: null,
  reactStatistics: null,
  statistics:
   _23 {
     objects: 0,
     objectProperties: 0,
     functions: 0,
     functionClones: 0,
     referentialized: 0,
     valueIds: 0,
     valuesInlined: 0,
     delayedValues: 0,
     acceleratedModules: 0,
     delayedModules: 0 },
  timingStats: undefined,
  heapGraph: undefined }

But I think I see what you mean. It will probably fail if I try to give it a program that requires part of prepack to be abstract itself. It looks like it was able to run prepack on the given program entirely in the prepacking phase, so none of prepack needed to be abstract.

@jlongster
Copy link
Author

Well, I bundled up my compiler+VM with this code in the entry point:

function runSource(src) {
  vm.runSource(src);
  console.log(vm.reg1);
}

Then pasted that code as a string into the prepack bundle, and it's now prepacking that file... been almost 5 minutes but it's still going! I doubt it will finish without an error but we'll see what happens.

@jlongster
Copy link
Author

But this does clarify that this isn't suited for what prepack currently is built for: optimizing the startup path, since core value (the source) isn't known AOT. It's fun to think about this though. I'll update with whatever happens and then close the issue.

@jlongster
Copy link
Author

It ran for a long time and then errored with this: Invariant Violation: FatalError must generate at least one CompilerDiagnostic. That's the end of this experiment. Thanks anyway!

@jlongster
Copy link
Author

I see in the prepack CLI it will not show any errors thrown if there are no compiler diagnostics. That's what results in seeing "FatalError must generate at least one CompilerDiagnostic" without any info. It might be nice to add an option or something to print out the internal error. (related to #1305)

Turns out it was just a stack overflow, so I bumped up the stack which should fix it. Still running.

@jlongster
Copy link
Author

I continually tried bumping the stack size up until it's using a huge number (--stack_size=100000) and I still get "maximum stack depth exceeded". Not sure if it's genuinely too complicated or if it's hitting an infinite loop. Either way, this probably isn't going to work and I'll stop spamming this issue!

@jlongster
Copy link
Author

Oh, I just realized this is not V8 hitting it's stack limit, this is an error thrown by prepack itself: https://github.com/facebook/prepack/blob/master/src/realm.js#L362. I hardcoded a much larger stack limit and we'll see what error comes next!

@sebmarkbage
Copy link
Contributor

sebmarkbage commented Jan 3, 2018

I think the document check is truthy but then fails because by default the "compatibility" mode is set to browser which includes a very limited stub of the document object. We should allow a compatibility mode that excludes the document object so that the check and just provides a very plain JS environment.

You can add something like plain here:

https://github.com/facebook/prepack/blob/master/src/options.js#L14-L15

That doesn't do anything special.

There are two other pieces of work that might be relevant here.

@hermanventer has been working on supporting loops over abstract values. That is still pretty fresh but seems like it would be critical to your use case. Definitely interesting to see.

I've also experimented with allowing abstract function calls to accept mutable objects/bindings. I don't think you'll need that but it might come up at some point when all possible functions aren't known. That requires a special flag atm. You'll know if you need it though since you'll get a warning that an abstract operation isn't supported.

@jlongster
Copy link
Author

That's awesome! I don't comprehend abstract values fully yet, or how prepack should work with libraries instead of apps, which is essentially what I'm doing. I'd love to study prepack more and contribute though when I find time. In the next week or so I'll try to make small tweaks.

Prepack seems to working on prepacking my use case - but it's been running for 2.5 hours now. Not sure if it's hit an infinite loop or not. Last time I let it run for a while and it eventually finished (after an hour maybe?). In that case I forgot to export the main function so prepack just compiled it all away. Now I'm exporting a function "run(src)" (which will actually compile, not run) to the global scope. I'll leave it running until something happens.

@hermanventer
Copy link
Contributor

We still need a lot of work before we can handle every loop in Prepack, but in principle this is doable this year. Another big gotcha is recursive calls, which is something I'm thinking about at the moment. The real killer, however, is dynamic dispatch, such as found in Environment.evaluateAbstract. Prepack will have to be pretty much completed end-to-end before we can deal with that.

@jlongster
Copy link
Author

@hermanventer would you expect it to get into an infinite loop while trying to partially evaluate it? I'm surprised it hasn't thrown any errors yet after almost 3 hours of running. But maybe it is just stuck in a loop.

Very cool to hear that this could even be possible though. I haven't wrapped my head around what kind of code this should output, but being able to generate a compiler from my interpreter would be pretty mind-blowing and open up use cases I thought were impossible for me (as a single dev).

@hermanventer
Copy link
Contributor

hermanventer commented Jan 3, 2018

I don't have a specific reason to expect an infinite loop, although there may be obscure bugs that cause non termination. The long runtime may have more mundane explanation: since Prepack currently unrolls every loop and inlines every call, its behavior is probably exponential for the kind of code found in Prepack itself.

I'm afraid that it will be several years before Futamura projections become something we can reasonably expect of Prepack.

@jlongster
Copy link
Author

jlongster commented Jan 4, 2018

To put a nail in this coffin, it finally did something after 7.5 hours of running: run out of heap memory!

<--- Last few GCs --->

[10255:0x102801000] 20261371 ms: Mark-sweep 8096.5 (8589.3) -> 8096.5 (8588.3) MB, 48354.9 / 0.0 ms  (+ 0.0 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 48357 ms) last resort
[10255:0x102801000] 20306160 ms: Mark-sweep 8096.5 (8588.3) -> 8096.4 (8588.3) MB, 44785.4 / 0.0 ms  last resort


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x1b8ab70266a1 <JS Object>
    1: getBindingIdentifiers [/usr/local/lib/node_modules/prepack/node_modules/babel-types/lib/retrievers.js:~20] [pc=0x268da40a5ce5](this=0x39519c655099 <an Object with map 0x1dd83c7d0b71>,node=0x67fb7a12f09 <a Node with map 0x2714d1043f99>,duplicates=0x36702f1823b1 <true>,outerOnly=0x36702f182311 <undefined>)
    2: arguments adaptor frame: 2->3
    3: FunctionDeclarationInstantiation [/usr/l...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: node::Abort() [/usr/local/bin/node]
 2: node::FatalException(v8::Isolate*, v8::Local<v8::Value>, v8::Local<v8::Message>) [/usr/local/bin/node]
 3: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/usr/local/bin/node]
 4: v8::internal::Factory::NewTransitionArray(int) [/usr/local/bin/node]
 5: v8::internal::TransitionArray::Insert(v8::internal::Handle<v8::internal::Map>, v8::internal::Handle<v8::internal::Name>, v8::internal::Handle<v8::internal::Map>, v8::internal::SimpleTransitionFlag) [/usr/local/bin/node]
<snip>

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

4 participants