///|
fn pdf_fun_name(text : String) -> PdfName {
  pdf_name_of_bytes(@ascii.encode(text))
}

///|
let pdf_fun_function_type_key_name : PdfName = pdf_fun_name("/FunctionType")

///|
fn pdf_fun_function_type_key() -> PdfName {
  pdf_fun_function_type_key_name
}

///|
let pdf_fun_domain_key_name : PdfName = pdf_fun_name("/Domain")

///|
fn pdf_fun_domain_key() -> PdfName {
  pdf_fun_domain_key_name
}

///|
let pdf_fun_range_key_name : PdfName = pdf_fun_name("/Range")

///|
fn pdf_fun_range_key() -> PdfName {
  pdf_fun_range_key_name
}

///|
let pdf_fun_c0_key_name : PdfName = pdf_fun_name("/C0")

///|
fn pdf_fun_c0_key() -> PdfName {
  pdf_fun_c0_key_name
}

///|
let pdf_fun_c1_key_name : PdfName = pdf_fun_name("/C1")

///|
fn pdf_fun_c1_key() -> PdfName {
  pdf_fun_c1_key_name
}

///|
let pdf_fun_n_key_name : PdfName = pdf_fun_name("/N")

///|
fn pdf_fun_n_key() -> PdfName {
  pdf_fun_n_key_name
}

///|
let pdf_fun_size_key_name : PdfName = pdf_fun_name("/Size")

///|
fn pdf_fun_size_key() -> PdfName {
  pdf_fun_size_key_name
}

///|
let pdf_fun_order_key_name : PdfName = pdf_fun_name("/Order")

///|
fn pdf_fun_order_key() -> PdfName {
  pdf_fun_order_key_name
}

///|
let pdf_fun_encode_key_name : PdfName = pdf_fun_name("/Encode")

///|
fn pdf_fun_encode_key() -> PdfName {
  pdf_fun_encode_key_name
}

///|
let pdf_fun_decode_key_name : PdfName = pdf_fun_name("/Decode")

///|
fn pdf_fun_decode_key() -> PdfName {
  pdf_fun_decode_key_name
}

///|
let pdf_fun_bits_per_sample_key_name : PdfName = pdf_fun_name("/BitsPerSample")

///|
fn pdf_fun_bits_per_sample_key() -> PdfName {
  pdf_fun_bits_per_sample_key_name
}

///|
let pdf_fun_functions_key_name : PdfName = pdf_fun_name("/Functions")

///|
fn pdf_fun_functions_key() -> PdfName {
  pdf_fun_functions_key_name
}

///|
let pdf_fun_bounds_key_name : PdfName = pdf_fun_name("/Bounds")

///|
fn pdf_fun_bounds_key() -> PdfName {
  pdf_fun_bounds_key_name
}

///|
/// PDF function type 2, exponential interpolation.
///
/// `c0` and `c1` are the output vectors at the low and high ends of the input
/// domain, and `n` is the exponent used to shape interpolation between them.
pub(all) struct PdfInterpolationFunction {
  c0 : Array[Double]
  c1 : Array[Double]
  n : Double
} derive(Debug, Eq, ToJson)

///|
/// PDF function type 0, sampled from a packed sample table.
///
/// `size` gives the sample count for each input dimension. `encode` maps
/// clamped domain inputs to sample coordinates, `decode` maps sample values to
/// output values, and `samples` stores decoded integer samples with the first
/// input dimension varying fastest.
pub(all) struct PdfSampledFunction {
  size : Array[Int]
  order : Int
  encode : Array[Double]
  decode : Array[Double]
  bits_per_sample : Int
  samples : Array[Int]
} derive(Debug, Eq, ToJson)

///|
/// PDF function type 3, stitching several child functions over one input.
///
/// `bounds` partitions the parent domain, and `stitch_encode` maps each chosen
/// segment into the selected child function's input domain.
pub(all) struct PdfStitchingFunction {
  functions : Array[PdfFunction]
  bounds : Array[Double]
  stitch_encode : Array[Double]
} derive(Debug, Eq, ToJson)

///|
/// Operation in a PDF type 4 PostScript calculator function.
///
/// Literals push values onto the calculator stack. `PdfCalculatorIf` and
/// `PdfCalculatorIfElse` store nested procedure bodies; the remaining variants
/// correspond to PDF calculator-language operators.
pub(all) enum PdfCalculatorOp {
  PdfCalculatorInt(Int)
  PdfCalculatorFloat(Double)
  PdfCalculatorAbs
  PdfCalculatorAdd
  PdfCalculatorAnd
  PdfCalculatorAtan
  PdfCalculatorBitshift
  PdfCalculatorBool(Bool)
  PdfCalculatorCeiling
  PdfCalculatorCos
  PdfCalculatorCopy
  PdfCalculatorCvi
  PdfCalculatorCvr
  PdfCalculatorSub
  PdfCalculatorMul
  PdfCalculatorDiv
  PdfCalculatorDup
  PdfCalculatorExch
  PdfCalculatorExp
  PdfCalculatorFloor
  PdfCalculatorIdiv
  PdfCalculatorIf(Array[PdfCalculatorOp])
  PdfCalculatorIfElse(Array[PdfCalculatorOp], Array[PdfCalculatorOp])
  PdfCalculatorIndex
  PdfCalculatorLn
  PdfCalculatorLog
  PdfCalculatorEq
  PdfCalculatorGe
  PdfCalculatorGt
  PdfCalculatorLe
  PdfCalculatorLt
  PdfCalculatorMod
  PdfCalculatorNe
  PdfCalculatorNeg
  PdfCalculatorNot
  PdfCalculatorOr
  PdfCalculatorPop
  PdfCalculatorRoll
  PdfCalculatorRound
  PdfCalculatorSin
  PdfCalculatorSqrt
  PdfCalculatorTruncate
  PdfCalculatorXor
} derive(Debug, Eq, ToJson)

///|
/// PDF function type 4, represented as parsed calculator operations.
///
/// Evaluation starts with the function inputs on the stack and returns the
/// numeric values left on the stack after all operations have run.
pub(all) struct PdfCalculatorFunction {
  operations : Array[PdfCalculatorOp]
} derive(Debug, Eq, ToJson)

///|
/// Discriminated PDF function body for supported `/FunctionType` values.
pub(all) enum PdfFunctionKind {
  PdfFunctionSampled(PdfSampledFunction)
  PdfFunctionInterpolation(PdfInterpolationFunction)
  PdfFunctionStitching(PdfStitchingFunction)
  PdfFunctionCalculator(PdfCalculatorFunction)
} derive(Debug, Eq, ToJson)

///|
/// Parsed PDF function with common domain and optional output range.
///
/// `domain` is stored as low/high pairs, one pair per input. `range`, when
/// present, is stored as output low/high pairs and is used to clamp evaluation
/// results.
pub(all) struct PdfFunction {
  func : PdfFunctionKind
  domain : Array[Double]
  range : Array[Double]?
} derive(Debug, Eq, ToJson)

