///|
fn library_origin(name : String, source : @ir.SourceLocation) -> @ion.IonValue {
  ion_record([
    ("name", @ion.IonValue::string(name)),
    ("coordinate_profile", @ion.IonValue::string("moonbit-parser-position-v1")),
    ("start_line", @ion.IonValue::int(source.start_line)),
    ("start_column", @ion.IonValue::int(source.start_column)),
    ("end_line", @ion.IonValue::int(source.end_line)),
    ("end_column", @ion.IonValue::int(source.end_column)),
  ])
}

///|
fn library_attributes(
  source : @ir.SourceLocation,
  ty : @ir.Type,
) -> @ir.ValueAttributes {
  { source: Some(source), inferred_type: Some(ty), extensions: Map([]), }
}

///|
fn library_arrow(
  signature : LibrarySignature,
  remaining : Int32,
  metadata : Bool,
) -> @ir.Type raise {
  let mut result = signature.output_type.to_ir(metadata)
  for i = signature.input_types.length() - 1
      i >= signature.input_types.length() - remaining
      i = i - 1 {
    result = @ir.Type::Function(
      @ir.TypeAttributes::default(),
      signature.input_types[i].to_ir(metadata),
      result,
    )
  }
  result
}

///|
priv struct LibraryLowerer {
  functions : Array[ResolvedFunction]
  bindings : PinnedSourceBindings
  entries : Array[@ir.FQName]
  boolean : @ir.Type
  types : Map[@ir.Name, LibraryTypeUse]
  metadata : Bool
  profile : String
  string_limit : Int32
  cancelled : () -> Bool
  origins : Array[@ion.IonValue]
  mut names : BinderNames
}

///|
fn LibraryLowerer::literal(
  self : LibraryLowerer,
  value : Bool,
  source : @ir.SourceLocation,
) -> @ir.Value {
  @ir.Value::Literal(
    library_attributes(source, self.boolean),
    @ir.Literal::BoolLiteral(value),
  )
}

///|
fn LibraryLowerer::negate(
  self : LibraryLowerer,
  value : @ir.Value,
  source : @ir.SourceLocation,
) -> @ir.Value {
  @ir.Value::IfThenElse(
    library_attributes(source, self.boolean),
    value,
    self.literal(false, source),
    self.literal(true, source),
  )
}

///|
fn LibraryLowerer::fresh(
  self : LibraryLowerer,
  value : @ir.Value,
  role : String,
  source : @ir.SourceLocation,
) -> @ir.Name raise {
  let base = match value {
    Variable(_, name) => name.to_canonical()
    _ => "boolean"
  }
  let name = self.names.fresh(base, role)
  self.origins.push(
    ion_record([
      ("temporary", library_origin(role, source)),
      ("identity", @ion.IonValue::string(name.to_canonical())),
      ("origin", @ion.IonValue::string(base)),
    ]),
  )
  name
}

