///|
fn PinnedSourceBindings::require_type(
  self : PinnedSourceBindings,
  ty : LibraryTypeUse,
) -> Unit raise {
  let required = match ty.scalar {
    SdkInteger | OptionalSdkInteger => Some(SourceSdkType::Integer)
    SdkDecimal => Some(SourceSdkType::Decimal)
    _ => None
  }
  if required is Some(required) && !self.types.contains(required) {
    library_error(
      "binding_identity",
      "Source type requires its audited SDK binding: " + ty.spelling,
      Some(ty.source),
    )
  }
}

///|
priv struct LibraryResolver {
  functions : Map[String, Int32]
  bindings : PinnedSourceBindings
  cancelled : () -> Bool
  names : BinderNames
}

///|
fn LibraryResolver::expression(
  self : LibraryResolver,
  expr : LibraryExpr,
  scope : Map[String, @ir.Name],
) -> ResolvedExpr raise {
  library_poll(self.cancelled)
  match expr {
    Boolean(value, source) => Boolean(value, source)
    Number(value, authored, scalar, source) =>
      Number(value, authored, scalar, source)
    Floating(value, authored, scalar, source) =>
      Floating(value, authored, scalar, source)
    Text(value, authored, source) => Text(value, authored, source)
    Constraint(value, ty, source) => {
      self.bindings.require_type(ty)
      Constraint(self.expression(value, scope), ty, source)
    }
    Identifier(text, source) =>
      match scope.get(text) {
        Some(name) => Variable(name, source)
        None => {
          library_error(
            if self.functions.contains(text) {
              "function_value"
            } else {
              "unknown_reference"
            },
            "Expected a bound value: " + text,
            Some(source),
          )
          abort("unreachable")
        }
      }
    Negation(value, source) => Negation(self.expression(value, scope), source)
    NumericNegation(value, source) =>
      NumericNegation(self.expression(value, scope), source)
    StringLength(value, source) =>
      StringLength(self.expression(value, scope), source)
    StringSlice(value, start, end, source) =>
      StringSlice(
        self.expression(value, scope),
        self.expression(start, scope),
        self.expression(end, scope),
        source,
      )
    Binary(op, lhs, rhs, source) =>
      Binary(
        op,
        self.expression(lhs, scope),
        self.expression(rhs, scope),
        source,
      )
    Conditional(cond, yes, no, source) =>
      Conditional(
        self.expression(cond, scope),
        self.expression(yes, scope),
        self.expression(no, scope),
        source,
      )
    Binding(text, binder_source, value, body, source) => {
      // The initializer sees the preceding scope, including an older shadowed name.
      let value = self.expression(value, scope)
      let name = self.names.bind(text)
      let nested = scope.copy()
      nested[text] = name
      Binding(
        name,
        text,
        binder_source,
        value,
        self.expression(body, nested),
        source,
      )
    }
    Call(text, arguments, source) => {
      if scope.contains(text) {
        library_error(
          "not_callable",
          "A value binding shadows the call target: " + text,
          Some(source),
        )
      }
      guard self.functions.get(text) is Some(index) else {
        library_error(
          "unknown_function",
          "Unknown library function: " + text,
          Some(source),
        )
        abort("unreachable")
      }
      Call(index, arguments.map(arg => self.expression(arg, scope)), source)
    }
    QualifiedCall(package_alias, source_name, arguments, source) => {
      if scope.contains(package_alias) {
        library_error(
          "shadowed_binding",
          "A local value shadows the source package alias: " + package_alias,
          Some(source),
        )
      }
      guard self.bindings.find(package_alias, source_name) is Some(index) else {
        library_error(
          "unknown_binding",
          "Unknown pinned source binding: @" + package_alias + "." + source_name,
          Some(source),
        )
        abort("unreachable")
      }
      SdkCall(index, arguments.map(arg => self.expression(arg, scope)), source)
    }
    MethodCall(name, receiver, arguments, source) => {
      guard self.bindings.find_method(name) is Some(index) else {
        if is_string_predicate(name) {
          library_error(
            "unknown_binding",
            "String::" + name + " requires its pinned MoonBit core binding",
            Some(source),
          )
        }
        library_error(
          "unsupported_method",
          "Method is outside the pinned String source subset: " + name,
          Some(source),
        )
        abort("unreachable")
      }
      let receiver = self.expression(receiver, scope)
      SdkCall(
        index,
        [receiver, ..arguments.map(arg => self.expression(arg, scope))],
        source,
      )
    }
  }
}

///|
fn library_resolve(
  functions : Array[LibraryFunction],
  cancelled : () -> Bool,
  bindings : PinnedSourceBindings,
) -> Array[ResolvedFunction] raise {
  let indices : Map[String, Int32] = Map([])
  for i = 0; i < functions.length(); i = i + 1 {
    indices[functions[i].name] = i
  }
  let resolved = []
  for declaration in functions {
    library_poll(cancelled)
    for ty in [..declaration.input_types, declaration.output_type] {
      bindings.require_type(ty)
    }
    if declaration.effect != Pure &&
      !bindings.types.contains(
        if declaration.effect == DecimalError {
          SourceSdkType::Decimal
        } else {
          SourceSdkType::Error
        },
      ) {
      library_error(
        "binding_identity",
        "Error annotation requires its audited SDK binding",
        Some(declaration.source),
      )
    }
    let scope : Map[String, @ir.Name] = Map([])
    let bound_names = declaration.parameters.copy()
    collect_bindings(declaration.body, bound_names)
    let names = BinderNames::new(bound_names, "binding:" + declaration.name)
    let resolver = LibraryResolver::{
      functions: indices,
      bindings,
      cancelled,
      names,
    }
    let parameters = declaration.parameters.map(p => names.bind(p.0))
    for i = 0; i < parameters.length(); i = i + 1 {
      scope[declaration.parameters[i].0] = parameters[i]
    }
    resolved.push({
      declaration: {
        name: declaration.name,
        doc: declaration.doc,
        public: declaration.public,
        parameters: declaration.parameters,
        input_types: declaration.input_types,
        output_type: declaration.output_type,
        effect: declaration.effect,
        source: declaration.source,
        body_source: declaration.body_source,
      },
      parameters,
      names,
      types: Map([]),
      body: resolver.expression(declaration.body, scope),
    })
  }
  resolved
}