Skip to content

Commit

Permalink
Streamline CSE machine (#1711)
Browse files Browse the repository at this point in the history
* Remove RESUME_CONT instruction from active CSE machine instructions

* Remove GENERATE_CONT instruction from active CSE machine instructions

* Remove all trace of GENERATE_CONT and RESUME_CONT

* Remove unnecessary types and imports

* Make  the implementation of continuations cleaner

* Change representation of call/cc

* Add test file for call/cc

* Improve testing for continuations
  • Loading branch information
Kyriel Abad authored Jul 18, 2024
1 parent f4adb13 commit e4e4631
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 140 deletions.
20 changes: 20 additions & 0 deletions src/cse-machine/__tests__/continuations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Call_cc, Continuation, isCallWithCurrentContinuation } from '../continuations'
import { Control, Stash } from '../interpreter'

test('call/cc is a singleton', () => {
expect(Call_cc.get()).toBe(Call_cc.get())
})

test('call/cc toString', () => {
expect(Call_cc.get().toString()).toBe('call/cc')
})

test('isCallWithCurrentContinuation works on call/cc only', () => {
expect(isCallWithCurrentContinuation(Call_cc.get())).toBe(true)
expect(isCallWithCurrentContinuation(1)).toBe(false)
})

test('Continuation toString', () => {
const cont = new Continuation(new Control(), new Stash(), [])
expect(cont.toString()).toBe('continuation')
})
87 changes: 42 additions & 45 deletions src/cse-machine/continuations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,26 @@ import { Control, Stash } from './interpreter'
* If the interpreter sees this specific function, a continuation at the current
* point of evaluation is executed instead of a regular function call.
*/
export class Call_cc extends Function {
private static instance: Call_cc = new Call_cc()

export function call_with_current_continuation(f: any): any {
return f()
private constructor() {
super()
}

public static get(): Call_cc {
return Call_cc.instance
}

public toString(): string {
return 'call/cc'
}
}

/**
* Checks if the function refers to the designated function object call/cc.
*/
export function isCallWithCurrentContinuation(f: Function): boolean {
return f === call_with_current_continuation
export const call_with_current_continuation = Call_cc.get()

export function isCallWithCurrentContinuation(value: any): boolean {
return value === call_with_current_continuation
}

/**
Expand All @@ -28,54 +38,41 @@ export function isCallWithCurrentContinuation(f: Function): boolean {
* 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 class Continuation extends Function {
private control: Control
private stash: Stash
private env: Environment[]

export function getContinuationStash(cn: Continuation): Stash {
return cn.stash.copy()
}

export function getContinuationEnv(cn: Continuation): Environment[] {
return [...cn.env]
}
constructor(control: Control, stash: Stash, env: Environment[]) {
super()
this.control = control.copy()
this.stash = stash.copy()
this.env = [...env]
}

export function makeContinuation(control: Control, stash: Stash, env: Environment[]): Function {
// Cast a function into a continuation
// a continuation may take any amount of arguments
const fn: Function = (...x: any[]) => x
const cn: Continuation = fn as Continuation
// As the continuation needs to be immutable (we can call it several times)
// we need to copy its elements whenever we access them
public getControl(): Control {
return this.control.copy()
}

// 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]
public getStash(): Stash {
return this.stash.copy()
}

// Return the continuation as a function so that
// the type checker allows it to be called
return cn as Function
}
public getEnv(): Environment[] {
return [...this.env]
}

/**
* 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
public toString(): string {
return 'continuation'
}
}

/**
* Provides an adequate representation of what calling
* call/cc or continuations looks like, to give to the
* GENERATE_CONT and RESUME_CONT instructions.
* APPLICATION instruction.
*/
export function makeDummyContCallExpression(callee: string, argument: string): es.CallExpression {
return {
Expand Down
13 changes: 0 additions & 13 deletions src/cse-machine/instrCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ import {
BranchInstr,
EnvInstr,
ForInstr,
GenContInstr,
Instr,
InstrType,
ResumeContInstr,
UnOpInstr,
WhileInstr
} from './types'
Expand Down Expand Up @@ -138,14 +136,3 @@ export const breakMarkerInstr = (srcNode: Node): Instr => ({
instrType: InstrType.BREAK_MARKER,
srcNode
})

export const genContInstr = (srcNode: Node): GenContInstr => ({
instrType: InstrType.GENERATE_CONT,
srcNode
})

export const resumeContInstr = (numOfArgs: number, srcNode: es.Node): ResumeContInstr => ({
numOfArgs: numOfArgs,
instrType: InstrType.RESUME_CONT,
srcNode
})
112 changes: 44 additions & 68 deletions src/cse-machine/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,7 @@ import { checkProgramForUndefinedVariables } from '../validator/validator'
import Closure from './closure'
import {
Continuation,
getContinuationControl,
getContinuationEnv,
getContinuationStash,
isCallWithCurrentContinuation,
isContinuation,
makeContinuation,
makeDummyContCallExpression
} from './continuations'
import * as instr from './instrCreator'
Expand All @@ -45,10 +40,8 @@ import {
CseError,
EnvInstr,
ForInstr,
GenContInstr,
Instr,
InstrType,
ResumeContInstr,
UnOpInstr,
WhileInstr
} from './types'
Expand Down Expand Up @@ -937,33 +930,65 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = {
// Check for number of arguments mismatch error
checkNumberOfArguments(context, func, args, command.srcNode)

// generate a continuation here
const contControl = control.copy()
const contStash = stash.copy()
const contEnv = context.runtime.environments.slice()

// at this point, the extra CALL instruction
// has been removed from the control stack.
// additionally, the single closure argument has been
// removed (as the parameter of call/cc) from the stash
// and additionally, call/cc itself has been removed from the stash.

// as such, there is no further need to modify the
// copied C, S and E!

const continuation = new Continuation(contControl, contStash, contEnv)

// Get the callee
const cont_callee: Value = args[0]

const dummyFCallExpression = makeDummyContCallExpression('f', 'cont')

// Prepare a function call for the continuation-consuming function
// along with a newly generated continuation
control.push(instr.appInstr(command.numOfArgs, dummyFCallExpression))
control.push(instr.genContInstr(dummyFCallExpression.arguments[0]))

// push the argument (the continuation caller) back onto the stash
stash.push(cont_callee)

// finally, push the continuation onto the stash
stash.push(continuation)
return
}

if (isContinuation(func)) {
if (func instanceof Continuation) {
// Check for number of arguments mismatch error
checkNumberOfArguments(context, func, args, command.srcNode)

const dummyContCallExpression = makeDummyContCallExpression('f', 'cont')
// const dummyContCallExpression = makeDummyContCallExpression('f', 'cont')

// // Restore the state of the stash,
// // but replace the function application instruction with
// // a resume continuation instruction
// stash.push(func)
// // we need to push the arguments back onto the stash
// // as well
// stash.push(...args)
// control.push(instr.resumeContInstr(command.numOfArgs, dummyContCallExpression))

// get the C, S, E from the continuation
const contControl = func.getControl()
const contStash = func.getStash()
const contEnv = func.getEnv()

// Restore the state of the stash,
// but replace the function application instruction with
// a resume continuation instruction
stash.push(func)
// we need to push the arguments back onto the stash
// as well
// update the C, S, E of the current context
control.setTo(contControl)
stash.setTo(contStash)
context.runtime.environments = contEnv

// push the arguments back onto the stash
stash.push(...args)
control.push(instr.resumeContInstr(command.numOfArgs, dummyContCallExpression))
return
}

Expand Down Expand Up @@ -1163,54 +1188,5 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = {
}
},

[InstrType.BREAK_MARKER]: function () {},

[InstrType.GENERATE_CONT]: function (
_command: GenContInstr,
context: Context,
control: Control,
stash: Stash
) {
const contControl = control.copy()
const contStash = stash.copy()
const contEnv = context.runtime.environments

// Remove all data related to the continuation-consuming function
contControl.pop()
contStash.pop()

// Now this will accurately represent the slice of the
// program execution at the time of the call/cc call
const continuation = makeContinuation(contControl, contStash, contEnv)

stash.push(continuation)
},

[InstrType.RESUME_CONT]: function (
command: ResumeContInstr,
context: Context,
control: Control,
stash: Stash
) {
// pop the arguments
const args: Value[] = []
for (let i = 0; i < command.numOfArgs; i++) {
args.unshift(stash.pop())
}
const cn: Continuation = stash.pop() as Continuation

const contControl = getContinuationControl(cn)
const contStash = getContinuationStash(cn)
const contEnv = getContinuationEnv(cn)

// Set the control and stash to the continuation's control and stash
control.setTo(contControl)
stash.setTo(contStash)

// Push the arguments given to the continuation back onto the stash
stash.push(...args)

// Restore the environment pointer to that of the continuation's environment
context.runtime.environments = contEnv
}
[InstrType.BREAK_MARKER]: function () {}
}
12 changes: 1 addition & 11 deletions src/cse-machine/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ export enum InstrType {
CONTINUE = 'Continue',
CONTINUE_MARKER = 'ContinueMarker',
BREAK = 'Break',
BREAK_MARKER = 'BreakMarker',
GENERATE_CONT = 'GenerateContinuation',
RESUME_CONT = 'ResumeContinuation'
BREAK_MARKER = 'BreakMarker'
}

interface BaseInstr {
Expand Down Expand Up @@ -76,12 +74,6 @@ export interface ArrLitInstr extends BaseInstr {
arity: number
}

export type GenContInstr = BaseInstr

export interface ResumeContInstr extends BaseInstr {
numOfArgs: number
}

export type Instr =
| BaseInstr
| WhileInstr
Expand All @@ -90,8 +82,6 @@ export type Instr =
| BranchInstr
| EnvInstr
| ArrLitInstr
| GenContInstr
| ResumeContInstr

export type ControlItem = (Node | Instr) & {
isEnvDependent?: boolean
Expand Down
15 changes: 12 additions & 3 deletions src/cse-machine/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import * as errors from '../errors/errors'
import { RuntimeSourceError } from '../errors/runtimeSourceError'
import type { Environment, Node, StatementSequence, Value } from '../types'
import * as ast from '../utils/ast/astCreator'
import { isContinuation } from './continuations'
import Heap from './heap'
import * as instr from './instrCreator'
import { Control } from './interpreter'
import { AppInstr, EnvArray, ControlItem, Instr, InstrType } from './types'
import Closure from './closure'
import { Continuation, isCallWithCurrentContinuation } from './continuations'

/**
* Typeguard for Instr to distinguish between program statements and instructions.
Expand Down Expand Up @@ -505,10 +505,19 @@ export const checkNumberOfArguments = (
)
)
}
} else if (isContinuation(callee)) {
} else if (isCallWithCurrentContinuation(callee)) {
// call/cc should have a single argument
if (args.length !== 1) {
return handleRuntimeError(
context,
new errors.InvalidNumberOfArguments(exp, 1, args.length, false)
)
}
return undefined
} else if (callee instanceof Continuation) {
// Continuations have variadic arguments,
// and so we can let it pass
// in future, if we can somehow check the number of arguments
// TODO: in future, if we can somehow check the number of arguments
// expected by the continuation, we can add a check here.
return undefined
} else {
Expand Down

0 comments on commit e4e4631

Please sign in to comment.