///|
fn WgslIrLowerer::lower_function_declaration(
  self : WgslIrLowerer,
  function : @ast.WgslFunction,
) -> Unit raise WgslIrLowerError {
  let stage = wgsl_ir_shader_stage_from_attributes(function.attributes())
  let referenced_stage_function = match stage {
    Some(_) => self.referenced_names.contains(function.name().name())
    None => false
  }
  let ir_function = if referenced_stage_function {
    wgsl_ir_strip_function_io_bindings(self.lower_function(function))
  } else {
    self.lower_function(function)
  }
  match self.function_names.get(function.name().name()) {
    Some(handle) => self.shader_module.set_function(handle, ir_function)
    None => {
      let handle = self.shader_module.append_function(ir_function)
      self.function_names.set(function.name().name(), handle)
    }
  }
  match stage {
    Some(stage) =>
      if !self.referenced_names.contains(function.name().name()) &&
        !self.entry_point_name_is_lowered(function.name().name()) {
        let (workgroup_size, workgroup_size_overrides) = self.workgroup_size_from_attributes(
          function.attributes(),
        )
        self.shader_module.append_entry_point({
          name: function.name().name(),
          source_start: function.fn_start(),
          stage,
          early_depth_test: wgsl_ir_early_depth_test_from_attributes(
            function.attributes(),
          ),
          workgroup_size,
          workgroup_size_overrides,
          function: ir_function,
          mesh_info: self.mesh_stage_info_from_attributes(
            stage,
            function.attributes(),
          ),
          task_payload: self.payload_variable_from_attributes(
            function.attributes(),
          ),
          incoming_ray_payload: None,
        })
      }
    None => ()
  }
}

///|
fn WgslIrLowerer::entry_point_name_is_lowered(
  self : WgslIrLowerer,
  name : String,
) -> Bool {
  for entry_point in self.shader_module.entry_points {
    if entry_point.name == name {
      return true
    }
  }
  false
}

///|
fn wgsl_ir_strip_function_io_bindings(function : Function) -> Function {
  let arguments : Array[FunctionArgument] = []
  for argument in function.arguments {
    arguments.push({ name: argument.name, ty: argument.ty, binding: None, })
  }
  let result = match function.result {
    Some(value) => Some(FunctionResult::{ ty: value.ty, binding: None, })
    None => None
  }
  {
    name: function.name,
    source_start: function.source_start,
    arguments,
    result,
    local_variables: function.local_variables,
    expressions: function.expressions,
    named_expressions: function.named_expressions,
    body: function.body,
    diagnostic_filter_leaf: function.diagnostic_filter_leaf,
  }
}

///|
fn WgslIrLowerer::resolve_ambient_global_function_parameters(
  self : WgslIrLowerer,
) -> Unit {
  for index in 0.. {
        let rewritten = self.function_without_ambient_global_arguments(
          function, global_handles,
        )
        self.shader_module.set_function(handle, rewritten)
      }
      None => ()
    }
  }
}

///|
fn WgslIrLowerer::function_is_called_only_without_arguments(
  self : WgslIrLowerer,
  target : Handle,
) -> Bool {
  let mut has_call = false
  let mut has_non_empty_call = false
  for function in self.shader_module.functions.items {
    let (function_has_call, function_has_non_empty_call) = self.function_call_argument_shape(
      function, target,
    )
    has_call = has_call || function_has_call
    has_non_empty_call = has_non_empty_call || function_has_non_empty_call
  }
  for entry_point in self.shader_module.entry_points {
    let (function_has_call, function_has_non_empty_call) = self.function_call_argument_shape(
      entry_point.function,
      target,
    )
    has_call = has_call || function_has_call
    has_non_empty_call = has_non_empty_call || function_has_non_empty_call
  }
  has_call && !has_non_empty_call
}

///|
fn WgslIrLowerer::function_call_argument_shape(
  self : WgslIrLowerer,
  function : Function,
  target : Handle,
) -> (Bool, Bool) {
  ignore(self)
  let mut has_call = false
  let mut has_non_empty_call = false
  for expression in function.expressions.items {
    match expression {
      FunctionCall(handle, arguments) =>
        if handle == target {
          has_call = true
          if !arguments.is_empty() {
            has_non_empty_call = true
          }
        }
      _ => ()
    }
  }
  let (body_has_call, body_has_non_empty_call) = self.block_call_argument_shape(
    function.body,
    target,
  )
  has_call = has_call || body_has_call
  has_non_empty_call = has_non_empty_call || body_has_non_empty_call
  (has_call, has_non_empty_call)
}

///|
fn WgslIrLowerer::block_call_argument_shape(
  self : WgslIrLowerer,
  block : Block,
  target : Handle,
) -> (Bool, Bool) {
  let mut has_call = false
  let mut has_non_empty_call = false
  for statement in block.statements {
    let (statement_has_call, statement_has_non_empty_call) = self.statement_call_argument_shape(
      statement, target,
    )
    has_call = has_call || statement_has_call
    has_non_empty_call = has_non_empty_call || statement_has_non_empty_call
  }
  (has_call, has_non_empty_call)
}

