///|
/// Expression evaluation: Figures 4.5 to 4.10.
///
/// Every rule that needs a condition -- `if`, `and`, `or`, a conditional
/// expression, `assert` -- requires an actual `True` or `False`. Python's
/// truthiness is not PurePy's, so `if 5:` has no rule and the run is
/// undefined. That is one of the conformance suite's dynamically excluded
/// tests, and it is the reason `Stuck` is threaded everywhere.
///
/// Where an abort happened is recorded by `sited`, at each of the six places
/// an expression can produce one. Not by wrapping this function: it recurses
/// and it is `async`, so a step after the recursive call is a continuation
/// allocated per expression rather than a tail call, and that measured 21%
/// on a program that does nothing but call.
pub async fn Interp::eval_expr(
self : Interp,
e : @ast.Expr,
env : Env,
) -> Outcome noraise {
self.machine(Ev(e, env))
}
///|
/// The values of a parameter list's default arguments (#56).
///
/// Only a literal can be a default -- the sieve refuses anything else under
/// the profile that opens them, and the profile is what lets one through at
/// all -- and a literal has no effect and reads no name. So evaluating it here
/// rather than once when the `def` ran is not observable, which is the whole
/// reason the restriction is worth having: Python evaluates a default once and
/// this evaluator has nowhere to put a once, so the feature is drawn where the
/// difference cannot be seen.
///
/// Anything unexpected answers with no defaults, which makes the call an arity
/// error rather than a wrong answer.
fn literal_defaults(args : @ast.Arguments) -> Array[Value] {
let out : Array[Value] = []
for d in args.defaults {
match d {
Constant(value=Int(n), ..) => out.push(Int(n))
Constant(value=Float(x), ..) => out.push(Float(x))
Constant(value=Str(t), ..) => out.push(Str(t))
Constant(value=Bool(b), ..) => out.push(Bool(b))
Constant(value=None, ..) => out.push(Value::None)
_ => return []
}
}
out
}
///|
/// The arguments a call really binds: what it passed, then defaults for the
/// parameters it left out.
///
/// Defaults cover the LAST parameters, as Python's do. `None` is an arity the
/// function has no shape for, which the caller turns into the `TypeError`
/// Python raises.
fn fill_defaults(
arity : Int,
defaults : Array[Value],
actual : Array[Value],
) -> Array[Value]? {
if actual.length() == arity {
return Some(actual)
}
let required = arity - defaults.length()
if actual.length() < required || actual.length() > arity {
return None
}
let out = actual.copy()
for i in (actual.length() - required).. Outcome {
match v {
Mod(q, members) =>
match members.get(attr) {
Some(w) => Val(w)
None => Stuck("module '" + q + "' has no member '" + attr + "'")
}
Obj(_, fields_env) =>
match fields_env.get(attr) {
Some(w) => Val(w)
None => self.sited(at, Aborts(AttributeError))
}
_ => Stuck("an attribute of " + v.kind_name())
}
}
///|
/// A method call on a builtin value (`Feature::BuiltinMethods`).
///
/// The tables live in `lib/value` because they are rules about values; the
/// dispatch is here because only the evaluator knows a call happened. A name
/// no table has is the message an unknown attribute already produces, so a
/// typo under this profile reads the way it reads without one.
fn call_method(recv : Value, name : String, args : Array[Value]) -> Outcome {
let answer = match recv {
Str(s) => @value.str_method(s, name, args)
List(xs) | Tuple(xs) => @value.seq_method(xs, name, args)
Dict(entries) => @value.dict_method(entries, name, args)
_ => None
}
match answer {
Some(o) => o
None => Stuck("an attribute of " + recv.kind_name())
}
}
// ---------------------------------------------------------------------------
// Calls
///|
/// eval-call-lambda, eval-call-def, eval-call-prim and their arity rules.
///
/// An entry into the machine, so that an embedder calling a guest function
/// gets the same unbounded recursion a guest call inside the machine gets.
/// The depth this call takes is counted there, by the call boundary it pushes.
pub async fn Interp::apply(
self : Interp,
callee : Value,
actual : Array[Value],
) -> Outcome noraise {
self.machine_apply(callee, actual)
}
///|
fn bind(env : Env, params : Array[String], actual : Array[Value]) -> Env {
let entries : Array[(String, Value)] = []
for i, p in params {
entries.push((p, actual[i]))
}
@value.override_env(env, @value.env_of(entries))
}
///|
/// `{f⃗: v⃗}`: every definition of a region, as closures over the same
/// environment.
///
/// `in_module` is the module the region was written in, which the closures
/// carry so that an abort inside one is reported against the right file.
pub fn region_bindings(
env : Env,
region : Array[@ast.Stmt],
in_module : String,
) -> Env {
let entries : Array[(String, Value)] = []
for i, d in region {
match d {
FunctionDef(name~, ..) =>
entries.push((name, Value::Def({ env, region, index: i, in_module, })))
_ => ()
}
}
@value.override_env(env, @value.env_of(entries))
}