Script set

// Value type wrapping a set of `Script` values. Used both as the
// return type for `Text.scripts()` / `Scripts.of` / `Scripts.dominant`
// and as the input to `Scripts.restrict_to` (an identifier
// allowlist).
//
// Internally we store a sorted, deduplicated `Array[U8] val` of
// script bytes (the same encoding `_ScriptCodec.to_byte` produces).
// Sorted-deduped means equal sets have equal byte arrays, which keeps
// `contains` simple and makes `resolved` a one-pass filter.

class val ScriptSet
  let _bytes: Array[U8] val

  new val create(scripts: Array[Script] val) =>
    """
    Build a set from a (possibly-unsorted, possibly-duplicate) list
    of scripts.
    """
    _bytes = _ScriptSetUtils.sort_dedup(scripts)

  new val empty() =>
    """The empty set — useful as a starting point for unions."""
    _bytes = recover val Array[U8] end

  new val _from_bytes(sorted_bytes: Array[U8] val) =>
    """
    Package-private constructor. `sorted_bytes` MUST already be
    sorted and deduplicated; callers in the unicode package use this
    when they already have a valid byte array (e.g., `resolved`,
    `_ScriptCodec.from_byte` chains) and want to skip the sort.
    """
    _bytes = sorted_bytes

  fun size(): USize => _bytes.size()

  fun contains(s: Script): Bool =>
    """True iff `s` is in this set."""
    let target = _ScriptCodec.to_byte(s)
    for b in _bytes.values() do
      if b == target then return true end
    end
    false

  fun to_array(): Array[Script] val =>
    """
    Return the scripts in this set as an `Array[Script] val`, in the
    same byte-sorted order the set stores internally.
    """
    let out = recover trn Array[Script](_bytes.size()) end
    for b in _bytes.values() do
      out.push(_ScriptCodec.from_byte(b))
    end
    consume out

  fun resolved(): ScriptSet val =>
    """
    The set with `Common` and `Inherited` removed. Useful for the
    "which actual scripts does this text use?" question — those two
    fit anything and rarely belong in the answer.
    """
    let common = _ScriptCodec.to_byte(ScriptCommon)
    let inherited = _ScriptCodec.to_byte(ScriptInherited)
    let filtered = recover trn Array[U8] end
    for b in _bytes.values() do
      if (b != common) and (b != inherited) then
        filtered.push(b)
      end
    end
    ScriptSet._from_bytes(consume filtered)

  fun bytes(): Array[U8] val =>
    """
    Package-private view of the underlying sorted byte array. Used by
    `Scripts.restrict_to` to avoid re-encoding scripts on every cp.
    """
    _bytes

primitive _ScriptSetUtils
  fun sort_dedup(scripts: Array[Script] val): Array[U8] val =>
    let raw = recover trn Array[U8](scripts.size()) end
    for s in scripts.values() do
      raw.push(_ScriptCodec.to_byte(s))
    end
    let n = raw.size()
    var i: USize = 1
    while i < n do
      try
        var j = i
        let cur = raw(j)?
        while (j > 0) and (raw(j - 1)? > cur) do
          raw(j)? = raw(j - 1)?
          j = j - 1
        end
        raw(j)? = cur
      end
      i = i + 1
    end
    let out = recover trn Array[U8](n) end
    var prev: U8 = 0
    var first: Bool = true
    for b in raw.values() do
      if first or (b != prev) then out.push(b); first = false end
      prev = b
    end
    consume out