-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement continuations through the extension of the CSE-Machine (#1547)
* switch scm-slang to continuations * Create continuation type * Enable detection of call/cc * Implement first-class continuations in the ECE evaluator * Improve tests for call/cc * Document and clean up changes * Document and clean up changes * Refactor non-function value applicaton as guard clause case * Add error testing for continuation and call/cc calls * Streamline the representation of continuations * Change description of continuations --------- Co-authored-by: Martin Henz <[email protected]>
- Loading branch information
1 parent
1ab6d7d
commit e4aefd6
Showing
9 changed files
with
511 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
src/cse-machine/__tests__/__snapshots__/cse-machine-callcc.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`basic call/cc works: expectResult 1`] = ` | ||
Object { | ||
"alertResult": Array [], | ||
"code": " | ||
(+ 1 2 (call/cc | ||
(lambda (k) (k 3))) | ||
4) | ||
", | ||
"displayResult": Array [], | ||
"numErrors": 0, | ||
"parsedErrors": "", | ||
"result": 10, | ||
"resultStatus": "finished", | ||
"visualiseListResult": Array [], | ||
} | ||
`; | ||
|
||
exports[`call/cc can be stored as a value: expectResult 1`] = ` | ||
Object { | ||
"alertResult": Array [], | ||
"code": " | ||
;; storing a continuation | ||
(define a #f) | ||
(+ 1 2 3 (call/cc (lambda (k) (set! a k) 0)) 4 5) | ||
;; continuations are treated as functions | ||
;; so we can do this: | ||
(procedure? a) | ||
", | ||
"displayResult": Array [], | ||
"numErrors": 0, | ||
"parsedErrors": "", | ||
"result": true, | ||
"resultStatus": "finished", | ||
"visualiseListResult": Array [], | ||
} | ||
`; | ||
exports[`call/cc can be used to escape a computation: expectResult 1`] = ` | ||
Object { | ||
"alertResult": Array [], | ||
"code": " | ||
(define test 1) | ||
(call/cc (lambda (k) | ||
(set! test 2) | ||
(k 'escaped) | ||
(set! test 3))) | ||
;; test should be 2 | ||
test | ||
", | ||
"displayResult": Array [], | ||
"numErrors": 0, | ||
"parsedErrors": "", | ||
"result": 2, | ||
"resultStatus": "finished", | ||
"visualiseListResult": Array [], | ||
} | ||
`; | ||
exports[`call/cc throws error given >1 argument: expectParsedError 1`] = ` | ||
Object { | ||
"alertResult": Array [], | ||
"code": " | ||
(+ 1 2 (call/cc | ||
(lambda (k) (k 3)) | ||
'wrongwrongwrong!) | ||
4) | ||
", | ||
"displayResult": Array [], | ||
"numErrors": 1, | ||
"parsedErrors": "Line 2: Expected 1 arguments, but got 2.", | ||
"result": undefined, | ||
"resultStatus": "error", | ||
"visualiseListResult": Array [], | ||
} | ||
`; | ||
exports[`call/cc throws error given no arguments: expectParsedError 1`] = ` | ||
Object { | ||
"alertResult": Array [], | ||
"code": " | ||
(+ 1 2 (call/cc) 4) | ||
", | ||
"displayResult": Array [], | ||
"numErrors": 1, | ||
"parsedErrors": "Line 2: Expected 1 arguments, but got 0.", | ||
"result": undefined, | ||
"resultStatus": "error", | ||
"visualiseListResult": Array [], | ||
} | ||
`; | ||
exports[`cont throws error given >1 argument: expectParsedError 1`] = ` | ||
Object { | ||
"alertResult": Array [], | ||
"code": " | ||
(+ 1 2 (call/cc | ||
(lambda (k) (k 3 'wrongwrongwrong!))) | ||
4) | ||
", | ||
"displayResult": Array [], | ||
"numErrors": 1, | ||
"parsedErrors": "Line 3: Expected 1 arguments, but got 2.", | ||
"result": undefined, | ||
"resultStatus": "error", | ||
"visualiseListResult": Array [], | ||
} | ||
`; | ||
exports[`cont throws error given no arguments: expectParsedError 1`] = ` | ||
Object { | ||
"alertResult": Array [], | ||
"code": " | ||
(+ 1 2 (call/cc | ||
(lambda (k) (k))) | ||
4) | ||
", | ||
"displayResult": Array [], | ||
"numErrors": 1, | ||
"parsedErrors": "Line 3: Expected 1 arguments, but got 0.", | ||
"result": undefined, | ||
"resultStatus": "error", | ||
"visualiseListResult": Array [], | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { Chapter, Variant } from '../../types' | ||
import { expectParsedError, expectResult } from '../../utils/testing' | ||
|
||
// as continuations mostly target the scheme implementation, we will test continuations | ||
// using a scheme context. | ||
const optionECScm = { chapter: Chapter.FULL_SCHEME, variant: Variant.EXPLICIT_CONTROL } | ||
|
||
test('basic call/cc works', () => { | ||
return expectResult( | ||
` | ||
(+ 1 2 (call/cc | ||
(lambda (k) (k 3))) | ||
4) | ||
`, | ||
optionECScm | ||
).toMatchInlineSnapshot(`10`) | ||
}) | ||
|
||
test('call/cc can be used to escape a computation', () => { | ||
return expectResult( | ||
` | ||
(define test 1) | ||
(call/cc (lambda (k) | ||
(set! test 2) | ||
(k 'escaped) | ||
(set! test 3))) | ||
;; test should be 2 | ||
test | ||
`, | ||
optionECScm | ||
).toMatchInlineSnapshot(`2`) | ||
}) | ||
|
||
test('call/cc throws error given no arguments', () => { | ||
return expectParsedError( | ||
` | ||
(+ 1 2 (call/cc) 4) | ||
`, | ||
optionECScm | ||
).toMatchInlineSnapshot(`"Line 2: Expected 1 arguments, but got 0."`) | ||
}) | ||
|
||
test('call/cc throws error given >1 argument', () => { | ||
return expectParsedError( | ||
` | ||
(+ 1 2 (call/cc | ||
(lambda (k) (k 3)) | ||
'wrongwrongwrong!) | ||
4) | ||
`, | ||
optionECScm | ||
).toMatchInlineSnapshot(`"Line 2: Expected 1 arguments, but got 2."`) | ||
}) | ||
|
||
test('cont throws error given no arguments', () => { | ||
return expectParsedError( | ||
` | ||
(+ 1 2 (call/cc | ||
(lambda (k) (k))) | ||
4) | ||
`, | ||
optionECScm | ||
).toMatchInlineSnapshot(`"Line 3: Expected 1 arguments, but got 0."`) | ||
}) | ||
|
||
test('cont throws error given >1 argument', () => { | ||
return expectParsedError( | ||
` | ||
(+ 1 2 (call/cc | ||
(lambda (k) (k 3 'wrongwrongwrong!))) | ||
4) | ||
`, | ||
optionECScm | ||
).toMatchInlineSnapshot(`"Line 3: Expected 1 arguments, but got 2."`) | ||
}) | ||
|
||
test('call/cc can be stored as a value', () => { | ||
return expectResult( | ||
` | ||
;; storing a continuation | ||
(define a #f) | ||
(+ 1 2 3 (call/cc (lambda (k) (set! a k) 0)) 4 5) | ||
;; continuations are treated as functions | ||
;; so we can do this: | ||
(procedure? a) | ||
`, | ||
optionECScm | ||
).toMatchInlineSnapshot(`true`) | ||
}) | ||
|
||
// both of the following tests generate infinite loops so they are omitted | ||
|
||
// test('call/cc can be stored as a value and called', () => { | ||
// return expectResult( | ||
// ` | ||
// ;; storing a continuation and calling it | ||
// (define a #f) | ||
|
||
// (+ 1 2 3 (call/cc (lambda (k) (set! a k) 0)) 4 5) | ||
|
||
// ;; as continuations are represented with dummy | ||
// ;; identity functions, we should not expect to see 6 | ||
// (a 6) | ||
// `, | ||
// optionECScm | ||
// ).toMatchInlineSnapshot(`21`) | ||
// }) | ||
|
||
// test('when stored as a value, calling a continuation should alter the execution flow', () => { | ||
// return expectResult( | ||
// ` | ||
// ;; storing a continuation and calling it | ||
// (define a #f) | ||
// (+ 1 2 3 (call/cc (lambda (k) (set! a k) 0)) 4 5) | ||
|
||
// ;; the following addition should be ignored | ||
// (+ 7 (a 6)) | ||
// `, | ||
// optionECScm | ||
// ).toMatchInlineSnapshot(`21`) | ||
// }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import * as es from 'estree' | ||
|
||
import { Environment } from '../types' | ||
import { Control, Stash } from './interpreter' | ||
|
||
/** | ||
* A dummy function used to detect for the call/cc function object. | ||
* If the interpreter sees this specific function, a continuation at the current | ||
* point of evaluation is executed instead of a regular function call. | ||
*/ | ||
|
||
export function call_with_current_continuation(f: any): any { | ||
return f() | ||
} | ||
|
||
/** | ||
* Checks if the function refers to the designated function object call/cc. | ||
*/ | ||
export function isCallWithCurrentContinuation(f: Function): boolean { | ||
return f === call_with_current_continuation | ||
} | ||
|
||
/** | ||
* An object representing a continuation of the ECE machine. | ||
* When instantiated, it copies the control stack, and | ||
* current environment at the point of capture. | ||
* | ||
* Continuations and functions are treated as the same by | ||
* the typechecker so that they can be first-class values. | ||
*/ | ||
export interface Continuation extends Function { | ||
control: Control | ||
stash: Stash | ||
env: Environment[] | ||
} | ||
|
||
// As the continuation needs to be immutable (we can call it several times) | ||
// we need to copy its elements whenever we access them | ||
export function getContinuationControl(cn: Continuation): Control { | ||
return cn.control.copy() | ||
} | ||
|
||
export function getContinuationStash(cn: Continuation): Stash { | ||
return cn.stash.copy() | ||
} | ||
|
||
export function getContinuationEnv(cn: Continuation): Environment[] { | ||
return [...cn.env] | ||
} | ||
|
||
export function makeContinuation(control: Control, stash: Stash, env: Environment[]): Function { | ||
// Cast a function into a continuation | ||
const fn: any = (x: any) => x | ||
const cn: Continuation = fn as Continuation | ||
|
||
// Set the control, stash and environment | ||
// as shallow copies of the given program equivalents | ||
cn.control = control.copy() | ||
cn.stash = stash.copy() | ||
cn.env = [...env] | ||
|
||
// Return the continuation as a function so that | ||
// the type checker allows it to be called | ||
return cn as Function | ||
} | ||
|
||
/** | ||
* Checks whether a given function is actually a continuation. | ||
*/ | ||
export function isContinuation(f: Function): f is Continuation { | ||
return 'control' in f && 'stash' in f && 'env' in f | ||
} | ||
|
||
/** | ||
* Provides an adequate representation of what calling | ||
* call/cc or continuations looks like, to give to the | ||
* GENERATE_CONT and RESUME_CONT instructions. | ||
*/ | ||
export function makeDummyContCallExpression(callee: string, argument: string): es.CallExpression { | ||
return { | ||
type: 'CallExpression', | ||
optional: false, | ||
callee: { | ||
type: 'Identifier', | ||
name: callee | ||
}, | ||
arguments: [ | ||
{ | ||
type: 'Identifier', | ||
name: argument | ||
} | ||
] | ||
} | ||
} |
Oops, something went wrong.