This commit is contained in:
Yura Dupyn 2026-02-07 15:21:44 +01:00
parent d45207342c
commit cd84d74ec7
12 changed files with 581 additions and 569 deletions

View file

@ -1,6 +1,6 @@
// AI GENERATED
import { Expr, Pattern, ProductPattern, Literal, FieldAssignment, FieldPattern } from '../value';
import { Expr, FieldAssignment, FieldPattern, Literal, Pattern, ProductPattern } from "../expr";
// AI GENERATED
export function exprToString(expr: Expr): string {
switch (expr.tag) {
case "literal":

View file

@ -5,7 +5,8 @@ import { parse, ParseError } from '../parser/parser';
import { SourceText, renderSpan, sourceText } from '../parser/source_text';
import { exprToString } from '../debug/expr_show';
import { valueToString } from '../debug/value_show';
import { eval_start, Program } from '../value';
import { Program } from '../program';
import { eval_start } from '../eval/evaluator';
// ANSI Color Codes
const C = {

View file

@ -1,5 +1,5 @@
// AI GENERATED
import { Value, Env, Closure, EnvFrame } from '../value';
import { Closure, Value, Env, EnvFrame } from '../eval/value';
import { exprToString, productPatternToString } from './expr_show';
export function valueToString(val: Value): string {

37
src/lang/eval/error.ts Normal file
View file

@ -0,0 +1,37 @@
import { FunctionName, Pattern, VariableName } from "../expr"
import { Closure, Value, ValueTag } from "./value"
export type RuntimeError =
| { tag: "FunctionLookupFailure", name: FunctionName }
| { tag: "FunctionCallArityMismatch", name: FunctionName, expected: number, actual: number }
| { tag: "ClosureApplicationArityMismatch", closure: Closure, expected: number, actual: number }
| { tag: "VariableLookupFailure", name: VariableName }
// | { tag: "CellLookupFailure", name: CellName }
| { tag: "UnableToFindMatchingPattern", value: Value }
| { tag: "TypeMismatch", expected: ValueTag, received: Value }
| { tag: "DuplicateVariableNamesInPattern", pattern: Pattern, duplicates: VariableName[] }
// | { tag: "DuplicateVariableNamesInProductPattern", pattern: ProductPattern, duplicates: VariableName[] }
export type Result<T> =
| { tag: "ok", value: T }
| { tag: "error", error: RuntimeError }
export namespace Result {
export function ok<T>(value: T): Result<T> { return { tag: "ok", value } }
export function error<T>(error: RuntimeError): Result<T> { return { tag: "error", error } }
}
// This is an internal type - use it in all internal evaluation functions.
export type ThrownRuntimeError = {
kind: "RuntimeError",
error: RuntimeError
}
export namespace ThrownRuntimeError {
// use as follows
// `throw ThrownRuntimeError.error(e)`
export function error(error: RuntimeError): ThrownRuntimeError {
return { kind: "RuntimeError", error };
}
}

174
src/lang/eval/evaluator.ts Normal file
View file

@ -0,0 +1,174 @@
import { Expr, ExprBinding, FieldName, ProductPattern } from "../expr"
import { Closure, Env, EnvFrame, Value } from "./value"
import { FunctionDefinition, Program, UserFunctionDefinition } from "../program";
import { Result, RuntimeError, ThrownRuntimeError } from "./error";
import { match_pattern, match_product_pattern, match_product_pattern_mut } from "./pattern_match";
export function eval_start(program: Program, e: Expr): Result<Value> {
try {
return Result.ok(eval_expr(program, Env.nil(), e));
} catch (err) {
if (typeof err === "object" && (err as any).kind === "RuntimeError") {
return Result.error((err as ThrownRuntimeError).error);
} else {
throw err;
}
}
}
// may throw `ThrownRuntimeError`
function eval_expr(program: Program, env: Env, e: Expr): Value {
switch (e.tag) {
case "literal":
switch (e.literal.tag) {
case "number":
return Value.number(e.literal.value);
case "string":
return Value.string(e.literal.value);
}
case "tag":
return Value.tag(e.tag_name);
case "tagged":
return Value.tagged(e.tag_name, eval_expr(program, env, e.expr));
case "tuple":
return Value.tuple(eval_sequence(program, env, e.exprs));
case "record":
const fields = new Map<FieldName, Value>();
for (const field of e.fields) {
const value = eval_expr(program, env, field.expr);
fields.set(field.name, value);
}
return Value.record(fields);
case "lambda":
return Value.closure({
env,
parameters: e.parameters,
body: e.body,
});
case "var_use":
return Env.lookup(env, e.name);
case "call":
const fn = Program.lookup_function(program, e.name);
const fn_args = eval_sequence(program, env, e.args);
return call_function(program, fn, fn_args);
case "apply":
const closure = force_closure(eval_expr(program, env, e.callee));
const closure_args = eval_sequence(program, env, e.args);
return apply_closure(program, closure, closure_args);
case "let":
const new_env = eval_bindings(program, env, e.bindings);
return eval_expr(program, new_env, e.body);
case "match":
const match_val = eval_expr(program, env, e.arg);
for (const branch of e.branches) {
const res = match_pattern(branch.pattern, match_val);
if (res.tag === "match") {
return eval_expr(program, Env.push_frame(env, res.frame), branch.body);
}
}
throw ThrownRuntimeError.error({
tag: "UnableToFindMatchingPattern",
value: match_val,
});
}
}
// may throw `ThrownRuntimeError`
function eval_bindings(program: Program, env: Env, bindings: ExprBinding[]): Env {
// note that `let { x = 123, y = x + 1 ... } is allowed. Ofcourse later bindings can't be referenced by earlier bindings (i.e. no recursion).
let cur_env = env;
for (const { pattern: var_name, expr } of bindings) {
const value = eval_expr(program, cur_env, expr);
const res = match_product_pattern(var_name, value);
if (res.tag === "failure") {
throw ThrownRuntimeError.error({
tag: "UnableToFindMatchingPattern",
value,
});
} else {
cur_env = Env.push_frame(cur_env, res.frame);
}
}
return cur_env;
}
// may throw `ThrownRuntimeError`
function eval_sequence(program: Program, env: Env, args: Expr[]): Value[] {
return args.map(arg => eval_expr(program, env, arg));
}
// may throw `ThrownRuntimeError`
function call_function(program: Program, fn_def: FunctionDefinition, args: Value[]): Value {
switch (fn_def.tag) {
case "user":
return call_user_function(program, fn_def.def, args);
case "primitive":
return fn_def.def.implementation(args);
}
}
// may throw `ThrownRuntimeError`
function call_user_function(program: Program, fn_def: UserFunctionDefinition, args: Value[]): Value {
const frame = bind_arguments_to_parameters(
fn_def.parameters,
args,
(expected, actual) => ({ tag: "FunctionCallArityMismatch", name: fn_def.name, expected, actual })
);
return eval_expr(program, Env.push_frame(Env.nil(), frame), fn_def.body);
}
// may throw `ThrownRuntimeError`
function apply_closure(program: Program, closure: Closure, args: Value[]): Value {
const frame = bind_arguments_to_parameters(
closure.parameters,
args,
(expected, actual) => ({ tag: "ClosureApplicationArityMismatch", closure, expected, actual })
);
return eval_expr(program, Env.push_frame(closure.env, frame), closure.body);
}
// may throw `ThrownRuntimeError`
function force_closure(value: Value): Closure {
if (value.tag !== "closure") {
throw ThrownRuntimeError.error({
tag: "TypeMismatch",
expected: "closure",
received: value,
});
}
return value.closure;
}
// may throw `ThrownRuntimeError`
function bind_arguments_to_parameters(
patterns: ProductPattern[],
values: Value[],
onArityMismatchError: (expected: number, actual: number) => RuntimeError
): EnvFrame {
const expected = patterns.length;
const actual = values.length;
if (expected !== actual) {
throw ThrownRuntimeError.error(onArityMismatchError(expected, actual));
}
const frame: EnvFrame = new Map();
for (let i = 0; i < patterns.length; i++) {
const pattern = patterns[i];
const value = values[i];
const res = match_product_pattern_mut(frame, pattern, value);
if (res.tag === "failure") {
throw ThrownRuntimeError.error({
tag: "UnableToFindMatchingPattern",
value,
});
}
}
return frame;
}

View file

@ -0,0 +1,67 @@
import { Pattern, ProductPattern, VariableName } from "../expr";
import { EnvFrame, Value } from "./value";
// A pattern match will result either in a succesfull match with a new EnvFrame
type PatternMatchingResult =
| { tag: "match", frame: EnvFrame }
| { tag: "failure", pattern: Pattern, value: Value }
export function match_pattern(pattern: Pattern, value: Value): PatternMatchingResult {
const frame = new Map<VariableName, Value>();
return match_pattern_mut(frame, pattern, value);
}
export function match_pattern_mut(frame: EnvFrame, pattern: Pattern, value: Value): PatternMatchingResult {
switch (pattern.tag) {
case "tag":
if (value.tag === "tag" && value.tag_name === pattern.tag_name) {
return { tag: "match", frame }
} else {
return { tag: "failure", pattern, value }
}
case "tagged":
if (value.tag === "tagged" && value.tag_name === pattern.tag_name) {
return match_pattern_mut(frame, pattern.pattern, value.value);
} else {
return { tag: "failure", pattern, value };
}
default:
return match_product_pattern_mut(frame, pattern, value);
}
}
export function match_product_pattern(pattern: ProductPattern, value: Value): PatternMatchingResult {
const frame = new Map<VariableName, Value>();
return match_product_pattern_mut(frame, pattern, value);
}
export function match_product_pattern_mut(frame: EnvFrame, pattern: ProductPattern, value: Value): PatternMatchingResult {
switch (pattern.tag) {
case "any":
frame.set(pattern.name, value);
return { tag: "match", frame };
case "tuple":
if (value.tag !== "tuple" || pattern.patterns.length !== value.values.length) return { tag: "failure", pattern, value };
for (let i = 0; i < pattern.patterns.length; i++) {
const res = match_product_pattern_mut(frame, pattern.patterns[i], value.values[i]);
if (res.tag === "failure") return res;
}
return { tag: "match", frame };
case "record":
if (value.tag !== "record") return { tag: "failure", pattern, value };
for (const { fieldName, pattern: p } of pattern.fields) {
const field_value = value.fields.get(fieldName);
if (field_value === undefined) {
return { tag: "failure", pattern, value };
} else {
const res = match_product_pattern_mut(frame, p, field_value);
if (res.tag === "failure") {
return res;
}
}
}
return { tag: "match", frame };
}
}

73
src/lang/eval/value.ts Normal file
View file

@ -0,0 +1,73 @@
import { Expr, FieldName, ProductPattern, Tag, VariableName } from "../expr";
import { ThrownRuntimeError } from "./error";
export type Value =
| { tag: "string", value: string }
| { tag: "number", value: number }
| { tag: "tag", tag_name: Tag }
| { tag: "tagged", tag_name: Tag, value: Value }
| { tag: "tuple", values: Value[] }
| { tag: "record", fields: Map<FieldName, Value> }
| { tag: "closure", closure: Closure }
export type ValueTag =
| "string"
| "number"
| "tag"
| "tagged"
| "tuple"
| "record"
| "closure"
// Used as a Stack of frames. Basically a linked list.
export type Env =
| { tag: "nil" }
| { tag: "frame", frame: EnvFrame, parent: Env }
export type EnvFrame = Map<VariableName, Value>;
export type Closure = {
env: Env,
parameters: ProductPattern[],
body: Expr,
}
export namespace Value {
export const string = (value: string): Value => ({ tag: "string", value });
export const number = (value: number): Value => ({ tag: "number", value });
export const tag = (tag_name: Tag): Value => ({ tag: "tag", tag_name });
export const tagged = (tag_name: Tag, value: Value): Value => ({ tag: "tagged", tag_name, value });
export const tuple = (values: Value[]): Value => ({ tag: "tuple", values });
export const record = (fields: Map<FieldName, Value>): Value => ({ tag: "record", fields });
export const closure = (closure: Closure): Value => ({ tag: "closure", closure });
}
export namespace Env {
export function nil(): Env {
return { tag: "nil" };
}
export function push_frame(env: Env, frame: EnvFrame): Env {
return { tag: "frame", frame, parent: env };
}
// may throw `ThrownRuntimeError`
export function lookup(env: Env, var_name: VariableName): Value {
let cur = env;
while (cur.tag !== "nil") {
if (cur.frame.has(var_name)) {
return cur.frame.get(var_name)!;
}
cur = cur.parent;
}
throw ThrownRuntimeError.error({
tag: "VariableLookupFailure",
name: var_name,
});
}
export function frame_insert_mut(frame: EnvFrame, var_name: VariableName, value: Value) {
frame.set(var_name, value);
}
}

86
src/lang/expr.ts Normal file
View file

@ -0,0 +1,86 @@
// === Identifiers ===
export type VariableName = string
export type FunctionName = string
// type CellName = string
export type Tag = string
export type FieldName = string
// === Expr ===
export type Expr =
| { tag: "literal", literal: Literal }
| { tag: "var_use", name: VariableName }
// | { tag: "cell_ref", name: CellName }
| { tag: "call", name: FunctionName, args: Expr[] }
| { tag: "let", bindings: ExprBinding[], body: Expr }
| { tag: "tag", tag_name: Tag }
| { tag: "tagged", tag_name: Tag, expr: Expr }
| { tag: "tuple", exprs: Expr[] }
| { tag: "record", fields: FieldAssignment[] }
| { tag: "match", arg: Expr, branches: MatchBranch[] }
| { tag: "lambda", parameters: ProductPattern[], body: Expr }
| { tag: "apply", callee: Expr, args: Expr[] }
export type Literal =
| { tag: "number", value: number }
| { tag: "string", value: string }
export type ExprBinding = {
pattern: ProductPattern,
expr: Expr,
}
export type MatchBranch = {
pattern: Pattern,
body: Expr,
}
export type FieldAssignment = { name: FieldName, expr: Expr };
// === Pattern ===
export type ProductPattern =
| { tag: "any", name: VariableName }
| { tag: "tuple", patterns: ProductPattern[] }
| { tag: "record", fields: FieldPattern[] }
export type FieldPattern = { fieldName: FieldName, pattern: ProductPattern };
export type Pattern =
| ProductPattern
| { tag: "tag", tag_name: Tag }
| { tag: "tagged", tag_name: Tag, pattern: Pattern }
// === Constructors ===
export namespace Expr {
const literal = (literal: Literal): Expr => ({ tag: "literal", literal });
export const number = (value: number): Expr => literal({ tag: "number", value });
export const string = (value: string): Expr => literal({ tag: "string", value });
export const call = (name: FunctionName, args: Expr[]): Expr => ({ tag: "call", name, args, });
export const tag = (tag_name: Tag): Expr => ({ tag: "tag", tag_name, });
export const tagged = (tag_name: Tag, expr: Expr): Expr => ({ tag: "tagged", tag_name, expr, });
export const tuple = (exprs: Expr[]): Expr => ({ tag: "tuple", exprs });
export const record = (fields: FieldAssignment[]): Expr => ({ tag: "record", fields });
export const match = (arg: Expr, branches: MatchBranch[]): Expr => ({ tag: "match", arg, branches, });
export const var_use = (name: VariableName): Expr => ({ tag: "var_use", name, });
export const let_ = (bindings: ExprBinding[], body: Expr): Expr => ({ tag: "let", bindings, body, });
export const apply = (callee: Expr, args: Expr[]): Expr => ({ tag: "apply", callee, args, });
export const lambda = (parameters: ProductPattern[], body: Expr): Expr => ({ tag: "lambda", parameters, body, });
export const matchBranch = (pattern: Pattern, expr: Expr): MatchBranch => ({ pattern, body: expr });
export const exprBinding = (pattern: ProductPattern, expr: Expr): ExprBinding => ({ pattern, expr });
export const fieldAssignment = (name: FieldName, expr: Expr): FieldAssignment => ({ name, expr });
}
export namespace ProductPattern {
export const any = (name: VariableName): ProductPattern => ({ tag: "any", name });
export const tuple = (patterns: ProductPattern[]): ProductPattern => ({ tag: "tuple", patterns });
export const record = (fields: FieldPattern[]): ProductPattern => ({ tag: "record", fields });
export const fieldPattern = (fieldName: FieldName, pattern: ProductPattern): FieldPattern => ({ fieldName, pattern });
}
export namespace Pattern {
export const tag = (tag_name: Tag): Pattern => ({ tag: "tag", tag_name });
export const tagged = (tag_name: Tag, pattern: Pattern): Pattern => ({ tag: "tagged", tag_name, pattern });
}

View file

@ -1,8 +1,8 @@
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern } from '../value';
import { Cursor } from './cursor';
import { ExprScanError, exprStart, ExprStartToken, IdentifierKind, identifierScanner, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, skipWhitespaceAndComments } from './scanner';
import { char, CodePoint, SourceText, Span } from './source_text';
import { Result } from '../result';
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern } from '../expr';
// CONVENTION: Every parser is responsible to consume whitespace/comments at the end.
// Every parser is not responsible for cleaning up whitespace/comments at the start - only the final `parse` that's exposed to the public.
@ -465,3 +465,4 @@ export function parse(source: SourceText): Result<Expr, ParseError> {
return Result.error(e as ParseError);
}
}

View file

@ -1,4 +1,3 @@
import { CARRIAGE_RETURN, char, NEW_LINE } from './source_text';
import type { Span, CodePoint } from './source_text';
import { isDigit, isWhitespace, scanNumber, scanString } from './cursor';

137
src/lang/program.ts Normal file
View file

@ -0,0 +1,137 @@
import { ThrownRuntimeError } from "./eval/error";
import { Value } from "./eval/value";
import { Expr, FunctionName, ProductPattern } from "./expr";
export type Timestamp = number;
export type Program = {
function_definitions: Map<FunctionName, FunctionDefinition>,
function_definition_order: FunctionName[],
// TODO: Perhaps include the story and the environment?
// story should be a list of currently viewed bindings
// environment should be like the store... maybe call it store! It should map names to values and perhaps expressions that generated the value...
// like a reactive cell. This is the analogue of the tiddler.
// store: Map<CellName, Cell>
};
// type Cell = {
// name: CellName,
// expression: Expr,
// cached_value?: Value,
// status: CellStatus
// // TODO: Dependencies? Not sure about this yet...
// // Operational Semantics of Cells is gonna be thought up much later.
// // dependencies?: Set<CellName>,
// }
// type CellStatus =
// | "clean"
// | "dirty"
// | "error"
export type FunctionDefinition =
| { tag: "user", def: UserFunctionDefinition }
| { tag: "primitive", def: PrimitiveFunctionDefinition }
export type UserFunctionDefinition = {
// Raw user input (authoritative)
name: FunctionName,
raw_parameters: string;
raw_body: string;
// parsed
parameters: ProductPattern[],
body: Expr,
// metadata
created_at: Timestamp;
last_modified_at: Timestamp;
}
export type PrimitiveFunctionDefinition = {
name: FunctionName,
implementation: (args: Value[]) => Value,
}
export namespace Program {
type Error =
| { tag: "DuplicateFunctionName", name: FunctionName }
| { tag: "FunctionNotFound", name: FunctionName };
type Result<T> =
| { tag: "ok", value: T }
| { tag: "error", error: Error };
// | { tag: "ParseError", message: string } // TODO
export namespace Result {
export function ok<T>(value: T): Result<T> { return { tag: "ok", value } }
export function error<T>(error: Error): Result<T> { return { tag: "error", error } }
}
export function makeEmpty(): Program {
return {
function_definitions: new Map(),
function_definition_order: [],
};
}
// TODO: Primitive functions like +, -, *, div, <, <=, ==, mod
// TODO: function to create initial program (with the above primitive functions otherwise empty)
// may throw `ThrownRuntimeError`
export function lookup_function(program: Program, name: FunctionName): FunctionDefinition {
const fn = program.function_definitions.get(name);
if (!fn) {
throw ThrownRuntimeError.error({
tag: "FunctionLookupFailure",
name,
});
}
return fn;
}
export type CreateFunction = {
raw_name: string,
raw_parameters: string,
raw_body: string,
}
export type UpdateFunction = {
raw_name?: string,
raw_parameters?: string,
raw_body?: string,
}
export function add_user_function(program: Program, description: CreateFunction): Result<void> {
// TODO:
// - parsing/validation
// - raw_name (check if function already exists)
// - raw_parameters
// - raw_body
// - compute timestamp for now
return (0 as any);
}
// TODO: What about result type? Should it on deletion return the original data of the function, and if there's a failure, how detailed should it be?
export function delete_user_function(program: Program, name: FunctionName): Result<void> {
// TODO:
// - see if the user function exists
// - if it does, delete it
// - if it doesn't ???
return (0 as any);
}
export function update_user_function(program: Program, name: FunctionName): Result<void> {
// TODO:
return (0 as any);
}
export function get_user_function(program: Program, name: FunctionName): Result<UserFunctionDefinition> {
// TODO:
return (0 as any);
}
}

View file

@ -1,563 +0,0 @@
// === Identifiers ===
export type VariableName = string
export type FunctionName = string
// type CellName = string
export type Tag = string
export type FieldName = string
// === Program ===
export type Timestamp = number;
export type Program = {
function_definitions: Map<FunctionName, FunctionDefinition>,
function_definition_order: FunctionName[],
// TODO: Perhaps include the story and the environment?
// story should be a list of currently viewed bindings
// environment should be like the store... maybe call it store! It should map names to values and perhaps expressions that generated the value...
// like a reactive cell. This is the analogue of the tiddler.
// store: Map<CellName, Cell>
};
// type Cell = {
// name: CellName,
// expression: Expr,
// cached_value?: Value,
// status: CellStatus
// // TODO: Dependencies? Not sure about this yet...
// // Operational Semantics of Cells is gonna be thought up much later.
// // dependencies?: Set<CellName>,
// }
// type CellStatus =
// | "clean"
// | "dirty"
// | "error"
export type FunctionDefinition =
| { tag: "user", def: UserFunctionDefinition }
| { tag: "primitive", def: PrimitiveFunctionDefinition }
export type UserFunctionDefinition = {
// Raw user input (authoritative)
name: FunctionName,
raw_parameters: string;
raw_body: string;
// parsed
parameters: ProductPattern[],
body: Expr,
// metadata
created_at: Timestamp;
last_modified_at: Timestamp;
}
export type PrimitiveFunctionDefinition = {
name: FunctionName,
implementation: (args: Value[]) => Value,
}
export namespace Program {
type Error =
| { tag: "DuplicateFunctionName", name: FunctionName }
| { tag: "FunctionNotFound", name: FunctionName };
type Result<T> =
| { tag: "ok", value: T }
| { tag: "error", error: Error };
// | { tag: "ParseError", message: string } // TODO
export namespace Result {
export function ok<T>(value: T): Result<T> { return { tag: "ok", value } }
export function error<T>(error: Error): Result<T> { return { tag: "error", error } }
}
export function makeEmpty(): Program {
return {
function_definitions: new Map(),
function_definition_order: [],
};
}
// TODO: Primitive functions like +, -, *, div, <, <=, ==, mod
// TODO: function to create initial program (with the above primitive functions otherwise empty)
// may throw `ThrownRuntimeError`
export function lookup_function(program: Program, name: FunctionName): FunctionDefinition {
const fn = program.function_definitions.get(name);
if (!fn) {
throw ThrownRuntimeError.error({
tag: "FunctionLookupFailure",
name,
});
}
return fn;
}
export type CreateFunction = {
raw_name: string,
raw_parameters: string,
raw_body: string,
}
export type UpdateFunction = {
raw_name?: string,
raw_parameters?: string,
raw_body?: string,
}
export function add_user_function(program: Program, description: CreateFunction): Result<void> {
// TODO:
// - parsing/validation
// - raw_name (check if function already exists)
// - raw_parameters
// - raw_body
// - compute timestamp for now
return (0 as any);
}
// TODO: What about result type? Should it on deletion return the original data of the function, and if there's a failure, how detailed should it be?
export function delete_user_function(program: Program, name: FunctionName): Result<void> {
// TODO:
// - see if the user function exists
// - if it does, delete it
// - if it doesn't ???
return (0 as any);
}
export function update_user_function(program: Program, name: FunctionName): Result<void> {
// TODO:
return (0 as any);
}
export function get_user_function(program: Program, name: FunctionName): Result<UserFunctionDefinition> {
// TODO:
return (0 as any);
}
}
// === Expressions ===
export type Expr =
| { tag: "literal", literal: Literal }
| { tag: "var_use", name: VariableName }
// | { tag: "cell_ref", name: CellName }
| { tag: "call", name: FunctionName, args: Expr[] }
| { tag: "let", bindings: ExprBinding[], body: Expr }
| { tag: "tag", tag_name: Tag }
| { tag: "tagged", tag_name: Tag, expr: Expr }
| { tag: "tuple", exprs: Expr[] }
| { tag: "record", fields: FieldAssignment[] }
| { tag: "match", arg: Expr, branches: MatchBranch[] }
| { tag: "lambda", parameters: ProductPattern[], body: Expr }
| { tag: "apply", callee: Expr, args: Expr[] }
export type Literal =
| { tag: "number", value: number }
| { tag: "string", value: string }
export type ExprBinding = {
pattern: ProductPattern,
expr: Expr,
}
export type MatchBranch = {
pattern: Pattern,
body: Expr,
}
export type FieldAssignment = { name: FieldName, expr: Expr };
export type ProductPattern =
| { tag: "any", name: VariableName }
| { tag: "tuple", patterns: ProductPattern[] }
| { tag: "record", fields: FieldPattern[] }
export type FieldPattern = { fieldName: FieldName, pattern: ProductPattern };
export type Pattern =
| ProductPattern
| { tag: "tag", tag_name: Tag }
| { tag: "tagged", tag_name: Tag, pattern: Pattern }
// === Values ===
export type Value =
| { tag: "string", value: string }
| { tag: "number", value: number }
| { tag: "tag", tag_name: Tag }
| { tag: "tagged", tag_name: Tag, value: Value }
| { tag: "tuple", values: Value[] }
| { tag: "record", fields: Map<FieldName, Value> }
| { tag: "closure", closure: Closure }
export type ValueTag =
| "string"
| "number"
| "tag"
| "tagged"
| "tuple"
| "record"
| "closure"
// Used as a Stack of frames. Basically a linked list.
export type Env =
| { tag: "nil" }
| { tag: "frame", frame: EnvFrame, parent: Env }
export type EnvFrame = Map<VariableName, Value>;
export type Closure = {
env: Env,
parameters: ProductPattern[],
body: Expr,
}
// === Constructors ===
export namespace Expr {
const literal = (literal: Literal): Expr => ({ tag: "literal", literal });
export const number = (value: number): Expr => literal({ tag: "number", value });
export const string = (value: string): Expr => literal({ tag: "string", value });
export const call = (name: FunctionName, args: Expr[]): Expr => ({ tag: "call", name, args, });
export const tag = (tag_name: Tag): Expr => ({ tag: "tag", tag_name, });
export const tagged = (tag_name: Tag, expr: Expr): Expr => ({ tag: "tagged", tag_name, expr, });
export const tuple = (exprs: Expr[]): Expr => ({ tag: "tuple", exprs });
export const record = (fields: FieldAssignment[]): Expr => ({ tag: "record", fields });
export const match = (arg: Expr, branches: MatchBranch[]): Expr => ({ tag: "match", arg, branches, });
export const var_use = (name: VariableName): Expr => ({ tag: "var_use", name, });
export const let_ = (bindings: ExprBinding[], body: Expr): Expr => ({ tag: "let", bindings, body, });
export const apply = (callee: Expr, args: Expr[]): Expr => ({ tag: "apply", callee, args, });
export const lambda = (parameters: ProductPattern[], body: Expr): Expr => ({ tag: "lambda", parameters, body, });
export const matchBranch = (pattern: Pattern, expr: Expr): MatchBranch => ({ pattern, body: expr });
export const exprBinding = (pattern: ProductPattern, expr: Expr): ExprBinding => ({ pattern, expr });
export const fieldAssignment = (name: FieldName, expr: Expr): FieldAssignment => ({ name, expr });
}
export namespace Value {
export const string = (value: string): Value => ({ tag: "string", value });
export const number = (value: number): Value => ({ tag: "number", value });
export const tag = (tag_name: Tag): Value => ({ tag: "tag", tag_name });
export const tagged = (tag_name: Tag, value: Value): Value => ({ tag: "tagged", tag_name, value });
export const tuple = (values: Value[]): Value => ({ tag: "tuple", values });
export const record = (fields: Map<FieldName, Value>): Value => ({ tag: "record", fields });
export const closure = (closure: Closure): Value => ({ tag: "closure", closure });
}
export namespace ProductPattern {
export const any = (name: VariableName): ProductPattern => ({ tag: "any", name });
export const tuple = (patterns: ProductPattern[]): ProductPattern => ({ tag: "tuple", patterns });
export const record = (fields: FieldPattern[]): ProductPattern => ({ tag: "record", fields });
export const fieldPattern = (fieldName: FieldName, pattern: ProductPattern): FieldPattern => ({ fieldName, pattern });
}
export namespace Pattern {
export const tag = (tag_name: Tag): Pattern => ({ tag: "tag", tag_name });
export const tagged = (tag_name: Tag, pattern: Pattern): Pattern => ({ tag: "tagged", tag_name, pattern });
}
// ===Errors===
type RuntimeError =
| { tag: "FunctionLookupFailure", name: FunctionName }
| { tag: "FunctionCallArityMismatch", name: FunctionName, expected: number, actual: number }
| { tag: "ClosureApplicationArityMismatch", closure: Closure, expected: number, actual: number }
| { tag: "VariableLookupFailure", name: VariableName }
// | { tag: "CellLookupFailure", name: CellName }
| { tag: "UnableToFindMatchingPattern", value: Value }
| { tag: "TypeMismatch", expected: ValueTag, received: Value }
| { tag: "DuplicateVariableNamesInPattern", pattern: Pattern, duplicates: VariableName[] }
// | { tag: "DuplicateVariableNamesInProductPattern", pattern: ProductPattern, duplicates: VariableName[] }
type Result<T> =
| { tag: "ok", value: T }
| { tag: "error", error: RuntimeError }
export namespace Result {
export function ok<T>(value: T): Result<T> { return { tag: "ok", value } }
export function error<T>(error: RuntimeError): Result<T> { return { tag: "error", error } }
}
// This is an internal type - use it in all internal evaluation functions.
type ThrownRuntimeError = {
kind: "RuntimeError",
error: RuntimeError
}
namespace ThrownRuntimeError {
// use as follows
// `throw ThrownRuntimeError.error(e)`
export function error(error: RuntimeError): ThrownRuntimeError {
return { kind: "RuntimeError", error };
}
}
// ===Evaluation===
export namespace Env {
export function nil(): Env {
return { tag: "nil" };
}
export function push_frame(env: Env, frame: EnvFrame): Env {
return { tag: "frame", frame, parent: env };
}
// may throw `ThrownRuntimeError`
export function lookup(env: Env, var_name: VariableName): Value {
let cur = env;
while (cur.tag !== "nil") {
if (cur.frame.has(var_name)) {
return cur.frame.get(var_name)!;
}
cur = cur.parent;
}
throw ThrownRuntimeError.error({
tag: "VariableLookupFailure",
name: var_name,
});
}
export function frame_insert_mut(frame: EnvFrame, var_name: VariableName, value: Value) {
frame.set(var_name, value);
}
}
export function eval_start(program: Program, e: Expr): Result<Value> {
try {
return Result.ok(eval_expr(program, Env.nil(), e));
} catch (err) {
if (typeof err === "object" && (err as any).kind === "RuntimeError") {
return Result.error((err as ThrownRuntimeError).error);
} else {
throw err;
}
}
}
// may throw `ThrownRuntimeError`
function eval_expr(program: Program, env: Env, e: Expr): Value {
switch (e.tag) {
case "literal":
switch (e.literal.tag) {
case "number":
return Value.number(e.literal.value);
case "string":
return Value.string(e.literal.value);
}
case "tag":
return Value.tag(e.tag_name);
case "tagged":
return Value.tagged(e.tag_name, eval_expr(program, env, e.expr));
case "tuple":
return Value.tuple(eval_sequence(program, env, e.exprs));
case "record":
const fields = new Map<FieldName, Value>();
for (const field of e.fields) {
const value = eval_expr(program, env, field.expr);
fields.set(field.name, value);
}
return Value.record(fields);
case "lambda":
return Value.closure({
env,
parameters: e.parameters,
body: e.body,
});
case "var_use":
return Env.lookup(env, e.name);
case "call":
const fn = Program.lookup_function(program, e.name);
const fn_args = eval_sequence(program, env, e.args);
return call_function(program, fn, fn_args);
case "apply":
const closure = force_closure(eval_expr(program, env, e.callee));
const closure_args = eval_sequence(program, env, e.args);
return apply_closure(program, closure, closure_args);
case "let":
const new_env = eval_bindings(program, env, e.bindings);
return eval_expr(program, new_env, e.body);
case "match":
const match_val = eval_expr(program, env, e.arg);
for (const branch of e.branches) {
const res = match_pattern(branch.pattern, match_val);
if (res.tag === "match") {
return eval_expr(program, Env.push_frame(env, res.frame), branch.body);
}
}
throw ThrownRuntimeError.error({
tag: "UnableToFindMatchingPattern",
value: match_val,
});
}
}
// may throw `ThrownRuntimeError`
function eval_bindings(program: Program, env: Env, bindings: ExprBinding[]): Env {
// note that `let { x = 123, y = x + 1 ... } is allowed. Ofcourse later bindings can't be referenced by earlier bindings (i.e. no recursion).
let cur_env = env;
for (const { pattern: var_name, expr } of bindings) {
const value = eval_expr(program, cur_env, expr);
const res = match_product_pattern(var_name, value);
if (res.tag === "failure") {
throw ThrownRuntimeError.error({
tag: "UnableToFindMatchingPattern",
value,
});
} else {
cur_env = Env.push_frame(cur_env, res.frame);
}
}
return cur_env;
}
// may throw `ThrownRuntimeError`
function eval_sequence(program: Program, env: Env, args: Expr[]): Value[] {
return args.map(arg => eval_expr(program, env, arg));
}
// may throw `ThrownRuntimeError`
function call_function(program: Program, fn_def: FunctionDefinition, args: Value[]): Value {
switch (fn_def.tag) {
case "user":
return call_user_function(program, fn_def.def, args);
case "primitive":
return fn_def.def.implementation(args);
}
}
// may throw `ThrownRuntimeError`
function call_user_function(program: Program, fn_def: UserFunctionDefinition, args: Value[]): Value {
const frame = bind_arguments_to_parameters(
fn_def.parameters,
args,
(expected, actual) => ({ tag: "FunctionCallArityMismatch", name: fn_def.name, expected, actual })
);
return eval_expr(program, Env.push_frame(Env.nil(), frame), fn_def.body);
}
// may throw `ThrownRuntimeError`
function apply_closure(program: Program, closure: Closure, args: Value[]): Value {
const frame = bind_arguments_to_parameters(
closure.parameters,
args,
(expected, actual) => ({ tag: "ClosureApplicationArityMismatch", closure, expected, actual })
);
return eval_expr(program, Env.push_frame(closure.env, frame), closure.body);
}
// may throw `ThrownRuntimeError`
function force_closure(value: Value): Closure {
if (value.tag !== "closure") {
throw ThrownRuntimeError.error({
tag: "TypeMismatch",
expected: "closure",
received: value,
});
}
return value.closure;
}
// may throw `ThrownRuntimeError`
function bind_arguments_to_parameters(
patterns: ProductPattern[],
values: Value[],
onArityMismatchError: (expected: number, actual: number) => RuntimeError
): EnvFrame {
const expected = patterns.length;
const actual = values.length;
if (expected !== actual) {
throw ThrownRuntimeError.error(onArityMismatchError(expected, actual));
}
const frame: EnvFrame = new Map();
for (let i = 0; i < patterns.length; i++) {
const pattern = patterns[i];
const value = values[i];
const res = match_product_pattern_mut(frame, pattern, value);
if (res.tag === "failure") {
throw ThrownRuntimeError.error({
tag: "UnableToFindMatchingPattern",
value,
});
}
}
return frame;
}
// === Pattern Matching ===
// A pattern match will result either in a succesfull match with a new EnvFrame
type PatternMatchingResult =
| { tag: "match", frame: EnvFrame }
| { tag: "failure", pattern: Pattern, value: Value }
function match_pattern(pattern: Pattern, value: Value): PatternMatchingResult {
const frame = new Map<VariableName, Value>();
return match_pattern_mut(frame, pattern, value);
}
function match_pattern_mut(frame: EnvFrame, pattern: Pattern, value: Value): PatternMatchingResult {
switch (pattern.tag) {
case "tag":
if (value.tag === "tag" && value.tag_name === pattern.tag_name) {
return { tag: "match", frame }
} else {
return { tag: "failure", pattern, value }
}
case "tagged":
if (value.tag === "tagged" && value.tag_name === pattern.tag_name) {
return match_pattern_mut(frame, pattern.pattern, value.value);
} else {
return { tag: "failure", pattern, value };
}
default:
return match_product_pattern_mut(frame, pattern, value);
}
}
function match_product_pattern(pattern: ProductPattern, value: Value): PatternMatchingResult {
const frame = new Map<VariableName, Value>();
return match_product_pattern_mut(frame, pattern, value);
}
function match_product_pattern_mut(frame: EnvFrame, pattern: ProductPattern, value: Value): PatternMatchingResult {
switch (pattern.tag) {
case "any":
frame.set(pattern.name, value);
return { tag: "match", frame };
case "tuple":
if (value.tag !== "tuple" || pattern.patterns.length !== value.values.length) return { tag: "failure", pattern, value };
for (let i = 0; i < pattern.patterns.length; i++) {
const res = match_product_pattern_mut(frame, pattern.patterns[i], value.values[i]);
if (res.tag === "failure") return res;
}
return { tag: "match", frame };
case "record":
if (value.tag !== "record") return { tag: "failure", pattern, value };
for (const { fieldName, pattern: p } of pattern.fields) {
const field_value = value.fields.get(fieldName);
if (field_value === undefined) {
return { tag: "failure", pattern, value };
} else {
const res = match_product_pattern_mut(frame, p, field_value);
if (res.tag === "failure") {
return res;
}
}
}
return { tag: "match", frame };
}
}