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.

349 lines
12 KiB
TypeScript

import type { AST, Pattern } from './ast';
import type { Env } from './env';
import type { Value } from './types';
import { RuntimeError } from './error';
import { recordDependency } from './store';
export function evaluate(ast: AST, env: Env, source: string): Value {
switch (ast.kind) {
case 'literal':
return ast.value;
case 'variable': {
const val = env.get(ast.name);
if (val === undefined)
throw RuntimeError(`Unknown variable: ${ast.name}`, ast.line, ast.column, source);
recordDependency(ast.name);
return val;
}
case 'list': {
const elements: Value[] = [];
for (const item of ast.elements) {
// Spread
if ('spread' in item) {
const spreadValue = evaluate(item.spread, env, source);
if (spreadValue.kind !== 'list')
throw RuntimeError('can only spread lists', ast.line, ast.column, source);
elements.push(...spreadValue.elements);
} else {
elements.push(evaluate(item, env, source));
}
}
return { kind: 'list', elements };
}
case 'record': {
const fields: { [key: string]: Value } = {};
const fieldMeta: { [key: string]: { body: AST, dependencies: Set<string> } } = {};
const recordEnv = new Map(env);
const fieldNames = Object.keys(ast.fields);
for (const [k, fieldAst] of Object.entries(ast.fields)) {
// Track which siblings are accessed
const deps = new Set<string>();
const trackingEnv = new Map(recordEnv);
const originalGet = trackingEnv.get.bind(trackingEnv);
trackingEnv.get = (name: string) => {
if (fieldNames.includes(name) && name !== k) {
deps.add(name);
}
return originalGet(name);
}
const value = evaluate(fieldAst, trackingEnv, source);
fields[k] = value;
fieldMeta[k] = { body: fieldAst, dependencies: deps };
recordEnv.set(k, value);
}
return { kind: 'record', fields, fieldMeta };
}
case 'record-access': {
const record = evaluate(ast.record, env, source);
if (record.kind !== 'record')
throw RuntimeError('Not a record', ast.line, ast.column, source);
const value = record.fields[ast.field];
if (value === undefined) {
throw RuntimeError(`Field ${ast.field} not found`, ast.line, ast.column, source);
}
return value;
}
case 'record-update': {
const record = evaluate(ast.record, env, source);
if (record.kind !== 'record')
throw RuntimeError('Not a record', ast.line, ast.column, source);
const newFields: { [key: string]: Value } = { ...record.fields };
for (const [field, expr] of Object.entries(ast.updates)) {
newFields[field] = evaluate(expr, env, source);
}
return { kind: 'record', fields: newFields };
}
case 'constructor':
return {
kind: 'constructor',
name: ast.name,
args: []
};
case 'let': {
const newEnv = new Map(env);
const val = evaluate(ast.value, newEnv, source);
// Don't bind _
if (ast.name !== '_') {
newEnv.set(ast.name, val);
}
newEnv.set(ast.name, val);
return evaluate(ast.body, newEnv, source);
}
case 'lambda':
return {
kind: 'closure',
params: ast.params,
body: ast.body,
env
}
case 'apply': {
const func = evaluate(ast.func, env, source);
const argValues = ast.args.map(arg => evaluate(arg, env, source));
// Native functions
if (func.kind === 'native') {
// Exact args
if (argValues.length === func.arity) {
return func.fn(...argValues);
}
// Partial application
if (argValues.length < func.arity) {
const capturedArgs = argValues;
return {
kind: 'native',
name: func.name,
arity: func.arity - argValues.length,
fn: (...restArgs: Value[]) => {
return func.fn(...capturedArgs, ...restArgs);
}
};
}
throw RuntimeError(`Function expects ${func.arity} args, but got ${argValues.length}`, ast.line, ast.column, source);
}
// Constructor application
if (func.kind === 'constructor') {
const argValues = ast.args.map(arg => evaluate(arg, env, source));
return {
kind: 'constructor',
name: func.name,
args: [...func.args, ...argValues]
};
}
if (func.kind !== 'closure')
throw RuntimeError('Not a function', ast.line, ast.column, source);
// 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 RuntimeError('Too many arguments', ast.line, ast.column, source);
// 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, source);
}
case 'match': {
const value = evaluate(ast.expr, env, source);
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, source);
}
}
throw RuntimeError('Non-exhaustive pattern match', ast.line, ast.column, source);
}
case 'rebind': {
const value = evaluate(ast.value, env, source);
if (ast.target.kind === 'variable') {
const name = ast.target.name;
return {
kind: 'constructor',
name: 'Rebind',
args: [{ kind: 'string', value: name }, value]
};
}
if (ast.target.kind === 'record-access') {
let current: AST = ast.target;
const path: string[] = [];
while (current.kind === 'record-access') {
path.unshift(current.field);
current = current.record;
}
if (current.kind !== 'variable')
throw RuntimeError('Rebind target must be a variable or field access', ast.line, ast.column, source);
const rootName = current.name;
const rootValue = env.get(rootName);
if (!rootValue)
throw RuntimeError(`Unknown variable: ${rootName}`, ast.line, ast.column, source);
return {
kind: 'constructor',
name: 'Rebind',
args: [
{ kind: 'string', value: rootName },
{ kind: 'list', elements: path.map(p => ({ kind: 'string', value: p })) },
value
]
};
}
throw RuntimeError('Rebind target must be a variable or field access', ast.line, ast.column, source);
}
default:
throw RuntimeError('Syntax Error', ast.line, ast.column, source);
}
}
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 'list-spread': {
if (value.kind !== 'list') return null;
if (value.elements.length < pattern.head.length) return null;
const bindings: Bindings = {};
for (let i = 0; i < pattern.head.length; i++) {
const elemBindings = matchPattern(value.elements[i], pattern.head[i]);
if (elemBindings === null) return null;
Object.assign(bindings, elemBindings);
}
const rest = value.elements.slice(pattern.head.length);
bindings[pattern.spread] = { kind: 'list', elements: rest };
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;
}
}
}