diff --git a/src/index.ts b/src/index.ts index 29047f2..d162465 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { type LogLevel, type TracingLogger, } from '@/utils'; -import { evaluate } from '@/interpreter'; +import { doRepl } from './repl'; const LOG_LEVELS: LogLevel[] = ['info', 'warn', 'error']; @@ -30,18 +30,6 @@ const devMode = async (logger: TracingLogger) => { } }; -const doRepl = async (prompt = '~> ') => { - process.stdout.write(prompt); - - for await (const line of console) { - const result = await evaluate(line); - console.log(result); - break; - } - - await doRepl(prompt); -}; - export const main = async (args: Args) => { if (args.devMode) { LOG_LEVELS.push('debug'); @@ -55,9 +43,9 @@ export const main = async (args: Args) => { } if (args.repl) { - logger.info('Starting REPL...'); - logger.info('Welcome to the CPS interpreter!'); + await doRepl(logger); } + return 0; }; -main(args); +main(args).then(code => process.exit(code)); diff --git a/src/interpreter/builtins.ts b/src/interpreter/builtins.ts index bc666e9..16322f1 100644 --- a/src/interpreter/builtins.ts +++ b/src/interpreter/builtins.ts @@ -202,6 +202,51 @@ const addBinaryArithmeticOperationsTo = (env: Environment) => { return env; }; +const addIdentityFunctionTo = (env: Environment) => { + env.set('id', { + type: 'function', + value: { + signatures: [ + { + arguments: ['null'], + return: 'null', + }, + { + arguments: ['int'], + return: 'int', + }, + { + arguments: ['real'], + return: 'real', + }, + { + arguments: ['bool'], + return: 'bool', + }, + { + arguments: ['string'], + return: 'string', + }, + { + arguments: ['bytearray'], + return: 'bytearray', + }, + { + arguments: ['function'], + return: 'function', + }, + { + arguments: ['reference'], + return: 'reference', + }, + ], + body: ({ value }: Denotable) => value, + }, + }); + + return env; +}; + export const putBuiltinsOnEnvironemtn = (env: Environment) => { return [ addBinaryArithmeticOperationsTo, @@ -210,5 +255,6 @@ export const putBuiltinsOnEnvironemtn = (env: Environment) => { addNumberComparisonOperationsTo, addBooleanAlgebraOperationsTo, addEqualityOperationsTo, + addIdentityFunctionTo, ].reduce((acc, builtinsAdder) => builtinsAdder(acc), env); }; diff --git a/src/interpreter/interpreter.ts b/src/interpreter/interpreter.ts index 94e263c..278e027 100644 --- a/src/interpreter/interpreter.ts +++ b/src/interpreter/interpreter.ts @@ -1,6 +1,7 @@ import { type ContinuationExpression, type PrimitiveOperationExpression, + type ApplicationExpression, type Program, type Value, } from '@/parser'; @@ -16,7 +17,7 @@ import { putBuiltinsOnEnvironemtn } from './builtins'; const evaluateValue = ( value: Value, env: Environment, - logger: TracingLogger, + _logger: TracingLogger, ): Denotable => { if (typeof value === 'string') { return { type: 'string', value }; @@ -35,6 +36,18 @@ const evaluateValue = ( throw new InvalidStateError(`Invalid value: ${value}`); }; +const evaluateApplicationExpression = ( + { application }: ApplicationExpression, + env: Environment, + logger: TracingLogger, +): Denotable => { + const { fn, args } = application; + const argValues = args.map(arg => + evaluateValue(arg, env, logger.createChild('evaluateValue')), + ); + return env.apply(fn.name, argValues); +}; + const evaluatePrimitiveOperation = ( { primitiveOperation }: PrimitiveOperationExpression, env: Environment, @@ -61,6 +74,7 @@ const evaluatePrimitiveOperation = ( logger.warn( `Expected 2 continuations for boolean result, got ContinuationLength=(${continuations.length})`, ); + return result; } const [trueContinuation, falseContinuation] = continuations; @@ -79,7 +93,7 @@ const evaluatePrimitiveOperation = ( ); } else if (continuations.length === 0) { logger.warn( - `!! Expected 1 continuation in continuation list... implicitly returning result but PLEASE NOTE this is technically undefined behavior !!`, + "Expected 1 continuation for non-boolean result, but there wasn't any. Implicitly returning the result", // technically undefined behavior ); return result; } @@ -107,6 +121,15 @@ const evaluteContinuationExpression = ( ); } + if ('application' in expr) { + logger.debug('Evaluating function application'); + return evaluateApplicationExpression( + expr, + env, + logger.createChild('evaluateApplicationExpression'), + ); + } + if ('record' in expr) { throw new NotImplementedError('Continuation records are not supported yet'); } @@ -116,11 +139,6 @@ const evaluteContinuationExpression = ( if ('offset' in expr) { throw new NotImplementedError('Continuation offset is not supported yet'); } - if ('application' in expr) { - throw new NotImplementedError( - 'Continuation application is not supported yet', - ); - } if ('switch' in expr) { throw new NotImplementedError('Continuation switch is not supported yet'); } diff --git a/src/parser/grammar.pegjs b/src/parser/grammar.pegjs index e237af9..f586dcc 100644 --- a/src/parser/grammar.pegjs +++ b/src/parser/grammar.pegjs @@ -86,7 +86,7 @@ SwitchExpression RPAREN { return { switch: { switchIndex, continuations } }; } ApplicationExpression - = APP _? LPAREN _? fn:Value _? COMMA _? args:ValueList _? RPAREN { + = APP _? LPAREN _? fn:(LabelStatement / VarStatement) _? COMMA _? args:ValueList _? RPAREN { return { application: { fn, args } }; } diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 500a763..b196866 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -1716,7 +1716,12 @@ peg$parseApplicationExpression() { s4 = null; } // @ts-ignore - s5 = peg$parseValue(); + s5 = peg$parseLabelStatement(); +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = peg$parseVarStatement(); + } // @ts-ignore if (s5 !== peg$FAILED) { // @ts-ignore @@ -5337,7 +5342,7 @@ export type SwitchExpression = { switch: { switchIndex: Value; continuations: ContinuationList }; }; export type ApplicationExpression = { - application: { fn: Value; args: ValueList }; + application: { fn: LabelStatement | VarStatement; args: ValueList }; }; export type FixBinding = [ LPAREN, diff --git a/src/repl.ts b/src/repl.ts new file mode 100644 index 0000000..a42183a --- /dev/null +++ b/src/repl.ts @@ -0,0 +1,80 @@ +import type { TracingLogger } from './utils'; +import * as readline from 'node:readline/promises'; +import { stdin as input, stdout as output } from 'node:process'; +import { peggyParse } from './parser'; +import { evaluate } from './interpreter'; + +// cool asci logo for CPS +const LOGO = ` + _______ ________ ______ _______ __ +/ \\ / | / \\ / \\ / | +$$$$$$$ |$$$$$$$$/ /$$$$$$ |$$$$$$$ |$$ | +$$ |__$$ |$$ |__ $$ | $$/ $$ |__$$ |$$ | +$$ $$< $$ | $$ | $$ $$/ $$ | +$$$$$$$ |$$$$$/ $$ | __ $$$$$$$/ $$ | +$$ | $$ |$$ |_____ $$ \\__/ |$$ | $$ |_____ +$$ | $$ |$$ |$$ $$/ $$ | $$ | +$$/ $$/ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ + +`; + +const HELP = ` + This is the CPS REPL. You can enter CPS programs and see the result of evaluating them. + This REPL supports multi-line input. To end a multi-line input, enter an empty line. + + Commands: + help - Show this message + exit - Exit the REPL + + About: + Read "Compiling With Continuations" by Andrew W. Appel for more information about + this Intermediate Representation. + + Example: + ~> PRIMOP(+, [INT 1, INT 2], [result], [ + | APP(LABEL id, [VAR result]) + | ]) +`; + +export const doRepl = async ( + logger: TracingLogger, + prompt = 0, + rl = readline.createInterface({ input, output }), +): Promise => { + if (prompt === 0) { + logger.info('welcome to recpl (read eval continue print loop) :)' + LOGO); + } + + const promptString = `[ ${prompt} ] ~> `; + const lines: string[] = [await rl.question(promptString)]; + while (lines.at(-1)) { + const line = lines.at(-1)!; + + if (lines.length === 1 && line === 'help') { + logger.info(HELP); + return doRepl(logger, prompt + 1, rl); + } + if (line === 'exit') { + logger.info('Exiting REPL...'); + rl.close(); + return; + } + + lines.push( + await rl.question(`|`.padStart(promptString.length - 1, ' ') + ' '), + ); + } + + const program = lines.slice(0, -1).join('\n'); + try { + const ast = peggyParse(program); + logger.debug('AST: ' + JSON.stringify(ast, null, 2)); + + const result = await evaluate(ast, logger.createChild('evaluate')); + logger.info('Result: ' + JSON.stringify(result, null, 2) + '\n'); + } catch (e) { + logger.error(e!.toString() + '\n'); + } + + return doRepl(logger, prompt + 1, rl); +}; diff --git a/test/interpreter.spec.ts b/test/interpreter.spec.ts index 49b741e..6d3189a 100644 --- a/test/interpreter.spec.ts +++ b/test/interpreter.spec.ts @@ -25,18 +25,23 @@ test('Branching', async () => { expect(result).toEqual({ type: 'real', value: 2 }); }); -/* test('String equality', async () => { const ast = peggyParse(await TestPrograms.StringEquality); const result = await evaluate(ast, testingLogger); - expect(result).toEqual({ type: 'int', value: 1 }); + expect(result).toEqual({ type: 'bool', value: 1 }); }); test('String inequality', async () => { const ast = peggyParse(await TestPrograms.StringInEquality); const result = await evaluate(ast, testingLogger); - expect(result).toEqual({ type: 'int', value: 0 }); + expect(result).toEqual({ type: 'bool', value: 0 }); +}); + +test('Application of identity function', async () => { + const ast = peggyParse(await TestPrograms.Application); + + const result = await evaluate(ast, testingLogger); + expect(result).toEqual({ type: 'int', value: 3 }); }); -*/ diff --git a/test/programs/application.cps b/test/programs/application.cps new file mode 100644 index 0000000..169329e --- /dev/null +++ b/test/programs/application.cps @@ -0,0 +1 @@ +PRIMOP(+, [INT 1, INT 2], [result], [APP(LABEL id, [VAR result])]) \ No newline at end of file diff --git a/test/programs/index.ts b/test/programs/index.ts index 864169f..fae3b59 100644 --- a/test/programs/index.ts +++ b/test/programs/index.ts @@ -2,18 +2,21 @@ import { join } from 'path'; export namespace TestPrograms { export const AddOneThree = Bun.file( - join(import.meta.dir + '/add-1-3.cps'), + join(import.meta.dir, 'add-1-3.cps'), ).text(); export const PrimopScope = Bun.file( - join(import.meta.dir + '/primop-scope.cps'), + join(import.meta.dir, 'primop-scope.cps'), ).text(); export const Branching = Bun.file( - join(import.meta.dir + '/branching.cps'), + join(import.meta.dir, 'branching.cps'), ).text(); export const StringEquality = Bun.file( - join(import.meta.dir + '/string-equal.cps'), + join(import.meta.dir, 'string-equal.cps'), ).text(); export const StringInEquality = Bun.file( - join(import.meta.dir + '/string-unequal.cps'), + join(import.meta.dir, 'string-unequal.cps'), + ).text(); + export const Application = Bun.file( + join(import.meta.dir, 'application.cps'), ).text(); } diff --git a/test/programs/string-equal.cps b/test/programs/string-equal.cps index ea49b22..5a32526 100644 --- a/test/programs/string-equal.cps +++ b/test/programs/string-equal.cps @@ -1 +1 @@ -PRIMOP(==, ["asdf", "asdf"], [result], []) \ No newline at end of file +PRIMOP(==, [STRING "asdf", STRING "asdf"], [result], []) \ No newline at end of file diff --git a/test/programs/string-unequal.cps b/test/programs/string-unequal.cps index ccd278e..79ee7cf 100644 --- a/test/programs/string-unequal.cps +++ b/test/programs/string-unequal.cps @@ -1 +1 @@ -PRIMOP(==, ["asdfasdf", "asdf"], [result], []) +PRIMOP(==, [STRING "asdfasdf", STRING "asdf"], [result], [])