///|
/// 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 }),
defaults: literal_defaults(args),
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
}
// A chain is `a < b < c`, and Python's rule for it is two things at once:
// `b` is evaluated ONCE, and the chain stops at the first False. Written
// as a fold rather than as a desugaring to `and`, because the desugaring
// would evaluate `b` twice and PurePy's `and` wants a real `Bool` where
// the chain's own operators already answer with one.
//
// A single-operator comparison -- every comparison PurePy has -- takes the
// first turn of this loop and nothing else, so its behaviour is the rule
// it always had.
Compare(left~, ops~, comparators~, ..) => {
let mut a = match self.eval_expr(left, env) {
Val(v) => v
other => return other
}
let mut result = Outcome::Val(Value::Bool(true))
for i, op in ops {
let b = match self.eval_expr(comparators[i], env) {
Val(v) => v
other => return other
}
result = self.sited(e, @value.compare(op, a, b))
match result {
Val(Bool(true)) => a = b
_ => return result
}
}
result
}
// 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
}
// An f-string (#55), under a profile that opens it: the pieces in source
// order, each turned into text and joined.
//
// A hole uses `str`, as Python does -- `f"{x}"` and `str(x)` agree -- and
// `!r` asks for `repr` instead. Neither is the `str` BUILTIN, which is a
// different profile away; both are the same `Value::str` and `Value::repr`
// that `print` has always used.
JoinedStr(parts~, ..) => self.eval_fstring(parts, env)
// eval-attr-module, eval-attr-object, eval-attr-missing
Attribute(value~, attr~, ..) =>
match self.eval_expr(value, env) {
Val(v) => self.attribute_of(v, attr, e)
other => other
}
// eval-subscript
//
// A `Slice` in the subscript position is not an expression that evaluates
// to a value -- PurePy has no slice object and adding one would put a new
// kind across the host boundary for nothing. Its three bounds are
// evaluated in source order and handed to `getslice`, so the node never
// becomes a value at all. Reached only under a profile that opens
// slicing (#59).
Subscript(value~, slice~, ..) =>
match self.eval_expr(value, env) {
Val(v) =>
match slice {
Slice(lower~, upper~, step~, ..) => {
let lo = match self.eval_bound(lower, env) {
Ok(w) => w
Err(o) => return o
}
let hi = match self.eval_bound(upper, env) {
Ok(w) => w
Err(o) => return o
}
let st = match self.eval_bound(step, env) {
Ok(w) => w
Err(o) => return o
}
self.sited(
e,
@value.getslice(v, { lower: lo, upper: hi, step: st, }),
)
}
_ =>
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())
}
}
///|
/// 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 noraise {
let out = StringBuilder()
for part in parts {
match part {
Text(t) => out.write_string(t)
Hole(value~, conversion~, ..) => {
let v = match self.eval_expr(value, env) {
Val(w) => w
other => return other
}
let rendered = if conversion is Some('r') { v.repr() } else { v.str() }
match rendered {
Some(t) => out.write_string(t)
// A closure, a module or a class has no printable form, and an
// f-string is a print by another name.
None => return Stuck("formatting " + v.kind_name())
}
}
}
}
Val(Str(out.to_string()))
}
///|
/// The attribute rule, on a receiver that has already been evaluated.
///
/// A function so that a method call can consult it without evaluating the
/// receiver a second time: `eval_call` has the value in hand by the time it
/// knows the receiver is a module or an object.
///
/// The last case is the one that matters. The attributes of a builtin value
/// are undefined -- `xs.append` has no rule -- and that is what makes list
/// mutation unreachable. A profile that adds methods does NOT change it: it
/// adds a rule for `xs.append()`, the call, and leaves `xs.append` the bare
/// attribute exactly as undefined as it was.
fn Interp::attribute_of(
self : Interp,
v : Value,
attr : String,
at : @ast.Expr,
) -> 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())
}
}
///|
/// One bound of a slice: the value it evaluates to, or nothing when the source
/// left it out.
///
/// An omitted bound is not `None` the PurePy value. `xs[:2]` and `xs[None:2]`
/// are different programs -- Python makes the second a `TypeError` -- so the
/// absence has to survive as far as `getslice`, which is why this answers with
/// a `Value?` rather than substituting a default here.
pub async fn Interp::eval_bound(
self : Interp,
b : @ast.Expr?,
env : Env,
) -> Result[Value?, Outcome] noraise {
match b {
Some(x) =>
match self.eval_expr(x, env) {
Val(w) => Ok(Some(w))
other => Err(other)
}
None => Ok(None)
}
}
///|
/// 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 => {
// A method call on a builtin value, under a profile that has them.
//
// Handled HERE, as a call, rather than by giving `Attribute` a rule that
// answers with a bound method. A bound method would be a new kind of
// `Value`: it would need a `repr`, an `eq` and an ordering the
// specification does not define, and it would start crossing the host
// boundary -- `Host.call` receives `Value`s -- which is an embedding
// change wearing an evaluator change's clothes. So `"a".upper()` works
// and `f = "a".upper` is exactly the `Stuck` it has always been.
if self.profile.has(BuiltinMethods) &&
func is Attribute(value=recv_e, attr~, ..) {
let recv = match self.eval_expr(recv_e, env) {
Val(v) => v
other => return other
}
// A module or an object has its own attribute rule and reaches it
// unchanged: `math.sqrt(2)` and `p.field(...)` are not method calls on
// a builtin, and the receiver is already evaluated, so they finish
// here rather than going round again.
match recv {
Mod(_, _) | ModStub(_) | Obj(_, _) => {
let callee = match self.attribute_of(recv, attr, func) {
Val(v) => v
other => return other
}
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())
}
return o
}
_ => {
if !keywords.is_empty() {
return Stuck("a keyword argument on a method call")
}
let actual = match self.eval_exprs(args, env) {
Ok(vs) => vs
Err(o) => return o
}
let o = call_method(recv, attr, actual)
if o is Aborts(_) {
self.record(call.span())
}
return o
}
}
}
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) => {
// The no-defaults path is the one every PurePy program takes, and it
// takes exactly the code it took before defaults existed: no call, no
// allocation. That is not a micro-optimisation. A guest call costs about
// a dozen host frames under the async transform, `max_depth` is
// calibrated against a JavaScript engine's stack, and one more frame per
// guest call is enough to move where the limit has to sit.
let actual = if c.defaults.is_empty() {
if c.params.length() != actual.length() {
return Aborts(TypeError)
}
actual
} else {
match fill_defaults(c.params.length(), c.defaults, actual) {
Some(vs) => vs
None => 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, args_node, body) = match d {
FunctionDef(args~, body~, ..) =>
(args.args.map(fn(p) { p.arg }), args, body)
_ => return Stuck("a definition closure over something else")
}
// As above: a function with no defaults pays nothing for the feature,
// not even the walk that would find it has none.
let actual = if args_node.defaults.is_empty() {
if params.length() != actual.length() {
return Aborts(TypeError)
}
actual
} else {
match
fill_defaults(params.length(), literal_defaults(args_node), actual) {
Some(vs) => vs
None => 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))
}