///|
fn LibraryLowerer::expression(
  self : LibraryLowerer,
  expr : ResolvedExpr,
) -> @ir.Value raise {
  library_poll(self.cancelled)
  match expr {
    Boolean(value, source) => self.literal(value, source)
    Floating(value, authored, scalar, source) => {
      let float64 = @ir.Type::Reference(
        @ir.TypeAttributes::default(),
        @ir.FQName::parse("morphir/SDK:basics#float"),
        [],
      )
      let attrs = library_attributes(source, float64)
      attrs.extensions["moonbit-source-number-v1"] = @ir.Data::Object(
        Map::from_array([
          ("kind", @ir.Data::Text("literal")),
          ("authored_token", @ir.Data::Text(authored)),
          ("resolved_type", @ir.Data::Text(scalar.source_name())),
          (
            "float64_bits",
            @ir.Data::Text(value.reinterpret_as_uint64().to_string()),
          ),
          (
            "float32_bits",
            @ir.Data::Text(
              if scalar == Floating32 {
                Float::from_double(value).reinterpret_as_uint().to_string()
              } else {
                ""
              },
            ),
          ),
          (
            "precision",
            @ir.Data::Number(if scalar == Floating32 { "24" } else { "53" }),
          ),
        ]),
      )
      let literal = @ir.Value::Literal(attrs, @ir.Literal::FloatLiteral(value))
      if scalar == Floating32 {
        let float32 = self.expr_type(expr)
        let converted = self.invoke(
          "morphir/moonbit-source:float32#from-float64",
          [(literal, float64)],
          float32,
          source,
          "float32-literal-round",
          metadata_key="moonbit-source-number-v1",
          runtime_extension="morphir-moonbit-source-float-v1",
        )
        match converted {
          @ir.Value::Apply(conversion_attrs, _, _) =>
            conversion_attrs.extensions["moonbit-source-number-v1"] = @ir.Data::Object(
              Map::from_array([
                ("kind", @ir.Data::Text("conversion")),
                ("policy", @ir.Data::Text("rounded-to-binary32")),
                ("input_representation", @ir.Data::Text("IEEE-binary64")),
                ("output_representation", @ir.Data::Text("IEEE-binary32")),
                (
                  "binding",
                  @ir.Data::Text("morphir/moonbit-source:float32#from-float64"),
                ),
                (
                  "float32_bits",
                  @ir.Data::Text(
                    Float::from_double(value).reinterpret_as_uint().to_string(),
                  ),
                ),
                (
                  "source_start_line",
                  @ir.Data::Number(source.start_line.to_string()),
                ),
                (
                  "source_start_column",
                  @ir.Data::Number(source.start_column.to_string()),
                ),
              ]),
            )
          _ => abort("floating literal conversion must apply a binding")
        }
        converted
      } else {
        literal
      }
    }
    Number(value, authored, scalar, source) => {
      let exact = library_integer_type()
      let attrs = library_attributes(source, exact)
      let facts = Map::from_array([
        ("kind", @ir.Data::Text("literal")),
        ("authored_token", @ir.Data::Text(authored)),
        ("canonical_value", @ir.Data::Text(value)),
        ("resolved_type", @ir.Data::Text(scalar.source_name())),
        (
          "signedness",
          @ir.Data::Text(if scalar.is_signed() { "signed" } else { "unsigned" }),
        ),
        ("compiler", @ir.Data::Text("0.10.20+65efd5b93")),
      ])
      if scalar.bits() == 0 {
        facts["precision"] = @ir.Data::Text("arbitrary")
      } else {
        facts["width"] = @ir.Data::Number(scalar.bits().to_string())
      }
      attrs.extensions["moonbit-source-number-v1"] = @ir.Data::Object(facts)
      if scalar == UnboundedInteger {
        return @ir.Value::Literal(attrs, @ir.Literal::IntegerLiteral(value))
      }
      let result = LibraryTypeUse::{
        scalar,
        spelling: scalar.source_name(),
        source,
        profile: self.profile,
      }.to_ir(self.metadata)
      self.invoke(
        "morphir/moonbit-source:" + scalar.source_module() + "#normalize",
        [(@ir.Value::Literal(attrs, @ir.Literal::IntegerLiteral(value)), exact)],
        result,
        source,
        "literal-normalization",
        metadata_key="moonbit-source-number-v1",
        runtime_extension=if scalar == SignedInt32 {
          "morphir-moonbit-source-int32-v1"
        } else {
          "morphir-moonbit-source-integer-v1"
        },
      )
    }
    Text(value, authored, source) => {
      let attrs = library_attributes(source, self.expr_type(expr))
      attrs.extensions["moonbit-source-string-v1"] = @ir.Data::Object(
        Map::from_array([
          ("kind", @ir.Data::Text("literal")),
          ("authored_token", @ir.Data::Text(authored)),
          ("cooked", @ir.Data::Text(value)),
          ("utf16_code_units", @ir.Data::Number(value.length().to_string())),
        ]),
      )
      self.require_string(
        @ir.Value::Literal(attrs, @ir.Literal::StringLiteral(value)),
        source,
        "literal-boundary",
      )
    }
    Constraint(value, ty, source) => {
      let value = self.expression(value)
      if !self.metadata {
        return value
      }
      self.origins.push(
        ion_record([("constraint", library_origin(ty.spelling, ty.source))]),
      )
      // Retain this authored type use even when the constrained expression is a
      // reference whose own type metadata belongs to a different declaration.
      let name = self.fresh(value, "constraint-value", source)
      let ty = ty.to_ir(true)
      let attrs = library_attributes(source, ty)
      @ir.Value::LetDefinition(
        attrs,
        name,
        @ir.ValueDefinition::ExpressionBody([], ty, value),
        @ir.Value::Variable(attrs, name),
      )
    }
    Variable(name, source) =>
      @ir.Value::Variable(
        library_attributes(source, self.types[name].to_ir(self.metadata)),
        name,
      )
    Negation(value, source) => self.negate(self.expression(value), source)
    NumericNegation(value, source) => {
      if self.expr_type(value) is @ir.Type::Reference(_, name, []) &&
        (
          name.to_canonical() == "morphir/moonbit-source:float32#float32" ||
          name.to_canonical() == "morphir/SDK:basics#float"
        ) {
        let ty = self.expr_type(value)
        return self.invoke(
          if name.to_canonical() == "morphir/SDK:basics#float" {
            "morphir/SDK:basics#negate"
          } else {
            "morphir/moonbit-source:float32#negate"
          },
          [(self.expression(value), ty)],
          ty,
          source,
          "float-negate",
          runtime_extension=if name.to_canonical() == "morphir/SDK:basics#float" {
            ""
          } else {
            "morphir-moonbit-source-float-v1"
          },
        )
      }
      let scalar = self.integer_scalar(value).unwrap()
      if scalar == SignedInt32 {
        self.int32_negate(self.expression(value), source)
      } else {
        self.integer_negate(self.expression(value), scalar, source)
      }
    }
    StringLength(value, source) =>
      self.string_length(self.expression(value), source)
    StringSlice(value, start, end, source) =>
      self.string_slice(
        self.expression(value),
        self.expression(start),
        self.expression(end),
        source,
      )
    Conditional(cond, yes, no, source) =>
      @ir.Value::IfThenElse(
        library_attributes(source, self.expr_type(yes)),
        self.expression(cond),
        self.expression(yes),
        self.expression(no),
      )
    Binding(name, text, binder_source, value, body, source) => {
      self.origins.push(
        ion_record([
          ("binding", library_origin(text, binder_source)),
          (
            "allocation_reason",
            @ion.IonValue::string(
              if name == self.names.source.names[text] {
                self.names.source.reason(text)
              } else {
                "lexical-shadow"
              },
            ),
          ),
          ("identity", @ion.IonValue::string(name.to_canonical())),
        ]),
      )
      @ir.Value::LetDefinition(
        library_attributes(source, self.expr_type(body)),
        name,
        @ir.ValueDefinition::ExpressionBody(
          [],
          self.types[name].to_ir(self.metadata),
          self.expression(value),
        ),
        self.expression(body),
      )
    }
    Call(index, arguments, source) => {
      self.origins.push(
        ion_record([
          (
            "call",
            library_origin(self.functions[index].declaration.name, source),
          ),
        ]),
      )
      let arguments = arguments.map(arg => self.expression(arg))
      let temporaries = if self.metadata {
        arguments.map(arg => self.fresh(arg, "call-argument", source))
      } else {
        []
      }
      let mut value = @ir.Value::Reference(
        library_attributes(
          source,
          library_arrow(
            self.functions[index].declaration,
            arguments.length(),
            self.metadata,
          ),
        ),
        self.entries[index],
      )
      for i = 0; i < arguments.length(); i = i + 1 {
        value = @ir.Value::Apply(
          library_attributes(
            source,
            library_arrow(
              self.functions[index].declaration,
              arguments.length() - i - 1,
              self.metadata,
            ),
          ),
          value,
          if self.metadata {
            @ir.Value::Variable(
              library_attributes(
                source,
                self.functions[index].declaration.input_types[i].to_ir(
                  self.metadata,
                ),
              ),
              temporaries[i],
            )
          } else {
            arguments[i]
          },
        )
      }
      for i = temporaries.length() - 1; i >= 0; i = i - 1 {
        value = @ir.Value::LetDefinition(
          library_attributes(
            source,
            self.functions[index].declaration.output_type.to_ir(self.metadata),
          ),
          temporaries[i],
          @ir.ValueDefinition::ExpressionBody(
            [],
            self.functions[index].declaration.input_types[i].to_ir(
              self.metadata,
            ),
            arguments[i],
          ),
          value,
        )
      }
      value
    }
    SdkCall(index, arguments, source) => self.sdk_call(index, arguments, source)
    Binary(op, lhs, rhs, source) => {
      let attrs = library_attributes(source, self.boolean)
      let left = self.expression(lhs)
      let right = self.expression(rhs)
      if self.expr_type(lhs) is @ir.Type::Reference(_, name, _) &&
        name.to_canonical() == "morphir/SDK:string#string" {
        let text = self.expr_type(lhs)
        return if op == "+" {
          self.invoke(
            "morphir/moonbit-source:string#append-bounded",
            [
              (left, text),
              (right, text),
              (self.bound(source), library_integer_type()),
            ],
            text,
            source,
            "bounded-concatenation",
          )
        } else if ["<", "<=", ">", ">="].contains(op) {
          self.string_order(op, left, right, text, source)
        } else {
          self.invoke(
            if op == "==" {
              "morphir/SDK:basics#equal"
            } else {
              "morphir/SDK:basics#not-equal"
            },
            [(left, text), (right, text)],
            self.boolean,
            source,
            "string-equality",
          )
        }
      }
      if self.integer_scalar(lhs) is Some(scalar) {
        return if scalar == SignedInt32 {
          self.int32_binary(op, left, right, source)
        } else {
          self.integer_binary(op, left, right, scalar, source)
        }
      }
      if self.expr_type(lhs) is @ir.Type::Reference(_, float_name, []) &&
        (
          float_name.to_canonical() == "morphir/moonbit-source:float32#float32" ||
          float_name.to_canonical() == "morphir/SDK:basics#float"
        ) {
        let ty = self.expr_type(lhs)
        let operation = match op {
          "+" => "add"
          "-" => "subtract"
          "*" => "multiply"
          "/" => "divide"
          "==" => "equal"
          "!=" => "not-equal"
          "<" => "less-than"
          "<=" => "less-than-or-equal"
          ">" => "greater-than"
          _ => "greater-than-or-equal"
        }
        let float32 = float_name.to_canonical() ==
          "morphir/moonbit-source:float32#float32"
        let result = self.invoke(
          (if float32 {
            "morphir/moonbit-source:float32#"
          } else {
            "morphir/SDK:basics#"
          }) +
          operation,
          [(left, ty), (right, ty)],
          if ["+", "-", "*", "/"].contains(op) {
            ty
          } else {
            self.boolean
          },
          source,
          "float-" + operation,
          metadata_key="moonbit-source-number-v1",
          runtime_extension=if float32 {
            "morphir-moonbit-source-float-v1"
          } else {
            ""
          },
        )
        if float32 && ["+", "-", "*", "/"].contains(op) {
          match result {
            @ir.Value::Apply(operation_attrs, _, _) =>
              operation_attrs.extensions["moonbit-source-number-v1"] = @ir.Data::Object(
                Map::from_array([
                  ("kind", @ir.Data::Text("operation")),
                  ("policy", @ir.Data::Text("binary32-operation-rounding")),
                  (
                    "binding",
                    @ir.Data::Text(
                      "morphir/moonbit-source:float32#" + operation,
                    ),
                  ),
                  (
                    "source_start_line",
                    @ir.Data::Number(source.start_line.to_string()),
                  ),
                  (
                    "source_start_column",
                    @ir.Data::Number(source.start_column.to_string()),
                  ),
                ]),
              )
            _ => abort("floating operation must apply a binding")
          }
        }
        return result
      }
      match op {
        "&&" =>
          @ir.Value::IfThenElse(attrs, left, right, self.literal(false, source))
        "||" =>
          @ir.Value::IfThenElse(attrs, left, self.literal(true, source), right)
        _ => {
          // Strict operand bindings preserve order and avoid duplicating call trees.
          let l = self.fresh(left, "equality-left", source)
          let r = self.fresh(right, "equality-right", source)
          let lv = @ir.Value::Variable(attrs, l)
          let rv = @ir.Value::Variable(attrs, r)
          let comparison = if op == "!=" {
            @ir.Value::IfThenElse(attrs, lv, self.negate(rv, source), rv)
          } else {
            @ir.Value::IfThenElse(attrs, lv, rv, self.negate(rv, source))
          }
          @ir.Value::LetDefinition(
            attrs,
            l,
            @ir.ValueDefinition::ExpressionBody([], self.boolean, left),
            @ir.Value::LetDefinition(
              attrs,
              r,
              @ir.ValueDefinition::ExpressionBody([], self.boolean, right),
              comparison,
            ),
          )
        }
      }
    }
  }
}