///|
fn WgslIrLowerer::statement_call_argument_shape(
  self : WgslIrLowerer,
  statement : Statement,
  target : Handle,
) -> (Bool, Bool) {
  match statement {
    Call(handle, arguments, _) =>
      if handle == target {
        (true, !arguments.is_empty())
      } else {
        (false, false)
      }
    Block(block) => self.block_call_argument_shape(block, target)
    If(_, accept, reject) => {
      let (accept_has_call, accept_has_non_empty_call) = self.block_call_argument_shape(
        accept, target,
      )
      let (reject_has_call, reject_has_non_empty_call) = self.block_call_argument_shape(
        reject, target,
      )
      (
        accept_has_call || reject_has_call,
        accept_has_non_empty_call || reject_has_non_empty_call,
      )
    }
    Switch(_, cases) => {
      let mut has_call = false
      let mut has_non_empty_call = false
      for case in cases {
        let (case_has_call, case_has_non_empty_call) = self.block_call_argument_shape(
          case.body,
          target,
        )
        has_call = has_call || case_has_call
        has_non_empty_call = has_non_empty_call || case_has_non_empty_call
      }
      (has_call, has_non_empty_call)
    }
    Loop(body, continuing, _) => {
      let (body_has_call, body_has_non_empty_call) = self.block_call_argument_shape(
        body, target,
      )
      let (continuing_has_call, continuing_has_non_empty_call) = self.block_call_argument_shape(
        continuing, target,
      )
      (
        body_has_call || continuing_has_call,
        body_has_non_empty_call || continuing_has_non_empty_call,
      )
    }
    _ => (false, false)
  }
}

///|
fn WgslIrLowerer::ambient_global_arguments_for_function(
  self : WgslIrLowerer,
  function : Function,
) -> Array[Handle]? {
  if function.arguments.is_empty() {
    return None
  }
  let globals : Array[Handle] = []
  for argument in function.arguments {
    guard argument.binding == None else { return None }
    guard argument.name is Some(name) else { return None }
    guard self.global_names.get(name) is Some(global_handle) else {
      return None
    }
    guard self.shader_module.global_variables.items.get(global_handle.index())
      is Some(global) else {
      return None
    }
    guard global.ty == argument.ty else { return None }
    globals.push(global_handle)
  }
  Some(globals)
}

///|
fn WgslIrLowerer::function_without_ambient_global_arguments(
  self : WgslIrLowerer,
  function : Function,
  global_handles : Array[Handle],
) -> Function {
  let expressions = ExpressionArena()
  for index in 0.. value
      None => SourceExpression
    }
    ignore(
      expressions.append_with_origin(
        self.rewrite_ambient_global_function_expression(
          expression, global_handles,
        ),
        origin,
      ),
    )
  }
  {
    name: function.name,
    source_start: function.source_start,
    arguments: [],
    result: function.result,
    local_variables: function.local_variables,
    expressions,
    named_expressions: function.named_expressions,
    body: function.body,
    diagnostic_filter_leaf: function.diagnostic_filter_leaf,
  }
}

///|
fn WgslIrLowerer::rewrite_ambient_global_function_expression(
  self : WgslIrLowerer,
  expression : Expression,
  global_handles : Array[Handle],
) -> Expression {
  ignore(self)
  match expression {
    FunctionArgument(index) =>
      match global_handles.get(index) {
        Some(global_handle) => GlobalVariable(global_handle)
        None => FunctionArgument(index)
      }
    _ => expression
  }
}

///|
fn WgslIrLowerer::lower_function(
  self : WgslIrLowerer,
  function : @ast.WgslFunction,
) -> Function raise WgslIrLowerError {
  let result = match function.result() {
    Some(result) =>
      match result.ty_ref() {
        Some(ty_ref) =>
          Some(FunctionResult::{
            ty: self.lower_type_ref(ty_ref),
            binding: wgsl_ir_binding_from_attributes(result.attributes()),
          })
        None => None
      }
    None => None
  }
  let ir_function = Function::{
    name: Some(function.name().name()),
    source_start: function.fn_start(),
    arguments: [],
    result,
    local_variables: LocalVariableArena(),
    expressions: ExpressionArena(),
    named_expressions: [],
    body: Block(),
    diagnostic_filter_leaf: None,
  }
  let scope = WgslIrFunctionScope()
  let mut index = 0
  for argument in function.arguments() {
    guard argument.ty_ref() is Some(ty_ref) else {
      raise MissingType(argument.name().name())
    }
    ir_function.arguments.push({
      name: Some(argument.name().name()),
      ty: self.lower_type_ref(ty_ref),
      binding: wgsl_ir_binding_from_attributes(argument.attributes()),
    })
    scope.arguments.set(argument.name().name(), index)
    index = index + 1
  }
  self.predeclare_function_source_local_prefix(
    function.body(),
    ir_function,
    scope,
  )
  let body = self.lower_block(function.body(), ir_function, scope) catch {
    error =>
      match error {
        UnknownIdentifier(name) =>
          raise UnknownIdentifier(
            "\{name} in function \{function.name().name()} with args \{wgsl_ir_function_argument_names(function)}",
          )
        UnsupportedExpression(message) =>
          raise UnsupportedExpression(
            "\{message} in function \{function.name().name()}",
          )
        UnsupportedStatement(message) =>
          raise UnsupportedStatement(
            "\{message} in function \{function.name().name()}",
          )
        _ => raise error
      }
  }
  if result == None &&
    !wgsl_ir_block_ends_with_return(body) &&
    !wgsl_ir_absorb_implicit_tail_return(body) {
    body.push(ImplicitReturn)
  }
  {
    name: ir_function.name,
    source_start: ir_function.source_start,
    arguments: ir_function.arguments,
    result,
    local_variables: ir_function.local_variables,
    expressions: ir_function.expressions,
    named_expressions: ir_function.named_expressions,
    body,
    diagnostic_filter_leaf: ir_function.diagnostic_filter_leaf,
  }
}

///|
fn wgsl_ir_function_argument_names(function : @ast.WgslFunction) -> String {
  let names : Array[String] = []
  for argument in function.arguments() {
    names.push(argument.name().name())
  }
  names.join(",")
}