///|
priv struct Context {
  scopes : Array[Map[String, Value]]
}

///|
fn Context::new(root : Map[String, Value]) -> Context {
  { scopes: [root.copy()] }
}

///|
fn Context::push_values(self : Context, values : Map[String, Value]) -> Unit {
  self.scopes.push(values)
}

///|
fn Context::pop(self : Context) -> Unit {
  if self.scopes.length() > 1 {
    ignore(self.scopes.pop())
  }
}

///|
fn Context::set(self : Context, name : String, value : Value) -> Unit {
  self.scopes[self.scopes.length() - 1][name] = value
}

///|
fn Context::resolve(self : Context, path : VarPath) -> Value? {
  guard path.parts is [first, .. rest] else { return None }
  let mut current : Value? = None
  for index = self.scopes.length() - 1; index >= 0; index = index - 1 {
    match self.scopes[index].get(first) {
      Some(value) => {
        current = Some(value)
        break
      }
      None => ()
    }
  }
  for part in rest {
    current = match current {
      Some(MapValue(values)) => values.get(part)
      _ => return None
    }
  }
  current
}

///|
fn Context::snapshot(self : Context) -> Array[Map[String, Value]] {
  self.scopes.map(scope => scope.copy())
}

///|
fn Context::copy(self : Context) -> Context {
  { scopes: self.snapshot() }
}

///|
priv struct BlockFrame {
  name : String
  index : Int
}

///|
priv struct MacroDefinition {
  name : String
  params : Array[MacroParam]
  body : Array[Expr]
  captured : Array[Map[String, Value]]
}

///|
priv struct EngineState {
  environment : Environment
  options : RenderOptions
  mut fuel : Int
  mut recursion : Int
  mut output_size : Int
  block_chains : Map[String, Array[Array[Expr]]]
  block_stack : Array[BlockFrame]
  macros : Map[String, MacroDefinition]
  template_stack : Array[String]
}

///|
priv enum Flow {
  Normal
  BreakFlow
  ContinueFlow
} derive(Eq)

///|
fn EngineState::consume(
  self : EngineState,
  amount : Int,
) -> Unit raise JinjaError {
  self.fuel -= amount
  if self.fuel < 0 {
    raise RenderError("Template fuel exhausted")
  }
}

///|
priv enum RenderOutput {
  Buffered(StringBuilder)
  Streaming((String) -> Unit)
}

///|
fn RenderOutput::write(self : RenderOutput, text : String) -> Unit {
  match self {
    Buffered(buffer) => buffer.write_string(text)
    Streaming(write) => write(text)
  }
}

///|
fn RenderOutput::to_string(self : RenderOutput) -> String {
  match self {
    Buffered(buffer) => buffer.to_string()
    Streaming(_) => ""
  }
}

///|
fn EngineState::enter(self : EngineState) -> Unit raise JinjaError {
  if self.recursion >= self.options.max_recursion {
    raise RenderError("Template recursion limit exceeded")
  }
  self.recursion += 1
}

///|
fn EngineState::leave(self : EngineState) -> Unit {
  self.recursion -= 1
}

///|
fn write_checked(
  output : RenderOutput,
  text : String,
  state : EngineState,
) -> Unit raise JinjaError {
  state.output_size += utf8_length(text)
  if state.output_size > state.options.max_output_size {
    raise RenderError("Template output size limit exceeded")
  }
  output.write(text)
}

///|
fn render_compiled(
  template : Template,
  root : Map[String, Value],
  environment : Environment,
  name : String,
) -> String raise JinjaError {
  let output = RenderOutput::Buffered(StringBuilder())
  render_compiled_into(template, root, environment, name, output) catch {
    RenderError(message) =>
      raise RenderError("Template '" + name + "': " + message)
    error => raise error
  }
  output.to_string()
}

///|
fn render_compiled_to(
  template : Template,
  root : Map[String, Value],
  environment : Environment,
  name : String,
  write : (String) -> Unit,
) -> Unit raise JinjaError {
  render_compiled_into(
    template,
    root,
    environment,
    name,
    RenderOutput::Streaming(write),
  ) catch {
    RenderError(message) =>
      raise RenderError("Template '" + name + "': " + message)
    error => raise error
  }
}

