///|
fn WgslIrLowerer::lower_loop_blocks(
  self : WgslIrLowerer,
  statement : @ast.WgslStatement,
  function : Function,
  scope : WgslIrFunctionScope,
) -> (Block, Block, Handle?) raise WgslIrLowerError {
  let body = Block::Block()
  let mut continuing = Block::Block()
  let mut break_if : Handle? = None
  match statement.blocks() {
    [body_block, ..] => {
      let child_scope = scope.loop_child()
      let mut saw_continuing = false
      let body_statements = body_block.statements()
      for index in 0.. {
            saw_continuing = true
            match child.blocks() {
              [continuing_block, ..] => {
                let continuing_scope = child_scope.loop_child()
                let result = self.lower_continuing_block(
                  continuing_block, function, continuing_scope,
                )
                continuing = result.0
                break_if = result.1
              }
              [] => continuing = Block()
            }
          }
          _ =>
            if !saw_continuing {
              self.lower_statement_with_block_context(
                child, body_statements, index, function, child_scope, body,
              )
            } else {
              raise UnsupportedStatement("statement after continuing in loop")
            }
        }
      }
    }
    [] => ()
  }
  (body, continuing, break_if)
}

///|
fn WgslIrLowerer::lower_continuing_block(
  self : WgslIrLowerer,
  block : @ast.WgslBlock,
  function : Function,
  scope : WgslIrFunctionScope,
) -> (Block, Handle?) raise WgslIrLowerError {
  let ir_block = Block::Block()
  let mut break_if : Handle? = None
  for statement in block.statements() {
    match statement.payload() {
      Break(condition) =>
        match condition {
          Some(expr_ref) =>
            break_if = Some(
              self.lower_function_expression_ref(expr_ref, function, scope),
            )
          None => ir_block.push(Break)
        }
      _ => self.lower_statement(statement, function, scope, ir_block)
    }
  }
  (ir_block, break_if)
}

///|
fn WgslIrLowerer::lower_ray_query_statement_expr_ref(
  self : WgslIrLowerer,
  expr_ref : @ast.WgslExprRef,
  function : Function,
  scope : WgslIrFunctionScope,
  block : Block,
) -> Bool raise WgslIrLowerError {
  match expr_ref.root() {
    Some(root) =>
      self.lower_ray_query_statement_node(root, function, scope, block)
    None => false
  }
}

///|
fn WgslIrLowerer::lower_ray_query_statement_node(
  self : WgslIrLowerer,
  node : @ast.WgslExprNode,
  function : Function,
  scope : WgslIrFunctionScope,
  block : Block,
) -> Bool raise WgslIrLowerError {
  match node.kind() {
    Group(inner) =>
      self.lower_ray_query_statement_node(inner, function, scope, block)
    Sequence([single]) =>
      self.lower_ray_query_statement_node(single, function, scope, block)
    Call(callee, arguments) => {
      let arguments = wgsl_ir_call_arguments(arguments)
      match callee.kind() {
        Identifier(name) =>
          self.lower_ray_query_statement_call(
            name.name(),
            arguments,
            function,
            scope,
            block,
          )
        _ => false
      }
    }
    _ => false
  }
}

///|
fn wgsl_ir_expr_ref_is_call(expr_ref : @ast.WgslExprRef) -> Bool {
  match expr_ref.root() {
    Some(root) => wgsl_ir_expr_node_is_call(root)
    None => false
  }
}

///|
fn WgslIrLowerer::wgsl_ir_expr_ref_is_user_function_call(
  self : WgslIrLowerer,
  expr_ref : @ast.WgslExprRef,
) -> Bool {
  match expr_ref.root() {
    Some(root) => self.wgsl_ir_expr_node_is_user_function_call(root)
    None => false
  }
}

///|
fn WgslIrLowerer::wgsl_ir_expr_node_is_user_function_call(
  self : WgslIrLowerer,
  node : @ast.WgslExprNode,
) -> Bool {
  match node.kind() {
    Group(inner) => self.wgsl_ir_expr_node_is_user_function_call(inner)
    Sequence([single]) => self.wgsl_ir_expr_node_is_user_function_call(single)
    Call(callee, _) =>
      match callee.kind() {
        Identifier(name) => self.function_names.get(name.name()) != None
        _ => false
      }
    _ => false
  }
}

