///|
fn Parser::switch_mode(self : Parser, mode : Mode) -> Unit {
  guard self.mode != mode else { () }
  self.mode = mode
  self.gullet.switch_mode(mode)
}

///|
fn Parser::parse_arguments(
  self : Parser,
  func : String,
  func_data : FunctionSpec,
) -> (Array[ParseNode], Array[ParseNode?]) raise ParseFailure {
  let args : Array[ParseNode] = []
  let opt_args : Array[ParseNode?] = []
  let total_args = func_data.num_args + func_data.num_optional_args
  for i in 0.. kind
      None if func_data.primitive => PrimitiveArg
      None if func_data.primitive_after_missing_optional is Some(optional_index) &&
        i == func_data.num_optional_args &&
        opt_args.get(optional_index) is Some(None) => PrimitiveArg
      None => OriginalArg
    }
    match
      self.parse_group_of_type("argument to '" + func + "'", arg_type, optional) {
      None if optional => {
        opt_args.push(None)
        continue
      }
      None =>
        raise InternalInvariant(
          message="Null mandatory function argument after parser validation",
        )
      Some(arg) if optional => {
        opt_args.push(Some(arg))
        continue
      }
      Some(arg) => {
        args.push(arg)
        continue
      }
    }
  } nobreak {
    (args, opt_args)
  }
}

///|
fn Parser::parse_group_of_type(
  self : Parser,
  name : String,
  arg_type : ArgType,
  optional : Bool,
) -> ParseNode? raise ParseFailure {
  match arg_type {
    ColorArg => self.parse_color_group(optional)
    SizeArg => self.parse_size_group(optional)
    UrlArg => self.parse_url_group(optional)
    RawArg =>
      self
      .parse_string_group(optional)
      .map(token => Raw(mode=Text, string=token.text))
    MathArg => self.parse_argument_group(optional, Some(Math))
    TextArg => self.parse_argument_group(optional, Some(Text))
    HboxArg =>
      self
      .parse_argument_group(optional, Some(Text))
      .map(group => {
        Styling(
          mode=group.mode(),
          body=[group],
          style=TextStyle,
          reset_font=true,
        )
      })
    PrimitiveArg => self.parse_primitive_group(name, optional)
    OriginalArg => self.parse_argument_group(optional, None)
  }
}

///|
fn Parser::parse_primitive_group(
  self : Parser,
  name : String,
  optional : Bool,
) -> ParseNode? raise ParseFailure {
  guard !optional else {
    raise InvalidArgument(
      message="A primitive argument cannot be optional",
      loc=None,
    )
  }
  guard self.parse_group(name, None) is Some(EmitAtom(group)) else {
    let token = self.fetch()
    raise InvalidArgument(message="Expected group as " + name, loc=token.loc)
  }
  Some(group)
}

///|
fn Parser::parse_argument_group(
  self : Parser,
  optional : Bool,
  mode : Mode?,
) -> ParseNode? raise ParseFailure {
  match self.gullet.scan_argument(optional) {
    None => None
    Some(arg_token) => {
      let outer_mode = self.mode
      if mode is Some(argument_mode) {
        self.switch_mode(argument_mode)
      }
      self.gullet.begin_group()
      let result : Result[ParseNode, ParseFailure] = capture_parse_result(() => {
        let body = self.parse_expression(false, Some("EOF"))
        self.expect("EOF")
        OrdGroup(mode=self.mode, loc=arg_token.loc, body~, semisimple=false)
      })
      let close_result : Result[Unit, ParseFailure] = capture_parse_result(() => {
        self.gullet.end_group()
      })
      self.switch_mode(outer_mode)
      match (close_result, result) {
        (Err(err), _) => raise err
        (_, Err(err)) => raise err
        (Ok(_), Ok(node)) => Some(node)
      }
    }
  }
}