///|
fn render_compiled_into(
  template : Template,
  root : Map[String, Value],
  environment : Environment,
  name : String,
  output : RenderOutput,
) -> Unit raise JinjaError {
  let (root_template, chains, prefixes) = resolve_inheritance(
    template, environment,
  )
  let options = environment.options_for(name)
  let state : EngineState = {
    environment,
    options,
    fuel: options.fuel,
    recursion: 0,
    output_size: 0,
    block_chains: chains,
    block_stack: [],
    macros: Map([]),
    template_stack: [name],
  }
  let context = Context::new(root)
  for prefix in prefixes {
    ignore(render_engine_nodes(prefix, context, state, output))
  }
  ignore(render_engine_nodes(root_template.body, context, state, output))
}

///|
fn resolve_inheritance(
  template : Template,
  environment : Environment,
) -> (Template, Map[String, Array[Array[Expr]]], Array[Array[Expr]]) raise JinjaError {
  let templates : Array[Template] = [template]
  let mut current = template
  let seen : Map[String, Bool] = Map([])
  let mut inheritance_depth = 0
  while current.parent is Some(parent_name) {
    inheritance_depth += 1
    if inheritance_depth > environment.options().max_include_depth {
      raise RenderError("Template inheritance depth limit exceeded")
    }
    if seen.contains(parent_name) {
      raise RenderError("Template inheritance cycle at: " + parent_name)
    }
    seen[parent_name] = true
    current = environment.resolve_template(parent_name)
    templates.push(current)
  }
  let chains : Map[String, Array[Array[Expr]]] = Map([])
  let prefixes : Array[Array[Expr]] = []
  for item in templates {
    if !item.prefix.is_empty() {
      prefixes.push(item.prefix)
    }
    let blocks = collect_blocks(item.body)
    for name, body in blocks {
      match chains.get(name) {
        Some(chain) => chain.push(body)
        None => chains[name] = [body]
      }
    }
  }
  (current, chains, prefixes)
}

///|
fn collect_blocks(nodes : Array[Expr]) -> Map[String, Array[Expr]] {
  let blocks : Map[String, Array[Expr]] = Map([])
  collect_blocks_into(nodes, blocks)
  blocks
}

///|
fn collect_blocks_into(
  nodes : Array[Expr],
  blocks : Map[String, Array[Expr]],
) -> Unit {
  for node in nodes {
    match node {
      BlockNode(name, body) => {
        blocks[name] = body
        collect_blocks_into(body, blocks)
      }
      IfBlock(_, then_body, else_body) => {
        collect_blocks_into(then_body, blocks)
        collect_blocks_into(else_body, blocks)
      }
      ForBlock(_, _, body, else_body) => {
        collect_blocks_into(body, blocks)
        collect_blocks_into(else_body, blocks)
      }
      WithBlock(_, body) | MacroDef(_, _, body) | CallBlock(_, body) =>
        collect_blocks_into(body, blocks)
      _ => ()
    }
  }
}