///|
fn wgsl_ir_expr_node_is_call(node : @ast.WgslExprNode) -> Bool {
  match node.kind() {
    Call(_, _) => true
    Group(inner) => wgsl_ir_expr_node_is_call(inner)
    Sequence([single]) => wgsl_ir_expr_node_is_call(single)
    _ => false
  }
}

///|
fn WgslIrLowerer::lower_for_statement(
  self : WgslIrLowerer,
  statement : @ast.WgslStatement,
  expressions : Array[@ast.WgslStatementExpression],
  function : Function,
  scope : WgslIrFunctionScope,
  block : Block,
) -> Unit raise WgslIrLowerError {
  let loop_scope = scope.loop_child()
  let source_var_store_initializers = wgsl_ir_for_source_var_store_initializers(
    statement.local_bindings(),
    scope,
  )
  for local_decl in statement.local_bindings() {
    self.lower_local_declaration(
      local_decl,
      function,
      loop_scope,
      block,
      DeclarationArenaLocalConstant,
      source_var_store_initializers,
    )
  }
  for expr in expressions {
    if expr.role() == ForInitializer {
      self.lower_for_header_statement(
        expr.expression(),
        function,
        loop_scope,
        block,
      )
    }
  }
  let loop_body = Block::Block()
  match wgsl_ir_for_expression_by_role(expressions, ForCondition) {
    Some(condition_ref) => {
      let condition = self.lower_for_loop_condition_ref(
        condition_ref, function, loop_scope, loop_body,
      )
      loop_body.push(If(condition, Block(), { statements: [Break], }))
    }
    None => ()
  }
  match statement.blocks() {
    [body_block, ..] => {
      let body = self.lower_child_block(body_block, function, loop_scope)
      if body.statements.length() > 0 {
        loop_body.push(Block(body))
      }
    }
    [] => ()
  }
  let continuing = Block::Block()
  for expr in expressions {
    if expr.role() == ForUpdate {
      self.lower_for_header_statement(
        expr.expression(),
        function,
        loop_scope,
        continuing,
      )
    }
  }
  block.push(Loop(loop_body, continuing, None))
}

///|
fn wgsl_ir_for_source_var_store_initializers(
  locals : Array[@ast.WgslLocalDeclaration],
  parent_scope : WgslIrFunctionScope,
) -> Array[String] {
  let names : Array[String] = []
  guard parent_scope.loop_depth > 0 else { return names }
  for local_decl in locals {
    guard local_decl.kind() == Var else { continue }
    guard local_decl.initializer_ref() is Some(_) else { continue }
    names.push(local_decl.name().name())
  }
  names
}

///|
fn wgsl_ir_for_expression_by_role(
  expressions : Array[@ast.WgslStatementExpression],
  role : @ast.WgslStatementExpressionRole,
) -> @ast.WgslExprRef? {
  for expr in expressions {
    if expr.role() == role {
      return Some(expr.expression())
    }
  }
  None
}

///|
fn WgslIrLowerer::lower_switch_cases(
  self : WgslIrLowerer,
  switch_block : @ast.WgslBlock,
  function : Function,
  scope : WgslIrFunctionScope,
  selector_handle : Handle,
  cases : Array[SwitchCase],
) -> Unit raise WgslIrLowerError {
  for statement in switch_block.statements() {
    match statement.payload() {
      Case(selectors) => {
        let body = match statement.blocks() {
          [case_block, ..] =>
            self.lower_child_block(case_block, function, scope)
          [] => Block()
        }
        for selector in selectors {
          match selector {
            DefaultSelector =>
              cases.push({
                value: Default,
                body,
                fall_through: false,
                constant_dependencies: [],
              })
            ExpressionSelector(expr) =>
              cases.push({
                value: wgsl_ir_switch_value_from_ast_expression(
                  self, function, scope, selector_handle, expr,
                ),
                body,
                fall_through: false,
                constant_dependencies: self.switch_case_constant_dependencies(
                  expr, function, scope,
                ),
              })
          }
        }
      }
      DefaultCase => {
        let body = match statement.blocks() {
          [case_block, ..] =>
            self.lower_child_block(case_block, function, scope)
          [] => Block()
        }
        cases.push({
          value: Default,
          body,
          fall_through: false,
          constant_dependencies: [],
        })
      }
      Empty => ()
      _ => raise UnsupportedStatement("switch body contains non-case statement")
    }
  }
}

