///|
fn infer_binary(
  op : String,
  a : Expr,
  b : Expr,
  matching : VectorMatching,
  depth : Int,
  options : ParserOptions,
) -> QueryType raise ParseError {
  let x = infer_type_at(a, depth + 1, options)
  let y = infer_type_at(b, depth + 1, options)
  let comparison = ["==", "!=", ">", "<", ">=", "<="].contains(op)
  let set = ["and", "or", "unless"].contains(op)
  if precedence(op) == 0 ||
    x == RangeVector ||
    y == RangeVector ||
    x == StringValue ||
    y == StringValue {
    raise Invalid("binary operands must be scalar or instant vector")
  }
  if matching.return_bool && !comparison {
    raise Invalid("bool only applies to comparison")
  }
  if comparison && x == Scalar && y == Scalar && !matching.return_bool {
    raise Invalid("scalar comparison requires bool")
  }
  if (set || !matching.labels.is_empty()) &&
    (x != InstantVector || y != InstantVector) {
    raise Invalid("set operators and matching require two instant vectors")
  }
  if set && matching.group != None {
    raise Invalid("set operators cannot use grouping modifiers")
  }
  if matching.mode == Some("on") &&
    matching.include_labels.iter().any(label => matching.labels.contains(label)) {
    raise Invalid("label cannot occur in both on and group include_labels")
  }
  if x == Scalar && y == Scalar {
    Scalar
  } else {
    InstantVector
  }
}

///|
fn infer_call(
  name : String,
  args : Array[Expr],
  depth : Int,
  options : ParserOptions,
) -> QueryType raise ParseError {
  let signature = match function_signature(name) {
    Some(s) => s
    None => raise Invalid("unknown function: " + name)
  }
  if signature.experimental && !options.experimental_functions {
    raise Invalid("experimental function is not enabled: " + name)
  }
  if args.length() < signature.min_args ||
    (signature.max_args is Some(n) && args.length() > n) {
    raise Invalid("function argument count: " + name)
  }
  for i, arg in args {
    let expected = signature.arguments[i.min(signature.arguments.length() - 1)]
    let actual = if name == "info" && i == 1 {
      info_labels(arg)
      InstantVector
    } else {
      infer_type_at(arg, depth + 1, options)
    }
    if actual != expected {
      raise Invalid("function argument type: " + name)
    }
  }
  signature.result
}