diff --git a/continuations-playground/ts/index.tsx b/continuations-playground/ts/index.tsx index 590b5cc2..2b8209dd 100644 --- a/continuations-playground/ts/index.tsx +++ b/continuations-playground/ts/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import MonacoEditor from 'react-monaco-editor'; import { Hook, Console, Decode } from 'console-feed'; -import { compile } from 'stopify-continuations-compiler'; +import { compile } from '@stopify/continuations'; function appendLog(method: 'log' | 'error', msg: any) { return function(prevState: { logs: any[] }) { @@ -18,7 +18,7 @@ function appendLog(method: 'log' | 'error', msg: any) { type CodeLoaderState = { kind: 'loading' } | { kind: 'ok', code: string } | - { kind: 'error', message: string } + { kind: 'error', message: string }; class CodeLoader extends React.Component<{}, CodeLoaderState> { @@ -97,8 +97,11 @@ class ContinuationsPlayground extends React.Component<{ initialCode: string }, { console: { log: (msg: any) => this.setState(appendLog('log', msg)) }, - control: function(f: any) { - return runner.control(f); + shift: function(f: any) { + return runner.shift(f); + }, + reset: function(f: any) { + return runner.reset(f); } }; runner.g = globals; diff --git a/stopify-continuations-compiler/src/index.ts b/stopify-continuations-compiler/src/index.ts index 6bf6d892..66696431 100644 --- a/stopify-continuations-compiler/src/index.ts +++ b/stopify-continuations-compiler/src/index.ts @@ -39,7 +39,7 @@ export const reserved = [ '$top', '$S' ]; - + const visitor: babel.Visitor = { Program(path, state) { const opts: types.CompilerOpts = { @@ -73,7 +73,7 @@ const visitor: babel.Visitor = { [ [ callcc.default, opts ] ]); path.stop(); } -} +}; /** * Compiles a program to support callCC. @@ -138,12 +138,16 @@ class RunnerImpl implements types.Runner { return eval(this.code); } - control(f: (k: (v: any) => void) => void): void { + shift(f: (k: (v: any) => void) => void): void { return this.rts.captureCC(k => { return f((x: any) => k({ type: 'normal', value: x })); }); } + reset(f: () => any): any { + return this.rts.reset(f); + } + processEvent(body: () => any, receiver: (x: Result) => void): void { this.rts.runtime(body, receiver); diff --git a/stopify-continuations-compiler/src/types.ts b/stopify-continuations-compiler/src/types.ts index 0cfa6b95..129c2397 100644 --- a/stopify-continuations-compiler/src/types.ts +++ b/stopify-continuations-compiler/src/types.ts @@ -1,5 +1,6 @@ import * as t from 'babel-types'; import { Result } from '@stopify/continuations-runtime'; +import { reset } from '@stopify/hygiene'; export interface LineMapping { getLine: (line: number, column: number) => number | null @@ -27,6 +28,7 @@ export interface CompilerOpts { export type Runner = { run: (onDone: (result: Result) => void) => void, processEvent: (body: () => any, receiver: (x: Result) => void) => void, - control: (f: (k: (v: any) => void) => void) => void, + shift: (f: (k: (v: any) => void) => void) => void, + reset: (f: () => any) => any, g: { [key: string]: any} }; \ No newline at end of file diff --git a/stopify-continuations-compiler/test/callcc.test.ts b/stopify-continuations-compiler/test/callcc.test.ts index 0d782084..aadd1eff 100644 --- a/stopify-continuations-compiler/test/callcc.test.ts +++ b/stopify-continuations-compiler/test/callcc.test.ts @@ -13,10 +13,9 @@ import { compile } from '../src/index'; function compileForTest(code: string) { let runner = compile(code).unwrap(); - runner.g.callCC = function(f: any) { - return continuationRTS.newRTS('lazy').captureCC(k => - f((x: any) => k({ type: 'normal', value: x }))); - }; + runner.g.callCC = (f: any) => runner.shift(f); + runner.g.shift = runner.g.callCC; + runner.g.reset = (f: any) => runner.reset(f); runner.g.assert = assert; // NOTE(arjun): For testing, we are relying on the fact that runner.run @@ -197,8 +196,6 @@ test('fringe generator rest', () => { }); }); -test - test.skip('resume with an exception', () => { // TODO(arjun): We do not have the right API to resume with an exception. @@ -233,4 +230,148 @@ test.skip('resume with an exception', () => { // console.log("Result of restart:", result); // assert(result == "throw-this"); +}); + +test('shift/reset: discard context within reset', () => { + let { run, globals } = compileForTest(` + let r = 20 + reset(function() { + return 300 + shift(function(k) { return 1; }) + }); + `); + expect(run()).toMatchObject({ type: 'normal' }); + expect(globals).toMatchObject({ + r: 21 + }); +}); + +test('shift/reset: restore context within reset', () => { + let { run, globals } = compileForTest(` + let r = 20 + reset(function() { + return 300 + shift(function(k) { return k(1); }) + }); + `); + expect(run()).toMatchObject({ type: 'normal' }); + expect(globals).toMatchObject({ + r: 321 + }); +}); + +test('shift/reset: return to context within shift', () => { + let { run, globals } = compileForTest(` + let r = 20 + reset(function() { + return 300 + shift(function(k) { return k(1) + 4000; }) + }); + `); + expect(run()).toMatchObject({ type: 'normal' }); + expect(globals).toMatchObject({ + r: 4321 + }); +}); + +test('shift/reset: nested reset, discard inner context', () => { + let { run, globals } = compileForTest(` + let r = 1 + reset(function() { + return 20 + reset(function() { + return 300 + shift(function(k) { + return 4000; + }); + }); + }); + `); + expect(run()).toMatchObject({ type: 'normal' }); + expect(globals).toMatchObject({ + r: 4021 + }); +}); + +test('shift/reset: save continuation to global variable', () => { + let { run, globals } = compileForTest(` + let saved = false; + let r1 = 1 + reset(function() { + return 20 + shift(function(k) { + saved = k; + return 300; + }); + }); + let r2 = saved(4000); + `); + expect(run()).toMatchObject({ type: 'normal' }); + expect(globals).toMatchObject({ + r1: 1 + 300, + r2: 20 + 4000 + }); +}); + +test('shift/reset: invoke continuation twice', () => { + let { run, globals } = compileForTest(` + r = reset(function() { + return 100 + shift(function(k) { + return k(1) + k(2); + }); + }); + `); + expect(run()).toMatchObject({ type: 'normal' }); + expect(globals).toMatchObject({ + r: 100 + 1 + 100 + 2 + }); +}); + +test('shift/reset: two shifts', () => { + let { run, globals } = compileForTest(` + r = reset(function() { + return 100 + shift(function(k) { + return k(1) + k(2); + }) + shift(function(k) { + return k(3) + k(4); + }); + }); + `); + expect(run()).toMatchObject({ type: 'normal' }); + expect(globals).toMatchObject({ + r: (100 + 1 + 3) + (100 + 1 + 4) + (100 + 2 + 3) + (100 + 2 + 4) + }); +}); + + +test('shift/reset: non-determinism', () => { + let { run, globals } = compileForTest(` + function choose(args) { + return shift(function(k) { + let results = [ ]; + for (let i = 0; i < args.length; i++) { + results = results.concat(k(args[i])); + } + return results; + }); + } + + function fail() { + return shift(function(k) { + return []; + }); + } + + function driver(body) { + return reset(function() { + return [body()]; + }); + } + + function F() { + return choose([1,2,3]); + } + + results = driver(function() { + let x = F(); + let y = F(); + if ((x + y) % 2 !== 0) { + fail(); + } + return x + y; + });; + `); + expect(run()).toMatchObject({ type: 'normal' }); + // Prelude> [ x + y | x <- [ 1 .. 3 ], y <- [ 1 .. 3], (x + y) `mod` 2 == 0 ] + // [2,4,4,4,6] + expect(globals.results).toMatchObject([2,4,4,4,6]); }); \ No newline at end of file diff --git a/stopify-continuations/src/runtime/abstractRuntime.ts b/stopify-continuations/src/runtime/abstractRuntime.ts index 2c415d83..882e7ebb 100644 --- a/stopify-continuations/src/runtime/abstractRuntime.ts +++ b/stopify-continuations/src/runtime/abstractRuntime.ts @@ -180,4 +180,6 @@ export abstract class RuntimeImpl implements Runtime { abstract abstractRun(body: () => any): RunResult; abstract endTurn(callback: (onDone: (x: Result) => any) => any): never; + + abstract reset(f: () => any): any; } diff --git a/stopify-continuations/src/runtime/eagerRuntime.ts b/stopify-continuations/src/runtime/eagerRuntime.ts index be2d063a..241b1b7b 100644 --- a/stopify-continuations/src/runtime/eagerRuntime.ts +++ b/stopify-continuations/src/runtime/eagerRuntime.ts @@ -12,6 +12,10 @@ export class EagerRuntime extends common.RuntimeImpl { this.eagerStack = []; } + reset(f: () => any): any { + throw 'Not implemented'; + } + captureCC(f: (k: (x: Result) => any) => any): any { this.capturing = true; throw new common.Capture(f, [...this.eagerStack]); diff --git a/stopify-continuations/src/runtime/fudgeRuntime.ts b/stopify-continuations/src/runtime/fudgeRuntime.ts index 7d838f63..227f7063 100644 --- a/stopify-continuations/src/runtime/fudgeRuntime.ts +++ b/stopify-continuations/src/runtime/fudgeRuntime.ts @@ -28,6 +28,10 @@ export class FudgeRuntime extends common.RuntimeImpl { super(Infinity, Infinity); } + reset(f: () => any): any { + throw 'Not implemented'; + } + captureCC(f: (k: any) => any): void { throw new common.Capture(f, []); } diff --git a/stopify-continuations/src/runtime/lazyRuntime.ts b/stopify-continuations/src/runtime/lazyRuntime.ts index f3da07b9..f01ba4ea 100644 --- a/stopify-continuations/src/runtime/lazyRuntime.ts +++ b/stopify-continuations/src/runtime/lazyRuntime.ts @@ -10,6 +10,39 @@ export class LazyRuntime extends common.RuntimeImpl { super(stackSize, restoreFrames); } + reset(f: () => any): any { + try { + return f(); + } + catch (exn) { + if (exn instanceof common.Capture) { + if (this.mode !== true) { + throw new Error('capture while restoring'); + } + this.capturing = false; + let f = exn.f; + let k = this.makeCont(exn.stack); + return this.reset(() => f((v) => this.reset(() => k(v)))); + } + else if (exn instanceof common.Restore) { + if (exn.savedStack.length > 0) { + throw new Error('savedStack found within shift/reset'); + } + let stack = exn.stack; + this.stack = stack; + this.mode = false; + let frame = stack[stack.length - 1] as types.KFrameRest; + return this.reset(() => { + return frame.f.apply(frame.this || global, (frame.params || []) as any); + }); + + } + else { + throw exn; + } + } + } + captureCC(f: (k: (x: Result) => any) => any): any { this.capturing = true; throw new common.Capture(f, []); diff --git a/stopify-continuations/src/runtime/retvalRuntime.ts b/stopify-continuations/src/runtime/retvalRuntime.ts index deb2e0dc..8444f704 100644 --- a/stopify-continuations/src/runtime/retvalRuntime.ts +++ b/stopify-continuations/src/runtime/retvalRuntime.ts @@ -10,6 +10,10 @@ export class RetvalRuntime extends common.RuntimeImpl { super(stackSize, restoreFrames); } + reset(f: () => any): any { + throw 'Not implemented'; + } + captureCC(f: (k: (x: Result) => any) => any): any { this.capturing = true; return new common.Capture(f, []); diff --git a/stopify-continuations/src/types.ts b/stopify-continuations/src/types.ts index f7cc2fba..5e6965bd 100644 --- a/stopify-continuations/src/types.ts +++ b/stopify-continuations/src/types.ts @@ -60,4 +60,6 @@ export interface Runtime { // Called when the stack needs to be captured. captureCC(f: (k: (x: Result) => any) => any): void; + + reset(f: () => any): any; }