///|
fn WgslIrLowerer::lower_for_header_statement(
  self : WgslIrLowerer,
  expr_ref : @ast.WgslExprRef,
  function : Function,
  scope : WgslIrFunctionScope,
  block : Block,
) -> Unit raise WgslIrLowerError {
  let tokens = expr_ref.tokens()
  match wgsl_ir_expr_tokens_update_operator(tokens) {
    Some((target_tokens, increment)) => {
      let target = self.lower_function_lvalue_node(
        wgsl_ir_expr_node_from_tokens(target_tokens),
        function,
        scope,
      )
      self.lower_update_handle(target, function, scope, block, increment)
      return
    }
    None => ()
  }
  match wgsl_ir_expr_tokens_assignment(tokens) {
    Some((op, lhs_tokens, rhs_tokens)) => {
      if wgsl_ir_expr_tokens_are_phony(lhs_tokens) {
        let rhs_value = self.lower_function_expression_node(
          wgsl_ir_expr_node_from_tokens(rhs_tokens),
          function,
          scope,
        )
        block.push(Emit(HandleRange(rhs_value, rhs_value)))
        return
      }
      let (target, value) = match wgsl_ir_compound_assignment_operator(op) {
        Some(binary_op) => {
          let rhs_node = wgsl_ir_expr_node_from_tokens(rhs_tokens)
          if wgsl_ir_direct_access_chain_root_is_function_argument(
              rhs_node, scope,
            ) {
            self.predeclare_function_reference_roots_in_source_order(
              rhs_node, function, scope,
            )
          }
          let raw_target = self.lower_function_lvalue_node(
            wgsl_ir_expr_node_from_tokens(lhs_tokens),
            function,
            scope,
          )
          let target = self.materialize_store_pointer_if_needed(
            function, scope, block, raw_target,
          )
          let lhs_node = wgsl_ir_expr_node_from_tokens(lhs_tokens)
          if wgsl_ir_expr_node_plain_local_identifier(lhs_node, scope) {
            if wgsl_ir_compound_assignment_rhs_predeclares_reference_roots(
                rhs_node, scope,
              ) {
              self.predeclare_function_binary_chain_reference_roots_in_source_order(
                rhs_node, function, scope,
              )
            }
          }
          if wgsl_ir_direct_access_chain_root_is_function_argument(
              rhs_node, scope,
            ) {
            self.predeclare_function_reference_roots_in_source_order(
              rhs_node, function, scope,
            )
          }
          let target_value = self.compound_assignment_target_value(
            function, target,
          )
          let materialized_target = self.materialize_function_expression(
            function, scope, block, target_value,
          )
          scope.compound_assignment_rhs_depth += 1
          let rhs_value = self.lower_function_expression_node(
            wgsl_ir_expr_node_from_tokens(rhs_tokens),
            function,
            scope,
          )
          scope.compound_assignment_rhs_depth -= 1
          let rhs_value = self.materialize_store_value_if_needed(
            function, scope, block, rhs_value,
          )
          let (materialized_target, rhs_value) = self.normalize_function_binary_operands(
            function, binary_op, materialized_target, rhs_value,
          )
          let value = function.expressions.append(
            Binary(binary_op, materialized_target, rhs_value),
          )
          (target, value)
        }
        None => {
          let lhs_node = wgsl_ir_expr_node_from_tokens(lhs_tokens)
          let rhs_node = wgsl_ir_expr_node_from_tokens(rhs_tokens)
          if wgsl_ir_expr_node_plain_local_identifier(lhs_node, scope) {
            let early_raw_target : Handle? = if wgsl_ir_expr_node_is_literal_like(
                rhs_node,
              ) {
              Some(self.lower_function_lvalue_node(lhs_node, function, scope))
            } else {
              None
            }
            let rhs_value = self.lower_function_expression_node(
              rhs_node, function, scope,
            )
            let raw_target = match early_raw_target {
              Some(target) => target
              None => self.lower_function_lvalue_node(lhs_node, function, scope)
            }
            let target = self.materialize_store_pointer_if_needed(
              function, scope, block, raw_target,
            )
            let value = self.materialize_store_value_for_pointer_if_needed(
              function, scope, block, target, rhs_value,
            )
            (target, value)
          } else if self.wgsl_ir_expr_node_is_function_argument_pointer_deref(
              lhs_node, function, scope,
            ) {
            let rhs_value = self.lower_function_expression_node(
              rhs_node, function, scope,
            )
            let raw_target = self.lower_function_lvalue_node(
              lhs_node, function, scope,
            )
            let target = self.materialize_store_pointer_if_needed(
              function, scope, block, raw_target,
            )
            let value = self.materialize_store_value_for_pointer_if_needed(
              function, scope, block, target, rhs_value,
            )
            (target, value)
          } else {
            predeclare_assignment_rhs_node_root_before_target_if_needed(
              rhs_node, function, scope,
            )
            let raw_target = self.lower_function_lvalue_node(
              lhs_node, function, scope,
            )
            let target = self.materialize_store_pointer_if_needed(
              function, scope, block, raw_target,
            )
            let rhs_value = self.lower_function_expression_node(
              rhs_node, function, scope,
            )
            let value = self.materialize_store_value_for_pointer_if_needed(
              function, scope, block, target, rhs_value,
            )
            (target, value)
          }
        }
      }
      wgsl_ir_record_store_value_temporary_references(function, value)
      block.push(Store(target, value))
      return
    }
    None => ()
  }
  ignore(self.lower_function_expression_ref(expr_ref, function, scope))
}