///|
fn library_lower(
  input : Input,
  package_name : @ir.Path,
  module_name : @ir.Path,
  checked : CheckedLibrary,
  cancelled : () -> Bool,
  string_limit : Int32,
  bindings : PinnedSourceBindings,
  channel : SourceChannel,
) -> LibraryOutput raise {
  let boolean = boolean_type()
  let allocated = allocate_source_names(
    checked.functions.map(f => (f.declaration.name, f.declaration.source)),
    package_name.to_canonical() + ":" + module_name.to_canonical(),
  )
  let warnings = allocated.warnings.copy()
  for function in checked.functions {
    warnings.append(function.names.source.warnings)
  }
  let entries = checked.functions.map(f => {
    @ir.FQName::parse(
      package_name.to_canonical() +
      ":" +
      module_name.to_canonical() +
      "#" +
      allocated.names[f.declaration.name].to_canonical(),
    )
  })
  let lowerer = LibraryLowerer::{
    functions: checked.functions,
    bindings,
    entries,
    boolean,
    types: Map([]),
    metadata: true,
    profile: channel.profile(),
    string_limit,
    cancelled,
    origins: [],
    names: BinderNames::new([], ""),
  }
  let values : Map[
    @ir.Name,
    @ir.AccessControlled[@ir.Documented[@ir.ValueDefinition]],
  ] = Map([])
  let metadata = []
  let public_entries = []
  for i = 0; i < checked.functions.length(); i = i + 1 {
    library_poll(cancelled)
    let function = checked.functions[i]
    let declaration = function.declaration
    lowerer.origins.clear()
    lowerer.names = function.names
    lowerer.types.clear()
    for name, ty in function.types {
      lowerer.types[name] = ty
    }
    let mut body = lowerer.expression(function.body)
    if declaration.public {
      if declaration.output_type.scalar == Floating32 {
        body = lowerer.round_float32(
          body,
          declaration.body_source,
          "public-result",
        )
      }
      for j = function.parameters.length() - 1; j >= 0; j = j - 1 {
        if declaration.input_types[j].scalar == Floating32 {
          let ty = declaration.input_types[j].to_ir(true)
          let name = function.parameters[j]
          let source = declaration.parameters[j].1
          let rounded = lowerer.round_float32(
            @ir.Value::Variable(library_attributes(source, ty), name),
            source,
            "public-argument",
          )
          body = @ir.Value::LetDefinition(
            library_attributes(
              declaration.body_source,
              declaration.output_type.to_ir(true),
            ),
            name,
            @ir.ValueDefinition::ExpressionBody([], ty, rounded),
            body,
          )
        }
      }
    }
    if declaration.public {
      if declaration.output_type.scalar.has_integer_boundary() {
        body = lowerer.require_integer_boundary(
          body,
          declaration.output_type.scalar,
          declaration.body_source,
          "public-result",
        )
      }
      for j = function.parameters.length() - 1; j >= 0; j = j - 1 {
        if declaration.input_types[j].scalar.has_integer_boundary() {
          let ty = declaration.input_types[j].to_ir(true)
          let name = function.parameters[j]
          let source = declaration.parameters[j].1
          let value = lowerer.require_integer_boundary(
            @ir.Value::Variable(library_attributes(source, ty), name),
            declaration.input_types[j].scalar,
            source,
            "public-argument",
          )
          body = @ir.Value::LetDefinition(
            library_attributes(
              declaration.body_source,
              declaration.output_type.to_ir(true),
            ),
            name,
            @ir.ValueDefinition::ExpressionBody([], ty, value),
            body,
          )
        }
      }
    }
    if declaration.public {
      if declaration.output_type.scalar == Text {
        body = lowerer.require_string(
          body,
          declaration.body_source,
          "public-result",
        )
      }
      for j = function.parameters.length() - 1; j >= 0; j = j - 1 {
        if declaration.input_types[j].scalar == Text {
          let ty = declaration.input_types[j].to_ir(true)
          let name = function.parameters[j]
          let source = declaration.parameters[j].1
          let value = lowerer.require_string(
            @ir.Value::Variable(library_attributes(source, ty), name),
            source,
            "public-argument",
          )
          body = @ir.Value::LetDefinition(
            library_attributes(
              declaration.body_source,
              declaration.output_type.to_ir(true),
            ),
            name,
            @ir.ValueDefinition::ExpressionBody([], ty, value),
            body,
          )
        }
      }
    }
    values[allocated.names[declaration.name]] = {
      access: if declaration.public {
        @ir.Access::Public
      } else {
        @ir.Access::Private
      },
      value: {
        doc: declaration.doc,
        value: @ir.ValueDefinition::ExpressionBody(
          function.parameters.mapi((j, name) => {
            (name, declaration.input_types[j].to_ir(true))
          }),
          declaration.output_type.to_ir(true),
          body,
        ),
      },
    }
    if declaration.public {
      public_entries.push(entries[i])
    }
    let id = "value:" + entries[i].to_canonical()
    metadata.push({
      node_id: id,
      data: ion_record([
        ("declaration", library_origin(declaration.name, declaration.source)),
        (
          "source_error_contract",
          @ion.IonValue::string(declaration.effect.identifier()),
        ),
        (
          "naming",
          ion_record([
            ("policy", @ion.IonValue::string(NAMING_POLICY)),
            ("original", @ion.IonValue::string(declaration.name)),
            (
              "allocation_reason",
              @ion.IonValue::string(allocated.reason(declaration.name)),
            ),
            (
              "identity",
              @ion.IonValue::string(
                allocated.names[declaration.name].to_canonical(),
              ),
            ),
          ]),
        ),
        (
          "parameters",
          @ion.IonValue::list(
            declaration.parameters.mapi((i, p) => {
              allocated_origin(
                library_origin(p.0, p.1),
                function.parameters[i],
                function.names.source.reason(p.0),
              )
            }),
          ),
        ),
      ]),
    })
    metadata.push({
      node_id: id + "/body",
      data: ion_record([
        ("body", library_origin(declaration.name, declaration.body_source)),
        ("nodes", @ion.IonValue::list(lowerer.origins.copy())),
      ]),
    })
  }
  public_entries.sort_by(fn(a, b) { a.to_canonical().compare(b.to_canonical()) })
  let module_def : @ir.ModuleDefinition = { types: Map([]), doc: None, values, }
  let source_modules : Map[@ir.ModuleName, @ir.ModuleSpecification] = Map([])
  source_modules[@ir.Path::parse("string")] = @sdk_spec.source_string_specification().modules[@ir.Path::parse(
      "string",
    )]

  source_modules[@ir.Path::parse("int32")] = @sdk_spec.source_int32_module_specification()

  for (name, module_spec) in @sdk_spec.source_integer_specification().modules {
    source_modules[name] = module_spec
  }

  for (name, module_spec) in @sdk_spec.source_float_specification().modules {
    source_modules[name] = module_spec
  }

  let ir = @ir.IRFile::new(
    @ir.Distribution::Library(
      package_name,
      if !source_modules.is_empty() {
        Map::from_array([
          (
            @sdk_spec.source_string_package_name(),
            @ir.PackageSpecification::{ modules: source_modules, },
          ),
        ])
      } else {
        Map([])
      },
      {
        modules: Map::from_array([
          (module_name, { access: @ir.Access::Public, value: module_def, }),
        ]),
      },
    ),
  )
  {
    unit_id: input.unit_id,
    ir,
    entries: public_entries,
    metadata,
    annotations: [@ion.symbol("morphir_pipeline")],
    warnings,
    provenance: [
      ion_record([
        ("language", @ion.IonValue::string("moonbit")),
        ("profile", @ion.IonValue::string(channel.profile())),
        ("parser_version", @ion.IonValue::string(PARSER_VERSION)),
        ("naming_policy", @ion.IonValue::string(NAMING_POLICY)),
        ("unit_id", @ion.IonValue::string(input.unit_id)),
      ]).with_annotation(@ion.symbol("morphir_moonbit_frontend")),
    ] +
    warnings.map(w => w.to_ion()),
  }
}

