nuprl / Stopify

A JS-to-JS compiler that makes it easier to build Web IDEs and compile to JS.
https://zenodo.org/records/10408254
BSD 3-Clause "New" or "Revised" License
169 stars 12 forks source link

Tell if Stopify is “running” #512

Open jpolitz opened 4 months ago

jpolitz commented 4 months ago

Here's a problem: We want Pyret's arrays to be the same kind of arrays as those on the rest of the page.

We don't want to enforce a wrapping/unwrapping step at the Pyret boundary; we've tried this in other implementations and it's a huge headache (think nested arrays, 3rd party libraries that .map, etc).

The current polyfill strategy for HoFs makes Stopified arrays special. If a client of some Stopified code tries to map or filter on an array, they'll almost certainly get (nondeterministically!) some kind of uncaught Capture or similar, because that array's callbacks have been replaced. For the same reason, it doesn't work to patch in the Stopified polyfills on the array prototype, since that would break innocuous non-stopped code.

One thing we'd like to try is writing polyfills with a dispatch, for example:

  function stopifyDispatch(stopped : any, native : any) {
    // @stopify flat
    return function(this : any) {
        // @stopify flat
        if(isStopifyRunning()) {
            return stopped.apply(this, arguments);
        }
        else {
            return native.apply(this, arguments);
        }
    }
  }

Array.prototype.map = stopifyDispatch(array_map_polyfill, Array.prototype.map);
Array.prototype.filter = ...

Thing is, we can't figure out how to write isStopifyRunning()

There's a lot of states Stopify can be in – is there a way provided by the Stopify runtime to write this predicate? We tried various combinations of eventMode and rts.mode and rts.capturing, but can't seem to express it. Is there a concise way to tell if the stack is currently a Stopify stack that's ready for captures/suspends, or if it's on the plain JS stack?

jpolitz commented 4 months ago

OK, I think we have this working with this:

/**
 * This works around what may be state management issues related to eventMode. The relevant enum is
 * 
 * enum EventMode { Running, Paused, Waiting }
 * 
 * First, in its initial state, the runner is set to `RUNNING` when it should be set to `WAITING`
 * 
 * Second, when pausing with pauseK, it doesn't set the mode to `PAUSED` (there is a comment that this has to do with the debugger in that code)
 * 
 * The pauseK wrapper handles pausing/unpausing, and doing the run on the empty program sets things to `WAITING`.
 */
currentRunner = stopify.stopifyLocally("", { newMethod: 'direct' });
currentRunner.run(() => { });

let originalPauseK = currentRunner.pauseK;

currentRunner.pauseK = function patchedPauseK(k : any) {
  return originalPauseK.call(this, (resumer : (result: any) => void) => {
    const oldMode = currentRunner.eventMode;
    currentRunner.eventMode = 1;
    return k((result : any) => {
      currentRunner.eventMode = oldMode;
      return resumer(result);
    });
  })
}

Relevant comment for pauseK: https://github.com/nuprl/Stopify/blob/275d950193a9fc04175ed810834f9e1778108229/stopify/src/runtime/abstractRunner.ts#L200

Then our predicate is:

  function isStopifyRunning() {
    /**
     * The relevant enum is
     * 
     * enum EventMode { Running = 0, Paused = 1, Waiting = 2 }
     * 
     * The check below returns true when that mode is Running, indicating the Stopify stack is live.
     * Paused means “a stack is captured and waiting to be resumed”
     * Waiting means “we are between/after a completed run of a stopify program/event handler”
     */
    // @ts-ignore
    return typeof $STOPIFY !== "undefined" && $STOPIFY.eventMode === 0;
  }

@blerner and I spent some time reconstructing this. I think this could be useful as an exposed API, though may end up finding other cases where the state needs to be managed a bit to have eventMode match the right definition of isStopifyRunning. But we pushed through a bunch of issues and got a .map that seamlessly works with this dispatch (and we had versions that very much didn't work, breaking both the stopped side and the page side).