Skip to content

Commit

Permalink
Add detection for whether we are on an active Stopify stack
Browse files Browse the repository at this point in the history
Adds a stackActive field to abstractRuntime that tracks whether the JavaScript
stack is currently using Stopified frames, and exposes it through isRunning()
on abstractRuntime.

The use case for this, beyond it being a likely-useful utility, is writing
polyfills for HOFs that can switch on and off depending on if they can expect
to capture or not.

We've done some of this work for Pyret
(https://github.com/brownplt/pyret-lang/blob/8e0ce78fb0ca1c10bcc06dfcaeb534d0ae2c02e4/src/runtime/hof-array-polyfill.ts#L347)
because the Pyret runtime co-exists on a page with regular old React code.

Because everything in React and in the Stopified runtime is getting
asynchronously chopped up and scheduled all over the place, and because we want
JS arrays to be arrays whether in the stopify runtime or the page runtime, it's
useful to have polyfills that automatically do the right thing. This avoids
these problems:

- If using the default array polyfill strategy from Stopify, non-stopified code
  can get access to arrays with stopified map/filter/fold. Say that's called in
  some `didComponentUpdate` or other scheduled React event – now it's not on a
  Stopify stack and lots of e.g. uncaught Captures result. It is really, really
  hard to remember that every array may or may not be from the Stopify runtime
  and have a different prototype.
- When calling into Stopified code, it's natural to want to pass in “regular”
  arrays. It's also hard to remember and design APIs around introducing
  wrapping on these as they *enter* stopified code.
- The flag lets us handle cases like:
  - A didComponentUpdate starts a Pyret evaluation for e.g. rendering a Pyret
    value to a React element, which calls back into some Stopified Pyret code.
    This goes in a suitable wrapper.
  - That code suspends for whatever reason, yielding control back to
    didComponentUpdate, which uses e.g. map/filter/fold. The map/filter/fold in
    didComponentUpdate will correctly use the plain, un-instrumented
    map/filter/fold.
  - The Stopified code resumes in the next turn and does map/filter/fold in the
    dynamic extent of the Pyret code. This correctly uses the stopified
    versions of the HOFs.
  • Loading branch information
jpolitz authored and arjunguha committed Jul 31, 2024
1 parent 84f9740 commit d35d329
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 0 deletions.
13 changes: 13 additions & 0 deletions stopify-continuations/src/runtime/abstractRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export abstract class RuntimeImpl implements Runtime {
// represents 'restore' mode.
mode: Mode;

// Represents whether the stack is currently active – that is, if you
// call a function if it can expect to do its capture/restore logic
// with the right things available on the stack.
stackActive: boolean;

/**
* A saved stack trace. This field is only used when a user-mode exception
* is thrown.
Expand All @@ -68,6 +73,7 @@ export abstract class RuntimeImpl implements Runtime {
this.stackSize = stackSize;
this.remainingStack = stackSize;
this.mode = true;
this.stackActive = false;
this.kind = undefined as any; // the worst
}

Expand All @@ -77,6 +83,7 @@ export abstract class RuntimeImpl implements Runtime {
f: () => {
this.stack = [];
this.mode = true;
this.stackActive = true;
return f();
},
this: this,
Expand All @@ -86,6 +93,7 @@ export abstract class RuntimeImpl implements Runtime {
runtime<T>(body: () => any, onDone: (x: Result) => T): T {

while(true) {
this.stackActive = true;
const result = this.abstractRun(body);

if (result.type === 'normal' || result.type === 'exception') {
Expand All @@ -110,19 +118,23 @@ export abstract class RuntimeImpl implements Runtime {
}
else if(result.type === 'normal') {
assert(this.mode, 'execution completed in restore mode');
this.stackActive = false;
return onDone(result);
}
else if(result.type === 'exception') {
assert(this.mode, `execution completed in restore mode, error was: ${result.value}`);
const stack = this.stackTrace;
this.stackTrace = [];
this.stackActive = false;
return onDone({ type: 'exception', value: result.value, stack });
}
}
else if (result.type === 'capture') {
this.stackActive = false;
body = () => result.f.call(global, this.makeCont(result.stack));
}
else if (result.type === 'restore') {
this.stackActive = false;
body = () => {
if (result.stack.length === 0) {
throw new Error(`Can't restore from empty stack`);
Expand All @@ -135,6 +147,7 @@ export abstract class RuntimeImpl implements Runtime {
};
}
else if (result.type === 'end-turn') {
this.stackActive = false;
return result.callback(onDone);
}
}
Expand Down
1 change: 1 addition & 0 deletions stopify-continuations/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface Runtime {

mode: Mode;
stack: Stack;
stackActive: boolean;

endTurn(callback: (onDone: (x: Result) => any) => any): never;

Expand Down
4 changes: 4 additions & 0 deletions stopify/src/runtime/abstractRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ export abstract class AbstractRunner implements AsyncRun {
this.continuationsRTS.runtime(body, callback);
}

isRunning(): boolean {
return this.continuationsRTS.stackActive;
}

processEvent(body: () => void, receiver: (x: Result) => void): void {
this.eventQueue.push({ body, receiver } );
this.processQueuedEvents();
Expand Down
1 change: 1 addition & 0 deletions stopify/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface AsyncRun {
onBreakpoint?: (line: number) => void): void;
pause(onPaused: (line?: number) => void): void;
resume(): void;
isRunning(): boolean;
setBreakpoints(line: number[]): void;
step(onStep: (line: number) => void): void;
pauseK(callback: (k: (r: Result) => void) => void): void;
Expand Down
32 changes: 32 additions & 0 deletions stopify/test/semantics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,38 @@ describe('integration tests', function () {
}
});

describe('Test cases that check running status',() => {
test('Running status should be paused (not running) in synchronous code after starting to run', onDone => {
const runner = harness(`
function sum(x) {
if (x % 20 === 0) { checkRunning(); }
if (x % 30 === 0) { pauseAndCheckRunning(); }
if (x <= 1) {
return 1;
} else {
return x + sum(x-1);
}
}
assert.equal(sum(100), 5050);
`, { captureMethod: 'lazy' });
runner.g.checkRunning = function() {
assert.equal(runner.isRunning(), true);
};
runner.g.pauseAndCheckRunning = function() {
runner.pauseK(k => {
assert.equal(runner.isRunning(), false);
k({ type: 'normal', value: 'restart' });
});
};
runner.run(result => {
expect(result).toEqual({ type: 'normal' });
onDone();
expect(runner.isRunning()).toBe(false);
});
}, 10000);

});

describe('Test cases that require deep stacks',() => {
const runtimeOpts: Partial<types.RuntimeOpts> = {
stackSize: 100,
Expand Down

0 comments on commit d35d329

Please sign in to comment.