// Port of sqlglot/executor/env.py: the functions available to generated code.

///|
fn arity_error(name : String, min : Int, max : Int, got : Int) -> PyException {
  if got < min {
    type_error("\{name}() missing \{min - got} required positional argument(s)")
  } else {
    type_error(
      "\{name}() takes \{max} positional argument(s) but \{got} were given",
    )
  }
}

///|
/// A Python function with `min..=max` positional parameters (`max < 0`: variadic).
fn pyfunc(
  name : String,
  min : Int,
  max : Int,
  f : (Array[Value]) -> Value raise,
) -> Value {
  Func({
    name,
    call: args => {
      if args.length() < min || (max >= 0 && args.length() > max) {
        raise arity_error(name, min, max, args.length())
      }
      f(args)
    },
  })
}

///|
/// `null_if_any(*required)`: returns None if any of the required arguments (given by
/// position; all arguments when `required` is empty) is None.
fn null_if_any(
  name : String,
  min : Int,
  max : Int,
  f : (Array[Value]) -> Value raise,
  required? : Array[Int] = [],
) -> Value {
  pyfunc(name, min, max, args => {
    if required.is_empty() {
      if args.iter().any(a => a is Null) {
        return Null
      }
    } else {
      for i in required {
        if i < args.length() && args[i] is Null {
          return Null
        }
      }
    }
    f(args)
  })
}

///|
/// `filter_nulls(func, empty_null)` for aggregate functions.
fn filter_nulls(
  name : String,
  f : (Array[Value]) -> Value raise,
  empty_null? : Bool = true,
) -> Value {
  pyfunc(name, 1, 1, args => {
    let filtered = py_iter(args[0]).filter(v => !(v is Null))
    if filtered.is_empty() && empty_null {
      return Null
    }
    f(filtered)
  })
}

///|
fn sql_not(value : Value) -> Value {
  match value {
    Null => Null
    v => Bool(!v.truthy())
  }
}

///|
fn call_value(f : Value, args : Array[Value]) -> Value raise {
  match f {
    Func(func) => (func.call)(args)
    _ => raise type_error("'\{f.type_name()}' object is not callable")
  }
}

///|
fn to_sql_bool(v : Value) -> Bool? {
  match v {
    Null => None
    v => Some(v.truthy())
  }
}

///|
fn sql_and(left : Value, right : Value) -> Value raise {
  let l = to_sql_bool(call_value(left, []))
  if l is Some(false) {
    return Bool(false)
  }
  let r = to_sql_bool(call_value(right, []))
  if r is Some(false) {
    return Bool(false)
  }
  if l is None || r is None {
    Null
  } else {
    Bool(true)
  }
}

///|
fn sql_or(left : Value, right : Value) -> Value raise {
  let l = to_sql_bool(call_value(left, []))
  if l is Some(true) {
    return Bool(true)
  }
  let r = to_sql_bool(call_value(right, []))
  if r is Some(true) {
    return Bool(true)
  }
  if l is None || r is None {
    Null
  } else {
    Bool(false)
  }
}