///|
fn pdf_fun_join_strings(
  parts : ArrayView[String],
  separator : String,
) -> String {
  let mut text = ""
  for index in 0.. 0 {
      text = text + separator
    }
    text = text + parts[index]
  }
  text
}

///|
/// Return the calculator-language spelling for one operation.
///
/// Procedure operations are formatted recursively in the same brace-delimited
/// form accepted by the type 4 parser.
pub fn PdfCalculatorOp::debug_name(self : PdfCalculatorOp) -> String {
  match self {
    PdfCalculatorIf(ops) => pdf_string_of_calculator(ops) + " if"
    PdfCalculatorIfElse(true_ops, false_ops) =>
      pdf_string_of_calculator(true_ops) +
      " " +
      pdf_string_of_calculator(false_ops) +
      " ifelse"
    PdfCalculatorBool(true) => "true"
    PdfCalculatorBool(false) => "false"
    PdfCalculatorFloat(value) => value.to_string()
    PdfCalculatorInt(value) => value.to_string()
    PdfCalculatorAbs => "abs"
    PdfCalculatorAdd => "add"
    PdfCalculatorAtan => "atan"
    PdfCalculatorCeiling => "ceiling"
    PdfCalculatorCos => "cos"
    PdfCalculatorCvi => "cvi"
    PdfCalculatorCvr => "cvr"
    PdfCalculatorDiv => "div"
    PdfCalculatorExp => "exp"
    PdfCalculatorFloor => "floor"
    PdfCalculatorIdiv => "idiv"
    PdfCalculatorLn => "ln"
    PdfCalculatorLog => "log"
    PdfCalculatorMod => "mod"
    PdfCalculatorMul => "mul"
    PdfCalculatorNeg => "neg"
    PdfCalculatorRound => "round"
    PdfCalculatorSin => "sin"
    PdfCalculatorSqrt => "sqrt"
    PdfCalculatorSub => "sub"
    PdfCalculatorTruncate => "truncate"
    PdfCalculatorAnd => "and"
    PdfCalculatorBitshift => "bitshift"
    PdfCalculatorEq => "eq"
    PdfCalculatorGe => "ge"
    PdfCalculatorGt => "gt"
    PdfCalculatorLe => "le"
    PdfCalculatorLt => "lt"
    PdfCalculatorNe => "ne"
    PdfCalculatorNot => "not"
    PdfCalculatorOr => "or"
    PdfCalculatorXor => "xor"
    PdfCalculatorCopy => "copy"
    PdfCalculatorExch => "exch"
    PdfCalculatorPop => "pop"
    PdfCalculatorDup => "dup"
    PdfCalculatorIndex => "index"
    PdfCalculatorRoll => "roll"
  }
}

///|
/// Format calculator operations as a brace-delimited diagnostic program.
pub fn pdf_string_of_calculator(ops : ArrayView[PdfCalculatorOp]) -> String {
  "{" + pdf_fun_join_strings([ for op in ops => op.debug_name() ], " ") + "}"
}

///|
fn pdf_fun_debug_doubles(values : ArrayView[Double]) -> String {
  pdf_fun_join_strings([ for value in values => value.to_string() ], " ")
}

///|
fn pdf_fun_debug_ints(values : ArrayView[Int]) -> String {
  pdf_fun_join_strings([ for value in values => value.to_string() ], " ")
}

///|
/// Format a parsed function as multi-line diagnostic text.
///
/// The output follows CamlPDF's debug printer shape and is intended for
/// inspection, not for writing a PDF function object.
pub fn PdfFunction::debug_string(self : PdfFunction) -> String {
  let range_text = match self.range {
    Some(values) => pdf_fun_debug_doubles(values)
    None => "null range"
  }
  let prefix = "Domain...\n" +
    pdf_fun_debug_doubles(self.domain) +
    "\n" +
    range_text +
    "\n"
  match self.func {
    PdfFunctionSampled(sampled) =>
      prefix +
      "Sampled\n" +
      "size: " +
      pdf_fun_debug_ints(sampled.size) +
      "\norder: " +
      sampled.order.to_string() +
      "\nencode:\n" +
      pdf_fun_debug_doubles(sampled.encode) +
      "\ndecode:\n" +
      pdf_fun_debug_doubles(sampled.decode) +
      "\noriginal bits per sample..\n" +
      sampled.bits_per_sample.to_string() +
      "\ndata:\n" +
      pdf_fun_debug_ints(sampled.samples)
    PdfFunctionInterpolation(interpolation) =>
      prefix +
      "Interpolation\n" +
      "C0:\n" +
      pdf_fun_debug_doubles(interpolation.c0) +
      "\nC1:\n" +
      pdf_fun_debug_doubles(interpolation.c1) +
      "\nn = " +
      interpolation.n.to_string()
    PdfFunctionStitching(stitching) =>
      prefix +
      "Stitching\n" +
      pdf_fun_join_strings(
        [
          for child in stitching.functions => child.debug_string()
        ],
        "\n",
      ) +
      "\nBounds:\n" +
      pdf_fun_debug_doubles(stitching.bounds) +
      "\nEncode:\n" +
      pdf_fun_debug_doubles(stitching.stitch_encode)
    PdfFunctionCalculator(calculator) =>
      prefix + "Calculator:\n" + pdf_string_of_calculator(calculator.operations)
  }
}

///|
/// Compatibility wrapper for `PdfFunction::debug_string`.
pub fn pdf_string_of_function(function : PdfFunction) -> String {
  function.debug_string()
}

///|
fn PdfDocument::pdf_fun_number_array(
  self : PdfDocument,
  object : PdfObject,
) -> Array[Double] raise PdfError {
  match self.direct(object) {
    PdfArray(values) => {
      let output : Array[Double] = []
      for value in values {
        output.push(self.get_number(value))
      }
      output
    }
    _ => raise PdfError::NumberExpected
  }
}

///|
fn PdfDocument::pdf_fun_lookup_number_array_or(
  self : PdfDocument,
  dictionary : PdfObject,
  key : PdfName,
  default : ArrayView[Double],
) -> Array[Double] raise PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.pdf_fun_number_array(value)
    None => [ for value in default => value ]
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_number_array(
  self : PdfDocument,
  dictionary : PdfObject,
  key : PdfName,
) -> Array[Double] raise PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.pdf_fun_number_array(value)
    None => raise PdfError::DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_number(
  self : PdfDocument,
  dictionary : PdfObject,
  key : PdfName,
) -> Double raise PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.get_number(value)
    None => raise PdfError::DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_int_array(
  self : PdfDocument,
  object : PdfObject,
) -> Array[Int] raise PdfError {
  match self.direct(object) {
    PdfArray(values) => {
      let output : Array[Int] = []
      for value in values {
        match self.direct(value) {
          PdfInteger(integer) => output.push(integer)
          _ => raise PdfError::NumberExpected
        }
      }
      output
    }
    _ => raise PdfError::NumberExpected
  }
}

