// === 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 } } } // 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: { name: FieldName, expr: Expr }[] } | { tag: "match", arg: Expr, branches: { pattern: Pattern, body: Expr }[] } | { 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 = { var: ProductPattern, expr: Expr, } export type ProductPattern = | { tag: "any", name: VariableName } | { tag: "tuple", patterns: ProductPattern[] } | { tag: "record", fields: { field_name: FieldName, pattern: ProductPattern }[] } export type Pattern = | ProductPattern | { tag: "tag", tag_name: Tag } | { tag: "tagged", tag_name: Tag, pattern: Pattern } // === Values === 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 } type ValueTag = | "string" | "number" | "tag" | "tagged" | "tuple" | "record" | "closure" // 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, 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 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 }; } }