///|
pub fn evaluate(
  expression : @ast.Expr,
  context : EvalContext,
) -> @model.Value raise @model.PromqlError {
  match expression {
    @ast.Number(value) => @model.Scalar(value)
    @ast.StringLiteral(value) => @model.StringValue(value)
    @ast.Selector(selector) =>
      match selector.range_ms {
        Some(range_ms) => select_range(selector, range_ms, context)
        None => select_instant(selector, context)
      }
    @ast.Unary(operator, value) => {
      let evaluated = evaluate(value, context)
      match operator {
        @ast.UnaryOp::Pos => evaluated
        @ast.UnaryOp::Neg =>
          match evaluated {
            @model.Scalar(value) => @model.Scalar(-value)
            @model.Instant(points) => {
              let output : Array[@model.Point] = []
              for point in points {
                output.push({ labels: point.labels, value: -point.value, })
              }
              @model.Instant(output)
            }
            _ => raise @model.EvalError("unary minus expects scalar or vector")
          }
      }
    }
    @ast.Call(name, args) => {
      let values : Array[@model.Value] = []
      for arg in args {
        values.push(evaluate(arg, context))
      }
      @functions.call(name, values, context.eval_time_ms)
    }
    @ast.Aggregate(operator, args, without, names) =>
      evaluate_aggregate(operator, args, without, names, context)
    @ast.Binary(operator, left, right, bool_modifier, vector_match) => {
      let left_value = evaluate(left, context)
      let right_value = evaluate(right, context)
      match (left_value, right_value) {
        (@model.Scalar(a), @model.Scalar(b)) =>
          if is_comparison(operator) {
            let matched = comparison_operator(operator, a, b)
            @model.Scalar(
              if bool_modifier {
                if matched {
                  1.0
                } else {
                  0.0
                }
              } else if matched {
                a
              } else {
                0.0 / 0.0
              },
            )
          } else {
            @model.Scalar(numeric_operator(operator, a, b))
          }
        (@model.Instant(points), @model.Scalar(scalar)) =>
          @model.Instant(
            map_instant_scalar(points, scalar, false, operator, bool_modifier),
          )
        (@model.Scalar(scalar), @model.Instant(points)) =>
          @model.Instant(
            map_instant_scalar(points, scalar, true, operator, bool_modifier),
          )
        (@model.Instant(left_points), @model.Instant(right_points)) =>
          @model.Instant(
            apply_vector_vector(
              operator, left_points, right_points, bool_modifier, vector_match,
            ),
          )
        _ =>
          raise @model.EvalError(
            "binary operator requires compatible scalar or instant vector values",
          )
      }
    }
  }
}

///|
/// Parse and evaluate one PromQL query.
pub fn query(
  input : StringView,
  context : EvalContext,
) -> @model.Value raise @model.PromqlError {
  evaluate(@parser.parse(input), context)
}