///|
fn sql_in(args : Array[Value]) -> Value {
  let value = args[0]
  if value is Null {
    return Null
  }
  let mut has_null = false
  for i in 1.. String raise PyException {
  match v {
    Str(s) => s
    _ => raise type_error("\{what} must be str, not \{v.type_name()}")
  }
}

///|
/// `str.find(sub, start)` + 1 in code points.
fn str_position(args : Array[Value]) -> Value raise {
  let this = expect_str(args[0], "argument")
  let substr = match args[1] {
    Str(s) => s
    v => raise type_error("must be str, not \{v.type_name()}")
  }
  let position = if args.length() > 2 { args[2] } else { Null }
  let start = match position {
    Null => Null
    p => py_sub(p, Int(1L))
  }
  let hay = this.to_array()
  let needle = substr.to_array()
  let n = hay.length()
  let mut s = match start {
    Null => 0
    Int(i) => i.to_int()
    Bool(b) => if b { 1 } else { 0 }
    v =>
      raise type_error(
        "slice indices must be integers or None or have an __index__ method, not \{v.type_name()}",
      )
  }
  if s < 0 {
    s = s + n
    if s < 0 {
      s = 0
    }
  }
  if s > n {
    return Int(0L)
  }
  let m = needle.length()
  for i in s..<=(n - m) {
    let mut ok = true
    for j in 0.. Value raise {
  let this = args[0]
  let start = if args.length() > 1 { args[1] } else { Null }
  let length = if args.length() > 2 { args[2] } else { Null }
  if start is Null {
    return this
  }
  if py_eq(start, Int(0L)) {
    return Str("")
  }
  let start = if py_lt(start, Int(0L)) {
    py_add(Int(py_len(this).to_int64()), start)
  } else {
    py_sub(start, Int(1L))
  }
  let end = match length {
    Null => Null
    l => py_add(start, l)
  }
  py_slice(this, start, end)
}

///|
fn in_types(t : @core.DType, types : Array[@core.DType]) -> Bool {
  types.contains(t)
}

///|
fn cast(this : Value, to : Value) -> Value raise {
  let t = match to {
    DTypeV(t) => Some(t)
    _ => None
  }
  let is_t = fn(x : @core.DType) { t == Some(x) }
  if is_t(DATE) {
    match this {
      DateTime(dt) => return Date(dt.date)
      Date(_) => return this
      Str(_) => return Date(date_fromisoformat(this))
      _ => ()
    }
  }
  if is_t(TIME) {
    match this {
      DateTime(dt) => return Time(dt.time)
      Time(_) => return this
      Str(_) => return Time(time_fromisoformat(this))
      _ => ()
    }
  }
  if is_t(DATETIME) || is_t(TIMESTAMP) {
    match this {
      DateTime(_) => return this
      Date(d) =>
        return DateTime({
          date: d,
          time: { hour: 0, minute: 0, second: 0, microsecond: 0, },
          tz: None,
        })
      Str(_) => return DateTime(datetime_fromisoformat(this))
      _ => ()
    }
  }
  match t {
    Some(BOOLEAN) => return Bool(this.truthy())
    Some(x) if in_types(x, @core.dtype_text_types) => return Str(this.py_str())
    Some(FLOAT | DOUBLE) => return py_float(this)
    Some(x) if in_types(x, @core.dtype_numeric_types) => return py_int(this)
    _ => ()
  }
  let to_str = match to {
    DTypeV(t) => "DType.\{t.name()}"
    v => v.py_str()
  }
  raise PyException(
    "NotImplementedError",
    "Casting \{this.py_str()} to '\{to_str}' not implemented.",
  )
}

///|
let first : Int64 = 0L

///|
let last : Int64 = 1L

///|
fn ordered(this : Value, desc : Value, nulls_first : Value) -> Value {
  if this is Null {
    return Tuple([Int(if nulls_first.truthy() { first } else { last }), Int(0L)])
  }
  Tuple([
    Int(if nulls_first.truthy() { last } else { first }),
    if desc.truthy() {
      RevKey(this)
    } else {
      this
    },
  ])
}

///|
/// `_like`: `re.fullmatch` of the pattern with `_` as any char and `%` as any string.
fn like(this : Value, pattern : Value, ignore_case : Bool) -> Value raise {
  let p = match pattern {
    Str(s) => s
    v => raise type_error("expected str, got \{v.type_name()}")
  }
  let s = match this {
    Str(s) => s
    v =>
      raise type_error(
        "expected string or bytes-like object, got '\{v.type_name()}'",
      )
  }
  Bool(glob_match(s.to_array(), p.to_array(), ignore_case))
}

///|
/// Pattern character `a` matches `b` (`re.IGNORECASE` semantics when `ignore_case`).
fn char_eq(a : Char, b : Char, ignore_case : Bool) -> Bool {
  a == b || (ignore_case && @core.re_ignorecase_char_eq(a, b))
}

///|
fn glob_match(s : Array[Char], p : Array[Char], ignore_case : Bool) -> Bool {
  let mut si = 0
  let mut pi = 0
  let mut star_p = -1
  let mut star_s = 0
  while si < s.length() {
    if pi < p.length() && p[pi] == '%' {
      star_p = pi
      star_s = si
      pi += 1
    } else if pi < p.length() &&
      (p[pi] == '_' || char_eq(p[pi], s[si], ignore_case)) {
      pi += 1
      si += 1
    } else if star_p >= 0 {
      pi = star_p + 1
      star_s += 1
      si = star_s
    } else {
      return false
    }
  }
  while pi < p.length() && p[pi] == '%' {
    pi += 1
  }
  pi == p.length()
}

///|
let time_part_singulars : Array[String] = [
  "MICROSECONDS", "SECONDS", "MINUTES", "HOURS", "DAYS", "WEEKS", "MONTHS", "QUARTERS",
  "YEARS",
]

///|
fn interval(this : Value, unit : Value) -> Value raise {
  let unit = match unit {
    Str(u) => u
    v => raise type_error("unsupported operand: \{v.type_name()}")
  }
  let plural = unit + "S"
  let unit = if time_part_singulars.contains(plural) { plural } else { unit }
  let amount = match py_float(this) {
    Float(d) => d
    _ => 0.0
  }
  let factor = match @core.py_lower(unit) {
    "days" => 86400000000.0
    "seconds" => 1000000.0
    "microseconds" => 1.0
    "milliseconds" => 1000.0
    "minutes" => 60000000.0
    "hours" => 3600000000.0
    "weeks" => 604800000000.0
    u =>
      raise type_error(
        "\{@core.py_repr_str(u)} is an invalid keyword argument for __new__()",
      )
  }
  let us = round_half_even(amount * factor)
  if us.is_nan() || us.abs() > 8.64e19 {
    raise PyException(
      "OverflowError", "normalized days too large to fit in a C int",
    )
  }
  TimeDelta(check_td(PyTimeDelta::from_us(us.to_int64())))
}

///|
fn arrayconcat(args : Array[Value]) -> Value {
  let out = []
  for arg in args {
    match arg {
      List(l) => out.append(l)
      v => out.push(v)
    }
  }
  List(out)
}

///|
fn arraytostring(args : Array[Value]) -> Value raise {
  let this = args[0]
  let sep = args[1]
  let null = if args.length() > 2 { args[2] } else { Null }
  let items = py_iter(this)
    .map(x => if x is Null { null } else { x })
    .filter(x => !(x is Null))
  str_join(sep, items)
}

///|
/// Python `sep.join(items)`.
fn str_join(sep : Value, items : Array[Value]) -> Value raise {
  let s = match sep {
    Str(s) => s
    v =>
      raise type_error(
        "descriptor 'join' requires a 'str' object but received a '\{v.type_name()}'",
      )
  }
  let parts = []
  for i, x in items {
    match x {
      Str(t) => parts.push(t)
      v =>
        raise type_error(
          "sequence item \{i}: expected str instance, \{v.type_name()} found",
        )
    }
  }
  Str(parts.join(s))
}

///|
fn jsonextract(this : Value, path : Value) -> Value raise {
  let mut this = this
  for segment in py_iter(path) {
    match this {
      Dict(_) =>
        this = py_getitem(this, segment) catch {
          PyException("KeyError", _) => Null
          e => raise e
        }
      List(l) if segment is Str(s) && @core.parse_int_str(s) is Some(i) => {
        let i = i.to_int()
        this = if i >= 0 && i < l.length() {
          l[i]
        } else if i < 0 && -i <= l.length() {
          l[l.length() + i]
        } else {
          Null
        }
      }
      _ =>
        raise PyException(
          "NotImplementedError",
          "Unable to extract value for \{this.py_str()} at \{segment.py_str()}.",
        )
    }
    if this is Null {
      break
    }
  }
  this
}

///|
fn getattr(obj : Value, name : String) -> Value raise {
  let v = match (obj, name) {
    (Date(d), "year") | (DateTime({ date: d, .. }), "year") => Some(d.year)
    (Date(d), "month") | (DateTime({ date: d, .. }), "month") => Some(d.month)
    (Date(d), "day") | (DateTime({ date: d, .. }), "day") => Some(d.day)
    (Time(t), "hour") | (DateTime({ time: t, .. }), "hour") => Some(t.hour)
    (Time(t), "minute") | (DateTime({ time: t, .. }), "minute") =>
      Some(t.minute)
    (Time(t), "second") | (DateTime({ time: t, .. }), "second") =>
      Some(t.second)
    (Time(t), "microsecond") | (DateTime({ time: t, .. }), "microsecond") =>
      Some(t.microsecond)
    (TimeDelta(td), "days") => return Int(td.days)
    (TimeDelta(td), "seconds") => Some(td.seconds)
    (TimeDelta(td), "microseconds") => Some(td.microseconds)
    (Module("exp"), "DType") => return Module("exp.DType")
    (Module("exp.DType"), n) =>
      match @core.dtype_from_name(n) {
        Some(t) => return DTypeV(t)
        None => None
      }
    _ => None
  }
  match v {
    Some(i) => Int(i.to_int64())
    None =>
      raise PyException(
        "AttributeError",
        "'\{obj.type_name()}' object has no attribute '\{name}'",
      )
  }
}

///|
fn now_datetime() -> PyDateTime raise PyException {
  let ms = @env.now().reinterpret_as_int64()
  datetime_from_us(ms * 1000L + 62135596800L * 1000000L, None)
}

///|
fn struct_(args : Array[Value]) -> Value raise {
  let out : Array[(Value, Value)] = []
  let mut x = 0
  while x < args.length() {
    if x + 1 >= args.length() {
      raise PyException("IndexError", "tuple index out of range")
    }
    let k = args[x]
    let v = args[x + 1]
    if !(v is Null) && !(k is Null) {
      dict_set(out, k, v)
    }
    x += 2
  }
  Dict(out)
}

///|
fn dict_set(d : Array[(Value, Value)], k : Value, v : Value) -> Unit raise {
  ignore(hash_key(k))
  for i, kv in d {
    if py_eq(kv.0, k) {
      d[i] = (kv.0, v)
      return
    }
  }
  d.push((k, v))
}

///|
fn zip_values(args : Array[Value]) -> Array[Array[Value]] raise {
  if args.is_empty() {
    return []
  }
  let iters = args.map(a => py_iter(a))
  let mut n = iters[0].length()
  for it in iters {
    if it.length() < n {
      n = it.length()
    }
  }
  Array::makei(n, i => iters.map(it => it[i]))
}

///|
fn str_strip(this : Value, chars : Value) -> Value raise {
  let s = match this {
    Str(s) => s
    v =>
      raise type_error(
        "descriptor 'strip' for 'str' objects doesn't apply to a '\{v.type_name()}' object",
      )
  }
  match chars {
    Null => Str(py_strip_ws(s))
    Str(c) => {
      let set = c.to_array()
      let a = s.to_array()
      let mut i = 0
      let mut j = a.length()
      while i < j && set.contains(a[i]) {
        i += 1
      }
      while j > i && set.contains(a[j - 1]) {
        j -= 1
      }
      Str(String::from_array(a[i:j].to_array()))
    }
    v => raise type_error("strip arg must be None or str, not \{v.type_name()}")
  }
}

///|
fn str_method(
  this : Value,
  method_name : String,
  f : (String) -> String,
) -> Value raise {
  match this {
    Str(s) => Str(f(s))
    v =>
      raise PyException(
        "AttributeError",
        "'\{v.type_name()}' object has no attribute '\{method_name}'",
      )
  }
}

///|
fn attr_of(this : Value, name : String) -> Value raise {
  getattr(this, name)
}

///|
fn build_env() -> Map[String, Value] {
  let env : Map[String, Value] = {}
  let set = fn(v : Value) {
    match v {
      Func(f) => env[f.name] = v
      _ => ()
    }
  }
  env["exp"] = Module("exp")
  set(pyfunc("AND", 2, 2, args => sql_and(args[0], args[1])))
  // aggs
  set(pyfunc("ARRAYAGG", 1, 1, args => List(py_iter(args[0]).copy())))
  set(filter_nulls("ARRAYUNIQUEAGG", acc => List(py_set(acc))))
  set(
    filter_nulls("AVG", acc => Float(py_fsum(acc) / acc.length().to_double())),
  )
  set(
    filter_nulls("COUNT", acc => Int(acc.length().to_int64()), empty_null=false),
  )
  set(
    pyfunc("FIRST", 1, 1, args => {
      let items = py_iter(args[0])
      if items.is_empty() {
        Null
      } else {
        items[0]
      }
    }),
  )
  set(filter_nulls("MAX", acc => py_max(acc)))
  set(filter_nulls("MIN", acc => py_min(acc)))
  set(filter_nulls("SUM", acc => py_sum(acc)))
  // scalar functions
  set(null_if_any("ABS", 1, 1, args => py_abs(args[0])))
  set(null_if_any("ADD", 2, 2, args => py_add(args[0], args[1])))
  set(
    null_if_any("ARRAYANY", 2, 2, args => {
      for e in py_iter(args[0]) {
        if call_value(args[1], [e]).truthy() {
          return Bool(true)
        }
      }
      Bool(false)
    }),
  )
  set(null_if_any("ARRAYCONCAT", 0, -1, args => arrayconcat(args)))
  set(
    null_if_any("ARRAYTOSTRING", 2, 3, args => arraytostring(args), required=[
      0, 1,
    ]),
  )
  set(
    null_if_any("BETWEEN", 3, 3, args => {
      let l = py_le(args[1], args[0])
      if !l {
        return Bool(false)
      }
      Bool(py_le(args[0], args[2]))
    }),
  )
  set(null_if_any("BITWISEAND", 2, 2, args => py_bitop("&", args[0], args[1])))
  set(
    null_if_any("BITWISELEFTSHIFT", 2, 2, args => {
      py_bitop("<<", args[0], args[1])
    }),
  )
  set(null_if_any("BITWISEOR", 2, 2, args => py_bitop("|", args[0], args[1])))
  set(
    null_if_any("BITWISERIGHTSHIFT", 2, 2, args => {
      py_bitop(">>", args[0], args[1])
    }),
  )
  set(null_if_any("BITWISEXOR", 2, 2, args => py_bitop("^", args[0], args[1])))
  set(null_if_any("CAST", 2, 2, args => cast(args[0], args[1])))
  set(
    pyfunc("COALESCE", 0, -1, args => {
      for a in args {
        if !(a is Null) {
          return a
        }
      }
      Null
    }),
  )
  set(null_if_any("CONCAT", 0, -1, args => str_join(Str(""), args)))
  set(
    null_if_any("SAFECONCAT", 0, -1, args => {
      Str(args.map(a => a.py_str()).join(""))
    }),
  )
  set(
    null_if_any("CONCATWS", 1, -1, args => {
      str_join(args[0], args[1:].to_array())
    }),
  )
  set(
    null_if_any("DATEDIFF", 2, -1, args => {
      attr_of(py_sub(args[0], args[1]), "days")
    }),
  )
  set(
    null_if_any("DATESTRTODATE", 1, 1, args => Date(date_fromisoformat(args[0]))),
  )
  set(null_if_any("DIV", 2, 2, args => py_truediv(args[0], args[1])))
  set(null_if_any("DOT", 2, 2, args => py_getitem(args[0], args[1])))
  set(null_if_any("EQ", 2, 2, args => Bool(py_eq(args[0], args[1]))))
  set(
    null_if_any("EXTRACT", 2, 2, args => {
      match args[0] {
        Str(name) => getattr(args[1], name)
        v =>
          raise type_error(
            "attribute name must be string, not '\{v.type_name()}'",
          )
      }
    }),
  )
  set(null_if_any("GT", 2, 2, args => Bool(py_gt(args[0], args[1]))))
  set(null_if_any("GTE", 2, 2, args => Bool(py_ge(args[0], args[1]))))
  set(
    pyfunc("IF", 3, 3, args => if args[0].truthy() { args[1] } else { args[2] }),
  )
  set(pyfunc("IN", 1, -1, args => sql_in(args)))
  set(null_if_any("INT", 1, 1, args => py_int(args[0])))
  set(null_if_any("INTDIV", 2, 2, args => py_floordiv(args[0], args[1])))
  set(null_if_any("INTERVAL", 2, 2, args => interval(args[0], args[1])))
  set(
    null_if_any("JSONEXTRACT", 2, 2, args => jsonextract(args[0], args[1]), required=[
      0, 1,
    ]),
  )
  set(null_if_any("LEFT", 2, 2, args => py_slice(args[0], Null, args[1])))
  set(null_if_any("LENGTH", 1, 1, args => Int(py_len(args[0]).to_int64())))
  set(null_if_any("LIKE", 2, 2, args => like(args[0], args[1], false)))
  set(null_if_any("ILIKE", 2, 2, args => like(args[0], args[1], true)))
  set(
    null_if_any("LOWER", 1, 1, args => {
      str_method(args[0], "lower", @core.py_lower)
    }),
  )
  set(null_if_any("LT", 2, 2, args => Bool(py_lt(args[0], args[1]))))
  set(null_if_any("LTE", 2, 2, args => Bool(py_le(args[0], args[1]))))
  set(
    null_if_any("MAP", 0, -1, args => {
      let out : Array[(Value, Value)] = []
      for pair in zip_values(args) {
        if pair.length() != 2 {
          raise value_error(
            "dictionary update sequence element #0 has length \{pair.length()}; 2 is required",
          )
        }
        dict_set(out, pair[0], pair[1])
      }
      Dict(out)
    }),
  )
  set(null_if_any("MOD", 2, 2, args => py_mod(args[0], args[1])))
  set(null_if_any("MUL", 2, 2, args => py_mul(args[0], args[1])))
  set(null_if_any("NEQ", 2, 2, args => Bool(!py_eq(args[0], args[1]))))
  set(
    null_if_any("ORD", 1, 1, args => {
      match args[0] {
        Str(s) if s.char_length() == 1 =>
          Int(s.get_char(0).unwrap().to_int().to_int64())
        Str(s) =>
          raise type_error(
            "ord() expected a character, but string of length \{s.char_length()} found",
          )
        v =>
          raise type_error(
            "ord() expected string of length 1, but \{v.type_name()} found",
          )
      }
    }),
  )
  set(pyfunc("NOT", 1, 1, args => sql_not(args[0])))
  set(pyfunc("OR", 2, 2, args => sql_or(args[0], args[1])))
  set(pyfunc("ORDERED", 3, 3, args => ordered(args[0], args[1], args[2])))
  set(null_if_any("POW", 2, 2, args => py_pow(args[0], args[1])))
  set(
    null_if_any("REVERSE", 1, 1, args => py_slice(args[0], Null, Null, step=-1)),
  )
  set(
    null_if_any("RIGHT", 2, 2, args => py_slice(args[0], py_neg(args[1]), Null)),
  )
  set(
    null_if_any("ROUND", 1, 3, args => {
      py_round(args[0], if args.length() > 1 { args[1] } else { Null })
    }),
  )
  set(
    null_if_any("STRPOSITION", 2, 3, args => str_position(args), required=[0, 1]),
  )
  set(null_if_any("SUB", 2, 2, args => py_sub(args[0], args[1])))
  set(null_if_any("SUBSTRING", 1, 3, args => substring(args), required=[0]))
  set(
    null_if_any("TIMESTRTOTIME", 1, 1, args => {
      DateTime(datetime_fromisoformat(args[0]))
    }),
  )
  set(
    null_if_any("UPPER", 1, 1, args => {
      str_method(args[0], "upper", @core.py_upper)
    }),
  )
  set(
    pyfunc("ZIPNOTNULL", 0, -1, args => {
      Iter(
        zip_values(args)
        .filter(row => row.iter().all(v => !(v is Null)))
        .map(row => Tuple(row)),
      )
    }),
  )
  set(null_if_any("YEAR", 1, 1, args => getattr(args[0], "year")))
  set(null_if_any("MONTH", 1, 1, args => getattr(args[0], "month")))
  set(null_if_any("DAY", 1, 1, args => getattr(args[0], "day")))
  set(pyfunc("CURRENTDATETIME", 0, 0, _ => DateTime(now_datetime())))
  set(pyfunc("CURRENTTIMESTAMP", 0, 0, _ => DateTime(now_datetime())))
  set(pyfunc("CURRENTTIME", 0, 0, _ => DateTime(now_datetime())))
  set(pyfunc("CURRENTDATE", 0, 0, _ => Date(now_datetime().date)))
  set(
    null_if_any("STRFTIME", 2, 2, args => {
      let fmt = expect_str(args[0], "strftime() argument 1")
      Str(strftime(datetime_fromisoformat(args[1]), fmt))
    }),
  )
  set(
    null_if_any("STRTOTIME", 2, 2, args => {
      let s = expect_str(args[0], "strptime() argument 1")
      let f = expect_str(args[1], "strptime() argument 2")
      DateTime(strptime(s, f))
    }),
  )
  set(
    null_if_any("TRIM", 1, 2, args => {
      str_strip(args[0], if args.length() > 1 { args[1] } else { Null })
    }),
  )
  set(pyfunc("STRUCT", 0, -1, args => struct_(args)))
  set(
    null_if_any("UNIXTOTIME", 1, 1, args => {
      DateTime(datetime_from_timestamp_utc(args[0]))
    }),
  )
  env
}

///|
/// The functions available to the generated code (Python `env.ENV`). Entries can be
/// replaced (e.g. to pin `CURRENT_TIMESTAMP` in tests), like patching the Python dict.
pub let default_env : Map[String, Value] = build_env()

///|
/// Python builtins reachable from generated code.
let builtins : Map[String, Value] = {
  "set": pyfunc("set", 0, 1, args => {
    if args.is_empty() {
      Set([])
    } else {
      Set(py_set(py_iter(args[0])))
    }
  }),
  "len": pyfunc("len", 1, 1, args => Int(py_len(args[0]).to_int64())),
  "list": pyfunc("list", 0, 1, args => {
    if args.is_empty() {
      List([])
    } else {
      List(py_iter(args[0]).copy())
    }
  }),
  "tuple": pyfunc("tuple", 0, 1, args => {
    if args.is_empty() {
      Tuple([])
    } else {
      Tuple(py_iter(args[0]).copy())
    }
  }),
  "str": pyfunc("str", 0, 1, args => {
    if args.is_empty() {
      Str("")
    } else {
      Str(args[0].py_str())
    }
  }),
  "int": pyfunc("int", 0, 1, args => {
    if args.is_empty() {
      Int(0L)
    } else {
      py_int(args[0])
    }
  }),
  "float": pyfunc("float", 0, 1, args => {
    if args.is_empty() {
      Float(0.0)
    } else {
      py_float(args[0])
    }
  }),
  "bool": pyfunc("bool", 0, 1, args => {
    Bool(!args.is_empty() && args[0].truthy())
  }),
  "abs": pyfunc("abs", 1, 1, args => py_abs(args[0])),
  "sum": pyfunc("sum", 1, 1, args => py_sum(py_iter(args[0]))),
  "max": pyfunc("max", 1, -1, args => {
    if args.length() == 1 {
      py_max(py_iter(args[0]))
    } else {
      py_max(args)
    }
  }),
  "min": pyfunc("min", 1, -1, args => {
    if args.length() == 1 {
      py_min(py_iter(args[0]))
    } else {
      py_min(args)
    }
  }),
  "round": pyfunc("round", 1, 2, args => {
    py_round(args[0], if args.length() > 1 { args[1] } else { Null })
  }),
  "pow": pyfunc("pow", 2, 2, args => py_pow(args[0], args[1])),
  "ord": pyfunc("ord", 1, 1, args => {
    match args[0] {
      Str(s) if s.char_length() == 1 =>
        Int(s.get_char(0).unwrap().to_int().to_int64())
      _ => raise type_error("ord() expected a character")
    }
  }),
}