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; } } }