///|
/// Dispatch a function call to user-defined functions, filter args, or builtins.
fn eval_func_call(
  name : String,
  args : Array[Filter],
  input : Json,
  env : Scope,
  yield_ : (Json) -> Unit raise JqError,
) -> Unit raise JqError {
  // Check variable reference
  if name.has_prefix("$") {
    if name == "$__loc__" {
      let m : Map[String, Json] = Map::new()
      m["file"] = Json::string("")
      m["line"] = Json::number(1.0)
      yield_(Json::object(m))
      return
    }
    match env.get_var(name) {
      Some(v) => {
        yield_(v)
        return
      }
      None => raise JqError("undefined variable: " + name)
    }
  }
  // Check filter argument reference (def parameters are filter closures)
  match env.get_filter_arg(name) {
    Some(filter) => {
      eval(filter, input, env, yield_)
      return
    }
    None => ()
  }
  // Check user-defined function
  match env.get_func(name) {
    Some((params, body)) => {
      let mut new_env = env
      // Bind each param to its corresponding arg filter as a filter closure
      for i = 0; i < params.length() && i < args.length(); i = i + 1 {
        new_env = new_env.bind_filter_arg(params[i], args[i])
      }
      eval(body, input, new_env, yield_)
      return
    }
    None => ()
  }
  // Builtins
  eval_builtin(name, args, input, env, yield_)
}