///|
fn PdfDocument::pdf_fun_object_array(
  self : PdfDocument,
  object : PdfObject,
) -> Array[PdfObject] raise PdfError {
  match self.direct(object) {
    PdfArray(values) => [ for value in values => value ]
    _ => raise PdfError::FunctionExpected
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_object_array(
  self : PdfDocument,
  dictionary : PdfObject,
  key : PdfName,
) -> Array[PdfObject] raise PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.pdf_fun_object_array(value)
    None => raise PdfError::DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_int_array(
  self : PdfDocument,
  dictionary : PdfObject,
  key : PdfName,
) -> Array[Int] raise PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.pdf_fun_int_array(value)
    None => raise PdfError::DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_int(
  self : PdfDocument,
  dictionary : PdfObject,
  key : PdfName,
) -> Int raise PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(PdfInteger(value)) => value
    Some(_) => raise PdfError::NumberExpected
    None => raise PdfError::DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_int_or(
  self : PdfDocument,
  dictionary : PdfObject,
  key : PdfName,
  default : Int,
) -> Int raise PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(PdfInteger(value)) => value
    Some(_) => raise PdfError::NumberExpected
    None => default
  }
}

///|
fn pdf_fun_default_encode(size : ArrayView[Int]) -> Array[Double] {
  let output : Array[Double] = []
  for value in size {
    output.push(0.0)
    output.push((value - 1).to_double())
  }
  output
}

