subsumes.pony

use "collections"
use ".."

primitive Subsumes
  fun apply(a: Item, b: Item, p: String = "/"): (Bool, String) =>
    """`a` subsumes `b` if `b` contains at least everything in `a`."""
    match a
    | let a_obj: Object box => _obj_subsumes(a_obj, b, p)
    | let a_seq: Sequence box => _seq_subsumes(a_seq, b, p)
    | let a_str: String box => _str_subsumes(a_str, b, p)
    | let a_int: I128 => _int_subsumes(a_int, b, p)
    | let a_float: F64 => _float_subsumes(a_float, b, p)
    | let a_bool: Bool => _bool_subsumes(a_bool, b, p)
    | let a_null: Null => _null_subsumes(a_null, b, p)
    end

  fun _obj_subsumes(a_obj: Object box, b: Item, p: String)
    : (Bool, String)
  =>
    match b
    | let b_obj: Object box =>
      for (key, a_val) in a_obj.pairs() do
        let path = recover val p + "/" + key end
        try
          (let res, let err) = apply(a_val, b_obj(key)?, path)
          if not res then
            return (res, err)
          end
        else
          return (false, "rhs of " + path + " does not exist")
        end
      end
      return (true, "")
    end
    (false, "rhs at " + p + " is not an object")

  fun _seq_subsumes(a_seq: Sequence box, b: Item, p: String)
    : (Bool, String)
  =>
    match b
    | let b_seq: Sequence box =>
      if b_seq.size() < a_seq.size() then
        return (false, "rhs at " + p + " does not have enough elements")
      end
      for i in Range[USize](0, a_seq.size()) do
        let path = recover val p + "/" + i.string() end
        try
          (let res, let err) = apply(a_seq(i)?, b_seq(i)?, path)
          if not res then
            return (res, err)
          end
        else
          return (false, "rhs at " + path + " does not exist")
        end
      end
      return (true, "")
    end
    (false, "rhs at " + p + " is not a sequence")

  fun _str_subsumes(a_str: String box, b: Item, p: String)
    : (Bool, String)
  =>
    match b
    | let b_str: String box =>
      if a_str == b_str then
        return (true, "")
      else
        return
          ( false
          , "'" + StringUtil.escape(a_str) + "' at " + p + " != '" +
              StringUtil.escape(b_str) + "'" )
      end
    end
    (false, "rhs at " + p + " is not a string")

  fun _int_subsumes(a_int: I128, b: Item, p: String): (Bool, String) =>
    match b
    | let b_int: I128 =>
      if a_int == b_int then
        return (true, "")
      else
        return (false, a_int.string() + " at " + p + " != " + b_int.string())
      end
    end
    (false, "rhs at " + p + " is not an integer")

  fun _float_subsumes(a_float: F64, b: Item, p: String)
    : (Bool, String)
  =>
    match b
    | let b_float: F64 =>
      (let a_fr, let a_exp) = a_float.frexp()
      (let b_fr, let b_exp) = b_float.frexp()
      if (a_exp == b_exp) and ((b_fr - a_fr).abs() < 0.00000000000001) then
        return (true, "")
      else
        return
          (false, a_float.string() + " at " + p + " != " + b_float.string())
      end
    end
    (false, "rhs at " + p + " is not a float")

  fun _bool_subsumes(a_bool: Bool, b: Item, p: String)
    : (Bool, String)
  =>
    match b
    | let b_bool: Bool =>
      if a_bool == b_bool then
        return (true, "")
      else
        return (false, a_bool.string() + " at " + p + " != " + b_bool.string())
      end
    end
    (false, "rhs at " + p + " is not a bool")

  fun _null_subsumes(a_null: Null, b: Item, p: String) : (Bool, String) =>
    match b
    | let b_null: Null =>
      return (true, "")
    end
    (false, "rhs at " + p + " is not null")