///|
fn LibraryLowerer::expr_type(
  self : LibraryLowerer,
  expr : ResolvedExpr,
) -> @ir.Type raise {
  match expr {
    Boolean(_, _) | Negation(_, _) => self.boolean
    NumericNegation(value, _) => self.expr_type(value)
    StringLength(_, source) =>
      LibraryTypeUse::{
        scalar: SignedInt32,
        spelling: "Int32",
        source,
        profile: self.profile,
      }.to_ir(self.metadata)
    StringSlice(_, _, _, source) =>
      LibraryTypeUse::{
        scalar: Text,
        spelling: "String",
        source,
        profile: self.profile,
      }.to_ir(self.metadata)
    Number(_, _, scalar, source) =>
      LibraryTypeUse::{
        scalar,
        spelling: scalar.source_name(),
        source,
        profile: self.profile,
      }.to_ir(self.metadata)
    Floating(_, _, scalar, source) =>
      LibraryTypeUse::{
        scalar,
        spelling: scalar.source_name(),
        source,
        profile: self.profile,
      }.to_ir(self.metadata)
    Text(_, _, source) =>
      LibraryTypeUse::{ scalar: Text, spelling: "String", source, profile: "", }.to_ir(
        self.metadata,
      )
    Binary(op, lhs, _, _) =>
      if ["+", "-", "*", "/"].contains(op) {
        self.expr_type(lhs)
      } else {
        self.boolean
      }
    Variable(name, _) => self.types[name].to_ir(self.metadata)
    Constraint(_, ty, _) => ty.to_ir(self.metadata)
    Conditional(_, yes, _, _) => self.expr_type(yes)
    Binding(_, _, _, _, body, _) => self.expr_type(body)
    Call(index, _, _) =>
      self.functions[index].declaration.output_type.to_ir(self.metadata)
    SdkCall(index, _, source) => {
      let scalar = self.bindings.entries[index].output
      LibraryTypeUse::{
        scalar,
        spelling: scalar.source_name(),
        source,
        profile: self.profile,
      }.to_ir(self.metadata)
    }
  }
}