///|
fn render_engine_nodes(
  nodes : Array[Expr],
  context : Context,
  state : EngineState,
  output : RenderOutput,
) -> Flow raise JinjaError {
  for node in nodes {
    state.consume(1)
    match node {
      TextNode(text, strip_start, strip_end) => {
        let mut rendered = text
        if strip_start {
          rendered = trim_ws_start(rendered)
        }
        if strip_end {
          rendered = trim_ws_end(rendered)
        }
        write_checked(output, rendered, state)
      }
      RawText(text) => write_checked(output, text, state)
      Literal(_)
      | ListLiteral(_)
      | MapLiteral(_)
      | Var(_)
      | Unary(_, _)
      | Binary(_, _, _)
      | Conditional(_, _, _)
      | Index(_, _)
      | TestCall(_, _, _, _, _)
      | FunctionCall(_)
      | FilterChain(_, _) => {
        let value = eval_engine_expr(node, context, state)
        reject_undefined(value, state, "output")
        write_checked(
          output,
          display_string(value, autoescape=state.options.autoescape),
          state,
        )
      }
      IfBlock(condition, then_nodes, else_nodes) => {
        let condition_value = eval_engine_expr(condition, context, state)
        reject_undefined(condition_value, state, "condition")
        let selected = if is_truthy(condition_value) {
          then_nodes
        } else {
          else_nodes
        }
        let flow = render_nested_nodes(selected, context, state, output)
        if flow != Normal {
          return flow
        }
      }
      ForBlock(name, iterable, body, else_body) =>
        match eval_engine_expr(iterable, context, state) {
          ListValue(items) =>
            if items.is_empty() {
              let flow = render_nested_nodes(else_body, context, state, output)
              if flow != Normal {
                return flow
              }
            } else {
              for index, item in items {
                let loop_values : Map[String, Value] = Map([])
                loop_values[name] = item
                loop_values["loop"] = MapValue({
                  "index": IntValue(index + 1),
                  "index0": IntValue(index),
                  "first": BoolValue(index == 0),
                  "last": BoolValue(index == items.length() - 1),
                  "length": IntValue(items.length()),
                })
                context.push_values(loop_values)
                let flow = render_nested_nodes(body, context, state, output)
                context.pop()
                match flow {
                  BreakFlow => break
                  ContinueFlow => continue
                  Normal => ()
                }
              }
            }
          _ => raise RenderError("for expects a list")
        }
      IncludeNode(name, with_context) => {
        if state.template_stack.length() >= state.options.max_include_depth {
          raise RenderError("Template include depth limit exceeded")
        }
        if state.template_stack.contains(name) {
          raise RenderError(
            "Template include cycle: " +
            state.template_stack.join(" -> ") +
            " -> " +
            name,
          )
        }
        state.enter()
        state.template_stack.push(name)
        let included = state.environment.resolve_template(name)
        let (included_root, included_chains, included_prefixes) = resolve_inheritance(
          included,
          state.environment,
        )
        let nested : EngineState = {
          ..state,
          options: state.environment.options_for(name),
          block_chains: included_chains,
          block_stack: [],
        }
        let include_context = if with_context {
          context.copy()
        } else {
          Context::new(Map([]))
        }
        for prefix in included_prefixes {
          ignore(render_engine_nodes(prefix, include_context, nested, output))
        }
        let flow = render_engine_nodes(
          included_root.body,
          include_context,
          nested,
          output,
        ) catch {
          RenderError(message) => {
            ignore(state.template_stack.pop())
            state.leave()
            raise RenderError("Template '" + name + "': " + message)
          }
          error => {
            ignore(state.template_stack.pop())
            state.leave()
            raise error
          }
        }
        state.fuel = nested.fuel
        state.output_size = nested.output_size
        ignore(state.template_stack.pop())
        state.leave()
        if flow != Normal {
          return flow
        }
      }
      BlockNode(name, body) => {
        let chain = match state.block_chains.get(name) {
          Some(chain) => chain
          None => [body]
        }
        let flow = render_block(name, 0, chain, context, state, output)
        if flow != Normal {
          return flow
        }
      }
      SetStmt(name, value) =>
        context.set(name, eval_engine_expr(value, context, state))
      WithBlock(assignments, body) => {
        let scope : Map[String, Value] = Map([])
        for name, value in assignments {
          scope[name] = eval_engine_expr(value, context, state)
        }
        context.push_values(scope)
        let flow = render_nested_nodes(body, context, state, output)
        context.pop()
        if flow != Normal {
          return flow
        }
      }
      MacroDef(name, params, body) =>
        state.macros[name] = {
          name,
          params,
          body,
          captured: context.snapshot(),
        }
      CallBlock(call, body) => {
        let previous = state.macros.get("caller")
        state.macros["caller"] = {
          name: "caller",
          params: [],
          body,
          captured: context.snapshot(),
        }
        let rendered = eval_engine_function(call, context, state)
        match previous {
          Some(caller) => state.macros["caller"] = caller
          None => state.macros.remove("caller")
        }
        write_checked(
          output,
          display_string(rendered, autoescape=state.options.autoescape),
          state,
        )
      }
      ImportNode(filename, import_name) =>
        register_imported_macros(filename, Some(import_name), None, state)
      FromImportNode(filename, names) =>
        register_imported_macros(filename, None, Some(names), state)
      Break => return BreakFlow
      Continue => return ContinueFlow
      ExtendsNode(_) =>
        raise RenderError("extends is only valid at template top level")
    }
  }
  Normal
}

