///|
/// Refuse using an undefined value, naming it.
fn undefined_use(name : String) -> @spec.Refused {
@spec.unknown("name", name)
}
///|
fn Run::eval(
self : Run,
expr : @syntax.Expr,
) -> @spec.Value raise @spec.Refused {
match expr {
Lit(v) => v
Name(n) => self.lookup(n)
List(items) => Seq(items.map(e => self.eval(e)))
Tuple(items) => Tuple(items.map(e => self.eval(e)))
Dict(pairs) => {
let out = Map([])
for pair in pairs {
let key = self.eval(pair.0)
out[self.call.text(key)] = self.eval(pair.1)
}
Dict(out)
}
Attr(target, name) => self.get_attr(self.eval(target), name)
Item(target, Slice(a, b, c)) => {
let base = self.eval(target)
let start = self.eval_opt(a)
let stop = self.eval_opt(b)
let step = self.eval_opt(c)
slice(base, start, stop, step)
}
Item(target, key) => self.get_item(self.eval(target), self.eval(key))
Slice(_, _, _) => raise @spec.invalid("a slice belongs inside brackets")
Call(target, args) => {
let callee = self.eval(target)
let (pos, named) = self.eval_args(args)
self.invoke(callee, pos, named)
}
Not(e) => Bool(!self.call.truthy(self.eval(e)))
Neg(e) =>
match self.eval(e) {
Int(n) => Int(-n)
Double(d) => Double(-d)
Bool(b) => Int(if b { -1 } else { 0 })
Undefined(name) => raise undefined_use(name)
v => raise @spec.invalid("bad operand type for unary -: \{v.kind()}")
}
Pos(e) =>
match self.eval(e) {
Int(_) | Double(_) as v => v
Bool(b) => Int(if b { 1 } else { 0 })
Undefined(name) => raise undefined_use(name)
v => raise @spec.invalid("bad operand type for unary +: \{v.kind()}")
}
Binary(And, l, r) => {
let left = self.eval(l)
if self.call.truthy(left) {
self.eval(r)
} else {
left
}
}
Binary(Or, l, r) => {
let left = self.eval(l)
if self.call.truthy(left) {
left
} else {
self.eval(r)
}
}
Binary(op, l, r) => self.arith(op, self.eval(l), self.eval(r))
Compare(first, ops) => {
let mut left = self.eval(first)
for pair in ops {
let right = self.eval(pair.1)
if !self.compare(pair.0, left, right) {
return Bool(false)
}
left = right
}
Bool(true)
}
Concat(parts) => {
let buf = StringBuilder()
let mut markup = false
let values = parts.map(p => self.eval(p))
for v in values {
if v is Markup(_) {
markup = true
}
}
for v in values {
match v {
Markup(s) => buf.write_string(s)
other => {
let t = self.call.text(other)
buf.write_string(if markup { @spec.escape(t) } else { t })
}
}
}
if markup {
Markup(buf.to_string())
} else {
Text(buf.to_string())
}
}
Cond(c, then, otherwise) =>
if self.call.truthy(self.eval(c)) {
self.eval(then)
} else {
match otherwise {
Some(e) => self.eval(e)
None => Undefined("")
}
}
Filter(input, name, args) => {
let value = match input {
Some(e) => self.eval(e)
None => raise @spec.invalid("a filter needs a value")
}
let (pos, named) = self.eval_args(args)
self.call.filter(name, value, pos, named)
}
Test(target, name, args) => {
let value = self.eval(target)
let (pos, named) = self.eval_args(args)
if !named.is_empty() {
raise @spec.invalid("tests take no named arguments")
}
Bool(self.call.check(name, value, pos))
}
Attributes(attrs, splats) => {
let merged = Map([])
for attr in attrs {
let value = match attr.value {
Some(e) => self.eval(e)
None => Bool(true)
}
merge_attr(merged, attr.name, value)
}
for splat in splats {
match self.eval(splat) {
Dict(fields) =>
for k, v in fields {
merge_attr(merged, k, v)
}
Undefined(name) => raise undefined_use(name)
v =>
raise @spec.invalid("&attributes takes a mapping, not \{v.kind()}")
}
}
Dict(merged)
}
}
}
///|
fn Run::eval_opt(
self : Run,
expr : @syntax.Expr?,
) -> @spec.Value? raise @spec.Refused {
match expr {
Some(e) => Some(self.eval(e))
None => None
}
}
///|
fn Run::eval_args(
self : Run,
args : Array[@syntax.Arg],
) -> (Array[@spec.Value], Map[String, @spec.Value]) raise @spec.Refused {
let pos = []
let named = Map([])
for arg in args {
match arg {
Pos(e) => pos.push(self.eval(e))
Named(k, e) => named[k] = self.eval(e)
Spread(e) =>
for v in self.call.items(self.eval(e)) {
pos.push(v)
}
SpreadNamed(e) =>
match self.eval(e) {
Dict(fields) =>
for k, v in fields {
named[k] = v
}
v =>
raise @spec.invalid(
"argument after ** must be a mapping, not \{v.kind()}",
)
}
}
}
(pos, named)
}
///|
/// `value.name`: a method of a string, list or mapping, an object's
/// attribute, else the item of that name; missing is undefined.
fn Run::get_attr(
self : Run,
value : @spec.Value,
name : String,
) -> @spec.Value raise @spec.Refused {
match value {
Undefined(outer) =>
match self.call.undefined {
Chainable => Undefined(outer + "." + name)
_ => raise undefined_use(outer)
}
Object(obj) =>
match obj.attr(name) {
Some(v) => v
None => Undefined(name)
}
_ =>
match @defaults.attr(value, name) {
Some(m) => m
None =>
match value.item(Text(name)) {
Some(v) => v
None => Undefined(name)
}
}
}
}
///|
/// `value[key]`: the item, else the attribute of that name; missing is
/// undefined.
fn Run::get_item(
self : Run,
value : @spec.Value,
key : @spec.Value,
) -> @spec.Value raise @spec.Refused {
match value {
Undefined(outer) =>
match self.call.undefined {
Chainable => Undefined(outer)
_ => raise undefined_use(outer)
}
_ =>
match value.item(key) {
Some(v) => v
None =>
match key {
Text(name) => self.get_attr(value, name)
_ => Undefined(key.to_string())
}
}
}
}
///|
/// Call a value: objects take the call; anything else is refused.
fn Run::invoke(
self : Run,
callee : @spec.Value,
pos : Array[@spec.Value],
named : Map[String, @spec.Value],
) -> @spec.Value raise @spec.Refused {
match callee {
Object(obj) => obj.call(self.call, pos, named)
Undefined(name) => raise undefined_use(name)
v => raise @spec.invalid("\{v.kind()} is not callable")
}
}
///|
/// A slice of a list, tuple or string, with Python's bounds and steps.
fn slice(
value : @spec.Value,
start : @spec.Value?,
stop : @spec.Value?,
step : @spec.Value?,
) -> @spec.Value raise @spec.Refused {
let index = fn(v : @spec.Value?) -> Int32? raise @spec.Refused {
match v {
None | Some(Null) => None
Some(x) =>
match x.index() {
Some(i) => Some(i)
None => raise @spec.invalid("slice indices must be integers or None")
}
}
}
let st = index(step).unwrap_or(1)
if st == 0 {
raise @spec.invalid("slice step cannot be zero")
}
let pick = fn(n : Int32) -> Array[Int32] raise @spec.Refused {
let clamp = fn(v : Int32?, default : Int32) {
match v {
None => default
Some(i) => {
let j = if i < 0 { i + n } else { i }
if st > 0 {
j.max(0).min(n)
} else {
j.max(-1).min(n - 1)
}
}
}
}
let a = clamp(index(start), if st > 0 { 0 } else { n - 1 })
let b = clamp(index(stop), if st > 0 { n } else { -1 })
let out = []
let mut i = a
if st > 0 {
while i < b {
out.push(i)
i += st
}
} else {
while i > b {
out.push(i)
i += st
}
}
out
}
match value {
Seq(items) => Seq(pick(items.length()).map(i => items[i]))
Tuple(items) => Tuple(pick(items.length()).map(i => items[i]))
Text(s) | Markup(s) => {
let cs = s.iter().collect()
let picked = String::from_array(pick(cs.length()).map(i => cs[i]))
if value is Markup(_) {
Markup(picked)
} else {
Text(picked)
}
}
Undefined(name) => raise undefined_use(name)
v => raise @spec.invalid("\{v.kind()} cannot be sliced")
}
}
///|
fn int_or_overflow(
a : Int64,
b : Int64,
r : Int64,
op : String,
) -> @spec.Value raise @spec.Refused {
let ok = match op {
"+" => !((a >= 0L) == (b >= 0L) && (r >= 0L) != (a >= 0L))
"-" => !((a >= 0L) != (b >= 0L) && (r >= 0L) != (a >= 0L))
_ =>
a == 0L || (r / a == b && !(a == -1L && b == -9223372036854775807L - 1L))
}
if ok {
Int(r)
} else {
raise @spec.invalid("integer result too large to hold in 64 bits")
}
}
///|
/// The arithmetic operators with Python's meaning: numbers, string and list
/// concatenation and repetition, `%` formatting, safe text escaping the
/// plain side.
fn Run::arith(
self : Run,
op : @syntax.Op,
l : @spec.Value,
r : @spec.Value,
) -> @spec.Value raise @spec.Refused {
match (l, r) {
(Undefined(name), _) | (_, Undefined(name)) => raise undefined_use(name)
_ => ()
}
match (op, l, r) {
(Add, Markup(_), Text(_) | Markup(_)) | (Add, Text(_), Markup(_)) =>
Markup(l.markup() + r.markup())
(Add, Text(a), Text(b)) => Text(a + b)
(Add, Seq(a), Seq(b)) => Seq(a + b)
(Add, Tuple(a), Tuple(b)) => Tuple(a + b)
(Mul, Text(s) | Markup(s), Int(n)) | (Mul, Int(n), Text(s) | Markup(s)) => {
let rep = if n > 0L { s.repeat(n.to_int()) } else { "" }
if l is Markup(_) || r is Markup(_) {
Markup(rep)
} else {
Text(rep)
}
}
(Mul, Seq(items), Int(n)) | (Mul, Int(n), Seq(items)) => {
let out = []
for _ in 0L.. {
let markup = l is Markup(_)
let formatted = @defaults.percent(self.call, s, r, escape=markup)
if markup {
Markup(formatted)
} else {
Text(formatted)
}
}
_ => numeric(op, l, r)
}
}
///|
fn as_int(v : @spec.Value) -> Int64? {
match v {
Int(n) => Some(n)
Bool(b) => Some(if b { 1 } else { 0 })
_ => None
}
}
///|
fn numeric(
op : @syntax.Op,
l : @spec.Value,
r : @spec.Value,
) -> @spec.Value raise @spec.Refused {
let symbol = match op {
Add => "+"
Sub => "-"
Mul => "*"
Div => "/"
FloorDiv => "//"
Mod => "%"
Pow => "**"
_ => "?"
}
match (as_int(l), as_int(r)) {
(Some(a), Some(b)) =>
return match op {
Add => int_or_overflow(a, b, a + b, "+")
Sub => int_or_overflow(a, b, a - b, "-")
Mul => int_or_overflow(a, b, a * b, "*")
Div => {
if b == 0L {
raise @spec.invalid("division by zero")
}
Double(a.to_double() / b.to_double())
}
FloorDiv | Mod => {
if b == 0L {
raise @spec.invalid("integer division or modulo by zero")
}
let q = a / b
let m = a % b
let (q, m) = if m != 0L && (m < 0L) != (b < 0L) {
(q - 1L, m + b)
} else {
(q, m)
}
if op == FloorDiv {
Int(q)
} else {
Int(m)
}
}
Pow =>
if b < 0L {
Double(@math.pow(a.to_double(), b.to_double()))
} else {
let mut result = 1L
let mut base = a
let mut e = b
while e > 0L {
if e % 2L == 1L {
let next = result * base
if base != 0L && next / base != result {
raise @spec.invalid(
"integer result too large to hold in 64 bits",
)
}
result = next
}
e = e / 2L
if e > 0L {
let sq = base * base
if base != 0L && sq / base != base {
raise @spec.invalid(
"integer result too large to hold in 64 bits",
)
}
base = sq
}
}
Int(result)
}
_ => raise @spec.invalid("unsupported operator")
}
_ => ()
}
match (l.number(), r.number()) {
(Some(a), Some(b)) =>
Double(
match op {
Add => a + b
Sub => a - b
Mul => a * b
Div => {
if b == 0.0 {
raise @spec.invalid("float division by zero")
}
a / b
}
FloorDiv => {
if b == 0.0 {
raise @spec.invalid("float floor division by zero")
}
(a / b).floor()
}
Mod => {
if b == 0.0 {
raise @spec.invalid("float modulo by zero")
}
let m = a - b * (a / b).floor()
m
}
Pow => @math.pow(a, b)
_ => raise @spec.invalid("unsupported operator")
},
)
_ =>
raise @spec.invalid(
"unsupported operand types for \{symbol}: \{l.kind()} and \{r.kind()}",
)
}
}
///|
fn Run::compare(
self : Run,
op : @syntax.Op,
l : @spec.Value,
r : @spec.Value,
) -> Bool raise @spec.Refused {
match (l, r) {
(Undefined(name), _) | (_, Undefined(name)) if self.call.refuses(name) =>
raise undefined_use(name)
_ => ()
}
match op {
Eq => l == r
Ne => l != r
In => self.contains(r, l)
NotIn => !self.contains(r, l)
Lt => l.compare(r) < 0
Gt => l.compare(r) > 0
Le => l.compare(r) <= 0
Ge => l.compare(r) >= 0
_ => raise @spec.invalid("not a comparison")
}
}
///|
fn Run::contains(
self : Run,
container : @spec.Value,
item : @spec.Value,
) -> Bool raise @spec.Refused {
match container {
Undefined(name) =>
if self.call.refuses(name) {
raise undefined_use(name)
} else {
false
}
_ => container.contains(item)
}
}
///|
/// Merge an attribute into a mapping the way an element merges `&attributes`:
/// classes gather into a list, styles join, anything else overrides.
fn merge_attr(
into : Map[String, @spec.Value],
name : String,
value : @spec.Value,
) -> Unit {
match (name, into.get(name)) {
("class", Some(before)) => {
let items = match before {
Seq(xs) => xs.copy()
other => [other]
}
match value {
Seq(more) => items.append(more)
other => items.push(other)
}
into[name] = Seq(items)
}
("style", Some(before)) => {
let a = style_text(before)
let b = style_text(value)
let a = if a.is_empty() || a.has_suffix(";") { a } else { a + ";" }
let b = if b.is_empty() || b.has_suffix(";") { b } else { b + ";" }
into[name] = Text(a + b)
}
_ => into[name] = value
}
}
///|
/// Pug's `style` attribute value: a mapping as `key:value;` pairs.
fn style_text(value : @spec.Value) -> String {
match value {
Dict(fields) => {
let out = StringBuilder()
for k, v in fields {
out.write_string(k + ":" + v.to_string() + ";")
}
out.to_string()
}
Null | Undefined(_) | Bool(false) => ""
other => other.to_string()
}
}