///|
fn library_integer_type() -> @ir.Type raise {
  @ir.Type::Reference(
    @ir.TypeAttributes::default(),
    @ir.FQName::parse("morphir/SDK:basics#int"),
    [],
  )
}

///|
fn LibraryLowerer::round_float32(
  self : LibraryLowerer,
  value : @ir.Value,
  source : @ir.SourceLocation,
  role : String,
) -> @ir.Value raise {
  let float32 = LibraryTypeUse::{
    scalar: Floating32,
    spelling: "Float",
    source,
    profile: self.profile,
  }.to_ir(self.metadata)
  self.invoke(
    "morphir/moonbit-source:float32#round-boundary",
    [(value, float32)],
    float32,
    source,
    role,
    metadata_key="moonbit-source-number-v1",
    runtime_extension="morphir-moonbit-source-float-v1",
  )
}

///|
/// MoonBit compares String lengths before UTF-16 lexical ties. Bind source
/// operands in order, then lengths, so each source expression executes once.
fn LibraryLowerer::string_order(
  self : LibraryLowerer,
  op : String,
  left : @ir.Value,
  right : @ir.Value,
  text : @ir.Type,
  source : @ir.SourceLocation,
) -> @ir.Value raise {
  let int = library_integer_type()
  let attrs = library_attributes(source, self.boolean)
  let l = self.fresh(left, "string-order-left", source)
  let r = self.fresh(right, "string-order-right", source)
  let lv = @ir.Value::Variable(library_attributes(source, text), l)
  let rv = @ir.Value::Variable(library_attributes(source, text), r)
  let ll = self.invoke(
    "morphir/SDK:string#length",
    [(lv, text)],
    int,
    source,
    "order-length",
  )
  let rl = self.invoke(
    "morphir/SDK:string#length",
    [(rv, text)],
    int,
    source,
    "order-length",
  )
  let ln = self.fresh(ll, "string-order-left-length", source)
  let rn = self.fresh(rl, "string-order-right-length", source)
  let lengths = [
    (@ir.Value::Variable(library_attributes(source, int), ln), int),
    (@ir.Value::Variable(library_attributes(source, int), rn), int),
  ]
  let operation_name = match op {
    "<" => "less-than"
    "<=" => "less-than-or-equal"
    ">" => "greater-than"
    _ => "greater-than-or-equal"
  }
  let operation = "morphir/SDK:basics#" + operation_name
  let mut result = @ir.Value::IfThenElse(
    attrs,
    self.invoke(
      "morphir/SDK:basics#equal",
      lengths,
      self.boolean,
      source,
      "order-equal-lengths",
    ),
    self.invoke(
      operation,
      [(lv, text), (rv, text)],
      self.boolean,
      source,
      "order-lexical-tie",
    ),
    self.invoke(operation, lengths, self.boolean, source, "order-length-first"),
  )
  let bindings = [
    (l, text, left),
    (r, text, right),
    (ln, int, ll),
    (rn, int, rl),
  ]
  for i = bindings.length() - 1; i >= 0; i = i - 1 {
    let (name, ty, value) = bindings[i]
    let owner = library_attributes(source, self.boolean)
    if i == 0 {
      owner.extensions["moonbit-source-string-order-v1"] = @ir.Data::Object(
        Map::from_array([
          ("operator", @ir.Data::Text(op)),
          ("ordering", @ir.Data::Text("utf16-shortlex")),
          ("lowering", @ir.Data::Text("length-first-lexical-tie")),
        ]),
      )
    }
    result = @ir.Value::LetDefinition(
      owner,
      name,
      @ir.ValueDefinition::ExpressionBody([], ty, value),
      result,
    )
  }
  result
}