///|
fn render_nested_nodes(
  nodes : Array[Expr],
  context : Context,
  state : EngineState,
  output : RenderOutput,
) -> Flow raise JinjaError {
  state.enter()
  let flow = render_engine_nodes(nodes, context, state, output)
  state.leave()
  flow
}

///|
fn render_block(
  name : String,
  index : Int,
  chain : Array[Array[Expr]],
  context : Context,
  state : EngineState,
  output : RenderOutput,
) -> Flow raise JinjaError {
  if index >= chain.length() {
    return Normal
  }
  state.block_stack.push({ name, index })
  let flow = render_nested_nodes(chain[index], context, state, output)
  ignore(state.block_stack.pop())
  flow
}

///|
fn eval_engine_expr(
  expression : Expr,
  context : Context,
  state : EngineState,
) -> Value raise JinjaError {
  state.consume(1)
  match expression {
    Literal(value) => value
    ListLiteral(expressions) =>
      ListValue(
        expressions.map(expression => {
          eval_engine_expr(expression, context, state)
        }),
      )
    MapLiteral(entries) => {
      let values : Map[String, Value] = Map([])
      for entry in entries {
        let (key, expression) = entry
        values[key] = eval_engine_expr(expression, context, state)
      }
      MapValue(values)
    }
    Var(path) => context.resolve(path).unwrap_or(Undefined)
    Unary(operator, operand) => {
      let value = eval_engine_expr(operand, context, state)
      reject_undefined(value, state, "unary expression")
      match (operator, value) {
        (UnOp::Not, value) => BoolValue(!is_truthy(value))
        (UnOp::Neg, IntValue(number)) => IntValue(-number)
        (UnOp::Neg, DoubleValue(number)) => DoubleValue(-number)
        (UnOp::Pos, IntValue(_) | DoubleValue(_) as number) => number
        _ => raise RenderError("Invalid unary operation")
      }
    }
    Binary(left, operator, right) => {
      if is_comparison(operator) && is_comparison_expression(left) {
        return eval_comparison_chain(expression, context, state)
      }
      let lhs = eval_engine_expr(left, context, state)
      reject_undefined(lhs, state, "binary expression")
      if operator == BinOp::And && !is_truthy(lhs) {
        return lhs
      }
      if operator == BinOp::Or && is_truthy(lhs) {
        return lhs
      }
      let rhs = eval_engine_expr(right, context, state)
      reject_undefined(rhs, state, "binary expression")
      if operator == BinOp::And || operator == BinOp::Or {
        rhs
      } else {
        apply_binop(lhs, operator, rhs)
      }
    }
    Conditional(value, condition, fallback) => {
      let condition_value = eval_engine_expr(condition, context, state)
      reject_undefined(condition_value, state, "conditional expression")
      if is_truthy(condition_value) {
        eval_engine_expr(value, context, state)
      } else {
        match fallback {
          Some(otherwise) => eval_engine_expr(otherwise, context, state)
          None => Undefined
        }
      }
    }
    Index(base, index) => {
      let value = eval_engine_expr(base, context, state)
      let key = eval_engine_expr(index, context, state)
      match (value, key) {
        (ListValue(items), IntValue(position)) =>
          if position < 0 || position >= items.length() {
            Undefined
          } else {
            items[position]
          }
        (MapValue(entries), StrValue(name) | SafeStr(name)) =>
          entries.get(name).unwrap_or(Undefined)
        (StrValue(text) | SafeStr(text), IntValue(position)) => {
          let chars = text.to_array()
          if position < 0 || position >= chars.length() {
            Undefined
          } else {
            StrValue(chars[position].to_string())
          }
        }
        _ => raise RenderError("Index expects a list, map, or string")
      }
    }
    TestCall(subject, name, negated, argument_exprs, keyword_exprs) => {
      ensure_extension_allowed(state, name)
      let value = eval_engine_expr(subject, context, state)
      let arguments = eval_engine_arguments(argument_exprs, context, state)
      let keywords = eval_engine_keywords(keyword_exprs, context, state)
      let isolate = state.environment.restricted_extensions.contains(name)
      let passed = match state.environment.context_tests.get(name) {
        Some(test_function) =>
          match
            test_function(
              extension_context(state),
              if isolate {
                deep_clone_value(value)
              } else {
                value
              },
              if isolate {
                arguments.map(deep_clone_value)
              } else {
                arguments
              },
              if isolate {
                deep_clone_map(keywords)
              } else {
                keywords
              },
            ) {
            Ok(result) => result
            Err(message) => raise RenderError(message)
          }
        None =>
          match state.environment.tests.get(name) {
            Some(test_function) => {
              if !keywords.is_empty() {
                raise RenderError(
                  "Test does not accept keyword arguments: " + name,
                )
              }
              match
                test_function(
                  if isolate {
                    deep_clone_value(value)
                  } else {
                    value
                  },
                  if isolate {
                    arguments.map(deep_clone_value)
                  } else {
                    arguments
                  },
                ) {
                Ok(result) => result
                Err(message) => raise RenderError(message)
              }
            }
            None => raise RenderError("Unknown test: " + name)
          }
      }
      BoolValue(if negated { !passed } else { passed })
    }
    FunctionCall(call) => eval_engine_function(call, context, state)
    FilterChain(base, calls) => {
      let mut value = eval_engine_expr(base, context, state)
      for call in calls {
        ensure_extension_allowed(state, call.name)
        let arguments = eval_engine_arguments(call.args, context, state)
        let keywords = eval_engine_keywords(call.kwargs, context, state)
        let isolate = state.environment.restricted_extensions.contains(
          call.name,
        )
        value = match state.environment.context_filters.get(call.name) {
          Some(filter) =>
            match
              filter(
                extension_context(state),
                if isolate {
                  deep_clone_value(value)
                } else {
                  value
                },
                if isolate {
                  arguments.map(deep_clone_value)
                } else {
                  arguments
                },
                if isolate {
                  deep_clone_map(keywords)
                } else {
                  keywords
                },
              ) {
              Ok(result) =>
                if isolate {
                  deep_clone_value(result)
                } else {
                  result
                }
              Err(message) => raise RenderError(message)
            }
          None =>
            match state.environment.filters.get(call.name) {
              Some(filter) => {
                if !keywords.is_empty() {
                  raise RenderError(
                    "Filter does not accept keyword arguments: " + call.name,
                  )
                }
                match
                  filter(
                    if isolate {
                      deep_clone_value(value)
                    } else {
                      value
                    },
                    if isolate {
                      arguments.map(deep_clone_value)
                    } else {
                      arguments
                    },
                  ) {
                  Ok(result) =>
                    if isolate {
                      deep_clone_value(result)
                    } else {
                      result
                    }
                  Err(message) => raise RenderError(message)
                }
              }
              None => raise RenderError("Unknown filter: " + call.name)
            }
        }
      }
      value
    }
    _ => {
      let output = RenderOutput::Buffered(StringBuilder())
      ignore(render_nested_nodes([expression], context, state, output))
      StrValue(output.to_string())
    }
  }
}

