summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/index.ts20
-rw-r--r--src/interpreter/builtins.ts46
-rw-r--r--src/interpreter/interpreter.ts32
-rw-r--r--src/parser/grammar.pegjs2
-rw-r--r--src/parser/parser.ts9
-rw-r--r--src/repl.ts80
-rw-r--r--test/interpreter.spec.ts13
-rw-r--r--test/programs/application.cps1
-rw-r--r--test/programs/index.ts13
-rw-r--r--test/programs/string-equal.cps2
-rw-r--r--test/programs/string-unequal.cps2
11 files changed, 183 insertions, 37 deletions
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<any> => {
+ 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], [])