///|
/// 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 {
match e {
// eval-var
Name(id~, ..) =>
match env.get(id) {
Some(v) => Val(v)
None => Stuck("the unbound name '" + id + "'")
}
// eval-literal
Constant(value~, ..) =>
match value {
Int(n) => Val(Int(n))
Float(d) => Val(Float(d))
Str(s) => Val(Str(s))
Bool(b) => Val(Bool(b))
None => Val(None)
_ => Stuck("a literal the sieve should have rejected")
}
// eval-lambda
Lambda(args~, body~, ..) =>
Val(
Lam({
env,
params: args.args.map(fn(p) { p.arg }),
body,
in_module: self.in_module,
}),
)
// eval-unop
UnaryOp(op~, operand~, ..) =>
match self.eval_expr(operand, env) {
Val(v) => self.sited(e, @value.unop(op, v))
other => other
}
// eval-binop
BinOp(left~, op~, right~, ..) =>
match self.eval_expr(left, env) {
Val(a) =>
match self.eval_expr(right, env) {
Val(b) => self.sited(e, @value.binop(op, a, b))
other => other
}
other => other
}
// eval-and-*, eval-or-*: the second operand is evaluated only when the
// first leaves the answer open.
BoolOp(op~, values~, ..) => {
let mut result = Outcome::Val(Value::None)
for i, sub in values {
let o = self.eval_expr(sub, env)
match o {
Val(Bool(b)) => {
let decides = if op is And { !b } else { b }
if decides {
return Val(Bool(b))
}
result = o
}
Val(v) =>
return Stuck("'" + op.symbol() + "' applied to " + v.kind_name())
other => return other
}
ignore(i)
}
result
}
Compare(left~, ops~, comparators~, ..) =>
match self.eval_expr(left, env) {
Val(a) =>
match self.eval_expr(comparators[0], env) {
Val(b) => self.sited(e, @value.compare(ops[0], a, b))
other => other
}
other => other
}
// eval-cond-true, eval-cond-false
IfExp(cond~, body~, or_else~, ..) =>
match self.eval_expr(cond, env) {
Val(Bool(true)) => self.eval_expr(body, env)
Val(Bool(false)) => self.eval_expr(or_else, env)
Val(v) => Stuck("a conditional expression on " + v.kind_name())
other => other
}
// eval-list, eval-tuple
List(elts~, ..) =>
match self.eval_exprs(elts, env) {
Ok(vs) => Val(List(vs))
Err(o) => o
}
Tuple(elts~, ..) =>
match self.eval_exprs(elts, env) {
Ok(vs) => Val(Tuple(vs))
Err(o) => o
}
// eval-dict
Dict(keys~, values~, ..) => {
let pairs : Array[(String, Value)] = []
for i, k in keys {
let key = match k {
None => return Stuck("dictionary unpacking")
Some(x) =>
match self.eval_expr(x, env) {
Val(Str(s)) => s
Val(v) => return Stuck("a dict key of type " + v.kind_name())
other => return other
}
}
match self.eval_expr(values[i], env) {
Val(v) => pairs.push((key, v))
other => return other
}
}
Val(Dict(@value.entries([], pairs)))
}
// eval-list-comp
ListComp(elt~, generators~, ..) =>
match self.eval_quals(generators[:], env) {
Ok(envs) => {
let out : Array[Value] = []
for e2 in envs {
match self.eval_expr(elt, e2) {
Val(v) => out.push(v)
other => return other
}
}
Val(List(out))
}
Err(o) => o
}
// eval-dict-comp
DictComp(key~, value~, generators~, ..) =>
match self.eval_quals(generators[:], env) {
Ok(envs) => {
let pairs : Array[(String, Value)] = []
for e2 in envs {
let k = match self.eval_expr(key, e2) {
Val(Str(s)) => s
Val(v) => return Stuck("a dict key of type " + v.kind_name())
other => return other
}
match self.eval_expr(value, e2) {
Val(v) => pairs.push((k, v))
other => return other
}
}
Val(Dict(@value.entries([], pairs)))
}
Err(o) => o
}
// eval-attr-module, eval-attr-object, eval-attr-missing
Attribute(value~, attr~, ..) =>
match self.eval_expr(value, env) {
Val(Mod(q, members)) =>
match members.get(attr) {
Some(v) => Val(v)
None => Stuck("module '" + q + "' has no member '" + attr + "'")
}
Val(Obj(_, fields_env)) =>
match fields_env.get(attr) {
Some(v) => Val(v)
None => self.sited(e, Aborts(AttributeError))
}
// The attributes of a built-in value are undefined: `xs.append` has
// no rule, which is what makes list mutation unreachable.
Val(v) => Stuck("an attribute of " + v.kind_name())
other => other
}
// eval-subscript
Subscript(value~, slice~, ..) =>
match self.eval_expr(value, env) {
Val(v) =>
match self.eval_expr(slice, env) {
Val(k) => self.sited(e, @value.getitem(v, k))
other => other
}
other => other
}
Call(func~, args~, keywords~, ..) =>
self.eval_call(func, args, keywords, env, e)
_ => Stuck("expression " + e.kind_name())
}
}
///|
/// A sequence of expressions, left to right, stopping at the first that does
/// not yield a value (Figure 4.7).
pub async fn Interp::eval_exprs(
self : Interp,
es : Array[@ast.Expr],
env : Env,
) -> Result[Array[Value], Outcome] noraise {
let out : Array[Value] = []
for e in es {
match self.eval_expr(e, env) {
Val(v) => out.push(v)
other => return Err(other)
}
}
Ok(out)
}
///|
/// Comprehension qualifiers (Figure 4.10): a list of environments, one per
/// binding the generators produce.
///
/// The enclosing environment is not extended by the result: a comprehension's
/// variables are local to it.
pub async fn Interp::eval_quals(
self : Interp,
generators : ArrayView[@ast.Comprehension],
env : Env,
) -> Result[Array[Env], Outcome] noraise {
if generators.is_empty() {
return Ok([env])
}
let g = generators[0]
let name = match g.target {
Name(id~, ..) => id
_ => return Err(Stuck("a comprehension target that is not a name"))
}
let source = match self.eval_expr(g.iter, env) {
Val(v) => v
other => return Err(other)
}
let items = match @value.iter(source) {
Some(xs) => xs
// eval-qual-generator-nonseq
None => {
self.record(g.iter.span())
return Err(Aborts(TypeError))
}
}
let out : Array[Env] = []
for u in items {
let bound = @value.override_env(env, @value.env_of([(name, u)]))
// The conditions of THIS generator are checked before the ones after it.
let mut keep = true
for cond in g.ifs {
match self.eval_expr(cond, bound) {
Val(Bool(true)) => ()
Val(Bool(false)) => {
keep = false
break
}
Val(v) => return Err(Stuck("a comprehension guard on " + v.kind_name()))
other => return Err(other)
}
}
if !keep {
continue
}
match self.eval_quals(generators[1:], bound) {
Ok(envs) =>
for e2 in envs {
out.push(e2)
}
Err(o) => return Err(o)
}
}
Ok(out)
}
// ---------------------------------------------------------------------------
// Calls
///|
/// A call: a constructor, a lambda, a function from a mutual region, or a
/// primitive. Anything else aborts with `TypeError`.
async fn Interp::eval_call(
self : Interp,
func : @ast.Expr,
args : Array[@ast.Expr],
keywords : Array[@ast.Keyword],
env : Env,
/// The call expression itself, for the aborts with no nearer span: a
/// constructor of the wrong shape, an arity mismatch, a callee that is not
/// a function, and whatever a host answers. Passed as the node rather than
/// its span so that a call which succeeds pays for no span at all.
call : @ast.Expr,
) -> Outcome noraise {
// eval-constr: the arguments are evaluated first, and the class is looked
// up rather than evaluated.
match self.resolve_class(func, env) {
Some(entry) => {
let positional = match self.eval_exprs(args, env) {
Ok(vs) => vs
Err(o) => return o
}
let kwd_names : Array[String] = []
let kwd_exprs : Array[@ast.Expr] = []
for k in keywords {
match k.arg {
Some(a) => {
kwd_names.push(a)
kwd_exprs.push(k.value)
}
None => return Stuck("`**` in a constructor call")
}
}
let kwd_values = match self.eval_exprs(kwd_exprs, env) {
Ok(vs) => vs
Err(o) => return o
}
match entry.field_map(positional, kwd_names, kwd_values) {
Some(fields) => Val(Obj(entry, @value.env_of(fields)))
None => {
self.record(call.span())
Aborts(TypeError)
}
}
}
None => {
let callee = match self.eval_expr(func, env) {
Val(v) => v
other => return other
}
// A keyword argument on a call that is not a constructor: the grammar
// has no rule for one, and the checker does not look at it either.
if !keywords.is_empty() {
return Stuck("a keyword argument on a call that is not a constructor")
}
let actual = match self.eval_exprs(args, env) {
Ok(vs) => vs
Err(o) => return o
}
let o = self.apply(callee, actual)
if o is Aborts(_) {
self.record(call.span())
}
o
}
}
}
///|
/// eval-call-lambda, eval-call-def, eval-call-prim and their arity rules.
pub async fn Interp::apply(
self : Interp,
callee : Value,
actual : Array[Value],
) -> Outcome noraise {
self.depth += 1
if self.depth > self.max_depth {
self.depth -= 1
return Stuck("a call stack deeper than \{self.max_depth}")
}
let result = self.apply_(callee, actual)
self.depth -= 1
result
}
///|
async fn Interp::apply_(
self : Interp,
callee : Value,
actual : Array[Value],
) -> Outcome noraise {
match callee {
Lam(c) => {
if c.params.length() != actual.length() {
return Aborts(TypeError)
}
// The body runs in the module it was WRITTEN in, not the one calling
// it, so that an abort inside it is reported against the right file.
let caller = self.in_module
self.in_module = c.in_module
let result = self.eval_expr(c.body, bind(c.env, c.params, actual))
self.in_module = caller
result
}
// Calling one function of a mutual region rebinds the WHOLE region in the
// callee's environment, which is what lets its siblings name each other.
Def(c) => {
let d = c.region[c.index]
let (params, body) = match d {
FunctionDef(args~, body~, ..) => (args.args.map(fn(p) { p.arg }), body)
_ => return Stuck("a definition closure over something else")
}
if params.length() != actual.length() {
return Aborts(TypeError)
}
let region_env = region_bindings(c.env, c.region, c.in_module)
let inner = bind(region_env, params, actual)
let caller = self.in_module
self.in_module = c.in_module
let result = self.eval_seq(@analysis.statements(body)[:], inner).outcome()
self.in_module = caller
result
}
Prim(p) => self.call_primitive(p, actual)
// eval-call-nonfun
_ => Aborts(TypeError)
}
}
///|
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))
}