Skip to content

Commit

Permalink
Implement continuations through the extension of the CSE-Machine (#1547)
Browse files Browse the repository at this point in the history
* 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
Kyriel Abad and martin-henz authored Feb 19, 2024
1 parent 1ab6d7d commit e4aefd6
Show file tree
Hide file tree
Showing 9 changed files with 511 additions and 27 deletions.
10 changes: 5 additions & 5 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ export const sourceLanguages: Language[] = [
]

export const scmLanguages: Language[] = [
{ chapter: Chapter.SCHEME_1, variant: Variant.DEFAULT },
{ chapter: Chapter.SCHEME_2, variant: Variant.DEFAULT },
{ chapter: Chapter.SCHEME_3, variant: Variant.DEFAULT },
{ chapter: Chapter.SCHEME_4, variant: Variant.DEFAULT },
{ chapter: Chapter.FULL_SCHEME, variant: Variant.DEFAULT }
{ chapter: Chapter.SCHEME_1, variant: Variant.EXPLICIT_CONTROL },
{ chapter: Chapter.SCHEME_2, variant: Variant.EXPLICIT_CONTROL },
{ chapter: Chapter.SCHEME_3, variant: Variant.EXPLICIT_CONTROL },
{ chapter: Chapter.SCHEME_4, variant: Variant.EXPLICIT_CONTROL },
{ chapter: Chapter.FULL_SCHEME, variant: Variant.EXPLICIT_CONTROL }
]

export const pyLanguages: Language[] = [{ chapter: Chapter.PYTHON_1, variant: Variant.DEFAULT }]
3 changes: 3 additions & 0 deletions src/createContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Variable determining chapter of Source is contained in this file.

import { GLOBAL, JSSLANG_PROPERTIES } from './constants'
import { call_with_current_continuation } from './cse-machine/continuations'
import * as gpu_lib from './gpu/lib'
import { AsyncScheduler } from './schedulers'
import * as scheme_libs from './scm-slang/src/stdlib/source-scheme-library'
Expand Down Expand Up @@ -401,6 +402,8 @@ export const importBuiltins = (context: Context, externalBuiltIns: CustomBuiltIn
if (context.chapter <= +Chapter.SCHEME_1 && context.chapter >= +Chapter.FULL_SCHEME) {
switch (context.chapter) {
case Chapter.FULL_SCHEME:
// Introduction to call/cc
defineBuiltin(context, 'call$47$cc(f)', call_with_current_continuation)

case Chapter.SCHEME_4:
// Introduction to eval
Expand Down
128 changes: 128 additions & 0 deletions src/cse-machine/__tests__/__snapshots__/cse-machine-callcc.ts.snap
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 [],
}
`;
123 changes: 123 additions & 0 deletions src/cse-machine/__tests__/cse-machine-callcc.ts
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`)
// })
94 changes: 94 additions & 0 deletions src/cse-machine/continuations.ts
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
}
]
}
}
Loading

0 comments on commit e4aefd6

Please sign in to comment.