You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
8.7 KiB
TypeScript
294 lines
8.7 KiB
TypeScript
import type { AST, Pattern } from './ast';
|
|
import type { Env } from './env';
|
|
import type { Value } from './types';
|
|
|
|
export function evaluate(ast: AST, env: Env): Value {
|
|
switch (ast.kind) {
|
|
case 'literal':
|
|
return ast.value;
|
|
|
|
case 'variable': {
|
|
const val = env.get(ast.name);
|
|
|
|
if (val === undefined)
|
|
throw new Error(`Unknown variable: ${ast.name}`);
|
|
|
|
return val;
|
|
}
|
|
|
|
case 'list':
|
|
return {
|
|
kind: 'list',
|
|
elements: ast.elements.map(el => evaluate(el, env))
|
|
};
|
|
|
|
case 'record':
|
|
const fields: { [key: string]: Value } = {};
|
|
|
|
Object.entries(ast.fields).forEach(([k, v]) => {
|
|
fields[k] = evaluate(v, env);
|
|
});
|
|
|
|
return { kind: 'record', fields };
|
|
|
|
case 'record-access': {
|
|
const record = evaluate(ast.record, env);
|
|
|
|
if (record.kind !== 'record')
|
|
throw new Error('Not a record');
|
|
|
|
const value = record.fields[ast.field];
|
|
|
|
if (value === undefined) {
|
|
throw new Error(`Field ${ast.field} not found`);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
case 'record-update': {
|
|
const record = evaluate(ast.record, env);
|
|
|
|
if (record.kind !== 'record')
|
|
throw new Error('Not a record');
|
|
|
|
const newFields: { [key: string]: Value } = { ...record.fields };
|
|
|
|
for (const [field, expr] of Object.entries(ast.updates)) {
|
|
newFields[field] = evaluate(expr, env);
|
|
}
|
|
|
|
return { kind: 'record', fields: newFields };
|
|
}
|
|
|
|
case 'constructor':
|
|
return {
|
|
kind: 'constructor',
|
|
name: ast.name,
|
|
args: [] // TODO: constructors args
|
|
};
|
|
|
|
case 'let': {
|
|
const newEnv = new Map(env);
|
|
|
|
const val = evaluate(ast.value, newEnv);
|
|
|
|
newEnv.set(ast.name, val);
|
|
|
|
return evaluate(ast.body, newEnv);
|
|
}
|
|
|
|
case 'lambda':
|
|
return {
|
|
kind: 'closure',
|
|
params: ast.params,
|
|
body: ast.body,
|
|
env
|
|
}
|
|
|
|
case 'apply': {
|
|
// Operators
|
|
if (ast.func.kind === 'variable') {
|
|
const name = ast.func.name;
|
|
const builtIns = ['+', '-', '*', '/', '>', '&'];
|
|
|
|
if (builtIns.includes(name)) {
|
|
const argValues = ast.args.map(arg => evaluate(arg, env));
|
|
|
|
if (argValues.length !== 2) {
|
|
throw new Error(`${name} expects 2 args`);
|
|
}
|
|
|
|
return evaluateBuiltIn(name, argValues[0], argValues[1]);
|
|
}
|
|
}
|
|
|
|
const func = evaluate(ast.func, env);
|
|
|
|
if (func.kind !== 'closure')
|
|
throw new Error('Not a function');
|
|
|
|
const argValues = ast.args.map(arg => evaluate(arg, env));
|
|
|
|
// Too few args (Currying)
|
|
if (argValues.length < func.params.length) {
|
|
// Bind the params we have
|
|
const newEnv = new Map(func.env);
|
|
|
|
for (let i = 0; i < argValues.length; i++) {
|
|
newEnv.set(func.params[i], argValues[i]);
|
|
}
|
|
|
|
return {
|
|
kind: 'closure',
|
|
params: func.params.slice(argValues.length),
|
|
body: func.body,
|
|
env: newEnv
|
|
};
|
|
}
|
|
|
|
// Too many args
|
|
if (argValues.length > func.params.length)
|
|
throw new Error('Too many arguments');
|
|
|
|
// Exact number of args
|
|
const callEnv = new Map(func.env);
|
|
for (let i = 0; i < argValues.length; i++) {
|
|
callEnv.set(func.params[i], argValues[i]);
|
|
}
|
|
|
|
return evaluate(func.body, callEnv);
|
|
}
|
|
|
|
case 'match': {
|
|
const value = evaluate(ast.expr, env);
|
|
|
|
for (const matchCase of ast.cases) {
|
|
const bindings = matchPattern(value, matchCase.pattern);
|
|
|
|
if (bindings !== null) {
|
|
const newEnv = new Map(env);
|
|
for (const [name, val] of Object.entries(bindings)) {
|
|
newEnv.set(name, val);
|
|
}
|
|
return evaluate(matchCase.result, newEnv);
|
|
}
|
|
}
|
|
|
|
throw new Error('Non-exhaustive pattern match');
|
|
}
|
|
|
|
default:
|
|
throw new Error('Syntax Error');
|
|
}
|
|
}
|
|
|
|
function evaluateBinaryOp(op: string, left: Value, right: Value): Value {
|
|
const leftKind = left.kind;
|
|
const rightKind = right.kind;
|
|
|
|
const bothNumbers = ((leftKind === 'int' || leftKind === 'float')) && ((rightKind === 'int' || rightKind === 'float'));
|
|
if (!bothNumbers)
|
|
throw new Error(`Not numbers: ${left}, ${right}`);
|
|
|
|
const leftValue = left.value;
|
|
const rightValue = right.value;
|
|
|
|
switch (op) {
|
|
case '+':
|
|
return { value: leftValue + rightValue, kind: 'int' };
|
|
|
|
case '-':
|
|
return { value: leftValue - rightValue, kind: 'int' };
|
|
|
|
case '*':
|
|
return { value: leftValue * rightValue, kind: 'int' }
|
|
|
|
case '/':
|
|
return { value: leftValue / rightValue, kind: 'int' }
|
|
|
|
default:
|
|
throw new Error(`Unknown operation: ${op}`);
|
|
}
|
|
}
|
|
|
|
function evaluateBuiltIn(op: string, left: Value, right: Value): Value {
|
|
if (op === '+' || op === '-' || op === '*' || op === '/') {
|
|
return evaluateBinaryOp(op, left, right);
|
|
}
|
|
|
|
if (op === '>') {
|
|
// x > f means f(x)
|
|
if (right.kind !== 'closure')
|
|
throw new Error('Right side of > must be a function');
|
|
|
|
if (right.params.length !== 1)
|
|
throw new Error('Pipe only works with 1-arg functions for now..');
|
|
|
|
const callEnv = new Map(right.env);
|
|
callEnv.set(right.params[0], left);
|
|
return evaluate(right.body, callEnv);
|
|
}
|
|
|
|
if (op === '&') {
|
|
if (left.kind == 'list' && right.kind === 'list') {
|
|
return {
|
|
kind: 'list',
|
|
elements: [...left.elements, ...right.elements]
|
|
};
|
|
}
|
|
|
|
if (left.kind == 'string' && right.kind === 'string') {
|
|
return {
|
|
kind: 'string',
|
|
value: left.value + right.value
|
|
};
|
|
}
|
|
}
|
|
|
|
throw new Error(`Unknown built-in: ${op}`);
|
|
}
|
|
|
|
type Bindings = { [key: string]: Value };
|
|
|
|
function matchPattern(value: Value, pattern: Pattern): Bindings | null {
|
|
switch (pattern.kind) {
|
|
case 'wildcard':
|
|
return {};
|
|
|
|
case 'var':
|
|
return { [pattern.name]: value };
|
|
|
|
case 'literal':
|
|
if (value.kind === 'int' || value.kind === 'float' || value.kind === 'string') {
|
|
if (value.value === pattern.value) {
|
|
return {};
|
|
}
|
|
}
|
|
return null;
|
|
|
|
case 'constructor': {
|
|
if (value.kind !== 'constructor') return null;
|
|
if (value.name !== pattern.name) return null;
|
|
if (value.args.length !== pattern.args.length) return null;
|
|
|
|
const bindings: Bindings = {};
|
|
for (let i = 0; i < pattern.args.length; i++) {
|
|
const argBindings = matchPattern(value.args[i], pattern.args[i]);
|
|
if (argBindings === null) return null;
|
|
Object.assign(bindings, argBindings);
|
|
}
|
|
return bindings;
|
|
}
|
|
|
|
case 'list': {
|
|
if (value.kind !== 'list') return null;
|
|
if (value.elements.length !== pattern.elements.length) return null;
|
|
|
|
const bindings: Bindings = {};
|
|
for (let i = 0; i < pattern.elements.length; i++) {
|
|
const elemBindings = matchPattern(value.elements[i], pattern.elements[i]);
|
|
if (elemBindings === null) return null;
|
|
Object.assign(bindings, elemBindings);
|
|
}
|
|
return bindings;
|
|
}
|
|
|
|
case 'record': {
|
|
if (value.kind !== 'record') return null;
|
|
|
|
const bindings: Bindings = {};
|
|
for (const [fieldName, fieldPattern] of Object.entries(pattern.fields)) {
|
|
const fieldValue = value.fields[fieldName];
|
|
if (fieldValue === undefined) return null;
|
|
|
|
const fieldBindings = matchPattern(fieldValue, fieldPattern);
|
|
if (fieldBindings === null) return null;
|
|
Object.assign(bindings, fieldBindings);
|
|
}
|
|
return bindings;
|
|
}
|
|
}
|
|
|
|
}
|