///|
fn LibraryLowerer::bound(
  self : LibraryLowerer,
  source : @ir.SourceLocation,
) -> @ir.Value raise {
  @ir.Value::Literal(
    library_attributes(source, library_integer_type()),
    @ir.Literal::IntegerLiteral(self.string_limit.to_string()),
  )
}

///|
fn LibraryLowerer::invoke(
  _self : LibraryLowerer,
  name : String,
  arguments : Array[(@ir.Value, @ir.Type)],
  result : @ir.Type,
  source : @ir.SourceLocation,
  role : String,
  metadata_key~ : String = "moonbit-source-string-v1",
  runtime_extension~ : String = "morphir-moonbit-source-string-v1",
) -> @ir.Value raise {
  let attrs = library_attributes(source, result)
  let facts = Map::from_array([
    ("kind", @ir.Data::Text("synthetic")),
    ("role", @ir.Data::Text(role)),
  ])
  if runtime_extension != "" {
    facts["runtime_extension"] = @ir.Data::Text(runtime_extension)
  }
  attrs.extensions[metadata_key] = @ir.Data::Object(facts)
  let mut signature = result
  for i = arguments.length() - 1; i >= 0; i = i - 1 {
    signature = @ir.Type::Function(
      @ir.TypeAttributes::default(),
      arguments[i].1,
      signature,
    )
  }
  let mut value = @ir.Value::Reference(
    { ..attrs, inferred_type: Some(signature), },
    @ir.FQName::parse(name),
  )
  for arg in arguments {
    guard signature is @ir.Type::Function(_, _, remaining) else {
      abort("invalid internal signature")
    }
    signature = remaining
    value = @ir.Value::Apply(
      { ..attrs, inferred_type: Some(signature), },
      value,
      arg.0,
    )
  }
  value
}

