simple interpreter

This commit is contained in:
Elizabeth Hunt 2024-02-26 17:58:53 -07:00
parent c8336ee487
commit ea7a11844a
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
10 changed files with 5490 additions and 5452 deletions

View File

@ -0,0 +1,106 @@
import { BadArgumentError } from '@/utils';
import type { DenotableValueType } from '.';
export type Signature = Array<Array<DenotableValueType | Signature>>;
export const primitiveOperationSignatures: Record<string, Signature> = {
'+': [
['int', 'int', 'int'],
['real', 'real', 'real'],
['int', 'real', 'real'],
['real', 'int', 'real'],
],
'-': [
['int', 'int', 'int'],
['real', 'real', 'real'],
['int', 'real', 'real'],
['real', 'int', 'real'],
],
'*': [
['int', 'int', 'int'],
['real', 'real', 'real'],
['int', 'real', 'real'],
['real', 'int', 'real'],
],
'/': [
['int', 'int', 'int'],
['real', 'real', 'real'],
['int', 'real', 'real'],
['real', 'int', 'real'],
],
'%': [['int', 'int', 'int']],
'**': [
['int', 'int', 'int'],
['real', 'real', 'real'],
['int', 'real', 'real'],
['real', 'int', 'real'],
],
'>>': [['int', 'int', 'int']],
'<<': [['int', 'int', 'int']],
'&': [['int', 'int', 'int']],
'|': [['int', 'int', 'int']],
'^': [['int', 'int', 'int']],
'==': [
['int', 'int', 'int'],
['real', 'real', 'int'],
['string', 'string', 'int'],
],
'!=': [
['int', 'int', 'int'],
['real', 'real', 'int'],
['string', 'string', 'int'],
],
'<': [
['int', 'int', 'int'],
['real', 'real', 'int'],
],
'>': [
['int', 'int', 'int'],
['real', 'real', 'int'],
],
'<=': [
['int', 'int', 'int'],
['real', 'real', 'int'],
],
'>=': [
['int', 'int', 'int'],
['real', 'real', 'int'],
],
'&&': [['int', 'int', 'int']],
'||': [['int', 'int', 'int']],
'!': [['int', 'int']],
};
export const getResultingType = (
opr: string,
types: DenotableValueType[],
): DenotableValueType | Signature => {
const signature = primitiveOperationSignatures[opr];
if (!signature) {
throw new BadArgumentError(`Invalid operation: ${opr}`);
}
const resultingType = signature.find(sig => {
if (sig.length !== types.length) {
return false;
}
return sig.every((type, i) => {
if (Array.isArray(type)) {
return getResultingType(
opr,
types.map((t, j) => (i === j ? t : 'int')),
);
}
return type === 'int' || type === types[i];
});
});
if (!resultingType) {
throw new TypeError(
`Invalid types for operation ${opr}: ${types.join(', ')}`,
);
}
return resultingType as DenotableValueType | Signature;
};

View File

@ -0,0 +1,44 @@
import { UnknownSymbolError } from '@/utils';
import type { DenotableValue } from '.';
export class Environment {
private scope: Map<string, DenotableValue>;
private parent: Environment | null;
constructor(parent: Environment | null = null) {
this.parent = parent;
this.scope = new Map();
}
public set(name: string, value: DenotableValue) {
this.scope.set(name, value);
}
public get(name: string): DenotableValue {
if (this.scope.has(name)) {
return this.scope.get(name);
}
if (this.parent) {
return this.parent.get(name);
}
throw new UnknownSymbolError(`Undefined variable: ${name}`);
}
public has(name: string): boolean {
if (this.scope.has(name)) {
return true;
}
if (this.parent) {
return this.parent.has(name);
}
return false;
}
public createChild(): Environment {
return new Environment(this);
}
}

View File

@ -1 +1,11 @@
export const evaluate = async (ast: Program) => {}; export type DenotableValueType =
| 'null'
| 'int'
| 'real'
| 'string'
| 'function'
| 'reference';
export type DenotableValue = { type: DenotableValueType; value: any };
export * from './environment';
export * from './interpreter';

View File