///|
fn is_comparison(operator : BinOp) -> Bool {
  match operator {
    Eq | Ne | Lt | Le | Gt | Ge | In | NotIn => true
    _ => false
  }
}

///|
fn is_comparison_expression(expression : Expr) -> Bool {
  match expression {
    Binary(_, operator, _) => is_comparison(operator)
    _ => false
  }
}

///|
fn flatten_comparison(
  expression : Expr,
  operands : Array[Expr],
  operators : Array[BinOp],
) -> Unit {
  match expression {
    Binary(left, operator, right) if is_comparison(operator) => {
      flatten_comparison(left, operands, operators)
      operators.push(operator)
      operands.push(right)
    }
    _ => operands.push(expression)
  }
}

///|
fn eval_comparison_chain(
  expression : Expr,
  context : Context,
  state : EngineState,
) -> Value raise JinjaError {
  let operands : Array[Expr] = []
  let operators : Array[BinOp] = []
  flatten_comparison(expression, operands, operators)
  let mut previous = eval_engine_expr(operands[0], context, state)
  reject_undefined(previous, state, "comparison")
  for index, operator in operators {
    let current = eval_engine_expr(operands[index + 1], context, state)
    reject_undefined(current, state, "comparison")
    match apply_binop(previous, operator, current) {
      BoolValue(true) => previous = current
      BoolValue(false) => return BoolValue(false)
      _ => raise RenderError("Comparison did not return a boolean")
    }
  }
  BoolValue(true)
}

