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 nil_frame(): EnvFrame { return new Map(); } export function frame_insert_mut(frame: EnvFrame, var_name: VariableName, value: Value) { frame.set(var_name, value); } } export function equals(v1: Value, v2: Value): boolean { if (v1 === v2) return true; // Reference equality optimization if (v1.tag !== v2.tag) return false; switch (v1.tag) { case "number": return v1.value === (v2 as Extract).value; case "string": return v1.value === (v2 as Extract).value; case "tag": return v1.tag_name === (v2 as Extract).tag_name; case "tagged": { const other = v2 as Extract; return v1.tag_name === other.tag_name && equals(v1.value, other.value); } case "tuple": { const other = v2 as Extract; if (v1.values.length !== other.values.length) return false; for (let i = 0; i < v1.values.length; i++) { if (!equals(v1.values[i], other.values[i])) return false; } return true; } case "record": { const other = v2 as Extract; if (v1.fields.size !== other.fields.size) return false; for (const [key, val1] of v1.fields) { const val2 = other.fields.get(key); if (val2 === undefined || !equals(val1, val2)) return false; } return true; } case "closure": // Philosophical/Mathematical barrier: throw error as requested throw ThrownRuntimeError.error({ tag: "ClosureEqualityComparison", value0: v1.closure, value1: v2, }); } } // Canonical bools are: // - True is `#T` // - False is `#F` // TODO: This is not a great design. Probably introducing completely new values would be better. export function forceBool(value: Value): boolean { if (value.tag === "tag") { if (value.tag_name === "T") return true; if (value.tag_name === "F") return false; } throw ThrownRuntimeError.error({ tag: "NotABoolean", value }); }