scrowl/src/lang/eval/value.ts

133 lines
4 KiB
TypeScript

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 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, { tag: "number" }>).value;
case "string":
return v1.value === (v2 as Extract<Value, { tag: "string" }>).value;
case "tag":
return v1.tag_name === (v2 as Extract<Value, { tag: "tag" }>).tag_name;
case "tagged": {
const other = v2 as Extract<Value, { tag: "tagged" }>;
return v1.tag_name === other.tag_name && equals(v1.value, other.value);
}
case "tuple": {
const other = v2 as Extract<Value, { tag: "tuple" }>;
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<Value, { tag: "record" }>;
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
});
}