///|
/// Evaluate a built-in jq function.
fn eval_builtin(
  name : String,
  args : Array[Filter],
  input : Json,
  env : Scope,
  yield_ : (Json) -> Unit raise JqError,
) -> Unit raise JqError {
  match name {
    "length" =>
      match input {
        String(s) => yield_(Json::number(s.iter().count().to_double()))
        Array(arr) => yield_(Json::number(arr.length().to_double()))
        Object(map) => yield_(Json::number(map.length().to_double()))
        Null => yield_(Json::number(0.0))
        _ => raise JqError("cannot get length of " + json_type(input))
      }
    "keys" =>
      match input {
        Object(map) => {
          let ks : Array[String] = []
          map.each((k, _v) => ks.push(k))
          ks.sort()
          let result : Array[Json] = []
          for k in ks {
            result.push(Json::string(k))
          }
          yield_(Json::array(result))
        }
        Array(arr) => {
          let ks : Array[Json] = []
          for i = 0; i < arr.length(); i = i + 1 {
            ks.push(Json::number(i.to_double()))
          }
          yield_(Json::array(ks))
        }
        _ => raise JqError("cannot get keys of " + json_type(input))
      }
    "keys_unsorted" =>
      match input {
        Object(map) => {
          let ks : Array[Json] = []
          map.each((k, _v) => ks.push(Json::string(k)))
          yield_(Json::array(ks))
        }
        Array(arr) => {
          let ks : Array[Json] = []
          for i = 0; i < arr.length(); i = i + 1 {
            ks.push(Json::number(i.to_double()))
          }
          yield_(Json::array(ks))
        }
        _ => raise JqError("cannot get keys of " + json_type(input))
      }
    "values" =>
      match input {
        Null => ()
        _ => yield_(input)
      }
    "type" => yield_(Json::string(json_type(input)))
    "empty" => ()
    "add" =>
      match input {
        Array(arr) => {
          let mut acc : Json = Json::null()
          for v in arr {
            acc = arith_op(ArithOp::Add, acc, v)
          }
          yield_(acc)
        }
        _ => raise JqError("add requires array input")
      }
    "not" => yield_(Json::boolean(!(is_truthy(input))))
    "reverse" =>
      match input {
        Array(arr) => {
          let result : Array[Json] = []
          for i = arr.length() - 1; i >= 0; i = i - 1 {
            result.push(arr[i])
          }
          yield_(Json::array(result))
        }
        String(s) => {
          let chars = Array::from_iter(s.iter())
          let buf = StringBuilder::new()
          for i = chars.length() - 1; i >= 0; i = i - 1 {
            buf.write_char(chars[i])
          }
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("cannot reverse " + json_type(input))
      }
    "sort" =>
      match input {
        Array(arr) => {
          let sorted = arr.copy()
          sorted.sort_by((a, b) => compare_json(a, b))
          yield_(Json::array(sorted))
        }
        _ => raise JqError("sort requires array input")
      }
    "flatten" =>
      match input {
        Array(arr) => {
          let depth = if args.length() == 1 {
            let mut d = 1
            eval(args[0], input, env, v => {
              match v {
                Number(n, ..) => {
                  if n < 0.0 {
                    raise JqError("flatten depth must not be negative")
                  }
                  d = n.to_int()
                }
                _ => raise JqError("flatten depth must be a number")
              }
            })
            d
          } else {
            999999999
          }
          let result : Array[Json] = []
          flatten_impl(arr, depth, result)
          yield_(Json::array(result))
        }
        _ => raise JqError("flatten requires array input")
      }
    "unique" =>
      match input {
        Array(arr) => {
          let sorted = arr.copy()
          sorted.sort_by((a, b) => compare_json(a, b))
          let result : Array[Json] = []
          for i = 0; i < sorted.length(); i = i + 1 {
            if i == 0 || sorted[i] != sorted[i - 1] {
              result.push(sorted[i])
            }
          }
          yield_(Json::array(result))
        }
        _ => raise JqError("unique requires array input")
      }
    "min" =>
      match input {
        Array(arr) =>
          if arr.is_empty() {
            yield_(Json::null())
          } else {
            let mut m = arr[0]
            for i = 1; i < arr.length(); i = i + 1 {
              if compare_json(arr[i], m) < 0 {
                m = arr[i]
              }
            }
            yield_(m)
          }
        _ => raise JqError("min requires array input")
      }
    "max" =>
      match input {
        Array(arr) =>
          if arr.is_empty() {
            yield_(Json::null())
          } else {
            let mut m = arr[0]
            for i = 1; i < arr.length(); i = i + 1 {
              if compare_json(arr[i], m) > 0 {
                m = arr[i]
              }
            }
            yield_(m)
          }
        _ => raise JqError("max requires array input")
      }
    "tostring" =>
      match input {
        String(_) => yield_(input)
        _ => yield_(Json::string(input.stringify()))
      }
    "tonumber" =>
      match input {
        Number(_) => yield_(input)
        String(s) => {
          let n = parse_number_str(s)
          yield_(Json::number(n))
        }
        _ => raise JqError("cannot convert " + json_type(input) + " to number")
      }
    "tojson" => yield_(Json::string(input.stringify()))
    "fromjson" =>
      match input {
        String(s) => {
          let trimmed = s.trim(chars=" \t\n\r").to_string()
          if trimmed == "nan" || trimmed == "NaN" {
            yield_(Json::number(0.0 / 0.0))
          } else if trimmed == "infinity" || trimmed == "Infinity" {
            yield_(Json::number(1.0 / 0.0))
          } else if trimmed == "-infinity" || trimmed == "-Infinity" {
            yield_(Json::number(-1.0 / 0.0))
          } else {
            let json = @json.parse(s) catch {
              _ => raise JqError("invalid JSON string")
            }
            yield_(json)
          }
        }
        _ => raise JqError("fromjson requires string input")
      }
    "ascii_downcase" =>
      match input {
        String(s) => {
          let buf = StringBuilder::new()
          s.iter().each(c => buf.write_char(c.to_ascii_lowercase()))
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("ascii_downcase requires string input")
      }
    "ascii_upcase" =>
      match input {
        String(s) => {
          let buf = StringBuilder::new()
          s.iter().each(c => buf.write_char(c.to_ascii_uppercase()))
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("ascii_upcase requires string input")
      }
    "to_entries" =>
      match input {
        Object(map) => {
          let result : Array[Json] = []
          map.each((k, v) => {
            let entry : Map[String, Json] = Map::new()
            entry["key"] = Json::string(k)
            entry["value"] = v
            result.push(Json::object(entry))
          })
          yield_(Json::array(result))
        }
        _ => raise JqError("to_entries requires object input")
      }
    "from_entries" =>
      match input {
        Array(arr) => {
          let result : Map[String, Json] = Map::new()
          for item in arr {
            match item {
              Object(entry) => {
                let key = match entry.get("key") {
                  Some(String(k)) => k
                  Some(Number(n, ..)) => n.to_string()
                  _ =>
                    match entry.get("Key") {
                      Some(String(k)) => k
                      Some(Number(n, ..)) => n.to_string()
                      _ =>
                        match entry.get("name") {
                          Some(String(k)) => k
                          _ =>
                            match entry.get("Name") {
                              Some(String(k)) => k
                              _ => raise JqError("from_entries: invalid entry")
                            }
                        }
                    }
                }
                let value = match entry.get("value") {
                  Some(v) => v
                  None =>
                    match entry.get("Value") {
                      Some(v) => v
                      None => Json::null()
                    }
                }
                result[key] = value
              }
              _ => raise JqError("from_entries: expected object entries")
            }
          }
          yield_(Json::object(result))
        }
        _ => raise JqError("from_entries requires array input")
      }
    "with_entries" => {
      guard args.length() == 1 else {
        raise JqError("with_entries takes 1 argument")
      }
      let f = args[0]
      match input {
        Object(map) => {
          let result : Map[String, Json] = Map::new()
          map.each((k, v) => {
            let entry : Map[String, Json] = Map::new()
            entry["key"] = Json::string(k)
            entry["value"] = v
            let entry_json = Json::object(entry)
            eval(f, entry_json, env, new_entry => {
              match new_entry {
                Object(ne) =>
                  match ne.get("key") {
                    Some(String(nk)) => {
                      let nv = match ne.get("value") {
                        Some(v) => v
                        None => Json::null()
                      }
                      result[nk] = nv
                    }
                    _ => raise JqError("with_entries: key must be string")
                  }
                _ => raise JqError("with_entries: expected object result")
              }
            })
          })
          yield_(Json::object(result))
        }
        _ => raise JqError("with_entries requires object input")
      }
    }
    "recurse" => recurse_impl(input, yield_)
    "debug" => {
      println(input.stringify())
      yield_(input)
    }
    "map" => {
      guard args.length() == 1 else { raise JqError("map takes 1 argument") }
      let f = args[0]
      match input {
        Array(arr) => {
          let result : Array[Json] = []
          for item in arr {
            eval(f, item, env, v => result.push(v))
          }
          yield_(Json::array(result))
        }
        _ => raise JqError("map requires array input")
      }
    }
    "map_values" => {
      guard args.length() == 1 else {
        raise JqError("map_values takes 1 argument")
      }
      let f = args[0]
      match input {
        Object(map) => {
          let result : Map[String, Json] = Map::new()
          map.each((k, v) => eval(f, v, env, nv => result[k] = nv))
          yield_(Json::object(result))
        }
        Array(arr) => {
          let result : Array[Json] = []
          for item in arr {
            eval(f, item, env, v => result.push(v))
          }
          yield_(Json::array(result))
        }
        _ => raise JqError("map_values requires array or object input")
      }
    }
    "select" => {
      guard args.length() == 1 else { raise JqError("select takes 1 argument") }
      let f = args[0]
      eval(f, input, env, v => if is_truthy(v) { yield_(input) })
    }
    "has" => {
      guard args.length() == 1 else { raise JqError("has takes 1 argument") }
      eval(args[0], input, env, key => {
        match (input, key) {
          (Object(map), String(k)) => yield_(Json::boolean(map.contains(k)))
          (Array(arr), Number(n, ..)) =>
            yield_(
              Json::boolean(
                !(is_nan(n)) && n.to_int() >= 0 && n.to_int() < arr.length(),
              ),
            )
          _ => raise JqError("has: invalid arguments")
        }
      })
    }
    "any" =>
      if args.is_empty() {
        match input {
          Array(arr) => {
            let mut found = false
            for v in arr {
              if is_truthy(v) {
                found = true
                break
              }
            }
            yield_(Json::boolean(found))
          }
          _ => raise JqError("any requires array input")
        }
      } else if args.length() == 1 {
        let f = args[0]
        match input {
          Array(arr) => {
            let mut found = false
            for item in arr {
              eval(f, item, env, v => if is_truthy(v) { found = true })
              if found {
                break
              }
            }
            yield_(Json::boolean(found))
          }
          _ => raise JqError("any requires array input")
        }
      } else if args.length() == 2 {
        let gen = args[0]
        let cond = args[1]
        let mut found = false
        eval(gen, input, env, v => {
          if !(found) {
            eval(cond, v, env, cv => {
              if is_truthy(cv) {
                found = true
                raise JqError("__break__")
              }
            })
          }
        }) catch {
          JqError("__break__") => ()
          e => raise e
        }
        yield_(Json::boolean(found))
      } else {
        raise JqError("any takes 0, 1, or 2 arguments")
      }
    "all" =>
      if args.is_empty() {
        match input {
          Array(arr) => {
            let mut ok = true
            for v in arr {
              if !(is_truthy(v)) {
                ok = false
                break
              }
            }
            yield_(Json::boolean(ok))
          }
          _ => raise JqError("all requires array input")
        }
      } else if args.length() == 1 {
        let f = args[0]
        match input {
          Array(arr) => {
            let mut ok = true
            for item in arr {
              eval(f, item, env, v => if !(is_truthy(v)) { ok = false })
              if !(ok) {
                break
              }
            }
            yield_(Json::boolean(ok))
          }
          _ => raise JqError("all requires array input")
        }
      } else if args.length() == 2 {
        let gen = args[0]
        let cond = args[1]
        let mut ok = true
        eval(gen, input, env, v => {
          if ok {
            eval(cond, v, env, cv => {
              if !(is_truthy(cv)) {
                ok = false
                raise JqError("__break__")
              }
            })
          }
        }) catch {
          JqError("__break__") => ()
          e => raise e
        }
        yield_(Json::boolean(ok))
      } else {
        raise JqError("all takes 0, 1, or 2 arguments")
      }
    "sort_by" => {
      guard args.length() == 1 else {
        raise JqError("sort_by takes 1 argument")
      }
      let f = args[0]
      match input {
        Array(arr) => {
          let keyed : Array[(Json, Json)] = []
          for item in arr {
            let keys : Array[Json] = []
            eval(f, item, env, v => keys.push(v))
            let key = if keys.length() == 1 {
              keys[0]
            } else {
              Json::array(keys)
            }
            keyed.push((key, item))
          }
          keyed.sort_by((a, b) => compare_json(a.0, b.0))
          let result : Array[Json] = []
          for entry in keyed {
            result.push(entry.1)
          }
          yield_(Json::array(result))
        }
        _ => raise JqError("sort_by requires array input")
      }
    }
    "group_by" => {
      guard args.length() == 1 else {
        raise JqError("group_by takes 1 argument")
      }
      let f = args[0]
      match input {
        Array(arr) => {
          let keyed : Array[(Json, Json)] = []
          for item in arr {
            let mut key = Json::null()
            eval(f, item, env, v => key = v)
            keyed.push((key, item))
          }
          keyed.sort_by((a, b) => compare_json(a.0, b.0))
          let groups : Array[Json] = []
          let mut current_group : Array[Json] = []
          let mut current_key = Json::null()
          for i = 0; i < keyed.length(); i = i + 1 {
            if i == 0 || keyed[i].0 != current_key {
              if !(current_group.is_empty()) {
                groups.push(Json::array(current_group))
              }
              current_group = []
              current_key = keyed[i].0
            }
            current_group.push(keyed[i].1)
          }
          if !(current_group.is_empty()) {
            groups.push(Json::array(current_group))
          }
          yield_(Json::array(groups))
        }
        _ => raise JqError("group_by requires array input")
      }
    }
    "unique_by" => {
      guard args.length() == 1 else {
        raise JqError("unique_by takes 1 argument")
      }
      let f = args[0]
      match input {
        Array(arr) => {
          let keyed : Array[(Json, Json)] = []
          for item in arr {
            let mut key = Json::null()
            eval(f, item, env, v => key = v)
            keyed.push((key, item))
          }
          keyed.sort_by((a, b) => compare_json(a.0, b.0))
          let result : Array[Json] = []
          for i = 0; i < keyed.length(); i = i + 1 {
            if i == 0 || keyed[i].0 != keyed[i - 1].0 {
              result.push(keyed[i].1)
            }
          }
          yield_(Json::array(result))
        }
        _ => raise JqError("unique_by requires array input")
      }
    }
    "min_by" => {
      guard args.length() == 1 else { raise JqError("min_by takes 1 argument") }
      let f = args[0]
      match input {
        Array(arr) =>
          if arr.is_empty() {
            yield_(Json::null())
          } else {
            let mut min_key = Json::null()
            let mut min_val = arr[0]
            eval(f, arr[0], env, v => min_key = v)
            for i = 1; i < arr.length(); i = i + 1 {
              let mut key = Json::null()
              eval(f, arr[i], env, v => key = v)
              if compare_json(key, min_key) < 0 {
                min_key = key
                min_val = arr[i]
              }
            }
            yield_(min_val)
          }
        _ => raise JqError("min_by requires array input")
      }
    }
    "max_by" => {
      guard args.length() == 1 else { raise JqError("max_by takes 1 argument") }
      let f = args[0]
      match input {
        Array(arr) =>
          if arr.is_empty() {
            yield_(Json::null())
          } else {
            let mut max_key = Json::null()
            let mut max_val = arr[0]
            eval(f, arr[0], env, v => max_key = v)
            for i = 1; i < arr.length(); i = i + 1 {
              let mut key = Json::null()
              eval(f, arr[i], env, v => key = v)
              if compare_json(key, max_key) > 0 {
                max_key = key
                max_val = arr[i]
              }
            }
            yield_(max_val)
          }
        _ => raise JqError("max_by requires array input")
      }
    }
    "contains" => {
      guard args.length() == 1 else {
        raise JqError("contains takes 1 argument")
      }
      eval(args[0], input, env, other => {
        yield_(Json::boolean(json_contains(input, other)))
      })
    }
    "startswith" => {
      guard args.length() == 1 else {
        raise JqError("startswith takes 1 argument")
      }
      eval(args[0], input, env, suffix => {
        match (input, suffix) {
          (String(s), String(p)) => yield_(Json::boolean(s.has_prefix(p)))
          _ => raise JqError("startswith requires string arguments")
        }
      })
    }
    "endswith" => {
      guard args.length() == 1 else {
        raise JqError("endswith takes 1 argument")
      }
      eval(args[0], input, env, suffix => {
        match (input, suffix) {
          (String(s), String(p)) => yield_(Json::boolean(s.has_suffix(p)))
          _ => raise JqError("endswith requires string arguments")
        }
      })
    }
    "ltrimstr" => {
      guard args.length() == 1 else {
        raise JqError("ltrimstr takes 1 argument")
      }
      eval(args[0], input, env, prefix => {
        match (input, prefix) {
          (String(s), String(p)) =>
            match s.strip_prefix(p) {
              Some(rest) => yield_(Json::string(rest.to_string()))
              None => yield_(input)
            }
          _ => yield_(input)
        }
      })
    }
    "rtrimstr" => {
      guard args.length() == 1 else {
        raise JqError("rtrimstr takes 1 argument")
      }
      eval(args[0], input, env, suffix => {
        match (input, suffix) {
          (String(s), String(p)) =>
            match s.strip_suffix(p) {
              Some(rest) => yield_(Json::string(rest.to_string()))
              None => yield_(input)
            }
          _ => yield_(input)
        }
      })
    }
    "trim" =>
      match input {
        String(s) => {
          let chars = Array::from_iter(s.iter())
          let len = chars.length()
          let mut start = 0
          while start < len && chars[start].is_whitespace() {
            start = start + 1
          }
          let mut end = len - 1
          while end >= start && chars[end].is_whitespace() {
            end = end - 1
          }
          let buf = StringBuilder::new()
          for i = start; i <= end; i = i + 1 {
            buf.write_char(chars[i])
          }
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("trim input must be a string")
      }
    "ltrim" =>
      match input {
        String(s) => {
          let chars = Array::from_iter(s.iter())
          let len = chars.length()
          let mut start = 0
          while start < len && chars[start].is_whitespace() {
            start = start + 1
          }
          let buf = StringBuilder::new()
          for i = start; i < len; i = i + 1 {
            buf.write_char(chars[i])
          }
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("trim input must be a string")
      }
    "rtrim" =>
      match input {
        String(s) => {
          let chars = Array::from_iter(s.iter())
          let len = chars.length()
          let mut end = len - 1
          while end >= 0 && chars[end].is_whitespace() {
            end = end - 1
          }
          let buf = StringBuilder::new()
          for i = 0; i <= end; i = i + 1 {
            buf.write_char(chars[i])
          }
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("trim input must be a string")
      }
    "trimstr" => {
      guard args.length() == 1 else {
        raise JqError("trimstr takes 1 argument")
      }
      eval(args[0], input, env, affix => {
        match (input, affix) {
          (String(s), String(p)) => {
            let mut result = s
            match result.strip_prefix(p) {
              Some(rest) => result = rest.to_string()
              None => ()
            }
            match result.strip_suffix(p) {
              Some(rest) => result = rest.to_string()
              None => ()
            }
            yield_(Json::string(result))
          }
          _ => yield_(input)
        }
      })
    }
    "split" => {
      guard args.length() == 1 else { raise JqError("split takes 1 argument") }
      eval(args[0], input, env, sep => {
        match (input, sep) {
          (String(s), String(d)) => {
            let parts : Array[Json] = []
            s.split(d).each(part => parts.push(Json::string(part.to_string())))
            yield_(Json::array(parts))
          }
          _ => raise JqError("split requires string arguments")
        }
      })
    }
    "join" => {
      guard args.length() == 1 else { raise JqError("join takes 1 argument") }
      eval(args[0], input, env, sep => {
        match (input, sep) {
          (Array(arr), String(d)) => {
            let buf = StringBuilder::new()
            for i = 0; i < arr.length(); i = i + 1 {
              if i > 0 {
                buf.write_string(d)
              }
              match arr[i] {
                String(s) => buf.write_string(s)
                Null => ()
                Number(n, ..) => buf.write_string(n.to_string())
                True => buf.write_string("true")
                False => buf.write_string("false")
                other =>
                  raise JqError(
                    "string (\"" +
                    buf.to_string() +
                    "\") and " +
                    json_type(other) +
                    " (" +
                    other.stringify() +
                    ") cannot be added",
                  )
              }
            }
            yield_(Json::string(buf.to_string()))
          }
          _ => raise JqError("join requires array input and string separator")
        }
      })
    }
    "limit" => {
      guard args.length() == 2 else { raise JqError("limit takes 2 arguments") }
      eval(args[0], input, env, nv => {
        match nv {
          Number(n, ..) => {
            let max = n.to_int()
            if max < 0 {
              raise JqError("limit doesn't support negative count")
            }
            if max > 0 {
              let mut count = 0
              eval(args[1], input, env, v => {
                if count < max {
                  yield_(v)
                  count += 1
                }
                if count >= max {
                  raise JqError("__break__")
                }
              }) catch {
                JqError("__break__") => ()
                e => raise e
              }
            }
          }
          _ => raise JqError("limit: first argument must be a number")
        }
      })
    }
    "first" =>
      if args.length() == 1 {
        let mut found = false
        eval(args[0], input, env, v => {
          if !(found) {
            found = true
            yield_(v)
            raise JqError("__break__")
          }
        }) catch {
          JqError("__break__") => ()
          e => raise e
        }
      } else {
        // first without args = .[0]
        match input {
          Array(arr) =>
            if arr.is_empty() {
              yield_(Json::null())
            } else {
              yield_(arr[0])
            }
          _ => raise JqError("first requires array input")
        }
      }
    "last" =>
      if args.length() == 1 {
        let mut last_val : Json? = None
        eval(args[0], input, env, v => last_val = Some(v))
        match last_val {
          Some(v) => yield_(v)
          None => ()
        }
      } else {
        match input {
          Array(arr) =>
            if arr.is_empty() {
              yield_(Json::null())
            } else {
              yield_(arr[arr.length() - 1])
            }
          _ => raise JqError("last requires array input")
        }
      }
    "range" =>
      if args.length() == 1 {
        eval(args[0], input, env, nv => {
          match nv {
            Number(n, ..) => {
              let max = n.to_int()
              for i = 0; i < max; i = i + 1 {
                yield_(Json::number(i.to_double()))
              }
            }
            _ => raise JqError("range: argument must be a number")
          }
        })
      } else if args.length() == 2 {
        eval(args[0], input, env, from_v => {
          eval(args[1], input, env, to_v => {
            match (from_v, to_v) {
              (Number(f, ..), Number(t, ..)) => {
                let start = f.to_int()
                let end = t.to_int()
                for i = start; i < end; i = i + 1 {
                  yield_(Json::number(i.to_double()))
                }
              }
              _ => raise JqError("range: arguments must be numbers")
            }
          })
        })
      } else if args.length() == 3 {
        eval(args[0], input, env, from_v => {
          eval(args[1], input, env, to_v => {
            eval(args[2], input, env, step_v => {
              match (from_v, to_v, step_v) {
                (Number(f, ..), Number(t, ..), Number(s, ..)) =>
                  if s > 0.0 {
                    let mut i = f
                    while i < t {
                      yield_(Json::number(i))
                      i = i + s
                    }
                  } else if s < 0.0 {
                    let mut i = f
                    while i > t {
                      yield_(Json::number(i))
                      i = i + s
                    }
                  } else {
                    raise JqError("range: step cannot be zero")
                  }
                _ => raise JqError("range: arguments must be numbers")
              }
            })
          })
        })
      } else {
        raise JqError("range takes 1, 2, or 3 arguments")
      }
    "input" => raise JqError("input not supported")
    "inputs" => raise JqError("inputs not supported")
    "path" => {
      guard args.length() == 1 else { raise JqError("path takes 1 argument") }
      eval_path(args[0], input, env, p => yield_(Json::array(p)))
    }
    "setpath" => {
      guard args.length() == 2 else {
        raise JqError("setpath takes 2 arguments")
      }
      eval(args[0], input, env, path_val => {
        eval(args[1], input, env, value => {
          match path_val {
            Array(p) => yield_(setpath_impl(input, p, value))
            _ => raise JqError("setpath: first argument must be an array")
          }
        })
      })
    }
    "delpaths" => {
      guard args.length() == 1 else {
        raise JqError("delpaths takes 1 argument")
      }
      eval(args[0], input, env, paths_val => {
        match paths_val {
          Array(arr) => {
            let paths : Array[Array[Json]] = []
            for item in arr {
              match item {
                Array(p) => paths.push(p)
                _ => raise JqError("delpaths: each path must be an array")
              }
            }
            yield_(delpaths_impl(input, paths))
          }
          _ => raise JqError("Paths must be specified as an array")
        }
      })
    }
    "del" => {
      guard args.length() == 1 else { raise JqError("del takes 1 argument") }
      let paths : Array[Array[Json]] = []
      eval_path(args[0], input, env, p => paths.push(p))
      yield_(delpaths_impl(input, paths))
    }
    "pick" => {
      guard args.length() == 1 else { raise JqError("pick takes 1 argument") }
      let mut result : Json = Json::null()
      eval_path(args[0], input, env, p => {
        let value = getpath_impl(input, p)
        result = setpath_impl(result, p, value)
      })
      yield_(result)
    }
    "paths" =>
      if args.length() == 1 {
        // paths(f) - return paths where f is truthy
        let f = args[0]
        fn collect_paths_filter(
          v : Json,
          prefix : Array[Json],
          f : Filter,
          env : Scope,
          yield_ : (Json) -> Unit raise JqError,
        ) -> Unit raise JqError {
          match v {
            Array(arr) =>
              for i = 0; i < arr.length(); i = i + 1 {
                let p = prefix.copy()
                p.push(Json::number(i.to_double()))
                let mut is_truthy_result = false
                eval(f, arr[i], env, fv => {
                  if is_truthy(fv) {
                    is_truthy_result = true
                  }
                })
                if is_truthy_result {
                  yield_(Json::array(p))
                }
                collect_paths_filter(arr[i], p, f, env, yield_)
              }
            Object(map) =>
              map.each((k, val) => {
                let p = prefix.copy()
                p.push(Json::string(k))
                let mut is_truthy_result = false
                eval(f, val, env, fv => {
                  if is_truthy(fv) {
                    is_truthy_result = true
                  }
                })
                if is_truthy_result {
                  yield_(Json::array(p))
                }
                collect_paths_filter(val, p, f, env, yield_)
              })
            _ => ()
          }
        }

        collect_paths_filter(input, [], f, env, yield_)
      } else {
        fn collect_paths(
          v : Json,
          prefix : Array[Json],
          yield_ : (Json) -> Unit raise JqError,
        ) -> Unit raise JqError {
          match v {
            Array(arr) =>
              for i = 0; i < arr.length(); i = i + 1 {
                let p = prefix.copy()
                p.push(Json::number(i.to_double()))
                yield_(Json::array(p))
                collect_paths(arr[i], p, yield_)
              }
            Object(map) =>
              map.each((k, val) => {
                let p = prefix.copy()
                p.push(Json::string(k))
                yield_(Json::array(p))
                collect_paths(val, p, yield_)
              })
            _ => ()
          }
        }

        collect_paths(input, [], yield_)
      }
    "getpath" => {
      guard args.length() == 1 else {
        raise JqError("getpath takes 1 argument")
      }
      eval(args[0], input, env, path_val => {
        match path_val {
          Array(parts) => yield_(getpath_impl(input, parts))
          _ => raise JqError("getpath: argument must be an array")
        }
      })
    }
    "null" => yield_(Json::null())
    "true" => yield_(Json::boolean(true))
    "false" => yield_(Json::boolean(false))
    "infinite" => yield_(Json::number(1.0 / 0.0))
    "nan" => yield_(Json::number(0.0 / 0.0))
    "isinfinite" =>
      match input {
        Number(n, ..) =>
          yield_(Json::boolean(n == 1.0 / 0.0 || n == -1.0 / 0.0))
        _ => yield_(Json::boolean(false))
      }
    "isnan" =>
      match input {
        Number(n, ..) => yield_(Json::boolean(is_nan(n)))
        _ => yield_(Json::boolean(false))
      }
    "isnormal" =>
      match input {
        Number(n, ..) =>
          yield_(
            Json::boolean(
              !(is_nan(n)) && n != 0.0 && n != 1.0 / 0.0 && n != -1.0 / 0.0,
            ),
          )
        _ => yield_(Json::boolean(false))
      }
    "isfinite" =>
      match input {
        Number(n, ..) =>
          yield_(
            Json::boolean(!(is_nan(n)) && n != 1.0 / 0.0 && n != -1.0 / 0.0),
          )
        _ => yield_(Json::boolean(false))
      }
    "abs" | "fabs" =>
      match input {
        Number(n, ..) => yield_(Json::number(if n < 0.0 { -n } else { n }))
        _ => yield_(input)
      }
    "floor" =>
      match input {
        Number(n, ..) => {
          let i = if n < 0.0 && n != n.to_int().to_double() {
            n.to_int() - 1
          } else {
            n.to_int()
          }
          yield_(Json::number(i.to_double()))
        }
        _ => raise JqError("floor requires number input")
      }
    "ceil" =>
      match input {
        Number(n, ..) => {
          let i = n.to_int()
          let result = if i.to_double() < n { i + 1 } else { i }
          yield_(Json::number(result.to_double()))
        }
        _ => raise JqError("ceil requires number input")
      }
    "round" =>
      match input {
        Number(n, ..) => yield_(Json::number((n + 0.5).to_int().to_double()))
        _ => raise JqError("round requires number input")
      }
    "ascii" =>
      match input {
        Number(n, ..) =>
          yield_(Json::string(n.to_int().unsafe_to_char().to_string()))
        _ => raise JqError("ascii requires number input")
      }
    "utf8bytelength" =>
      match input {
        String(s) =>
          yield_(Json::number(string_to_utf8(s).length().to_double()))
        _ =>
          raise JqError(
            json_type(input) +
            " (" +
            input.stringify() +
            ") only strings have UTF-8 byte length",
          )
      }
    "explode" =>
      match input {
        String(s) => {
          let result : Array[Json] = []
          s.iter().each(c => result.push(Json::number(c.to_int().to_double())))
          yield_(Json::array(result))
        }
        _ => raise JqError("explode requires string input")
      }
    "implode" =>
      match input {
        Array(arr) => {
          let buf = StringBuilder::new()
          for item in arr {
            match item {
              Number(n, ..) => {
                let cp = n.to_int()
                if cp >= 0 && cp <= 1114111 {
                  buf.write_char(cp.unsafe_to_char())
                }
              }
              _ => raise JqError("implode: array must contain numbers")
            }
          }
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("implode requires array input")
      }
    "indices" | "index" => {
      guard args.length() == 1 else {
        raise JqError(name + " takes 1 argument")
      }
      eval(args[0], input, env, target => {
        match (input, target) {
          (String(s), String(t)) =>
            if name == "index" {
              match s.find(t) {
                Some(idx) => yield_(Json::number(idx.to_double()))
                None => yield_(Json::null())
              }
            } else {
              let result : Array[Json] = []
              let chars_s = Array::from_iter(s.iter())
              let chars_t = Array::from_iter(t.iter())
              if chars_t.length() > 0 {
                for i = 0; i <= chars_s.length() - chars_t.length(); i = i + 1 {
                  let mut found = true
                  for j = 0; j < chars_t.length(); j = j + 1 {
                    if chars_s[i + j] != chars_t[j] {
                      found = false
                      break
                    }
                  }
                  if found {
                    result.push(Json::number(i.to_double()))
                  }
                }
              }
              yield_(Json::array(result))
            }
          (Array(arr), Array(sub)) =>
            if name == "index" {
              let mut idx : Json = Json::null()
              if sub.length() > 0 {
                for i = 0; i <= arr.length() - sub.length(); i = i + 1 {
                  let mut found = true
                  for j = 0; j < sub.length(); j = j + 1 {
                    if arr[i + j] != sub[j] {
                      found = false
                      break
                    }
                  }
                  if found {
                    idx = Json::number(i.to_double())
                    break
                  }
                }
              }
              yield_(idx)
            } else {
              let result : Array[Json] = []
              if sub.length() > 0 {
                for i = 0; i <= arr.length() - sub.length(); i = i + 1 {
                  let mut found = true
                  for j = 0; j < sub.length(); j = j + 1 {
                    if arr[i + j] != sub[j] {
                      found = false
                      break
                    }
                  }
                  if found {
                    result.push(Json::number(i.to_double()))
                  }
                }
              }
              yield_(Json::array(result))
            }
          (Array(arr), _) =>
            if name == "index" {
              let mut idx : Json = Json::null()
              for i = 0; i < arr.length(); i = i + 1 {
                if arr[i] == target {
                  idx = Json::number(i.to_double())
                  break
                }
              }
              yield_(idx)
            } else {
              let result : Array[Json] = []
              for i = 0; i < arr.length(); i = i + 1 {
                if arr[i] == target {
                  result.push(Json::number(i.to_double()))
                }
              }
              yield_(Json::array(result))
            }
          _ => raise JqError(name + ": requires string or array input")
        }
      })
    }
    "rindex" => {
      guard args.length() == 1 else { raise JqError("rindex takes 1 argument") }
      eval(args[0], input, env, target => {
        match (input, target) {
          (String(s), String(t)) => {
            let chars_s = Array::from_iter(s.iter())
            let chars_t = Array::from_iter(t.iter())
            let mut idx : Json = Json::null()
            if chars_t.length() > 0 {
              for i = chars_s.length() - chars_t.length(); i >= 0; i = i - 1 {
                let mut found = true
                for j = 0; j < chars_t.length(); j = j + 1 {
                  if chars_s[i + j] != chars_t[j] {
                    found = false
                    break
                  }
                }
                if found {
                  idx = Json::number(i.to_double())
                  break
                }
              }
            }
            yield_(idx)
          }
          (Array(arr), _) => {
            let mut idx : Json = Json::null()
            for i = arr.length() - 1; i >= 0; i = i - 1 {
              if arr[i] == target {
                idx = Json::number(i.to_double())
                break
              }
            }
            yield_(idx)
          }
          _ => raise JqError("rindex: requires string or array input")
        }
      })
    }
    "inside" => {
      guard args.length() == 1 else { raise JqError("inside takes 1 argument") }
      eval(args[0], input, env, other => {
        yield_(Json::boolean(json_contains(other, input)))
      })
    }
    "test" => {
      guard args.length() == 1 else { raise JqError("test takes 1 argument") }
      eval(args[0], input, env, pattern => {
        match (input, pattern) {
          (String(s), String(p)) => yield_(Json::boolean(s.contains(p)))
          _ => raise JqError("test requires string arguments")
        }
      })
    }
    "walk" => {
      guard args.length() == 1 else { raise JqError("walk takes 1 argument") }
      let f = args[0]
      walk_impl(input, f, env, yield_)
    }
    "transpose" =>
      match input {
        Array(arr) => {
          let mut max_len = 0
          for item in arr {
            match item {
              Array(sub) => if sub.length() > max_len { max_len = sub.length() }
              _ => ()
            }
          }
          let result : Array[Json] = []
          for j = 0; j < max_len; j = j + 1 {
            let col : Array[Json] = []
            for item in arr {
              match item {
                Array(sub) =>
                  if j < sub.length() {
                    col.push(sub[j])
                  } else {
                    col.push(Json::null())
                  }
                _ => col.push(Json::null())
              }
            }
            result.push(Json::array(col))
          }
          yield_(Json::array(result))
        }
        _ => raise JqError("transpose requires array input")
      }
    "numbers" =>
      match input {
        Number(_) => yield_(input)
        _ => ()
      }
    "strings" =>
      match input {
        String(_) => yield_(input)
        _ => ()
      }
    "booleans" =>
      match input {
        True | False => yield_(input)
        _ => ()
      }
    "nulls" =>
      match input {
        Null => yield_(input)
        _ => ()
      }
    "arrays" =>
      match input {
        Array(_) => yield_(input)
        _ => ()
      }
    "objects" =>
      match input {
        Object(_) => yield_(input)
        _ => ()
      }
    "iterables" =>
      match input {
        Array(_) | Object(_) => yield_(input)
        _ => ()
      }
    "scalars" =>
      match input {
        Null | True | False | Number(_) | String(_) => yield_(input)
        _ => ()
      }
    "bsearch" => {
      guard args.length() == 1 else {
        raise JqError("bsearch takes 1 argument")
      }
      eval(args[0], input, env, target => {
        match input {
          Array(arr) => {
            let mut lo = 0
            let mut hi = arr.length() - 1
            let mut found = false
            let mut result = 0
            while lo <= hi {
              let mid = (lo + hi) / 2
              let cmp = compare_json(arr[mid], target)
              if cmp == 0 {
                result = mid
                found = true
                break
              } else if cmp < 0 {
                lo = mid + 1
              } else {
                hi = mid - 1
              }
            }
            if found {
              yield_(Json::number(result.to_double()))
            } else {
              yield_(Json::number((-lo - 1).to_double()))
            }
          }
          _ =>
            raise JqError(
              json_type(input) +
              " (" +
              input.stringify() +
              ") cannot be searched from",
            )
        }
      })
    }
    "env" => yield_(Json::null())
    "sqrt" =>
      match input {
        Number(n, ..) => yield_(Json::number(n.sqrt()))
        _ => raise JqError("sqrt requires number input")
      }
    "sin" =>
      match input {
        Number(n, ..) => yield_(Json::number(@math.sin(n)))
        _ => raise JqError("sin requires number input")
      }
    "cos" =>
      match input {
        Number(n, ..) => yield_(Json::number(@math.cos(n)))
        _ => raise JqError("cos requires number input")
      }
    "atan" =>
      match input {
        Number(n, ..) => yield_(Json::number(@math.atan(n)))
        _ => raise JqError("atan requires number input")
      }
    "atan2" => {
      guard args.length() == 1 else { raise JqError("atan2 takes 1 argument") }
      eval(args[0], input, env, yv => {
        match (input, yv) {
          (Number(a, ..), Number(b, ..)) =>
            yield_(Json::number(@math.atan2(a, b)))
          _ => raise JqError("atan2 requires number arguments")
        }
      })
    }
    "log" =>
      match input {
        Number(n, ..) => yield_(Json::number(@math.ln(n)))
        _ => raise JqError("log requires number input")
      }
    "log2" =>
      match input {
        Number(n, ..) => yield_(Json::number(@math.log2(n)))
        _ => raise JqError("log2 requires number input")
      }
    "exp" =>
      match input {
        Number(n, ..) => yield_(Json::number(@math.exp(n)))
        _ => raise JqError("exp requires number input")
      }
    "exp2" =>
      match input {
        Number(n, ..) => yield_(Json::number(@math.pow(2.0, n)))
        _ => raise JqError("exp2 requires number input")
      }
    "pow" => {
      guard args.length() == 2 else { raise JqError("pow takes 2 arguments") }
      eval(args[0], input, env, base_v => {
        eval(args[1], input, env, exp_v => {
          match (base_v, exp_v) {
            (Number(b, ..), Number(e, ..)) =>
              yield_(Json::number(@math.pow(b, e)))
            _ => raise JqError("pow requires number arguments")
          }
        })
      })
    }
    "builtins" => {
      let names : Array[String] = [
        "length/0", "keys/0", "values/0", "type/0", "empty/0", "add/0", "not/0",
        "map/1", "select/1", "sort/0", "sort_by/1", "group_by/1", "unique/0", "unique_by/1",
        "reverse/0", "flatten/0", "flatten/1", "min/0", "max/0", "min_by/1", "max_by/1",
        "contains/1", "has/1", "any/0", "any/1", "all/0", "all/1", "range/1", "range/2",
        "range/3", "first/0", "first/1", "last/0", "last/1", "limit/2", "nth/1",
        "nth/2", "skip/2", "tostring/0", "tonumber/0", "toboolean/0", "tojson/0",
        "fromjson/0", "ascii_downcase/0", "ascii_upcase/0", "split/1", "join/1",
        "test/1", "startswith/1", "endswith/1", "ltrimstr/1", "rtrimstr/1", "trim/0",
        "ltrim/0", "rtrim/0", "trimstr/1", "to_entries/0", "from_entries/0", "with_entries/1",
        "keys_unsorted/0", "recurse/0", "recurse/1", "walk/1", "transpose/0", "explode/0",
        "implode/0", "abs/0", "fabs/0", "floor/0", "ceil/0", "round/0", "bsearch/1",
        "indices/1", "index/1", "rindex/1", "inside/1", "isnan/0", "isinfinite/0",
        "isfinite/0", "isnormal/0", "infinite/0", "nan/0", "error/0", "error/1",
        "debug/0", "paths/0", "paths/1", "path/1", "getpath/1", "setpath/2", "delpaths/1",
        "del/1", "pick/1", "map_values/1", "IN/1", "IN/2", "while/2", "until/2",
        "isempty/1", "ascii/0", "null/0", "true/0", "false/0", "numbers/0", "strings/0",
        "booleans/0", "nulls/0", "arrays/0", "objects/0", "iterables/0", "scalars/0",
        "sqrt/0", "sin/0", "cos/0", "atan/0", "atan2/1", "log/0", "log2/0", "exp/0",
        "exp2/0", "pow/2", "@base64/0", "@base64d/0", "@json/0", "@text/0", "@html/0",
        "@uri/0",
      ]
      let arr = names.map(fn(s) { Json::string(s) })
      arr.sort_by(fn(a, b) { compare_json(a, b) })
      yield_(Json::array(arr))
    }
    "@base64" =>
      match input {
        String(s) => {
          let bytes = string_to_utf8(s)
          let encoded = @base64.encode(bytes[:])
          yield_(Json::string(encoded))
        }
        _ => raise JqError("@base64 requires string input")
      }
    "@base64d" =>
      match input {
        String(s) => {
          let bytes = @base64.decode(s) catch {
            _ => raise JqError("@base64d: invalid base64 input")
          }
          let buf = StringBuilder::new()
          utf8_to_string(bytes, buf)
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("@base64d requires string input")
      }
    "@json" => yield_(Json::string(input.stringify()))
    "@text" =>
      match input {
        String(_) => yield_(input)
        _ => yield_(Json::string(input.stringify()))
      }
    "@html" =>
      match input {
        String(s) => {
          let buf = StringBuilder::new()
          s
          .iter()
          .each(c => {
            match c {
              '<' => buf.write_string("<")
              '>' => buf.write_string(">")
              '&' => buf.write_string("&")
              '\'' => buf.write_string("'")
              '"' => buf.write_string(""")
              _ => buf.write_char(c)
            }
          })
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("@html requires string input")
      }
    "@uri" =>
      match input {
        String(s) => {
          let bytes = string_to_utf8(s)
          let buf = StringBuilder::new()
          let hex = Array::from_iter("0123456789ABCDEF".iter())
          for i = 0; i < bytes.length(); i = i + 1 {
            let b = bytes[i].to_int()
            if (b >= 'A'.to_int() && b <= 'Z'.to_int()) ||
              (b >= 'a'.to_int() && b <= 'z'.to_int()) ||
              (b >= '0'.to_int() && b <= '9'.to_int()) ||
              b == '-'.to_int() ||
              b == '_'.to_int() ||
              b == '.'.to_int() ||
              b == '~'.to_int() {
              buf.write_char(b.unsafe_to_char())
            } else {
              buf.write_char('%')
              buf.write_char(hex[(b >> 4) & 0xf])
              buf.write_char(hex[b & 0xf])
            }
          }
          yield_(Json::string(buf.to_string()))
        }
        _ => raise JqError("@uri requires string input")
      }
    "IN" =>
      if args.length() == 1 {
        // IN(expr): check if input equals any output of expr
        let mut found = false
        eval(args[0], input, env, v => {
          if v == input {
            found = true
            raise JqError("__break__")
          }
        }) catch {
          JqError("__break__") => ()
          e => raise e
        }
        yield_(Json::boolean(found))
      } else if args.length() == 2 {
        // IN(gen; expr): yield single boolean — true if ANY gen output matches any expr output
        let mut found = false
        eval(args[0], input, env, gv => {
          eval(args[1], input, env, ev => {
            if ev == gv {
              found = true
              raise JqError("__break__")
            }
          }) catch {
            JqError("__break__") => ()
            e => raise e
          }
          if found {
            raise JqError("__break__")
          }
        }) catch {
          JqError("__break__") => ()
          e => raise e
        }
        yield_(Json::boolean(found))
      }
    "error" =>
      if args.length() == 0 {
        raise JqError(jq_error_encode(input))
      } else {
        eval(args[0], input, env, v => raise JqError(jq_error_encode(v)))
      }
    "isempty" => {
      guard args.length() == 1 else {
        raise JqError("isempty takes 1 argument")
      }
      let mut found = false
      eval(args[0], input, env, _ => {
        found = true
        raise JqError("__break__")
      }) catch {
        JqError("__break__") => ()
        e => raise e
      }
      yield_(Json::boolean(!(found)))
    }
    "nth" =>
      if args.length() == 2 {
        eval(args[0], input, env, nv => {
          match nv {
            Number(n, ..) => {
              let target = n.to_int()
              if target < 0 {
                raise JqError("nth doesn't support negative indices")
              }
              let mut count = 0
              eval(args[1], input, env, v => {
                if count == target {
                  yield_(v)
                  raise JqError("__break__")
                }
                count += 1
              }) catch {
                JqError("__break__") => ()
                e => raise e
              }
            }
            _ => raise JqError("nth: index must be a number")
          }
        })
      } else if args.length() == 1 {
        // nth(n) = first(.[n:] | .[0]) — but simpler: get nth output of .[]
        eval(args[0], input, env, nv => {
          match nv {
            Number(n, ..) => {
              let target = n.to_int()
              match input {
                Array(arr) =>
                  if target >= 0 && target < arr.length() {
                    yield_(arr[target])
                  } else {
                    yield_(Json::null())
                  }
                _ => raise JqError("nth requires array input")
              }
            }
            _ => raise JqError("nth: index must be a number")
          }
        })
      } else {
        raise JqError("nth takes 1 or 2 arguments")
      }
    "skip" => {
      guard args.length() == 2 else { raise JqError("skip takes 2 arguments") }
      eval(args[0], input, env, nv => {
        match nv {
          Number(n, ..) => {
            let target = n.to_int()
            if target < 0 {
              raise JqError("skip doesn't support negative count")
            }
            let mut count = 0
            eval(args[1], input, env, v => {
              if count >= target {
                yield_(v)
              }
              count += 1
            })
          }
          _ => raise JqError("skip: first argument must be a number")
        }
      })
    }
    "toboolean" =>
      match input {
        True | False => yield_(input)
        String(s) =>
          match s {
            "true" => yield_(Json::boolean(true))
            "false" => yield_(Json::boolean(false))
            _ =>
              raise JqError(
                "string (" +
                input.stringify() +
                ") cannot be parsed as a boolean",
              )
          }
        _ =>
          raise JqError(
            json_type(input) +
            " (" +
            input.stringify() +
            ") cannot be parsed as a boolean",
          )
      }
    "while" => {
      guard args.length() == 2 else { raise JqError("while takes 2 arguments") }
      let cond = args[0]
      let update = args[1]
      let mut current = input
      while true {
        if !(is_truthy(eval_single(cond, current, env))) {
          break
        }
        yield_(current)
        current = eval_single(update, current, env)
      }
    }
    "until" => {
      guard args.length() == 2 else { raise JqError("until takes 2 arguments") }
      let cond = args[0]
      let update = args[1]
      let mut current = input
      while true {
        if is_truthy(eval_single(cond, current, env)) {
          break
        }
        current = eval_single(update, current, env)
      }
      yield_(current)
    }
    "INDEX" =>
      if args.length() == 2 {
        let result : Map[String, Json] = Map::new()
        eval(args[0], input, env, x => {
          let key = eval_single(args[1], x, env)
          match key {
            String(k) => result[k] = x
            Number(n, ..) => result[n.to_string()] = x
            _ => result[key.stringify()] = x
          }
        })
        yield_(Json::object(result))
      } else if args.length() == 1 {
        let result : Map[String, Json] = Map::new()
        eval(Iterate, input, env, x => {
          let key = eval_single(args[0], x, env)
          match key {
            String(k) => result[k] = x
            Number(n, ..) => result[n.to_string()] = x
            _ => result[key.stringify()] = x
          }
        })
        yield_(Json::object(result))
      } else {
        raise JqError("INDEX takes 1 or 2 arguments")
      }
    _ => raise JqError("unknown function: " + name)
  }
}