// Module-level attribute validation.
//
// Ported from `check_attributes` in wax/src/lib-wax/typing.ml.
//
// These are the questions no single field can answer, because they are about the
// module as a whole: is this export name already taken, is there already a start
// function, is there already a module name. Each is decided per CONFIGURATION --
// two exports of the same name in mutually exclusive branches are not a clash,
// because only one of them is ever there.

///|
/// One remembered occurrence: the condition it was seen under, and where.
priv struct Occurrence {
  /// Not `guard`: that is a MoonBit keyword.
  when_ : @cond.T
  loc : @basic.Location
}

///|
/// The condition an attribute is actually present under.
///
/// The field's own branch assumption, narrowed by the attribute's optional
/// `if ` guard -- only `export` and `start` carry one.
fn attribute_cond(
  ctx : @typing_env.ModuleContext,
  a : @ast.Attribute,
) -> @cond.T {
  match a.attr_guard {
    None => ctx.cond.val
    Some(g) =>
      @cond.and_(
        ctx.cond.val,
        ctx.cond_env.of_cond(ctx.diagnostics, g.info, g.desc),
      )
  }
}

///|
/// The first remembered occurrence whose condition can hold at the same time as
/// `cond`, if any.
fn clashes(seen : Array[Occurrence], cond : @cond.T) -> @basic.Location? {
  for o in seen {
    if @cond.is_satisfiable(@cond.and_(o.when_, cond)) {
      return Some(o.loc)
    }
  }
  None
}

///|
/// Check the module-wide attribute constraints.
///
/// `walk_fields` descends into conditionals under each branch's assumption, so
/// `ctx.cond` already says what configuration a field belongs to by the time it
/// is seen -- which is exactly what makes "same name, different branches" fine.
/// Reject unknown annotations, and check the value shape and the placement of
/// the ones that are allowed.
///
/// `import_ok` is set for the declarations inside an `import "m" { .. }` block,
/// where a name-only `#[import = "nm"]` overrides the imported name;
/// `priority_ok` for a DEFINED function, the only thing a compilation hint can
/// attach to -- an imported one has no code-section entry to key an offset-0
/// hint in.
fn check_attribute_list(
  ctx : @typing_env.ModuleContext,
  attributes : Array[@ast.Attribute],
  export_ok~ : Bool,
  start_ok~ : Bool,
  module_ok~ : Bool,
  import_ok~ : Bool,
  priority_ok? : Bool = false,
) -> Unit {
  let d = ctx.diagnostics
  fn is_string(v : @ast.Instr[@basic.Location]?) -> Bool {
    v is Some({ desc: Str(_, _), .. })
  }

  for a in attributes {
    // The whole `#[...]`. A valueless attribute has no value span to fall back
    // on, and the field's span would underline the entire definition.
    let location = a.attr_span
    // A guard is only meaningful on `export` and `start`; blame its own `if`.
    if a.attr_guard is Some(g) &&
      a.attr_name != "export" &&
      a.attr_name != "start" {
      guard_not_allowed(d, g.info, a.attr_name)
    }
    match a.attr_name {
      // A bare `#[export]` reuses the entity's Wax name; an explicit one must
      // be a string.
      "export" => {
        if !(a.attr_value is None) && !is_string(a.attr_value) {
          annotation_value_mismatch(d, location, "export", "a string")
        }
        if !export_ok {
          annotation_not_allowed(d, location, "export")
        }
      }
      "start" => {
        if a.attr_value is Some(_) {
          annotation_value_mismatch(d, location, "start", "no value")
        }
        if !start_ok {
          annotation_not_allowed(d, location, "start")
        }
      }
      "module" => {
        if !is_string(a.attr_value) {
          annotation_value_mismatch(d, location, "module", "a string")
        }
        if !module_ok {
          annotation_not_allowed(d, location, "module")
        }
      }
      // Allowed exactly where `module` is: as an inner attribute.
      "feature" => {
        if !is_string(a.attr_value) {
          annotation_value_mismatch(d, location, "feature", "a string")
        }
        if !module_ok {
          annotation_not_allowed(d, location, "feature")
        }
      }
      "import" => {
        if !is_string(a.attr_value) {
          annotation_value_mismatch(d, location, "import", "a string")
        }
        if !import_ok {
          annotation_not_allowed(d, location, "import")
        }
      }
      // IN RANGE as well as an integer: the section stores the priority as a
      // ULEB, and an over-long literal would otherwise reach the lowering's
      // parse and take it down.
      "priority" | "optimization" => {
        let ok = match a.attr_value {
          Some(v) =>
            match int_literal_u64(v.desc) {
              Some(n) => n < 0x1_0000_0000UL
              None => false
            }
          None => false
        }
        if !ok {
          annotation_value_mismatch(
            d,
            location,
            a.attr_name,
            "an integer in the u32 range",
          )
        }
        if !priority_ok {
          annotation_not_allowed(d, location, a.attr_name)
        }
      }
      "run_once" => {
        if a.attr_value is Some(_) {
          annotation_value_mismatch(d, location, "run_once", "no value")
        }
        if !priority_ok {
          annotation_not_allowed(d, location, "run_once")
        }
      }
      other => unknown_annotation(d, location, other)
    }
  }
  // The section states an optimization priority only alongside a compilation
  // one, and `run_once` is a spelling of a particular optimization value -- so
  // either without `#[priority]` would have nowhere to go. Rejected rather than
  // given a compilation priority the author did not choose.
  guard priority_ok else { return }
  fn find(k : String) -> @ast.Attribute? {
    for a in attributes {
      if a.attr_name == k {
        return Some(a)
      }
    }
    None
  }

  if find("priority") is None {
    for k in ["optimization", "run_once"] {
      if find(k) is Some(a) {
        priority_required(d, a.attr_span, k)
      }
    }
  }
  if find("optimization") is Some(a) && find("run_once") is Some(b) {
    conflicting_optimization(d, a.attr_span, b.attr_span)
  }
}

