From a478542c2a8086c9b81fb279d305b4ea190d49e7 Mon Sep 17 00:00:00 2001 From: Yura Dupyn <2153100+omedusyo@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:28:42 +0100 Subject: [PATCH] Implement evaluator --- src/value.ts | 484 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 453 insertions(+), 31 deletions(-) diff --git a/src/value.ts b/src/value.ts index 58fc8f3..b7917ee 100644 --- a/src/value.ts +++ b/src/value.ts @@ -2,55 +2,144 @@ // === Identifiers === type VariableName = string type FunctionName = string -type CellName = string +// type CellName = string type Tag = string type FieldName = string +// === Program === +type Timestamp = number; + type Program = { - function_definitions: { name: FunctionName, definition: FunctionDefinition }[], + function_definitions: Map, + 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 + // store: Map }; -type Cell = { - name: CellName, - expression: Expr, - cached_value?: Value, - status: "clean" | "dirty" | "error", - // TODO: Dependencies? Not sure about this yet... - // dependencies?: Set, -} +// 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, +// } -type CellStatus = - | "clean" - | "dirty" - | "error" +// type CellStatus = +// | "clean" +// | "dirty" +// | "error" type FunctionDefinition = - | { tag: "user", def: UserFunctionDefinition } + | { tag: "user", def: UserFunctionDefinition } | { tag: "primitive", def: PrimitiveFunctionDefinition } 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; } type PrimitiveFunctionDefinition = { name: FunctionName, - number_of_parameters: number, implementation: (args: Value[]) => Value, } +export namespace Program { + + type Error = + | { tag: "DuplicateFunctionName", name: FunctionName } + | { tag: "FunctionNotFound", name: FunctionName }; + + type Result = + | { tag: "ok", value: T } + | { tag: "error", error: Error }; + // | { tag: "ParseError", message: string } // TODO + + export namespace Result { + export function ok(value: T): Result { return { tag: "ok", value } } + export function error(error: Error): Result { return { tag: "error", error } } + } + + // 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 { + // 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 { + // 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 { + // TODO: + return (0 as any); + } + + export function get_user_function(program: Program, name: FunctionName): Result { + // TODO: + return (0 as any); + } + +} + // === Expressions === type Expr = | { tag: "literal", literal: Literal } | { tag: "var_use", name: VariableName } - | { tag: "cell_ref", name: CellName } + // | { tag: "cell_ref", name: CellName } | { tag: "call", name: FunctionName, args: Expr[] } | { tag: "let", bindings: ExprBinding[], body: Expr } | { tag: "tag", tag_name: Tag } @@ -70,16 +159,16 @@ type ExprBinding = { expr: Expr, } -type Pattern = - | { tag: "any", name: VariableName } - | { tag: "tuple", patterns: ProductPattern[] } - | { tag: "tag", tag_name: Tag } - | { tag: "tagged", tag_name: Tag, pattern: Pattern } - type ProductPattern = | { tag: "any", name: VariableName } | { tag: "tuple", patterns: ProductPattern[] } - | { tag: "record", fields: { name: FieldName, pattern: ProductPattern }[] } + | { tag: "record", fields: { field_name: FieldName, pattern: ProductPattern }[] } + +type Pattern = + | ProductPattern + | { tag: "tag", tag_name: Tag } + | { tag: "tagged", tag_name: Tag, pattern: Pattern } + // === Values === @@ -89,15 +178,24 @@ type Value = | { tag: "tag", tag_name: Tag } | { tag: "tagged", tag_name: Tag, value: Value } | { tag: "tuple", values: Value[] } - | { tag: "record", fields: { name: FieldName, value: Value }[] } + | { tag: "record", fields: Map } | { tag: "closure", closure: Closure } -type EnvBinding = { - var: VariableName, - value: Value, -} +type ValueTag = + | "string" + | "number" + | "tag" + | "tagged" + | "tuple" + | "record" + | "closure" -type Env = EnvBinding[]; +// Used as a Stack of frames. Basically a linked list. +type Env = + | { tag: "nil" } + | { tag: "frame", frame: EnvFrame, parent: Env } + +type EnvFrame = Map; type Closure = { env: Env, @@ -105,3 +203,327 @@ type Closure = { 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 match = (arg: Expr, branches: { pattern: Pattern; body: Expr }[]): 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 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): Value => ({ tag: "record", fields }); + export const closure = (closure: Closure): Value => ({ tag: "closure", closure }); +} + +// ===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 = + | { tag: "ok", value: T } + | { tag: "error", error: RuntimeError } + +export namespace Result { + export function ok(value: T): Result { return { tag: "ok", value } } + export function error(error: RuntimeError): Result { 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 { + 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.error as RuntimeError); + } 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(); + 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 { var: 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(); + 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(); + 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 { field_name, pattern: p } of pattern.fields) { + const field_value = value.fields.get(field_name); + 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 }; + } +} +