///|
fn wgsl_ir_expr_ref_root_is_literal_like(expr_ref : @ast.WgslExprRef) -> Bool {
  match expr_ref.root() {
    Some(root) => wgsl_ir_expr_node_is_literal_like(root)
    None => false
  }
}

///|
fn predeclare_assignment_rhs_root_before_target_if_needed(
  rhs_ref : @ast.WgslExprRef,
  function : Function,
  scope : WgslIrFunctionScope,
) -> Unit {
  match rhs_ref.root() {
    Some(root) =>
      predeclare_assignment_rhs_node_root_before_target_if_needed(
        root, function, scope,
      )
    None => ()
  }
}

///|
fn WgslIrLowerer::rhs_user_call_reads_pointer_argument(
  self : WgslIrLowerer,
  rhs_ref : @ast.WgslExprRef,
  argument_name : String,
) -> Bool {
  match rhs_ref.root() {
    Some(root) =>
      self.expression_node_contains_user_function_call(root) &&
      wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
        root, argument_name,
      )
    None => false
  }
}

///|
fn WgslIrLowerer::function_argument_pointer_deref_name_ref(
  self : WgslIrLowerer,
  expr_ref : @ast.WgslExprRef,
  function : Function,
  scope : WgslIrFunctionScope,
) -> String? {
  match expr_ref.root() {
    Some(root) =>
      self.function_argument_pointer_deref_name_node(root, function, scope)
    None => None
  }
}

///|
fn WgslIrLowerer::function_argument_pointer_deref_name_node(
  self : WgslIrLowerer,
  node : @ast.WgslExprNode,
  function : Function,
  scope : WgslIrFunctionScope,
) -> String? {
  match node.kind() {
    Group(inner) =>
      self.function_argument_pointer_deref_name_node(inner, function, scope)
    Unary(op, inner) =>
      if op == Multiply {
        self.function_argument_pointer_name_node(inner, function, scope)
      } else {
        None
      }
    _ => None
  }
}

///|
fn WgslIrLowerer::function_argument_pointer_name_node(
  self : WgslIrLowerer,
  node : @ast.WgslExprNode,
  function : Function,
  scope : WgslIrFunctionScope,
) -> String? {
  match node.kind() {
    Group(inner) =>
      self.function_argument_pointer_name_node(inner, function, scope)
    Identifier(name) =>
      match scope.arguments.get(name.name()) {
        Some(index) =>
          if self.function_argument_is_pointer(function, index) {
            Some(name.name())
          } else {
            None
          }
        None => None
      }
    _ => None
  }
}

///|
fn wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
  node : @ast.WgslExprNode,
  argument_name : String,
) -> Bool {
  if wgsl_ir_expr_node_is_pointer_deref_of_argument_name(node, argument_name) {
    return true
  }
  match node.kind() {
    Group(inner) | Unary(_, inner) | PostfixUpdate(_, inner) =>
      wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
        inner, argument_name,
      )
    Binary(_, left, right) | Assignment(_, left, right) | Index(left, right) =>
      wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
        left, argument_name,
      ) ||
      wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
        right, argument_name,
      )
    Member(base, _) | Template(base, _) =>
      wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
        base, argument_name,
      )
    Call(callee, arguments) => {
      if wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
          callee, argument_name,
        ) {
        return true
      }
      let arguments = wgsl_ir_call_arguments(arguments)
      arguments.any(fn(argument) {
        wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
          argument, argument_name,
        )
      })
    }
    Sequence(items) =>
      items.any(fn(item) {
        wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
          item, argument_name,
        )
      })
    PathAccess(base, _) =>
      wgsl_ir_expr_node_contains_pointer_deref_of_argument_name(
        base, argument_name,
      )
    Identifier(_) | Literal(_) | ShaderDefTemplate(_) => false
  }
}

