///|
fn Interpreter::tick(self : Interpreter) -> Unit raise TclError {
  self.budget.val -= 1
  if self.budget.val < 0 {
    raise Invalid("execution budget")
  }
}

///|
fn Interpreter::invoke_procedure(
  self : Interpreter,
  definition : Procedure,
  values : Array[TclValue],
  depth : Int,
) -> TclValue raise TclError {
  let parameters = definition.parameters
  let body = definition.body
  let variadic = !parameters.is_empty() &&
    parameters[parameters.length() - 1].0 == "args"
  let fixed = parameters.length() - (if variadic { 1 } else { 0 })
  let supplied = values.length() - 1
  if !variadic && supplied > fixed {
    raise Invalid("procedure arity")
  }
  let mut required = 0
  for i in 0.. v
        None => raise Invalid("procedure arity")
      }
    }
    // Tcl 8.x duplicate formal names refer to the first slot.
    if !vars.contains(name) {
      vars[name] = value
    }
  }
  if variadic && !vars.contains("args") {
    vars["args"] = list_value(
      if supplied > fixed {
        values[fixed + 1:].to_owned()
      } else {
        []
      },
    )
  }
  let bindings : Map[String, Binding] = Map([])
  for name, value in vars {
    bindings[name] = {
      cell: { value: Some(Scalar(value)), declared: false, frame_local: true, },
      index: None,
      linked: false,
      array: None,
    }
  }
  let child = self.with_frame({
    vars: bindings,
    namespace_name: definition.namespace_name,
    procedure: true,
    parent: Some(self.frame),
  })
  let executed = Ok(child.execute_value(body, depth + 1)) catch {
    error => Err(error)
  }
  child.frame.release_variables()
  match executed {
    Ok(value) => value
    Err(error) => {
      let result = outcome(error)
      if result.level != 0 {
        self.propagate_value({ ..result, level: result.level - 1, })
      } else if result.code == 3 || result.code == 4 {
        let options = result.options.copy()
        option_set(options, "-errorcode", "TCL RESULT UNEXPECTED")
        raise Signal(
          completion(
            1,
            "invoked " +
            (if result.code == 3 { "break" } else { "continue" }) +
            " outside of a loop",
            options~,
          ),
        )
      } else if result.code == 1 {
        // Text arguments are only needed for the observable error context.
        let args = values.map(v => v.text)
        let options = result.options.copy()
        let line = option_get(options, "-errorline").unwrap_or("1")
        let info = option_get(options, "-errorinfo").unwrap_or(
          result.value.text,
        )
        option_set(
          options,
          "-errorinfo",
          info + "\n    (procedure \"" + args[0] + "\" line " + line + ")",
        )
        let stack = option_get(options, "-errorstack").unwrap_or("")
        option_set(
          options,
          "-errorstack",
          stack +
          (if stack.is_empty() { "" } else { " " }) +
          format_list(["CALL", format_list(args)]),
        )
        raise Signal({ ..result, options, })
      } else {
        raise Signal(result)
      }
    }
  }
}

///|
// 0 normal, 1 continue, 2 break; return and errors propagate across loop bodies.
fn Interpreter::loop_body(
  self : Interpreter,
  source : TclValue,
  depth : Int,
) -> Int raise TclError {
  try {
    ignore(self.execute_value_script(source, depth + 1, discard_result=true))
    0
  } catch {
    Break => 2
    Continue => 1
    Signal(result) if result.actual_code() == 3 => 2
    Signal(result) if result.actual_code() == 4 => 1
    error => raise error
  }
}

///|
fn Interpreter::control_command(
  self : Interpreter,
  values : Array[TclValue],
  depth : Int,
) -> TclValue raise TclError {
  let n = values.length()
  let name = values[0].text
  let text = match name {
    "break" => {
      if n != 1 {
        raise Invalid("break arity")
      }
      raise Break
    }
    "continue" => {
      if n != 1 {
        raise Invalid("continue arity")
      }
      raise Continue
    }
    "error" | "throw" => return self.error_command(values)
    "return" => return self.return_command(values)
    "catch" => self.catch_command(values, depth)
    "try" => return self.try_command(values, depth)
    "while" => {
      if n != 3 {
        raise Invalid("while arity")
      }
      while self.math(values[1].text, depth) != 0 {
        self.tick()
        if self.loop_body(values[2], depth) == 2 {
          break
        }
      }
      ""
    }
    "for" => {
      if n != 5 {
        raise Invalid("for arity")
      }
      ignore(
        self.execute_value_script(values[1], depth + 1, discard_result=true),
      )
      while self.math(values[2].text, depth) != 0 {
        self.tick()
        if self.loop_body(values[4], depth) == 2 {
          break
        }
        let code = self.loop_body(values[3], depth)
        if code == 2 {
          break
        }
        // Tcl forbids continue from the loop's next script.
        if code == 1 {
          raise Continue
        }
      }
      ""
    }
    "foreach" | "lmap" => {
      let collected = []
      if n < 4 || n % 2 != 0 {
        raise Invalid("foreach arity")
      }
      let groups : Array[(Array[String], Array[TclValue])] = []
      let mut count = 0
      let mut i = 1
      while i < n - 1 {
        let names = parse_list(values[i].text)
        let values = values[i + 1].as_list()
        if names.is_empty() {
          raise Invalid("foreach variable list is empty")
        }
        count = count.max(
          (values.length() + names.length() - 1) / names.length(),
        )
        groups.push((names, values))
        i += 2
      }
      for iteration in 0.. continue
            Break => break
            Signal(result) if result.actual_code() == 4 => continue
            Signal(result) if result.actual_code() == 3 => break
            error => raise error
          }
        } else if self.loop_body(values[n - 1], depth) == 2 {
          break
        }
      }
      if name == "lmap" {
        return list_value(collected)
      } else {
        self.state.return_options.val = []
        ""
      }
    }
    _ => raise Invalid("unsupported control command")
  }
  text_value(text)
}