///|
fn eval_engine_arguments(
  expressions : Array[Expr],
  context : Context,
  state : EngineState,
) -> Array[Value] raise JinjaError {
  let values : Array[Value] = []
  for expression in expressions {
    let value = eval_engine_expr(expression, context, state)
    reject_undefined(value, state, "function argument")
    values.push(value)
  }
  values
}

///|
fn eval_engine_keywords(
  expressions : Map[String, Expr],
  context : Context,
  state : EngineState,
) -> Map[String, Value] raise JinjaError {
  let values : Map[String, Value] = Map([])
  for name, expression in expressions {
    let value = eval_engine_expr(expression, context, state)
    reject_undefined(value, state, "keyword argument")
    values[name] = value
  }
  values
}

///|
fn extension_context(state : EngineState) -> ExtensionContext {
  {
    template_name: state.template_stack.last().unwrap_or(""),
    autoescape: state.options.autoescape,
    sandboxed: state.options.sandboxed,
  }
}

///|
fn ensure_extension_allowed(
  state : EngineState,
  name : String,
) -> Unit raise JinjaError {
  if state.options.sandboxed &&
    (name == "safe" || state.environment.restricted_extensions.contains(name)) {
    raise RenderError("Extension is not allowed in sandbox mode: " + name)
  }
}

///|
fn deep_clone_map(values : Map[String, Value]) -> Map[String, Value] {
  let cloned : Map[String, Value] = Map([])
  for name, value in values {
    cloned[name] = deep_clone_value(value)
  }
  cloned
}

///|
fn deep_clone_value(value : Value) -> Value {
  match value {
    ListValue(items) => ListValue(items.map(deep_clone_value))
    MapValue(values) => MapValue(deep_clone_map(values))
    _ => value
  }
}

///|
fn reject_undefined(
  value : Value,
  state : EngineState,
  usage : String,
) -> Unit raise JinjaError {
  match value {
    Undefined if state.options.strict_undefined =>
      raise RenderError("Undefined value used in " + usage)
    _ => ()
  }
}