///|
/// One declaration's attributes, plus the one rule that is about the LIST
/// rather than about any single attribute: an import takes one import-name
/// annotation, and a second has no name left to override.
fn check_import_attributes(
  ctx : @typing_env.ModuleContext,
  attributes : Array[@ast.Attribute],
) -> Unit {
  check_attribute_list(
    ctx,
    attributes,
    export_ok=true,
    start_ok=true,
    module_ok=false,
    import_ok=true,
  )
  let names = []
  for a in attributes {
    if a.attr_name == "import" {
      names.push(a.attr_span)
    }
  }
  if names.length() > 1 {
    multiple_import(ctx.diagnostics, names[1], names[0])
  }
}

///|
fn check_attributes(
  ctx : @typing_env.ModuleContext,
  fields : @ast.Module[@basic.Location],
) -> Unit {
  let exports : Map[String, Array[Occurrence]] = Map([])
  let starts : Array[Occurrence] = []
  let mut module_seen : @basic.Location? = None
  // `location` blames the ENTITY the attributes hang off, which is what a
  // start or a module name is a fact about -- the attribute's own span says
  // only where the word was typed, and the reader has to see which declaration
  // it claimed.
  fn visit(
    attrs : Array[@ast.Attribute],
    default_name : @ast.Ident?,
    location : @basic.Location,
  ) -> Unit {
    for a in attrs {
      let cond = attribute_cond(ctx, a)
      match a.attr_name {
        "export" => {
          // The name and the span to blame: the explicit string for
          // `#[export = "nm"]`, the entity's own name for a bare `#[export]`.
          let entry = match a.attr_value {
            Some(v) =>
              match v.desc {
                Str(_, bytes) =>
                  match @unicode.utf8_text(bytes) {
                    Some(name) => Some((name, v.info))
                    None => None
                  }
                _ => None
              }
            None =>
              match default_name {
                Some(id) => Some((id.name, id.loc))
                None => None
              }
          }
          guard entry is Some((name, location)) else { continue }
          let seen = exports.get(name).unwrap_or([])
          if clashes(seen, cond) is Some(prev) {
            duplicated_export(ctx.diagnostics, location, prev, name)
          }
          seen.push({ when_: cond, loc: location })
          exports[name] = seen
        }
        "start" => {
          if clashes(starts, cond) is Some(prev) {
            multiple_start(ctx.diagnostics, location, prev)
          }
          starts.push({ when_: cond, loc: location })
        }
        // A module name is not guarded -- it states a fact about the whole
        // module, like a feature -- so one is simply one too many.
        "module" =>
          match module_seen {
            Some(prev) => multiple_module(ctx.diagnostics, location, prev)
            None => module_seen = Some(location)
          }
        _ => ()
      }
    }
  }

  // Where each annotation is meaningful. A compilation hint needs a body to
  // attach to, so only a DEFINED function may carry one.
  fn per_field(
    attributes : Array[@ast.Attribute],
    export_ok : Bool,
    start_ok : Bool,
    module_ok : Bool,
    priority_ok : Bool,
  ) -> Unit {
    check_attribute_list(
      ctx,
      attributes,
      export_ok~,
      start_ok~,
      module_ok~,
      import_ok=false,
      priority_ok~,
    )
  }

  walk_fields(ctx, fields, field => {
    match field.desc {
      Func(attributes~, ..) => per_field(attributes, true, true, false, true)
      Global(attributes~, ..)
      | Memory(attributes~, ..)
      | Table(attributes~, ..)
      | Tag(attributes~, ..) => per_field(attributes, true, false, false, false)
      ModuleAnnotation(attrs) => per_field(attrs, false, false, true, false)
      Data(attributes~, ..) | Elem(attributes~, ..) =>
        per_field(attributes, false, false, false, false)
      // An import's attributes hang off each declaration, and `#[import]` is
      // allowed only there.
      Import(decl~, ..) => check_import_attributes(ctx, decl.desc.attributes)
      ImportGroup(decls~, ..) =>
        for dd in decls {
          check_import_attributes(ctx, dd.desc.attributes)
        }
      Type(_) | Conditional(..) => ()
    }
  })
  walk_fields(ctx, fields, field => {
    match field.desc {
      Func(name~, attributes~, ..) => visit(attributes, Some(name), field.info)
      Global(name~, attributes~, ..) =>
        visit(attributes, Some(name), field.info)
      Tag(name~, attributes~, ..) => visit(attributes, Some(name), field.info)
      Memory(name~, attributes~, ..) =>
        visit(attributes, Some(name), field.info)
      Table(name~, attributes~, ..) => visit(attributes, Some(name), field.info)
      Data(name~, attributes~, ..) => visit(attributes, name, field.info)
      Elem(name~, attributes~, ..) => visit(attributes, Some(name), field.info)
      ModuleAnnotation(attrs) => visit(attrs, None, field.info)
      Import(decl~, ..) =>
        visit(decl.desc.attributes, Some(decl.desc.id), decl.info)
      ImportGroup(decls~, ..) =>
        for d in decls {
          visit(d.desc.attributes, Some(d.desc.id), d.info)
        }
      Type(_) | Conditional(..) => ()
    }
  })
}