///|
fn normalize_filter_test_name(name : String) -> String {
  let mut has_ws = false
  for i in 0.. Value?,
) -> Value? {
  if idx < 0 {
    f()
  } else {
    match cache[idx] {
      Some(rv) => Some(rv)
      None => {
        let val = f()
        cache[idx] = val
        val
      }
    }
  }
}

///|
/// Evaluates a template.
fn vm_eval(
  env : Environment,
  instructions : Instructions,
  root : Value,
  blocks : Map[String, Instructions],
  out : Output,
  auto_escape : AutoEscape,
) -> (Value?, State) raise TemplateError {
  let state = State::new(
    Context::new_with_frame(env, Frame::new(root.validate())),
    auto_escape,
    instructions,
    prepare_blocks(blocks),
  )
  let rv = eval_state(state, out)
  (rv, state)
}

///|
/// Evaluates a macro by temporarily switching the active state.
fn eval_macro(
  state : State,
  instructions : Instructions,
  pc : Int,
  out : Output,
  closure : Closure?,
  caller : Value?,
  args : Array[Value],
) -> Value? raise TemplateError {
  let context_base = state.ctx.clone_base()
  let ctx = Context::new_with_frame(state.env(), Frame::new(context_base))
  ctx.push_frame({ ..Frame::default(), closure_context: closure, })
  if caller is Some(caller) {
    ctx.store("caller", caller)
  }
  ctx.incr_depth(state.ctx.depth() + macro_recursion_cost)
  let old_ctx = state.ctx
  state.ctx = ctx
  let auto_escape = state.auto_escape
  let rv = {
    errdefer {
      state.ctx = old_ctx
    }
    state.with_execution_state(instructions, auto_escape, None, Isolate, state => {
      do_eval(state, out, { values: args.copy(), }, pc)
    })
  }
  state.ctx = old_ctx
  rv
}

///|
fn eval_state(state : State, out : Output) -> Value? raise TemplateError {
  do_eval(state, out, Stack::new(), 0)
}