///|
fn eval_engine_function(
  call : Call,
  context : Context,
  state : EngineState,
) -> Value raise JinjaError {
  if call.name == "super" {
    if !call.kwargs.is_empty() {
      raise RenderError("super does not accept keyword arguments")
    }
    guard state.block_stack.last() is Some(frame) else {
      raise RenderError("super() can only be used inside a block")
    }
    guard state.block_chains.get(frame.name) is Some(chain) else {
      return SafeStr("")
    }
    let before_size = state.output_size
    let output = RenderOutput::Buffered(StringBuilder())
    ignore(
      render_block(frame.name, frame.index + 1, chain, context, state, output),
    )
    state.output_size = before_size
    return SafeStr(output.to_string())
  }
  if call.name.has_prefix("self.") {
    if !call.args.is_empty() || !call.kwargs.is_empty() {
      raise RenderError("self block calls do not accept arguments")
    }
    let block_name = call.name[5:].to_owned()
    guard state.block_chains.get(block_name) is Some(chain) else {
      raise RenderError("Unknown block: " + block_name)
    }
    let before_size = state.output_size
    let output = RenderOutput::Buffered(StringBuilder())
    ignore(render_block(block_name, 0, chain, context, state, output))
    state.output_size = before_size
    return SafeStr(output.to_string())
  }
  let arguments = eval_engine_arguments(call.args, context, state)
  let keywords = eval_engine_keywords(call.kwargs, context, state)
  match state.macros.get(call.name) {
    Some(definition) =>
      return invoke_macro(definition, arguments, keywords, state)
    None => ()
  }
  ensure_extension_allowed(state, call.name)
  let isolate = state.environment.restricted_extensions.contains(call.name)
  match state.environment.context_functions.get(call.name) {
    Some(function) =>
      match
        function(
          extension_context(state),
          if isolate {
            arguments.map(deep_clone_value)
          } else {
            arguments
          },
          if isolate {
            deep_clone_map(keywords)
          } else {
            keywords
          },
        ) {
        Ok(value) => if isolate { deep_clone_value(value) } else { value }
        Err(message) => raise RenderError(message)
      }
    None =>
      match state.environment.functions.get(call.name) {
        Some(function) => {
          if !keywords.is_empty() {
            raise RenderError(
              "Function does not accept keyword arguments: " + call.name,
            )
          }
          match
            function(
              if isolate {
                arguments.map(deep_clone_value)
              } else {
                arguments
              },
            ) {
            Ok(value) => if isolate { deep_clone_value(value) } else { value }
            Err(message) => raise RenderError(message)
          }
        }
        None => raise RenderError("Unknown function: " + call.name)
      }
  }
}

///|
fn invoke_macro(
  definition : MacroDefinition,
  arguments : Array[Value],
  keywords : Map[String, Value],
  state : EngineState,
) -> Value raise JinjaError {
  state.enter()
  let scopes = definition.captured.map(scope => scope.copy())
  if scopes.is_empty() {
    scopes.push(Map([]))
  }
  let context : Context = { scopes, }
  let parameters : Map[String, Value] = Map([])
  context.push_values(parameters)
  for index, parameter in definition.params {
    let value = if index < arguments.length() {
      if keywords.contains(parameter.name) {
        state.leave()
        raise RenderError("Duplicate macro argument: " + parameter.name)
      }
      arguments[index]
    } else {
      match keywords.get(parameter.name) {
        Some(value) => value
        None =>
          match parameter.default {
            Some(default) => eval_engine_expr(default, context, state)
            None => {
              state.leave()
              raise RenderError(
                "Missing argument '" +
                parameter.name +
                "' for macro " +
                definition.name,
              )
            }
          }
      }
    }
    context.set(parameter.name, value)
  }
  if arguments.length() > definition.params.length() {
    state.leave()
    raise RenderError("Too many arguments for macro " + definition.name)
  }
  for name, _ in keywords {
    if !definition.params.any(parameter => parameter.name == name) {
      state.leave()
      raise RenderError("Unknown macro argument: " + name)
    }
  }
  let before_size = state.output_size
  let output = RenderOutput::Buffered(StringBuilder())
  ignore(render_engine_nodes(definition.body, context, state, output))
  state.output_size = before_size
  state.leave()
  SafeStr(output.to_string())
}

///|
fn register_imported_macros(
  filename : String,
  import_name : String?,
  selected : Array[String]?,
  state : EngineState,
) -> Unit raise JinjaError {
  state.enter()
  let template = state.environment.resolve_template(filename)
  let found : Map[String, Bool] = Map([])
  for node in template.body {
    match node {
      MacroDef(name, params, body) => {
        let should_import = match selected {
          Some(names) => names.contains(name) && !name.has_prefix("_")
          None => !name.has_prefix("_")
        }
        if should_import {
          found[name] = true
          let exported_name = match import_name {
            Some(import_name) => import_name + "." + name
            None => name
          }
          state.macros[exported_name] = {
            name: exported_name,
            params,
            body,
            captured: [Map([])],
          }
        }
      }
      _ => ()
    }
  }
  match selected {
    Some(names) =>
      for name in names {
        if !found.contains(name) {
          state.leave()
          raise RenderError(
            "Template '" + filename + "' does not export macro '" + name + "'",
          )
        }
      }
    None => ()
  }
  state.leave()
}