///|
/// Lower only an adapter-resolved source binding. The call owns its source
/// binding facts; the canonical SDK declaration remains the executable IR.
fn LibraryLowerer::sdk_call(
  self : LibraryLowerer,
  index : Int32,
  arguments : Array[ResolvedExpr],
  source : @ir.SourceLocation,
) -> @ir.Value raise {
  let binding = self.bindings.entries[index]
  let authored = binding.authored()
  self.origins.push(
    ion_record([
      ("sdk_call", library_origin(authored, source)),
      ("canonical", @ion.IonValue::string(binding.canonical)),
      ("source_package", @ion.IonValue::string(binding.source_package)),
      ("source_tree", @ion.IonValue::string(binding.source_tree)),
      ("source_signature", @ion.IonValue::string(binding.source_signature)),
    ]),
  )
  let facts = @ir.Data::Object(
    Map::from_array([
      ("kind", @ir.Data::Text("source-call")),
      ("source_alias", @ir.Data::Text(binding.source_alias)),
      ("source_name", @ir.Data::Text(binding.source_name)),
      ("source_package", @ir.Data::Text(binding.source_package)),
      ("source_version", @ir.Data::Text(binding.source_version)),
      ("source_tree", @ir.Data::Text(binding.source_tree)),
      ("source_signature", @ir.Data::Text(binding.source_signature)),
      ("canonical", @ir.Data::Text(binding.canonical)),
      ("conversion_policy", @ir.Data::Text(binding.contract.identifier())),
      ("sdk_classification", @ir.Data::Text(binding.origin.identifier())),
    ]),
  )
  if binding.form is StringMethod(_) && facts is @ir.Data::Object(fields) {
    fields["source_form"] = @ir.Data::Text("string-method")
    fields["receiver_type"] = @ir.Data::Text("String")
    fields["argument_coercion"] = @ir.Data::Text(
      "pinned-implicit-string-to-stringview",
    )
    fields["canonical_argument_order"] = @ir.Data::Text(
      binding.canonical_order().map(i => i.to_string()).join(","),
    )
  }
  if self.profile == EXPERIMENTAL_PROFILE && facts is @ir.Data::Object(fields) {
    fields["source_error_contract"] = @ir.Data::Text(
      binding.effect.identifier(),
    )
  }
  if binding.contract is DecimalExactConstruction &&
    facts is @ir.Data::Object(fields) {
    if arguments[0] is Text(text, lexeme, _) {
      fields["original_literal"] = @ir.Data::Text(text)
      fields["original_source_spelling"] = @ir.Data::Text(lexeme)
      let mantissa = text.split("e").to_array()[0]
      let fraction = mantissa.split(".").to_array()
      fields["literal_scale"] = @ir.Data::Number(
        (if fraction.length() == 2 { fraction[1].length() } else { 0 }).to_string(),
      )
    }
  }
  let output_type = LibraryTypeUse::{
    scalar: binding.output,
    spelling: binding.output.source_name(),
    source,
    profile: self.profile,
  }.to_ir(self.metadata)
  let input_types = binding.inputs.map(scalar => {
    LibraryTypeUse::{
      scalar,
      spelling: scalar.source_name(),
      source,
      profile: self.profile,
    }.to_ir(self.metadata)
  })
  let values = arguments.map(argument => self.expression(argument))
  // A reordered method call always binds temporaries, so the SDK argument order
  // cannot change source evaluation order.
  let temporaries = if self.metadata || binding.form is StringMethod(_) {
    values.map(value => self.fresh(value, "sdk-call-argument", source))
  } else {
    []
  }
  let order = binding.canonical_order()
  let mut signature = output_type
  for k = order.length() - 1; k >= 0; k = k - 1 {
    signature = @ir.Type::Function(
      @ir.TypeAttributes::default(),
      input_types[order[k]],
      signature,
    )
  }
  let reference_attrs = library_attributes(source, signature)
  reference_attrs.extensions["moonbit-source-binding-v1"] = facts
  let mut value = @ir.Value::Reference(
    reference_attrs,
    @ir.FQName::parse(binding.canonical),
  )
  // Temporaries keep source evaluation order; application uses SDK order.
  for i in order {
    guard signature is @ir.Type::Function(_, _, remaining) else {
      abort("invalid pinned SDK signature")
    }
    signature = remaining
    let apply_attrs = library_attributes(source, signature)
    apply_attrs.extensions["moonbit-source-binding-v1"] = facts
    value = @ir.Value::Apply(
      apply_attrs,
      value,
      if temporaries.is_empty() {
        values[i]
      } else {
        @ir.Value::Variable(
          library_attributes(source, input_types[i]),
          temporaries[i],
        )
      },
    )
  }
  for i = temporaries.length() - 1; i >= 0; i = i - 1 {
    value = @ir.Value::LetDefinition(
      library_attributes(source, output_type),
      temporaries[i],
      @ir.ValueDefinition::ExpressionBody([], input_types[i], values[i]),
      value,
    )
  }
  value
}

///|
fn LibraryLowerer::require_string(
  self : LibraryLowerer,
  value : @ir.Value,
  source : @ir.SourceLocation,
  role : String,
) -> @ir.Value raise {
  let text = LibraryTypeUse::{
    scalar: Text,
    spelling: "String",
    source,
    profile: "",
  }.to_ir(self.metadata)
  self.invoke(
    "morphir/moonbit-source:string#require-valid",
    [(value, text), (self.bound(source), library_integer_type())],
    text,
    source,
    role,
  )
}