From e4e4631d4d352bcacd9e9dd0474963c9a53fc095 Mon Sep 17 00:00:00 2001 From: Kyriel Abad Date: Thu, 18 Jul 2024 16:41:30 +0800 Subject: [PATCH] Streamline CSE machine (#1711) * 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 --- src/cse-machine/__tests__/continuations.ts | 20 ++++ src/cse-machine/continuations.ts | 87 ++++++++-------- src/cse-machine/instrCreator.ts | 13 --- src/cse-machine/interpreter.ts | 112 ++++++++------------- src/cse-machine/types.ts | 12 +-- src/cse-machine/utils.ts | 15 ++- 6 files changed, 119 insertions(+), 140 deletions(-) create mode 100644 src/cse-machine/__tests__/continuations.ts diff --git a/src/cse-machine/__tests__/continuations.ts b/src/cse-machine/__tests__/continuations.ts new file mode 100644 index 000000000..b82e82ff9 --- /dev/null +++ b/src/cse-machine/__tests__/continuations.ts @@ -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') +}) diff --git a/src/cse-machine/continuations.ts b/src/cse-machine/continuations.ts index 998415b44..5d0b1f888 100644 --- a/src/cse-machine/continuations.ts +++ b/src/cse-machine/continuations.ts @@ -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 } /** @@ -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 { diff --git a/src/cse-machine/instrCreator.ts b/src/cse-machine/instrCreator.ts index 2fbfe8bd3..c360f00f5 100644 --- a/src/cse-machine/instrCreator.ts +++ b/src/cse-machine/instrCreator.ts @@ -13,10 +13,8 @@ import { BranchInstr, EnvInstr, ForInstr, - GenContInstr, Instr, InstrType, - ResumeContInstr, UnOpInstr, WhileInstr } from './types' @@ -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 -}) diff --git a/src/cse-machine/interpreter.ts b/src/cse-machine/interpreter.ts index 40057b6d0..4edcc39e4 100644 --- a/src/cse-machine/interpreter.ts +++ b/src/cse-machine/interpreter.ts @@ -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' @@ -45,10 +40,8 @@ import { CseError, EnvInstr, ForInstr, - GenContInstr, Instr, InstrType, - ResumeContInstr, UnOpInstr, WhileInstr } from './types' @@ -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 } @@ -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 () {} } diff --git a/src/cse-machine/types.ts b/src/cse-machine/types.ts index 2726ef6e7..9c1dbae3c 100644 --- a/src/cse-machine/types.ts +++ b/src/cse-machine/types.ts @@ -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 { @@ -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 @@ -90,8 +82,6 @@ export type Instr = | BranchInstr | EnvInstr | ArrLitInstr - | GenContInstr - | ResumeContInstr export type ControlItem = (Node | Instr) & { isEnvDependent?: boolean diff --git a/src/cse-machine/utils.ts b/src/cse-machine/utils.ts index a24610907..9930c8357 100644 --- a/src/cse-machine/utils.ts +++ b/src/cse-machine/utils.ts @@ -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. @@ -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 {