///|
/// Runtime values, environments and evaluation results: Figure 4.1.
///
/// Two things about this type are the whole shape of the language:
///
/// * **Nothing is mutable.** A list, a tuple, a dictionary and an object are
/// built once and never changed, so an environment can be shared by every
/// closure that captures it and nothing has to be copied defensively.
/// * **`Stuck` is a result, not a crash.** The semantics leaves some
/// operations undefined, and Chapter 1 allows an implementation either to
/// abort or to produce Python's answer. This one aborts, with the
/// operation named, so that the conformance suite's dynamically excluded
/// bucket is CHECKED rather than merely tolerated.
pub(all) enum Value {
None
Bool(Bool)
Int(BigInt)
Float(Double)
Str(String)
List(Array[Value])
Tuple(Array[Value])
/// Insertion order, later entries replacing earlier ones under the same key.
/// Keys are strings: a dictionary with any other key is undefined.
Dict(Array[(String, Value)])
/// `LamΓ(ρ, x⃗, e)`.
Lam(LamClosure)
/// `DefΓ(ρ, d⃗, i)`: the environment, the whole mutual region, and which of
/// its definitions this value is. Calling one rebinds the WHOLE region, and
/// that is what makes `even` and `odd` see each other.
Def(DefClosure)
/// `ObjΓ(C, ρ)`: the class entry, and every field, inherited first.
Obj(@context.ClassEntry, Env)
/// `Mod(q)`: a module named and not loaded.
ModStub(String)
/// `Mod(q, ρ)`.
Mod(String, Env)
/// `ClsΓ(q.c, x⃗, c')`.
Class(@context.ClassEntry)
/// `Prim(f)`.
Prim(Primitive)
} derive(Debug)
///|
/// `in_module` is not part of `LamΓ`: the spec's closures carry an
/// environment and a body, and nothing about where they were written. It is
/// here because a position without a file is a lie the moment a program has
/// two modules -- a `helper.py` function that aborts on its line 4 would be
/// read as line 4 of `__main__`. The evaluator's import stack cannot supply
/// it, because during a call it names the module that IMPORTED the callee,
/// not the one that defined it. Only the closure knows.
pub(all) struct LamClosure {
env : Env
params : Array[String]
body : @ast.Expr
/// The module whose source `body` was read from.
in_module : String
} derive(Debug)
///|
pub(all) struct DefClosure {
env : Env
region : Array[@ast.Stmt]
index : Int
/// The module whose source `region` was read from.
in_module : String
} derive(Debug)
///|
/// An environment: `ρ`. Immutable, because a closure captures one by value.
pub type Env = @hashmap.HashMap[String, Value]
///|
pub fn empty_env() -> Env {
@hashmap.HashMap::new()
}
///|
pub fn env_of(entries : Array[(String, Value)]) -> Env {
@hashmap.HashMap(entries[:])
}
///|
/// `ρ ⊗ ρ'`: the right-hand side wins outright.
pub fn override_env(a : Env, b : Env) -> Env {
let mut out = a
b.each(fn(k, v) { out = out.add(k, v) })
out
}
///|
/// `ρ ⊲ ρ'`: extension, which differs from override only on modules -- a
/// loaded module is preferred to a stub of the same module, and two loadings
/// of one module merge member by member.
pub fn extend_env(a : Env, b : Env) -> Env {
let mut out = a
b.each(fn(k, v) {
out = out.add(
k,
match a.get(k) {
Some(old) => extend_value(old, v)
None => v
},
)
})
out
}
///|
pub fn extend_value(a : Value, b : Value) -> Value {
match (a, b) {
(Mod(qa, ma), Mod(qb, mb)) if qa == qb => Mod(qa, extend_env(ma, mb))
(Mod(qa, _), ModStub(qb)) if qa == qb => a
_ => b
}
}
///|
/// The predefined functions of Figure 2.7.
///
/// `Opaque` is for the three members that are values only so that importing
/// them binds something: `typing.Any`, `typing.Callable` and
/// `dataclasses.dataclass` are never called.
pub(all) enum Primitive {
Print
Len
Range
Exit
Sqrt
Exp
Log
Sin
Cos
Tan
MathFloor
MathCeil
Opaque(String)
/// A function the HOST supplies, named rather than carried.
///
/// The name is data, not a closure, so a `Value` stays comparable,
/// printable and free of the host's types; the host dispatches on it when
/// the guest calls. That is what lets a `Value` cross the boundary at all.
Foreign(String)
} derive(Eq, Debug)
///|
/// How a run ends. Every kind but `SystemExit` is named after the exception
/// the same program raises under Python.
pub(all) enum Termination {
TypeError
IndexError
KeyError
ZeroDivisionError
AttributeError
AssertionError(String?)
SystemExit(BigInt)
} derive(Eq, Debug)
///|
/// The name Python prints for a termination, and the message after it.
pub fn Termination::name(self : Termination) -> String {
match self {
TypeError => "TypeError"
IndexError => "IndexError"
KeyError => "KeyError"
ZeroDivisionError => "ZeroDivisionError"
AttributeError => "AttributeError"
AssertionError(_) => "AssertionError"
SystemExit(_) => "SystemExit"
}
}
///|
/// `AssertionError: message` when there is one, the bare name otherwise --
/// the last line of the traceback Python prints.
pub fn Termination::to_text(self : Termination) -> String {
match self {
AssertionError(Some(m)) => "AssertionError: " + m
SystemExit(n) => "SystemExit: " + n.to_string()
other => other.name()
}
}
///|
/// The outcome of evaluating an expression: a value, an abort, or an
/// operation the semantics leaves undefined.
pub(all) enum Outcome {
Val(Value)
Aborts(Termination)
Stuck(String)
} derive(Debug)
///|
/// The result of evaluating a statement.
pub(all) enum StmtResult {
Assigns(Env)
Returns(Value)
ResultAborts(Termination)
ResultStuck(String)
} derive(Debug)
///|
/// Whether a pattern matched, and what it bound.
pub(all) enum MatchResult {
Match(Env)
NoMatch
MatchStuck(String)
} derive(Debug)
///|
/// `outcome(r)`: a statement's result read as the outcome of calling it.
pub fn StmtResult::outcome(self : StmtResult) -> Outcome {
match self {
Returns(v) => Val(v)
Assigns(_) => Val(None)
ResultAborts(k) => Aborts(k)
ResultStuck(op) => Stuck(op)
}
}
///|
/// Sequential composition of results: `assigns ρ ⊗ r`.
pub fn StmtResult::then(self : StmtResult, other : StmtResult) -> StmtResult {
match self {
Assigns(a) =>
match other {
Assigns(b) => Assigns(override_env(a, b))
_ => other
}
_ => self
}
}
///|
/// `m ⊗ m'`: a match result composes by union of bindings, and no-match is
/// absorbing.
pub fn MatchResult::then(
self : MatchResult,
other : MatchResult,
) -> MatchResult {
match (self, other) {
(MatchStuck(op), _) => MatchStuck(op)
(_, MatchStuck(op)) => MatchStuck(op)
(NoMatch, _) | (_, NoMatch) => NoMatch
(Match(a), Match(b)) => Match(override_env(a, b))
}
}
///|
/// What kind of value this is, for a message naming an undefined operation.
pub fn Value::kind_name(self : Value) -> String {
match self {
None => "None"
Bool(_) => "bool"
Int(_) => "int"
Float(_) => "float"
Str(_) => "str"
List(_) => "list"
Tuple(_) => "tuple"
Dict(_) => "dict"
Lam(_) | Def(_) => "function"
Obj(c, _) => c.short_name()
ModStub(_) | Mod(_, _) => "module"
Class(_) => "class"
Prim(_) => "builtin"
}
}
///|
/// A function the host supplies, as a value the guest can call.
///
/// The host puts one of these in a module (`@eval.HostModule`) and answers
/// calls to it by name.
pub fn host_fn(name : String) -> Value {
Prim(Foreign(name))
}