///|
fn wgsl_ir_expr_node_is_pointer_deref_of_argument_name(
  node : @ast.WgslExprNode,
  argument_name : String,
) -> Bool {
  match node.kind() {
    Group(inner) =>
      wgsl_ir_expr_node_is_pointer_deref_of_argument_name(inner, argument_name)
    Unary(op, inner) =>
      op == Multiply &&
      wgsl_ir_expr_node_is_identifier_name(inner, argument_name)
    _ => false
  }
}

///|
fn wgsl_ir_expr_node_is_identifier_name(
  node : @ast.WgslExprNode,
  name : String,
) -> Bool {
  match node.kind() {
    Group(inner) => wgsl_ir_expr_node_is_identifier_name(inner, name)
    Identifier(ident) => ident.name() == name
    _ => false
  }
}

///|
fn predeclare_assignment_rhs_node_root_before_target_if_needed(
  rhs_node : @ast.WgslExprNode,
  function : Function,
  scope : WgslIrFunctionScope,
) -> Unit {
  match wgsl_ir_expr_node_plain_mutable_local_identifier_name(rhs_node, scope) {
    Some(name) =>
      match scope.locals.get(name) {
        Some(handle) =>
          ignore(scope.local_pointer_expression(name, handle, function))
        None => ()
      }
    None => ()
  }
}

///|
fn wgsl_ir_compound_assignment_rhs_predeclares_reference_roots(
  root : @ast.WgslExprNode,
  scope : WgslIrFunctionScope,
) -> Bool {
  (
    wgsl_ir_expr_node_contains_member_access(root) &&
    wgsl_ir_expr_node_contains_plain_local_identifier(root, scope)
  ) ||
  wgsl_ir_binary_expr_node_contains_plain_local_identifier(root, scope) ||
  wgsl_ir_expr_node_is_root_call_with_function_argument_value(root, scope)
}

///|
fn wgsl_ir_binary_expr_node_contains_plain_local_identifier(
  root : @ast.WgslExprNode,
  scope : WgslIrFunctionScope,
) -> Bool {
  match wgsl_ir_expr_node_without_groups(root).kind() {
    Binary(_, _, _) =>
      wgsl_ir_expr_node_contains_plain_local_identifier(root, scope)
    _ => false
  }
}

///|
fn wgsl_ir_expr_node_is_root_call_with_function_argument_value(
  node : @ast.WgslExprNode,
  scope : WgslIrFunctionScope,
) -> Bool {
  match node.kind() {
    Group(inner) =>
      wgsl_ir_expr_node_is_root_call_with_function_argument_value(inner, scope)
    Call(_, arguments) => {
      let arguments = wgsl_ir_call_arguments(arguments)
      arguments.any(fn(argument) {
        wgsl_ir_expr_node_contains_function_argument_value(argument, scope)
      })
    }
    _ => false
  }
}

///|
fn wgsl_ir_expr_node_contains_plain_local_identifier(
  node : @ast.WgslExprNode,
  scope : WgslIrFunctionScope,
) -> Bool {
  match node.kind() {
    Identifier(ident) => scope.locals.contains(ident.name())
    Group(inner) | Unary(_, inner) | PostfixUpdate(_, inner) =>
      wgsl_ir_expr_node_contains_plain_local_identifier(inner, scope)
    Binary(_, left, right) | Assignment(_, left, right) | Index(left, right) =>
      wgsl_ir_expr_node_contains_plain_local_identifier(left, scope) ||
      wgsl_ir_expr_node_contains_plain_local_identifier(right, scope)
    Member(base, _) | Template(base, _) =>
      wgsl_ir_expr_node_contains_plain_local_identifier(base, scope)
    Call(callee, arguments) => {
      if wgsl_ir_expr_node_contains_plain_local_identifier(callee, scope) {
        return true
      }
      let arguments = wgsl_ir_call_arguments(arguments)
      arguments.any(fn(argument) {
        wgsl_ir_expr_node_contains_plain_local_identifier(argument, scope)
      })
    }
    Sequence(items) =>
      items.any(fn(item) {
        wgsl_ir_expr_node_contains_plain_local_identifier(item, scope)
      })
    Literal(_) | ShaderDefTemplate(_) | PathAccess(_, _) => false
  }
}