///|
fn PdfDocument::pdf_fun_lookup_encode(
  self : PdfDocument,
  dictionary : PdfObject,
  size : ArrayView[Int],
) -> Array[Double] raise PdfError {
  match self.lookup_direct(pdf_fun_encode_key(), dictionary) {
    Some(value) => {
      let encode = self.pdf_fun_number_array(value)
      if encode.length() == size.length() * 2 {
        encode
      } else {
        pdf_fun_default_encode(size)
      }
    }
    None => pdf_fun_default_encode(size)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_decode(
  self : PdfDocument,
  dictionary : PdfObject,
  range : Array[Double]?,
) -> Array[Double] raise PdfError {
  match self.lookup_direct(pdf_fun_decode_key(), dictionary) {
    Some(value) => self.pdf_fun_number_array(value)
    None =>
      match range {
        Some(range) => [ for value in range => value ]
        None => raise PdfError::DictionaryKeyNotFound(pdf_fun_range_key())
      }
  }
}

///|
fn pdf_fun_product(values : ArrayView[Int]) -> Int {
  let mut product = 1
  for value in values {
    product *= value
  }
  product
}

///|
fn PdfDocument::pdf_fun_stream_data(
  self : PdfDocument,
  object : PdfObject,
) -> PdfBytes raise PdfError {
  match self.direct(object) {
    PdfStreamObject(stream) =>
      pdf_stream_data_bytes(self.pdf_decode_stream_value_direct(stream).data)
    _ => raise PdfError::ParseStreamExpected
  }
}

///|
fn PdfDocument::pdf_fun_read_sampled(
  self : PdfDocument,
  function_object : PdfObject,
  dictionary : PdfObject,
  range : Array[Double]?,
) -> PdfSampledFunction raise PdfError {
  let size = self.pdf_fun_lookup_required_int_array(
    dictionary,
    pdf_fun_size_key(),
  )
  let order = self.pdf_fun_lookup_int_or(dictionary, pdf_fun_order_key(), 1)
  let encode = self.pdf_fun_lookup_encode(dictionary, size)
  let decode = self.pdf_fun_lookup_decode(dictionary, range)
  if decode.length() % 2 != 0 {
    raise PdfError::FunctionExpected
  }
  let bits_per_sample = self.pdf_fun_lookup_required_int(
    dictionary,
    pdf_fun_bits_per_sample_key(),
  )
  let sample_count = pdf_fun_product(size) * (decode.length() / 2)
  let bitstream = bitstream_of_bytes(self.pdf_fun_stream_data(function_object))
  let samples : Array[Int] = []
  for _ in 0.. PdfStitchingFunction raise PdfError {
  let function_objects = self.pdf_fun_lookup_required_object_array(
    dictionary,
    pdf_fun_functions_key(),
  )
  let functions : Array[PdfFunction] = []
  for function_object in function_objects {
    functions.push(self.parse_function(function_object))
  }
  let bounds = self.pdf_fun_lookup_required_number_array(
    dictionary,
    pdf_fun_bounds_key(),
  )
  let stitch_encode = self.pdf_fun_lookup_required_number_array(
    dictionary,
    pdf_fun_encode_key(),
  )
  if functions.length() == 0 ||
    bounds.length() != functions.length() - 1 ||
    stitch_encode.length() != functions.length() * 2 {
    raise PdfError::FunctionExpected
  }
  { functions, bounds, stitch_encode }
}

///|
priv enum PdfCalculatorToken {
  PdfCalculatorTokenOpen
  PdfCalculatorTokenClose
  PdfCalculatorTokenText(String)
}

///|
priv enum PdfCalculatorItem {
  PdfCalculatorItemOp(PdfCalculatorOp)
  PdfCalculatorItemProcedure(Array[PdfCalculatorOp])
}

///|
fn pdf_fun_calculator_number_of_token(text : String) -> PdfCalculatorOp? {
  let integer : Result[Int, Error] = try? @strconv.from_str(text)
  match integer {
    Ok(value) => Some(PdfCalculatorInt(value))
    Err(_) => {
      let real : Result[Double, Error] = try? @strconv.from_str(text)
      match real {
        Ok(value) => Some(PdfCalculatorFloat(value))
        Err(_) => None
      }
    }
  }
}

///|
fn pdf_fun_calculator_op_of_token(
  text : String,
) -> PdfCalculatorOp raise PdfError {
  match text {
    "abs" => PdfCalculatorAbs
    "add" => PdfCalculatorAdd
    "and" => PdfCalculatorAnd
    "atan" => PdfCalculatorAtan
    "bitshift" => PdfCalculatorBitshift
    "true" => PdfCalculatorBool(true)
    "false" => PdfCalculatorBool(false)
    "ceiling" => PdfCalculatorCeiling
    "cos" => PdfCalculatorCos
    "copy" => PdfCalculatorCopy
    "cvi" => PdfCalculatorCvi
    "cvr" => PdfCalculatorCvr
    "sub" => PdfCalculatorSub
    "mul" => PdfCalculatorMul
    "div" => PdfCalculatorDiv
    "dup" => PdfCalculatorDup
    "exch" => PdfCalculatorExch
    "exp" => PdfCalculatorExp
    "floor" => PdfCalculatorFloor
    "idiv" => PdfCalculatorIdiv
    "eq" => PdfCalculatorEq
    "ge" => PdfCalculatorGe
    "gt" => PdfCalculatorGt
    "index" => PdfCalculatorIndex
    "le" => PdfCalculatorLe
    "lt" => PdfCalculatorLt
    "ln" => PdfCalculatorLn
    "log" => PdfCalculatorLog
    "mod" => PdfCalculatorMod
    "ne" => PdfCalculatorNe
    "neg" => PdfCalculatorNeg
    "not" => PdfCalculatorNot
    "or" => PdfCalculatorOr
    "pop" => PdfCalculatorPop
    "roll" => PdfCalculatorRoll
    "round" => PdfCalculatorRound
    "sin" => PdfCalculatorSin
    "sqrt" => PdfCalculatorSqrt
    "truncate" => PdfCalculatorTruncate
    "xor" => PdfCalculatorXor
    _ =>
      match pdf_fun_calculator_number_of_token(text) {
        Some(number) => number
        None =>
          if text.has_prefix("--") {
            match
              pdf_fun_calculator_number_of_token(
                text.unsafe_substring(start=1, end=text.length()),
              ) {
              Some(number) => number
              None => raise PdfError::FunctionExpected
            }
          } else {
            raise PdfError::FunctionExpected
          }
      }
  }
}

///|
fn pdf_fun_calculator_text_token(
  token : ArrayView[Byte],
) -> PdfCalculatorToken? raise PdfError {
  if token.length() > 0 {
    let text = @ascii.decode(Bytes::from_array(token)) catch {
      _ => raise PdfError::FunctionExpected
    }
    Some(PdfCalculatorTokenText(text))
  } else {
    None
  }
}

///|
fn pdf_fun_calculator_push_text_token(
  tokens : Array[PdfCalculatorToken],
  token : ArrayView[Byte],
) -> Unit raise PdfError {
  match pdf_fun_calculator_text_token(token) {
    Some(text) => tokens.push(text)
    None => ()
  }
}

///|
fn pdf_fun_calculator_tokens(
  data : PdfBytes,
) -> Array[PdfCalculatorToken] raise PdfError {
  let tokens : Array[PdfCalculatorToken] = []
  let mut token : Array[Byte] = []
  let mut index = 0
  while index < data.length() {
    let byte = data[index]
    let value = byte.to_int()
    if pdf_is_whitespace_byte(value) {
      pdf_fun_calculator_push_text_token(tokens, token)
      token = []
      index += 1
    } else if value == 123 {
      pdf_fun_calculator_push_text_token(tokens, token)
      token = []
      tokens.push(PdfCalculatorTokenOpen)
      index += 1
    } else if value == 125 {
      pdf_fun_calculator_push_text_token(tokens, token)
      token = []
      tokens.push(PdfCalculatorTokenClose)
      index += 1
    } else if value == 37 {
      pdf_fun_calculator_push_text_token(tokens, token)
      token = []
      index += 1
      while index < data.length() &&
            data[index].to_int() != 10 &&
            data[index].to_int() != 13 {
        index += 1
      }
    } else {
      token.push(byte)
      index += 1
    }
  }
  pdf_fun_calculator_push_text_token(tokens, token)
  tokens
}

///|
fn pdf_fun_calculator_pop_procedure(
  items : Array[PdfCalculatorItem],
) -> Array[PdfCalculatorOp] raise PdfError {
  match items.pop() {
    Some(PdfCalculatorItemProcedure(operations)) => operations
    _ => raise PdfError::FunctionExpected
  }
}

///|
fn pdf_fun_calculator_items_to_operations(
  items : ArrayView[PdfCalculatorItem],
) -> Array[PdfCalculatorOp] raise PdfError {
  let operations : Array[PdfCalculatorOp] = []
  for item in items {
    match item {
      PdfCalculatorItemOp(operation) => operations.push(operation)
      PdfCalculatorItemProcedure(_) => raise PdfError::FunctionExpected
    }
  }
  operations
}

///|
fn pdf_fun_calculator_push_operator(
  items : Array[PdfCalculatorItem],
  text : String,
) -> Unit raise PdfError {
  match text {
    "if" => {
      let then_branch = pdf_fun_calculator_pop_procedure(items)
      items.push(PdfCalculatorItemOp(PdfCalculatorIf(then_branch)))
    }
    "ifelse" => {
      let else_branch = pdf_fun_calculator_pop_procedure(items)
      let then_branch = pdf_fun_calculator_pop_procedure(items)
      items.push(
        PdfCalculatorItemOp(PdfCalculatorIfElse(then_branch, else_branch)),
      )
    }
    _ => items.push(PdfCalculatorItemOp(pdf_fun_calculator_op_of_token(text)))
  }
}

///|
fn pdf_fun_calculator_parse_items(
  tokens : ArrayView[PdfCalculatorToken],
  start : Int,
  stop_at_close~ : Bool,
) -> (Array[PdfCalculatorItem], Int) raise PdfError {
  let items : Array[PdfCalculatorItem] = []
  let mut index = start
  let mut closed = false
  while index < tokens.length() && !closed {
    match tokens[index] {
      PdfCalculatorTokenOpen => {
        let (operations, next_index) = pdf_fun_calculator_parse_operations(
          tokens,
          index + 1,
          stop_at_close=true,
        )
        items.push(PdfCalculatorItemProcedure(operations))
        index = next_index
      }
      PdfCalculatorTokenClose =>
        if stop_at_close {
          closed = true
          index += 1
        } else {
          raise PdfError::FunctionExpected
        }
      PdfCalculatorTokenText(text) => {
        pdf_fun_calculator_push_operator(items, text)
        index += 1
      }
    }
  }
  if stop_at_close && !closed {
    raise PdfError::FunctionExpected
  }
  (items, index)
}

///|
fn pdf_fun_calculator_parse_operations(
  tokens : ArrayView[PdfCalculatorToken],
  start : Int,
  stop_at_close~ : Bool,
) -> (Array[PdfCalculatorOp], Int) raise PdfError {
  let (items, next_index) = pdf_fun_calculator_parse_items(
    tokens,
    start,
    stop_at_close~,
  )
  (pdf_fun_calculator_items_to_operations(items), next_index)
}

///|
fn pdf_fun_parse_calculator(
  data : PdfBytes,
) -> PdfCalculatorFunction raise PdfError {
  let tokens = pdf_fun_calculator_tokens(data)
  let (operations, next_index) = match tokens.get(0) {
    Some(PdfCalculatorTokenOpen) =>
      pdf_fun_calculator_parse_operations(tokens, 1, stop_at_close=true)
    _ => pdf_fun_calculator_parse_operations(tokens, 0, stop_at_close=false)
  }
  if next_index != tokens.length() {
    raise PdfError::FunctionExpected
  }
  { operations, }
}

///|
/// Parse a PDF function object from this document.
///
/// Supports PDF function types 0, 2, 3, and 4. Sampled and calculator functions
/// are read from decoded stream data; interpolation and stitching functions are
/// read from dictionaries. Raises for malformed dictionaries, invalid arities,
/// stream decoding failures, or unsupported function types.
pub fn PdfDocument::parse_function(
  self : PdfDocument,
  function_object : PdfObject,
) -> PdfFunction raise PdfError {
  let function_dictionary = self.direct(function_object)
  let domain = self.pdf_fun_lookup_required_number_array(
    function_dictionary,
    pdf_fun_domain_key(),
  )
  let range = match
    self.lookup_direct(pdf_fun_range_key(), function_dictionary) {
    Some(value) => Some(self.pdf_fun_number_array(value))
    None => None
  }
  let function_type = match
    self.lookup_direct(pdf_fun_function_type_key(), function_dictionary) {
    Some(PdfInteger(value)) => value
    Some(_) => raise PdfError::NumberExpected
    None => raise PdfError::DictionaryKeyNotFound(pdf_fun_function_type_key())
  }
  match function_type {
    0 =>
      {
        domain,
        range,
        func: PdfFunctionSampled(
          self.pdf_fun_read_sampled(function_object, function_dictionary, range),
        ),
      }
    2 => {
      let c0 = self.pdf_fun_lookup_number_array_or(
        function_dictionary,
        pdf_fun_c0_key(),
        [0.0],
      )
      let c1 = self.pdf_fun_lookup_number_array_or(
        function_dictionary,
        pdf_fun_c1_key(),
        [1.0],
      )
      if c0.length() != c1.length() {
        raise PdfError::FunctionExpected
      }
      {
        domain,
        range,
        func: PdfFunctionInterpolation({
          c0,
          c1,
          n: self.pdf_fun_lookup_required_number(
            function_dictionary,
            pdf_fun_n_key(),
          ),
        }),
      }
    }
    3 =>
      {
        domain,
        range,
        func: PdfFunctionStitching(
          self.pdf_fun_read_stitching(function_dictionary),
        ),
      }
    4 =>
      {
        domain,
        range,
        func: PdfFunctionCalculator(
          pdf_fun_parse_calculator(self.pdf_fun_stream_data(function_object)),
        ),
      }
    other => raise PdfError::FunctionNotSupported(other)
  }
}

///|
/// Compatibility wrapper for `PdfDocument::parse_function`.
pub fn pdf_parse_function(
  document : PdfDocument,
  function_object : PdfObject,
) -> PdfFunction raise PdfError {
  document.parse_function(function_object)
}

///|
fn pdf_fun_number_array_object(values : ArrayView[Double]) -> PdfObject {
  PdfArray([ for value in values => PdfReal(value) ])
}

///|
fn PdfFunction::pdf_fun_function_type(self : PdfFunction) -> Int {
  match self.func {
    PdfFunctionSampled(_) => 0
    PdfFunctionInterpolation(_) => 2
    PdfFunctionStitching(_) => 3
    PdfFunctionCalculator(_) => 4
  }
}

///|
/// Build a PDF object for a parsed function.
///
/// Interpolation functions are written as dictionaries. Stitching functions add
/// child function objects to this document and reference them from the returned
/// dictionary. Sampled and calculator variants currently write only the common
/// function dictionary fields and do not recreate their stream data.
pub fn PdfDocument::pdf_object_of_function(
  self : PdfDocument,
  function : PdfFunction,
) -> PdfObject {
  let entries : Array[(PdfName, PdfObject)] = [
    (pdf_fun_function_type_key(), PdfInteger(function.pdf_fun_function_type())),
    (pdf_fun_domain_key(), pdf_fun_number_array_object(function.domain)),
  ]
  match function.range {
    Some(range) =>
      entries.push((pdf_fun_range_key(), pdf_fun_number_array_object(range)))
    None => ()
  }
  match function.func {
    PdfFunctionInterpolation(interpolation) => {
      entries.push(
        (pdf_fun_c0_key(), pdf_fun_number_array_object(interpolation.c0)),
      )
      entries.push(
        (pdf_fun_c1_key(), pdf_fun_number_array_object(interpolation.c1)),
      )
      entries.push((pdf_fun_n_key(), PdfReal(interpolation.n)))
    }
    PdfFunctionStitching(stitching) => {
      let function_refs : Array[PdfObject] = []
      for child in stitching.functions {
        function_refs.push(
          PdfIndirect(self.add_object(self.pdf_object_of_function(child))),
        )
      }
      entries.push((pdf_fun_functions_key(), PdfArray(function_refs)))
      entries.push(
        (pdf_fun_bounds_key(), pdf_fun_number_array_object(stitching.bounds)),
      )
      entries.push(
        (
          pdf_fun_encode_key(),
          pdf_fun_number_array_object(stitching.stitch_encode),
        ),
      )
    }
    PdfFunctionSampled(_) | PdfFunctionCalculator(_) => ()
  }
  PdfDictionary(entries)
}

///|
/// Compatibility wrapper for `PdfDocument::pdf_object_of_function`.
pub fn pdf_object_of_function(
  document : PdfDocument,
  function : PdfFunction,
) -> PdfObject {
  document.pdf_object_of_function(function)
}

///|
fn pdf_fun_clamp(value : Double, low : Double, high : Double) -> Double {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
fn pdf_fun_interpolate(
  value : Double,
  source_min : Double,
  source_max : Double,
  target_min : Double,
  target_max : Double,
) -> Double {
  target_min +
  (value - source_min) * ((target_max - target_min) / (source_max - source_min))
}

///|
fn pdf_fun_clamp_values(
  values : ArrayView[Double],
  range : ArrayView[Double],
) -> Array[Double] raise PdfError {
  if range.length() != values.length() * 2 {
    raise PdfError::BadFunctionEvaluation("range arity mismatch")
  }
  let output : Array[Double] = []
  for index in 0.. Double {
  @math.pow(2.0, bits_per_sample.to_double()) - 1.0
}

///|
fn PdfSampledFunction::encoded_inputs(
  self : PdfSampledFunction,
  function : PdfFunction,
  clamped_inputs : ArrayView[Double],
) -> Array[Double] raise PdfError {
  if self.size.length() != clamped_inputs.length() ||
    self.encode.length() != clamped_inputs.length() * 2 {
    raise PdfError::BadFunctionEvaluation("sampled input arity mismatch")
  }
  let output : Array[Double] = []
  for index in 0.. Array[Double] raise PdfError {
  if self.size.length() != indices.length() {
    raise PdfError::BadFunctionEvaluation("sampled table arity mismatch")
  }
  let mut position = 0
  let mut multiplier = 1
  for index in 0.. self.samples.length() {
    raise PdfError::BadFunctionEvaluation("sampled table index out of range")
  }
  let values : Array[Double] = []
  for index in 0.. Array[Double] raise PdfError {
  let range = match function.range {
    Some(range) => range
    None =>
      raise PdfError::BadFunctionEvaluation("sampled function missing range")
  }
  if range.length() % 2 != 0 ||
    self.decode.length() != range.length() ||
    self.decode.length() % 2 != 0 {
    raise PdfError::BadFunctionEvaluation("sampled output arity mismatch")
  }
  let output_count = range.length() / 2
  let encoded = self.encoded_inputs(function, clamped_inputs)
  let floors : Array[Int] = []
  let ceilings : Array[Int] = []
  for value in encoded {
    floors.push(@math.floor(value).to_int())
    ceilings.push(@math.ceil(value).to_int())
  }
  let floor_values = self.read_table(floors, output_count)
  let ceiling_values = self.read_table(ceilings, output_count)
  let max_sample = pdf_fun_max_sample(self.bits_per_sample)
  let decoded : Array[Double] = []
  for index in 0.. Array[Double] raise PdfError {
  if clamped_inputs.length() != 1 || function.domain.length() != 2 {
    raise PdfError::BadFunctionEvaluation("stitching function arity mismatch")
  }
  let input = clamped_inputs[0]
  let points : Array[Double] = [function.domain[0]]
  for bound in self.bounds {
    points.push(bound)
  }
  points.push(function.domain[1])
  let mut chosen = -1
  for index in 0..= low && input < high) || input == high {
      chosen = index
      break
    }
  }
  if chosen < 0 {
    raise PdfError::BadFunctionEvaluation("stitching function bounds mismatch")
  }
  let encoded = pdf_fun_interpolate(
    input,
    points[chosen],
    points[chosen + 1],
    self.stitch_encode[chosen * 2],
    self.stitch_encode[chosen * 2 + 1],
  )
  self.functions[chosen].eval([encoded])
}

///|
priv enum PdfCalculatorValue {
  PdfCalculatorInt(Int)
  PdfCalculatorFloat(Double)
  PdfCalculatorBoolean(Bool)
}

///|
fn pdf_fun_calculator_pop_value(
  stack : Array[PdfCalculatorValue],
) -> PdfCalculatorValue raise PdfError {
  match stack.pop() {
    Some(value) => value
    None => raise PdfError::BadFunctionEvaluation("calculator stack underflow")
  }
}

///|
fn pdf_fun_calculator_pop_number(
  stack : Array[PdfCalculatorValue],
) -> Double raise PdfError {
  match pdf_fun_calculator_pop_value(stack) {
    PdfCalculatorInt(value) => value.to_double()
    PdfCalculatorFloat(value) => value
    PdfCalculatorBoolean(_) =>
      raise PdfError::BadFunctionEvaluation("calculator number expected")
  }
}

///|
fn pdf_fun_calculator_pop_int(
  stack : Array[PdfCalculatorValue],
) -> Int raise PdfError {
  match pdf_fun_calculator_pop_value(stack) {
    PdfCalculatorInt(value) => value
    _ =>
      raise PdfError::BadFunctionEvaluation(
        "calculator integer operand expected",
      )
  }
}

///|
fn pdf_fun_calculator_pop_bool(
  stack : Array[PdfCalculatorValue],
) -> Bool raise PdfError {
  match pdf_fun_calculator_pop_value(stack) {
    PdfCalculatorBoolean(value) => value
    PdfCalculatorInt(_) | PdfCalculatorFloat(_) =>
      raise PdfError::BadFunctionEvaluation("calculator boolean expected")
  }
}

///|
fn pdf_fun_calculator_copy(
  stack : Array[PdfCalculatorValue],
) -> Unit raise PdfError {
  let count = pdf_fun_calculator_pop_int(stack)
  if count < 0 || count > stack.length() {
    raise PdfError::BadFunctionEvaluation(
      "calculator copy operand out of range",
    )
  }
  let start = stack.length() - count
  let end = stack.length()
  for index in start.. Unit raise PdfError {
  let index = pdf_fun_calculator_pop_int(stack)
  if index < 0 || index >= stack.length() {
    raise PdfError::BadFunctionEvaluation(
      "calculator index operand out of range",
    )
  }
  stack.push(stack[stack.length() - index - 1])
}

///|
fn pdf_fun_calculator_roll(
  stack : Array[PdfCalculatorValue],
) -> Unit raise PdfError {
  let distance = pdf_fun_calculator_pop_int(stack)
  let count = pdf_fun_calculator_pop_int(stack)
  if count < 1 || count > stack.length() {
    raise PdfError::BadFunctionEvaluation(
      "calculator roll operand out of range",
    )
  }
  let start = stack.length() - count
  let offset = (distance % count + count) % count
  let rolled : Array[PdfCalculatorValue] = []
  for index in 0.. Bool {
  match (left, right) {
    (PdfCalculatorInt(left), PdfCalculatorInt(right)) => left == right
    (PdfCalculatorFloat(left), PdfCalculatorFloat(right)) => left == right
    (PdfCalculatorBoolean(left), PdfCalculatorBoolean(right)) => left == right
    _ => false
  }
}

///|
fn pdf_fun_calculator_numeric_binary(
  stack : Array[PdfCalculatorValue],
  int_op : (Int, Int) -> Int,
  float_op : (Double, Double) -> Double,
) -> Unit raise PdfError {
  let right = pdf_fun_calculator_pop_value(stack)
  let left = pdf_fun_calculator_pop_value(stack)
  match (left, right) {
    (PdfCalculatorInt(left), PdfCalculatorInt(right)) =>
      stack.push(PdfCalculatorInt(int_op(left, right)))
    (PdfCalculatorInt(left), PdfCalculatorFloat(right)) =>
      stack.push(PdfCalculatorFloat(float_op(left.to_double(), right)))
    (PdfCalculatorFloat(left), PdfCalculatorInt(right)) =>
      stack.push(PdfCalculatorFloat(float_op(left, right.to_double())))
    (PdfCalculatorFloat(left), PdfCalculatorFloat(right)) =>
      stack.push(PdfCalculatorFloat(float_op(left, right)))
    _ => raise PdfError::BadFunctionEvaluation("calculator type mismatch")
  }
}

///|
fn pdf_fun_calculator_boolean_or_integer_binary(
  stack : Array[PdfCalculatorValue],
  bool_op : (Bool, Bool) -> Bool,
  int_op : (Int, Int) -> Int,
) -> Unit raise PdfError {
  let right = pdf_fun_calculator_pop_value(stack)
  let left = pdf_fun_calculator_pop_value(stack)
  match (left, right) {
    (PdfCalculatorBoolean(left), PdfCalculatorBoolean(right)) =>
      stack.push(PdfCalculatorBoolean(bool_op(left, right)))
    (PdfCalculatorInt(left), PdfCalculatorInt(right)) =>
      stack.push(PdfCalculatorInt(int_op(left, right)))
    _ => raise PdfError::BadFunctionEvaluation("calculator type mismatch")
  }
}

///|
fn pdf_fun_calculator_not(
  stack : Array[PdfCalculatorValue],
) -> Unit raise PdfError {
  match pdf_fun_calculator_pop_value(stack) {
    PdfCalculatorBoolean(value) => stack.push(PdfCalculatorBoolean(!value))
    PdfCalculatorInt(value) => stack.push(PdfCalculatorInt(Int::lnot(value)))
    PdfCalculatorFloat(_) =>
      raise PdfError::BadFunctionEvaluation(
        "calculator integer operand expected",
      )
  }
}

///|
fn pdf_fun_calculator_int_min() -> Int {
  -2147483648
}

///|
fn pdf_fun_calculator_int_max() -> Int {
  2147483647
}

///|
fn pdf_fun_calculator_bitshift(value : Int, shift : Int) -> Int {
  if shift >= 0 {
    value << shift
  } else {
    (value.reinterpret_as_uint() >> -shift).reinterpret_as_int()
  }
}

///|
fn pdf_fun_degrees_to_radians(value : Double) -> Double {
  value * @math.PI / 180.0
}

///|
fn pdf_fun_radians_to_degrees(value : Double) -> Double {
  value * 180.0 / @math.PI
}

///|
fn pdf_fun_calculator_eval_operations(
  operations : ArrayView[PdfCalculatorOp],
  stack : Array[PdfCalculatorValue],
) -> Unit raise PdfError {
  for operation in operations {
    match operation {
      PdfCalculatorInt(value) => stack.push(PdfCalculatorInt(value))
      PdfCalculatorFloat(value) => stack.push(PdfCalculatorFloat(value))
      PdfCalculatorAbs =>
        match pdf_fun_calculator_pop_value(stack) {
          PdfCalculatorInt(value) =>
            if value == pdf_fun_calculator_int_min() {
              stack.push(
                PdfCalculatorFloat(pdf_fun_calculator_int_max().to_double()),
              )
            } else {
              stack.push(PdfCalculatorInt(value.abs()))
            }
          PdfCalculatorFloat(value) =>
            stack.push(PdfCalculatorFloat(value.abs()))
          PdfCalculatorBoolean(_) =>
            raise PdfError::BadFunctionEvaluation("calculator number expected")
        }
      PdfCalculatorAdd =>
        pdf_fun_calculator_numeric_binary(
          stack,
          fn(left, right) { left + right },
          fn(left, right) { left + right },
        )
      PdfCalculatorAnd =>
        pdf_fun_calculator_boolean_or_integer_binary(
          stack,
          fn(left, right) { left && right },
          fn(left, right) { left & right },
        )
      PdfCalculatorBool(value) => stack.push(PdfCalculatorBoolean(value))
      PdfCalculatorBitshift => {
        let shift = pdf_fun_calculator_pop_int(stack)
        let value = pdf_fun_calculator_pop_int(stack)
        stack.push(PdfCalculatorInt(pdf_fun_calculator_bitshift(value, shift)))
      }
      PdfCalculatorAtan => {
        let denominator = pdf_fun_calculator_pop_number(stack)
        let numerator = pdf_fun_calculator_pop_number(stack)
        stack.push(
          PdfCalculatorFloat(
            pdf_fun_radians_to_degrees(@math.atan2(numerator, denominator)),
          ),
        )
      }
      PdfCalculatorCeiling =>
        match pdf_fun_calculator_pop_value(stack) {
          PdfCalculatorInt(value) => stack.push(PdfCalculatorInt(value))
          PdfCalculatorFloat(value) =>
            stack.push(PdfCalculatorFloat(value.ceil()))
          PdfCalculatorBoolean(_) =>
            raise PdfError::BadFunctionEvaluation("calculator number expected")
        }
      PdfCalculatorCos =>
        stack.push(
          PdfCalculatorFloat(
            @math.cos(
              pdf_fun_degrees_to_radians(pdf_fun_calculator_pop_number(stack)),
            ),
          ),
        )
      PdfCalculatorCopy => pdf_fun_calculator_copy(stack)
      PdfCalculatorCvi =>
        match pdf_fun_calculator_pop_value(stack) {
          PdfCalculatorInt(value) => stack.push(PdfCalculatorInt(value))
          PdfCalculatorFloat(value) =>
            stack.push(PdfCalculatorInt(value.floor().to_int()))
          PdfCalculatorBoolean(_) =>
            raise PdfError::BadFunctionEvaluation("calculator number expected")
        }
      PdfCalculatorCvr =>
        match pdf_fun_calculator_pop_value(stack) {
          PdfCalculatorInt(value) =>
            stack.push(PdfCalculatorFloat(value.to_double()))
          PdfCalculatorFloat(value) => stack.push(PdfCalculatorFloat(value))
          PdfCalculatorBoolean(_) =>
            raise PdfError::BadFunctionEvaluation("calculator number expected")
        }
      PdfCalculatorSub =>
        pdf_fun_calculator_numeric_binary(
          stack,
          fn(left, right) { left - right },
          fn(left, right) { left - right },
        )
      PdfCalculatorMul =>
        pdf_fun_calculator_numeric_binary(
          stack,
          fn(left, right) { left * right },
          fn(left, right) { left * right },
        )
      PdfCalculatorDiv => {
        let right = pdf_fun_calculator_pop_number(stack)
        let left = pdf_fun_calculator_pop_number(stack)
        stack.push(PdfCalculatorFloat(left / right))
      }
      PdfCalculatorDup => {
        if stack.length() == 0 {
          raise PdfError::BadFunctionEvaluation("calculator stack underflow")
        }
        stack.push(stack[stack.length() - 1])
      }
      PdfCalculatorExch => {
        if stack.length() < 2 {
          raise PdfError::BadFunctionEvaluation("calculator stack underflow")
        }
        let last = stack.length() - 1
        let value = stack[last]
        stack[last] = stack[last - 1]
        stack[last - 1] = value
      }
      PdfCalculatorExp => {
        let exponent = pdf_fun_calculator_pop_number(stack)
        let base = pdf_fun_calculator_pop_number(stack)
        stack.push(PdfCalculatorFloat(@math.pow(base, exponent)))
      }
      PdfCalculatorFloor =>
        match pdf_fun_calculator_pop_value(stack) {
          PdfCalculatorInt(value) => stack.push(PdfCalculatorInt(value))
          PdfCalculatorFloat(value) =>
            stack.push(PdfCalculatorInt(value.floor().to_int()))
          PdfCalculatorBoolean(_) =>
            raise PdfError::BadFunctionEvaluation("calculator number expected")
        }
      PdfCalculatorIdiv => {
        let right = pdf_fun_calculator_pop_int(stack)
        let left = pdf_fun_calculator_pop_int(stack)
        stack.push(PdfCalculatorInt(left / right))
      }
      PdfCalculatorIf(then_branch) =>
        if pdf_fun_calculator_pop_bool(stack) {
          pdf_fun_calculator_eval_operations(then_branch, stack)
        }
      PdfCalculatorIfElse(then_branch, else_branch) =>
        if pdf_fun_calculator_pop_bool(stack) {
          pdf_fun_calculator_eval_operations(then_branch, stack)
        } else {
          pdf_fun_calculator_eval_operations(else_branch, stack)
        }
      PdfCalculatorIndex => pdf_fun_calculator_index(stack)
      PdfCalculatorLn =>
        stack.push(
          PdfCalculatorFloat(@math.ln(pdf_fun_calculator_pop_number(stack))),
        )
      PdfCalculatorLog =>
        stack.push(
          PdfCalculatorFloat(@math.log10(pdf_fun_calculator_pop_number(stack))),
        )
      PdfCalculatorEq => {
        let right = pdf_fun_calculator_pop_value(stack)
        let left = pdf_fun_calculator_pop_value(stack)
        stack.push(
          PdfCalculatorBoolean(pdf_fun_calculator_values_equal(left, right)),
        )
      }
      PdfCalculatorGe => {
        let right = pdf_fun_calculator_pop_number(stack)
        let left = pdf_fun_calculator_pop_number(stack)
        stack.push(PdfCalculatorBoolean(left >= right))
      }
      PdfCalculatorGt => {
        let right = pdf_fun_calculator_pop_number(stack)
        let left = pdf_fun_calculator_pop_number(stack)
        stack.push(PdfCalculatorBoolean(left > right))
      }
      PdfCalculatorLe => {
        let right = pdf_fun_calculator_pop_number(stack)
        let left = pdf_fun_calculator_pop_number(stack)
        stack.push(PdfCalculatorBoolean(left <= right))
      }
      PdfCalculatorLt => {
        let right = pdf_fun_calculator_pop_number(stack)
        let left = pdf_fun_calculator_pop_number(stack)
        stack.push(PdfCalculatorBoolean(left < right))
      }
      PdfCalculatorMod => {
        let right = pdf_fun_calculator_pop_int(stack)
        let left = pdf_fun_calculator_pop_int(stack)
        stack.push(PdfCalculatorInt(left % right))
      }
      PdfCalculatorNe => {
        let right = pdf_fun_calculator_pop_value(stack)
        let left = pdf_fun_calculator_pop_value(stack)
        stack.push(
          PdfCalculatorBoolean(!pdf_fun_calculator_values_equal(left, right)),
        )
      }
      PdfCalculatorNeg =>
        match pdf_fun_calculator_pop_value(stack) {
          PdfCalculatorInt(value) =>
            if value == pdf_fun_calculator_int_min() {
              stack.push(
                PdfCalculatorFloat(pdf_fun_calculator_int_max().to_double()),
              )
            } else {
              stack.push(PdfCalculatorInt(-value))
            }
          PdfCalculatorFloat(value) => stack.push(PdfCalculatorFloat(-value))
          PdfCalculatorBoolean(_) =>
            raise PdfError::BadFunctionEvaluation("calculator number expected")
        }
      PdfCalculatorNot => pdf_fun_calculator_not(stack)
      PdfCalculatorOr =>
        pdf_fun_calculator_boolean_or_integer_binary(
          stack,
          fn(left, right) { left || right },
          fn(left, right) { left | right },
        )
      PdfCalculatorPop => {
        let _ = pdf_fun_calculator_pop_value(stack)
      }
      PdfCalculatorRoll => pdf_fun_calculator_roll(stack)
      PdfCalculatorRound =>
        match pdf_fun_calculator_pop_value(stack) {
          PdfCalculatorInt(value) => stack.push(PdfCalculatorInt(value))
          PdfCalculatorFloat(value) =>
            stack.push(PdfCalculatorInt(value.round().to_int()))
          PdfCalculatorBoolean(_) =>
            raise PdfError::BadFunctionEvaluation("calculator number expected")
        }
      PdfCalculatorSin =>
        stack.push(
          PdfCalculatorFloat(
            @math.sin(
              pdf_fun_degrees_to_radians(pdf_fun_calculator_pop_number(stack)),
            ),
          ),
        )
      PdfCalculatorSqrt =>
        stack.push(
          PdfCalculatorFloat(pdf_fun_calculator_pop_number(stack).sqrt()),
        )
      PdfCalculatorTruncate =>
        match pdf_fun_calculator_pop_value(stack) {
          PdfCalculatorInt(value) => stack.push(PdfCalculatorInt(value))
          PdfCalculatorFloat(value) =>
            stack.push(PdfCalculatorInt(value.trunc().to_int()))
          PdfCalculatorBoolean(_) =>
            raise PdfError::BadFunctionEvaluation("calculator number expected")
        }
      PdfCalculatorXor =>
        pdf_fun_calculator_boolean_or_integer_binary(
          stack,
          fn(left, right) { left != right },
          fn(left, right) { left ^ right },
        )
    }
  }
}

///|
fn PdfCalculatorFunction::eval_calculator(
  self : PdfCalculatorFunction,
  clamped_inputs : ArrayView[Double],
) -> Array[Double] raise PdfError {
  let stack : Array[PdfCalculatorValue] = []
  for input in clamped_inputs {
    stack.push(PdfCalculatorFloat(input))
  }
  pdf_fun_calculator_eval_operations(self.operations, stack)
  let output : Array[Double] = []
  for value in stack {
    match value {
      PdfCalculatorInt(value) => output.push(value.to_double())
      PdfCalculatorFloat(value) => output.push(value)
      PdfCalculatorBoolean(_) =>
        raise PdfError::BadFunctionEvaluation(
          "calculator output must be numeric",
        )
    }
  }
  output
}

///|
fn PdfFunction::clamped_inputs(
  self : PdfFunction,
  inputs : ArrayView[Double],
) -> Array[Double] raise PdfError {
  if self.domain.length() != inputs.length() * 2 {
    raise PdfError::BadFunctionEvaluation("domain arity mismatch")
  }
  let output : Array[Double] = []
  for index in 0.. Array[Double] raise PdfError {
  let clamped_inputs = self.clamped_inputs(inputs)
  let output = match self.func {
    PdfFunctionSampled(sampled) => sampled.eval_sampled(self, clamped_inputs)
    PdfFunctionInterpolation({ c0, c1, n }) => {
      let values : Array[Double] = []
      for input in clamped_inputs {
        let factor = @math.pow(input, n)
        for index in 0..
      stitching.eval_stitching(self, clamped_inputs)
    PdfFunctionCalculator(calculator) =>
      calculator.eval_calculator(clamped_inputs)
  }
  match self.range {
    Some(range) => pdf_fun_clamp_values(output, range)
    None => output
  }
}

///|
/// Compatibility wrapper for `PdfFunction::eval`.
pub fn pdf_eval_function(
  function : PdfFunction,
  inputs : ArrayView[Double],
) -> Array[Double] raise PdfError {
  function.eval(inputs)
}

///|
/// Print the function's diagnostic representation to standard output.
pub fn pdf_print_function(function : PdfFunction) -> Unit {
  println(function.debug_string())
}