@ -0,0 +1,160 @@
import {
type ContinuationExpression,
type PrimitiveOperationExpression,
type Program,
type Value,
} from '@/parser';
import { Environment, type DenotableValue } from '.';
import {
BadArgumentError,
InvalidStateError,
NotImplementedError,
type TracingLogger,
} from '@/utils';
const evaluateValue = (
value: Value,
env: Environment,
logger: TracingLogger,
): DenotableValue => {
if (typeof value === 'string') {
return { type: 'string', value };
}
if ('real' in value) {
return { type: 'real', value: value.real };
}
if ('int' in value) {
return { type: 'int', value: value.int };
}
if ('name' in value) {
logger.debug(`Evaluating variable: ${value.name}`);
return env.get(value.name);
}
throw new InvalidStateError(`Invalid value: ${value}`);
};
const evaluatePrimitiveOperation = (
{ primitiveOperation }: PrimitiveOperationExpression,
env: Environment,
logger: TracingLogger,
) => {
const { opr, operands, resultBindings, continuations } = primitiveOperation;
if (operands.length !== 2) {
throw new BadArgumentError('Primitive operations must have 2 operands');
}
const operandValues = operands.map(operand =>
evaluateValue(operand, env, logger.createChild('evaluteValue')),
);
const rightReducingOperations = {
'+': (acc: number, operand: { value: number }) => acc + operand.value,
'-': (acc: number, operand: { value: number }) => acc - operand.value,
'*': (acc: number, operand: { value: number }) => acc * operand.value,
};
const leftReducingOperations = {
'/': (acc: number, operand: { value: number }) => acc / operand.value,
'%': (acc: number, operand: { value: number }) => acc % operand.value,
'**': (acc: number, operand: { value: number }) => acc ** operand.value,
'>>': (acc: number, operand: { value: number }) => acc >> operand.value,
'<<': (acc: number, operand: { value: number }) => acc << operand.value,
'&': (acc: number, operand: { value: number }) => acc & operand.value,
'|': (acc: number, operand: { value: number }) => acc | operand.value,
'^': (acc: number, operand: { value: number }) => acc ^ operand.value,
};
const someNumberIsReal = operandValues.some(({ type }) => type === 'real');
let result: DenotableValue = { type: 'null', value: null };
const continuationEnvironment = env.createChild();
if (opr in rightReducingOperations) {
logger.debug('Evaluating right reducing operation: ' + opr);
const sum = operandValues.reduce(
rightReducingOperations[opr as keyof typeof rightReducingOperations],
0,
);
result = { type: someNumberIsReal ? 'real' : 'int', value: sum };
for (const binding of resultBindings) {
continuationEnvironment.set(binding.name, result);
}
}
if (opr in leftReducingOperations) {
logger.debug('Evaluating left reducing operation: ' + opr);
const [first, ...rest] = operandValues;
const sum = rest.reduce(
leftReducingOperations[opr as keyof typeof leftReducingOperations],
first.value,
);
result = { type: someNumberIsReal ? 'real' : 'int', value: sum };
for (const binding of resultBindings) {
continuationEnvironment.set(binding.name, result);
}
}
// return the result of the last continuation
return continuations.reduce((_, continuation, i) => {
const childLogger = logger.createChild(`continuation[${i}]`);
return evaluteContinuationExpression(
continuation,
continuationEnvironment,
childLogger,
);
}, result);
};
const evaluteContinuationExpression = (
expr: ContinuationExpression,
env: Environment,
logger: TracingLogger,
): DenotableValue => {
if ('primitiveOperation' in expr) {
logger.debug('Evaluating primitive operation');
return evaluatePrimitiveOperation(
expr,
env,
logger.createChild('evaluatePrimitiveOperation'),
);
}
if ('record' in expr) {
throw new NotImplementedError('Continuation records are not supported yet');
}
if ('select' in expr) {
throw new NotImplementedError('Continuation select is not supported yet');
}
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');
}
if ('fix' in expr) {
throw new NotImplementedError('Continuation fix is not supported yet');
}
throw new InvalidStateError(`Invalid continuation expression: ${expr}`);
};
export const evaluate = async (
ast: Program,
logger: TracingLogger,
): Promise<DenotableValue> => {
const environment = new Environment();
return ast.reduce((_, continuation, i) => {
const exprLogger = logger.createChild(`statement[${i}]`);
return evaluteContinuationExpression(
continuation as ContinuationExpression,
environment,
exprLogger,
);
}, null);
};

View File

@ -285,6 +285,7 @@ ComparisonOperation
/ "!" / "!"
/ ">" / ">"
/ "<" / "<"
/ "||"
Integer = digits:("-"? [0-9]+) !"." { return { int: parseInt(digits.join(''), 10) }; } Integer = digits:("-"? [0-9]+) !"." { return { int: parseInt(digits.join(''), 10) }; }

File diff suppressed because it is too large Load Diff

View File

@ -1 +1,9 @@
export class NotImplementedException extends Error {} export class NotImplementedError extends Error {}
export class UnknownSymbolError extends Error {}
export class InvalidStateError extends Error {}
export class BadArgumentError extends Error {}
export class TypeError extends Error {}

12
test/interpreter.spec.ts Normal file
View File

@ -0,0 +1,12 @@
import { expect, test } from 'bun:test';
import { TestPrograms } from './programs';
import { peggyParse } from '@/parser';
import { evaluate } from '@/interpreter';
import { testingLogger } from './logger';
test('Add (1 real) and (3 int) => (4 real)', async () => {
const ast = peggyParse(await TestPrograms.AddOneThree);
const result = await evaluate(ast, testingLogger);
expect(result).toEqual({ type: 'real', value: 4 });
});

View File

@ -3,5 +3,13 @@ import { TestPrograms } from './programs';
import { peggyParse } from '@/parser'; import { peggyParse } from '@/parser';
test('Primitive Operations', async () => { test('Primitive Operations', async () => {
const ast = peggyParse(await TestPrograms.AddOneThree); const [operation] = peggyParse(await TestPrograms.AddOneThree);
const { primitiveOperation } = operation;
expect(primitiveOperation).toEqual({
opr: '+',
operands: [{ real: 1 }, { int: 3 }],
resultBindings: [{ name: 'result' }],
continuations: [],
});
}); });

View File

@ -1,2 +1 @@
PRIMOP(+, [INT 1, INT 2], [u], PRIMOP(+, [REAL 1, INT 3], [result], [])
[APP(LABEL identity, [VAR u])])