///|
fn do_eval(
  state : State,
  out : Output,
  stack : Stack,
  pc : Int,
) -> Value? raise TemplateError {
  let initial_auto_escape = state.auto_escape
  let undefined_behavior = state.undefined_behavior()
  let strict_undefined = undefined_behavior is (Strict | SemiStrict)
  let auto_escape_stack : Array[AutoEscape] = []
  let mut next_loop_recursion_jump : (Int, Bool)? = None
  let mut loaded_filters : Array[Value?] = Array::make(MAX_LOCALS, None)
  let mut loaded_tests : Array[Value?] = Array::make(MAX_LOCALS, None)
  // If we are extending we are holding the instructions of the target parent
  // template here.  This is used to detect multiple extends and the
  // evaluation uses these instructions when it makes it to the end of the
  // instructions.
  let mut parent_instructions : Instructions? = None
  let mut pc = pc
  for ;; {
    let instr = match state.instructions.get(pc) {
      Some(instr) => instr
      None => {
        // when an extends statement appears in a template, when we hit the
        // last instruction we need to check if parent instructions were
        // stashed away (which means we found an extends tag which invoked
        // `LoadBlocks`).  If we do find instructions, we reset back to 0
        // from the new instructions.
        match parent_instructions {
          Some(instr) => {
            parent_instructions = None
            state.instructions = instr
          }
          None => break
        }
        out.end_capture(NoEscape) |> ignore
        pc = 0
        // because we swap out the instructions we also need to unload all
        // the filters and tests to ensure that we are not accidentally
        // reusing the local_ids for completely different filters.
        loaded_filters = Array::make(MAX_LOCALS, None)
        loaded_tests = Array::make(MAX_LOCALS, None)
        continue
      }
    }
    // the next program counter (`None` means `pc + 1`)
    let jump : Int? = try {
      if state.fuel_tracker is Some(tracker) {
        tracker.track(instr)
      }
      match instr {
        Swap => {
          let a = stack.pop()
          let b = stack.pop()
          stack.push(a)
          stack.push(b)
          None
        }
        EmitRaw(val) => {
          out.write_str(val)
          None
        }
        Emit => {
          let value = stack.pop()
          if state.env().formatter_is_default {
            if strict_undefined && value is Undefined(Default) {
              raise TemplateError::from_kind(UndefinedError)
            }
            write_escaped(out, state.auto_escape, value)
          } else {
            state.env().format(value, state, out)
          }
          None
        }
        StoreLocal(name) => {
          state.ctx.store(name, stack.pop())
          None
        }
        Lookup(name) => {
          stack.push(
            state.lookup(name).unwrap_or(Value::undefined()).validate(),
          )
          None
        }
        GetAttr(name) => {
          let a = stack.pop()
          // This is a common enough operation that it's interesting to
          // consider a fast path here.
          stack.push(
            match a.get_attr_fast(name) {
              Some(value) => value.validate()
              None => undefined_behavior.handle_undefined(a.is_undefined())
            },
          )
          None
        }
        SetAttr(name) => {
          let b = stack.pop()
          let a = stack.pop()
          match b {
            Object({ inner: Namespace(ns), .. }) => ns[name] = a
            _ =>
              raise TemplateError::new(
                InvalidOperation,
                "can only assign to namespaces, not \{b.kind()}",
              )
          }
          None
        }
        GetItem => {
          let a = stack.pop()
          let b = stack.pop()
          stack.push(
            match b.get_item_opt(a) {
              Some(value) => value.validate()
              None => undefined_behavior.handle_undefined(b.is_undefined())
            },
          )
          None
        }
        Slice => {
          let step = stack.pop()
          let stop = stack.pop()
          let b = stack.pop()
          let a = stack.pop()
          if a.is_undefined() && undefined_behavior is Strict {
            raise TemplateError::from_kind(UndefinedError)
          }
          stack.push(value_slice(a, b, stop, step))
          None
        }
        LoadConst(value) => {
          stack.push(value)
          None
        }
        BuildMap(pair_count) => {
          let map : Map[Value, Value] = Map([])
          stack.reverse_top(pair_count * 2)
          for _ in 0.. {
          let map : Map[Value, Value] = Map([])
          stack.reverse_top(pair_count * 2)
          for _ in 0.. {
          let sources = []
          for _ in 0.. {
          let count = match n {
            Some(n) => n
            None => stack.pop().as_usize().unwrap_or(0)
          }
          let v = []
          for _ in 0.. {
          let count = match n {
            Some(n) => n
            None => stack.pop().as_usize().unwrap_or(0)
          }
          let v = []
          for _ in 0.. {
          unpack_list(stack, count)
          None
        }
        UnpackLists(count) => {
          let lists = []
          for _ in 0..= 0; i = i - 1 {
            for item in lists[i].try_iter() {
              stack.push(item)
              len += 1
            }
          }
          stack.push(Value::from_int(len))
          None
        }
        Add => binop(stack, value_add)
        Sub => binop(stack, value_sub)
        Mul => binop(stack, value_mul)
        Div => binop(stack, value_div)
        IntDiv => binop(stack, value_int_div)
        Rem => binop(stack, value_rem)
        Pow => binop(stack, value_pow)
        Eq => cmp_binop(stack, undefined_behavior, (a, b) => a == b)
        Ne => cmp_binop(stack, undefined_behavior, (a, b) => a != b)
        Gt => cmp_binop(stack, undefined_behavior, (a, b) => a.cmp(b) > 0)
        Gte => cmp_binop(stack, undefined_behavior, (a, b) => a.cmp(b) >= 0)
        Lt => cmp_binop(stack, undefined_behavior, (a, b) => a.cmp(b) < 0)
        Lte => cmp_binop(stack, undefined_behavior, (a, b) => a.cmp(b) <= 0)
        Not => {
          let a = stack.pop()
          stack.push(Value::from_bool(!undefined_behavior.is_true(a)))
          None
        }
        StringConcat => {
          let a = stack.pop()
          let b = stack.pop()
          undefined_behavior.assert_value_not_undefined(b)
          undefined_behavior.assert_value_not_undefined(a)
          stack.push(string_concat(b, a))
          None
        }
        In => {
          let a = stack.pop()
          let b = stack.pop()
          // the in-operator can fail if either side is undefined and we are
          // in strict mode.
          undefined_behavior.assert_iterable(a)
          undefined_behavior.assert_value_not_undefined(b)
          stack.push(value_contains(a, b))
          None
        }
        CompareAndPreserve(op) => {
          let b = stack.pop()
          let a = stack.pop()
          let result = match op {
            In | NotIn => {
              undefined_behavior.assert_iterable(b)
              undefined_behavior.assert_value_not_undefined(a)
              let contains = value_contains(b, a).is_true()
              if op is NotIn {
                !contains
              } else {
                contains
              }
            }
            _ => {
              undefined_behavior.assert_value_not_undefined(a)
              undefined_behavior.assert_value_not_undefined(b)
              match op {
                Eq => a == b
                Ne => a != b
                Lt => a.cmp(b) < 0
                Lte => a.cmp(b) <= 0
                Gt => a.cmp(b) > 0
                _ => a.cmp(b) >= 0
              }
            }
          }
          stack.push(b)
          stack.push(Value::from_bool(result))
          None
        }
        Neg => {
          let a = stack.pop()
          stack.push(value_neg(a))
          None
        }
        PushWith => {
          state.ctx.push_frame(Frame::default())
          None
        }
        PopFrame => {
          state.ctx.pop_frame() |> ignore
          None
        }
        PopLoopFrame => {
          let frame = state.ctx.pop_frame()
          match frame.current_loop {
            Some(l) =>
              match l.current_recursion_jump {
                Some((target, end_capture)) => {
                  l.current_recursion_jump = None
                  if end_capture {
                    stack.push(out.end_capture(state.auto_escape))
                  }
                  Some(target)
                }
                None => None
              }
            None => None
          }
        }
        IsUndefined => {
          let a = stack.pop()
          stack.push(Value::from_bool(a.is_undefined()))
          None
        }
        PushLoop(flags) => {
          let a = stack.pop()
          let jump = next_loop_recursion_jump
          next_loop_recursion_jump = None
          push_loop(state, a, flags, pc, jump)
          None
        }
        Iterate(jump_target) =>
          match state.ctx.next_loop_item() {
            Some(item) => {
              stack.push(item.validate())
              None
            }
            None => Some(jump_target)
          }
        PushDidNotIterate => {
          let did_not_iterate = match state.ctx.current_loop() {
            Some(l) => l.did_not_iterate()
            None => false
          }
          stack.push(Value::from_bool(did_not_iterate))
          None
        }
        Jump(jump_target) => Some(jump_target)
        JumpIfFalse(jump_target) => {
          let a = stack.pop()
          if !undefined_behavior.is_true(a) {
            Some(jump_target)
          } else {
            None
          }
        }
        JumpIfFalseOrPop(jump_target) =>
          if !undefined_behavior.is_true(stack.peek()) {
            Some(jump_target)
          } else {
            stack.pop() |> ignore
            None
          }
        JumpIfTrueOrPop(jump_target) =>
          if undefined_behavior.is_true(stack.peek()) {
            Some(jump_target)
          } else {
            stack.pop() |> ignore
            None
          }
        PushAutoEscape => {
          let a = stack.pop()
          auto_escape_stack.push(state.auto_escape)
          state.auto_escape = derive_auto_escape(a, initial_auto_escape)
          None
        }
        PopAutoEscape => {
          if auto_escape_stack.pop() is Some(ae) {
            state.auto_escape = ae
          }
          None
        }
        BeginCapture(mode) => {
          out.begin_capture(mode)
          None
        }
        EndCapture => {
          stack.push(out.end_capture(state.auto_escape))
          None
        }
        ApplyFilter(name, arg_count, local_id) => {
          let normalized_name = normalize_filter_test_name(name)
          let filter = match
            get_or_lookup_local(loaded_filters, local_id, () => {
              state.env().get_filter(normalized_name)
            }) {
            Some(f) => f
            None =>
              raise TemplateError::new(
                UnknownFilter,
                "filter \{normalized_name} is unknown",
              )
          }
          let args = stack.get_call_args(arg_count)
          let rv = filter.call(state, args)
          stack.drop_top(args.length())
          stack.push(rv)
          None
        }
        PerformTest(name, arg_count, local_id) => {
          let normalized_name = normalize_filter_test_name(name)
          let test_fn = match
            get_or_lookup_local(loaded_tests, local_id, () => {
              state.env().get_test(normalized_name)
            }) {
            Some(f) => f
            None =>
              raise TemplateError::new(
                UnknownTest,
                "test \{normalized_name} is unknown",
              )
          }
          let args = stack.get_call_args(arg_count)
          let rv = test_fn.call(state, args)
          stack.drop_top(args.length())
          stack.push(Value::from_bool(rv.is_true()))
          None
        }
        CallFunction(name, arg_count) => {
          let args = stack.get_call_args(arg_count)
          // super is a special function reserved for super-ing into blocks.
          let rv = if name == "super" {
            if !args.is_empty() {
              raise TemplateError::new(
                InvalidOperation,
                "super() takes no arguments",
              )
            }
            perform_super(state, out, true)
          } else {
            match state.lookup(name) {
              Some(Object({ inner: Loop(loop_object), .. })) => {
                // calling loops is a special operation that starts the
                // recursion process.
                if args.length() != 1 {
                  raise TemplateError::new(
                    InvalidOperation,
                    "loop() takes one argument",
                  )
                }
                guard loop_object.recurse_jump_target is Some(target) else {
                  raise TemplateError::new(
                    InvalidOperation,
                    "cannot recurse outside of recursive loop",
                  )
                }
                next_loop_recursion_jump = Some((pc + 1, true))
                out.begin_capture(Capture)
                return_jump(target)
              }
              Some(func) => Push(func.call(state, args))
              None =>
                raise TemplateError::new(UnknownFunction, "\{name} is unknown")
            }
          }
          match rv {
            JumpTo(target) => Some(target)
            Push(rv) => {
              stack.drop_top(args.length())
              stack.push(rv)
              None
            }
          }
        }
        CallMethod(name, arg_count) => {
          let args = stack.get_call_args(arg_count)
          let rv = args[0].call_method(
            state,
            name,
            args[1:args.length()].to_owned(),
          )
          stack.drop_top(args.length())
          stack.push(rv)
          None
        }
        CallObject(arg_count) => {
          let args = stack.get_call_args(arg_count)
          let rv = args[0].call(state, args[1:args.length()].to_owned())
          stack.drop_top(args.length())
          stack.push(rv)
          None
        }
        DupTop => {
          stack.push(stack.peek())
          None
        }
        DiscardTop => {
          stack.pop() |> ignore
          None
        }
        FastSuper => {
          perform_super(state, out, false) |> ignore
          None
        }
        FastRecurse =>
          match state.ctx.current_loop() {
            Some(l) => {
              guard l.object.recurse_jump_target is Some(target) else {
                raise TemplateError::new(
                  InvalidOperation,
                  "cannot recurse outside of recursive loop",
                )
              }
              next_loop_recursion_jump = Some((pc + 1, false))
              Some(target)
            }
            None => raise TemplateError::new(UnknownFunction, "loop is unknown")
          }
        // Explanation on the behavior of `LoadBlocks` and rendering of
        // inherited templates:
        //
        // MiniJinja inherits the behavior from Jinja2 where extending loads
        // the blocks (`LoadBlocks`) and the rest of the template keeps
        // executing but with output disabled, only at the end the parent
        // template is then invoked.  This has the effect that you can still
        // set variables or declare macros and that they become visible in
        // the blocks.
        LoadBlocks => {
          let a = stack.pop()
          if parent_instructions is Some(_) {
            raise TemplateError::new(
              InvalidOperation,
              "tried to extend a second time in a template",
            )
          }
          parent_instructions = Some(load_blocks(a, state))
          out.begin_capture(Discard)
          None
        }
        Include(ignore_missing) => {
          let a = stack.pop()
          perform_include(a, state, out, ignore_missing)
          None
        }
        ExportLocals => {
          let captured = stack.pop()
          let locals = state.ctx.current_locals()
          let values : Map[Value, Value] = Map([])
          // locals are a BTreeMap in MiniJinja, so exports are sorted
          for key in sorted_keys(locals) {
            values[Value::from_string(key)] = locals[key]
          }
          stack.push(Value::from_object(ModuleObject::{ values, captured, }))
          None
        }
        CallBlock(name) => {
          if parent_instructions is None && !out.is_discarding() {
            call_block(name, state, out) |> ignore
          }
          None
        }
        BuildMacro(name, offset, flags) => {
          build_macro(stack, state, offset, name, flags)
          None
        }
        Return => Some(-1)
        Enclose(name) => {
          // The first enclosed value creates a state-owned closure shared by
          // all macros declared in this frame.
          if state.ctx.closure() is None {
            state.ctx.reset_closure(Some({ values: Map([]), }))
          }
          state.ctx.enclose(name)
          None
        }
        GetClosure => {
          stack.push(
            match state.ctx.closure() {
              Some(closure) => Object(DynObject::new(ClosureRef(closure)))
              None => Value::undefined()
            },
          )
          None
        }
      }
    } catch {
      err => raise processed_err(err, pc, state)
    }
    match jump {
      Some(-1) => break // Return
      Some(target) => pc = target
      None => pc += 1
    }
  }
  stack.try_pop()
}

///|
priv enum CallResult {
  JumpTo(Int)
  Push(Value)
}

///|
fn return_jump(target : Int) -> CallResult {
  JumpTo(target)
}

///|
fn binop(
  stack : Stack,
  f : (Value, Value) -> Value raise TemplateError,
) -> Int? raise TemplateError {
  let b = stack.pop()
  let a = stack.pop()
  stack.push(f(a, b))
  None
}

///|
fn cmp_binop(
  stack : Stack,
  undefined_behavior : UndefinedBehavior,
  f : (Value, Value) -> Bool,
) -> Int? raise TemplateError {
  let b = stack.pop()
  let a = stack.pop()
  undefined_behavior.assert_value_not_undefined(a)
  undefined_behavior.assert_value_not_undefined(b)
  stack.push(Value::from_bool(f(a, b)))
  None
}

///|
fn merge_kwargs(
  state : State,
  values : Array[Value],
) -> Value raise TemplateError {
  let rv : Map[Value, Value] = Map([])
  for value in values {
    state.undefined_behavior().assert_iterable(value)
    let iter = match value {
      Object(obj) if obj.repr() is Map =>
        match obj.try_iter_pairs() {
          Some(iter) => iter
          None =>
            raise TemplateError::new(
              InvalidOperation,
              "attempted to apply keyword arguments from non map (got \{value.kind()})",
            )
        }
      _ =>
        raise TemplateError::new(
          InvalidOperation,
          "attempted to apply keyword arguments from non map (got \{value.kind()})",
        )
    }
    for pair in iter {
      rv[pair.0] = pair.1
    }
  }
  Value::from_kwargs_map(rv)
}

///|
fn perform_include(
  name : Value,
  state : State,
  out : Output,
  ignore_missing : Bool,
) -> Unit raise TemplateError {
  // candidates are consumed lazily: iteration stops at the first template
  // that exists (the value might be a one-shot iterator).
  let choices : Iter[Value] = match name {
    Object(obj) =>
      match obj.try_iter() {
        Some(iter) => iter
        None => Iter::empty()
      }
    _ => Iter::singleton(name)
  }
  let templates_tried = []
  for choice in choices {
    guard choice.as_str() is Some(name) else {
      raise TemplateError::new(
        InvalidOperation,
        "template name was not a string",
      )
    }
    let tmpl = state.get_template(name) catch {
      err => {
        if err.kind() == TemplateNotFound {
          templates_tried.push(choice)
        } else {
          raise err
        }
        continue
      }
    }
    let compiled = tmpl.compiled
    state.ctx.incr_depth(include_recursion_cost)
    let current_block = state.current_block
    let old_closure = state.ctx.take_closure()
    let rv = try {
      state.with_execution_state(
        compiled.instructions,
        compiled.initial_auto_escape,
        current_block,
        Replace(prepare_blocks(compiled.blocks)),
        state => eval_state(state, out),
      )
      |> ignore
      Ok(())
    } catch {
      err => Err(err)
    }
    state.ctx.reset_closure(old_closure)
    state.ctx.decr_depth(include_recursion_cost)
    if rv is Err(err) {
      raise TemplateError::new(BadInclude, "error in \"\{tmpl.name()}\"").with_source(
        err,
      )
    }
    return
  }
  if !templates_tried.is_empty() && !ignore_missing {
    raise TemplateError::new(
      TemplateNotFound,
      if templates_tried.length() == 1 {
        "tried to include non-existing template \{templates_tried[0].debug_string()}"
      } else {
        "tried to include one of multiple templates, none of which existed \{Value::from_array(templates_tried)}"
      },
    )
  }
}

///|
fn perform_super(
  state : State,
  out : Output,
  capture : Bool,
) -> CallResult raise TemplateError {
  guard state.current_block is Some(name) else {
    raise TemplateError::new(InvalidOperation, "cannot super outside of block")
  }
  guard state.blocks.get(name) is Some(block_stack) else {
    raise TemplateError::new(InvalidOperation, "no parent block exists")
  }
  if !block_stack.push() {
    raise TemplateError::new(InvalidOperation, "no parent block exists")
  }
  {
    errdefer block_stack.pop()
    state.ctx.push_frame(Frame::default())
  }
  if capture {
    out.begin_capture(Capture)
  }
  let instructions = block_stack.instructions()
  let auto_escape = state.auto_escape
  let current_block = state.current_block
  let rv = try {
    state.with_execution_state(instructions, auto_escape, current_block, Keep, state => {
      eval_state(state, out)
    })
    |> ignore
    Ok(())
  } catch {
    err => Err(err)
  }
  state.ctx.pop_frame() |> ignore
  block_stack.pop()
  if rv is Err(err) {
    raise TemplateError::new(EvalBlock, "error in super block").with_source(err)
  }
  if capture {
    Push(out.end_capture(state.auto_escape))
  } else {
    Push(Value::undefined())
  }
}

///|
fn load_blocks(name : Value, state : State) -> Instructions raise TemplateError {
  guard name.as_str() is Some(name) else {
    raise TemplateError::new(InvalidOperation, "template name was not a string")
  }
  if state.loaded_templates.contains(name) {
    raise TemplateError::new(
      InvalidOperation,
      "cycle in template inheritance. \{@rfmt.str_debug(name)} was referenced more than once",
    )
  }
  let tmpl = state.get_template(name)
  let compiled = tmpl.compiled
  state.loaded_templates.add(compiled.instructions.name)
  for block_name, instr in compiled.blocks {
    match state.blocks.get(block_name) {
      Some(stack) => stack.instructions.push(instr)
      None => state.blocks[block_name] = { instructions: [instr], depth: 0, }
    }
  }
  compiled.instructions
}

///|
fn call_block(
  name : String,
  state : State,
  out : Output,
) -> Value? raise TemplateError {
  guard state.blocks.get(name) is Some(block_stack) else {
    raise TemplateError::new(UnknownBlock, "block '\{name}' not found")
  }
  if block_stack.instructions.length() == 1 &&
    block_stack.instructions().required_block {
    raise TemplateError::new(
      InvalidOperation,
      "Required block '\{name}' not found",
    )
  }
  let instructions = block_stack.instructions()
  let auto_escape = state.auto_escape
  state.with_execution_state(instructions, auto_escape, Some(name), Keep, state => {
    state.ctx.push_frame(Frame::default())
    eval_state(state, out)
  })
}

///|
fn derive_auto_escape(
  value : Value,
  initial_auto_escape : AutoEscape,
) -> AutoEscape raise TemplateError {
  match (value.as_str(), value == Value::from_bool(true)) {
    (Some("html"), _) => Html
    (Some("json"), _) => Json
    (Some("none"), _) | (None, false) => NoEscape
    (None, true) =>
      if initial_auto_escape is NoEscape {
        Html
      } else {
        initial_auto_escape
      }
    _ =>
      raise TemplateError::new(
        InvalidOperation,
        "invalid value to autoescape tag",
      )
  }
}

///|
fn push_loop(
  state : State,
  iterable : Value,
  flags : Int,
  pc : Int,
  current_recursion_jump : (Int, Bool)?,
) -> Unit raise TemplateError {
  let iter = state.undefined_behavior().try_iter(iterable) catch {
      err =>
        match current_recursion_jump {
          Some((jump_instr, _)) => {
            // When a recursion error happens, we need to process the error at
            // both the recursion call site and the loop definition.
            process_err(err, pc, state)
            let call_err = TemplateError::new(
              InvalidOperation,
              "cannot recurse because of non-iterable value",
            )
            process_err(call_err, jump_instr - 1, state)
            raise call_err.with_source(err)
          }
          None => raise err
        }
    }
  let depth = match state.ctx.current_loop() {
    Some(l) if l.object.recurse_jump_target is Some(_) => l.object.depth + 1
    _ => 0
  }
  state.ctx.push_frame({
    ..Frame::default(),
    current_loop: Some(
      LoopState::new(
        iter,
        depth,
        (flags & LOOP_FLAG_WITH_LOOP_VAR) != 0,
        if (flags & LOOP_FLAG_RECURSIVE) != 0 {
          Some(pc)
        } else {
          None
        },
        current_recursion_jump,
      ),
    ),
  })
}

///|
fn unpack_list(stack : Stack, count : Int) -> Unit raise TemplateError {
  let top = stack.pop()
  let iter = match top {
    Object(obj) =>
      match obj.try_iter() {
        Some(iter) => iter
        None => raise TemplateError::new(CannotUnpack, "value is not iterable")
      }
    _ => raise TemplateError::new(CannotUnpack, "value is not iterable")
  }
  let mut n = 0
  for item in iter {
    stack.push(item)
    n += 1
  }
  if n == count {
    stack.reverse_top(n)
  } else {
    raise TemplateError::new(
      CannotUnpack,
      "sequence of wrong length (expected \{count}, got \{n})",
    )
  }
}

///|
fn build_macro(
  stack : Stack,
  state : State,
  offset : Int,
  name : String,
  flags : Int,
) -> Unit raise TemplateError {
  let arg_spec = stack.pop().try_iter().to_array()
  let closure = match stack.pop() {
    Object({ inner: ClosureRef(c), .. }) => Some(c)
    _ => None
  }
  stack.push(
    Value::from_object(MacroObject::{
      name: Value::from_string(name),
      arg_spec,
      instructions: state.instructions,
      offset,
      state_id: state.id,
      closure,
      caller_reference: (flags & MACRO_CALLER) != 0,
    }),
  )
}

///|
/// Attaches the location and debug info to `err` and returns it.
fn processed_err(err : TemplateError, pc : Int, state : State) -> TemplateError {
  process_err(err, pc, state)
  err
}

///|
fn process_err(err : TemplateError, pc : Int, state : State) -> Unit {
  // only attach line information if the error does not have line info yet.
  if err.line() is None {
    match state.instructions.get_span(pc) {
      Some(span) => err.set_filename_and_span(state.instructions.name, span)
      None =>
        if state.instructions.get_line(pc) is Some(lineno) {
          err.set_filename_and_line(state.instructions.name, lineno)
        }
    }
  }
  // only attach debug info if we don't have one yet and we are in debug
  // mode.
  if state.env().debug && err.debug_info() is None {
    err.attach_debug_info(state.make_debug_info(pc, state.instructions))
  }
}