diff --git a/src/lang/debug/expr_show.ts b/src/lang/debug/expr_show.ts index 08acf9f..468eba2 100644 --- a/src/lang/debug/expr_show.ts +++ b/src/lang/debug/expr_show.ts @@ -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": diff --git a/src/lang/debug/repl.ts b/src/lang/debug/repl.ts index efd8f6b..513b754 100644 --- a/src/lang/debug/repl.ts +++ b/src/lang/debug/repl.ts @@ -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 = { diff --git a/src/lang/debug/value_show.ts b/src/lang/debug/value_show.ts index a59b859..8bb8e36 100644 --- a/src/lang/debug/value_show.ts +++ b/src/lang/debug/value_show.ts @@ -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 { diff --git a/src/lang/eval/error.ts b/src/lang/eval/error.ts new file mode 100644 index 0000000..bbbc574 --- /dev/null +++ b/src/lang/eval/error.ts @@ -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 = + | { 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. +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 }; + } +} + diff --git a/src/lang/eval/evaluator.ts b/src/lang/eval/evaluator.ts new file mode 100644 index 0000000..9cc109d --- /dev/null +++ b/src/lang/eval/evaluator.ts @@ -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 { + 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(); + 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; +} + diff --git a/src/lang/eval/pattern_match.ts b/src/lang/eval/pattern_match.ts new file mode 100644 index 0000000..9b04af2 --- /dev/null +++ b/src/lang/eval/pattern_match.ts @@ -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(); + 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(); + 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 }; + } +} + diff --git a/src/lang/eval/value.ts b/src/lang/eval/value.ts new file mode 100644 index 0000000..0168dc9 --- /dev/null +++ b/src/lang/eval/value.ts @@ -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 } + | { 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; + +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): 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); + } +} + diff --git a/src/lang/expr.ts b/src/lang/expr.ts new file mode 100644 index 0000000..329796f --- /dev/null +++ b/src/lang/expr.ts @@ -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 }); +} + diff --git a/src/lang/parser/parser.ts b/src/lang/parser/parser.ts index fec1a14..4156b70 100644 --- a/src/lang/parser/parser.ts +++ b/src/lang/parser/parser.ts @@ -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 { return Result.error(e as ParseError); } } + diff --git a/src/lang/parser/scanner.ts b/src/lang/parser/scanner.ts index 757397f..4b9562d 100644 --- a/src/lang/parser/scanner.ts +++ b/src/lang/parser/scanner.ts @@ -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'; diff --git a/src/lang/program.ts b/src/lang/program.ts new file mode 100644 index 0000000..ac5a10d --- /dev/null +++ b/src/lang/program.ts @@ -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, + 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 +}; + +// 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" + +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 = + | { 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 } } + } + + 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 { + // 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); + } + +} + diff --git a/src/lang/value.ts b/src/lang/value.ts deleted file mode 100644 index e1a3e2a..0000000 --- a/src/lang/value.ts +++ /dev/null @@ -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, - 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 -}; - -// 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" - -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 = - | { 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 } } - } - - 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 { - // 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 === - -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 } - | { 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; - -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): 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 = - | { 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 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(); - 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(); - 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 { 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 }; - } -} -