/-
Copyright (c) 2022 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
import Lean.Meta.Basic
import Lean.Meta.FunInfo

namespace Lean

def Expr.ctorWeight : Expr → UInt8
  | bvar ..    => 0
  | fvar ..    => 1
  | mvar ..    => 2
  | sort ..    => 3
  | const ..   => 4
  | lit ..     => 5
  | mdata ..   => 6
  | proj ..    => 7
  | app ..     => 8
  | lam ..     => 9
  | forallE .. => 10
  | letE ..    => 11

namespace Meta
namespace ACLt

mutual

/--
  An AC-compatible ordering.

  Recall that an AC-compatible ordering if it is monotonic, well-founded, and total.
  Both KBO and LPO are AC-compatible. KBO is faster, but we do not cache the weight of
  each expression in Lean 4. Even if we did, we would need to have a weight where implicit instace arguments are ignored.
  So, we use a LPO-like term ordering.

  Remark: this method is used to implement ordered rewriting. We ignore implicit instance
  arguments to address an issue reported at issue #972.

  Remark: the order is not really total on terms since
   - We instance implicit arguments.
   - We ignore metadata.
   - We ignore universe parameterst at constants.
-/
unsafe def lt (a b : Expr) : MetaM Bool :=
  if ptrAddrUnsafe a == ptrAddrUnsafe b then
    return false
  -- We ignore metadata
  else if a.isMData then
    lt a.mdataExpr! b
  else if b.isMData then
    lt a b.mdataExpr!
  else
    lpo a b
where
  ltPair (a₁ a₂ b₁ b₂ : Expr) : MetaM Bool := do
    if (← lt a₁ b₁) then
      return true
    else if (← lt b₁ a₁) then
      return false
    else
      lt a₂ b₂

  ltApp (a b : Expr) : MetaM Bool := do
    let aFn := a.getAppFn
    let bFn := b.getAppFn
    if (← lt aFn bFn) then
      return true
    else if (← lt bFn aFn) then
      return false
    else
      let aArgs := a.getAppArgs
      let bArgs := b.getAppArgs
      if aArgs.size < bArgs.size then
        return true
      else if aArgs.size > bArgs.size then
        return false
      else
        let infos := (← getFunInfoNArgs aFn aArgs.size).paramInfo
        for i in [:infos.size] do
          -- We ignore instance implicit arguments during comparison
          if !infos[i]!.isInstImplicit then
            if (← lt aArgs[i]! bArgs[i]!) then
              return true
            else if (← lt bArgs[i]! aArgs[i]!) then
              return false
        for i in [infos.size:aArgs.size] do
          if (← lt aArgs[i]! bArgs[i]!) then
            return true
          else if (← lt bArgs[i]! aArgs[i]!) then
            return false
        return false

  lexSameCtor (a b : Expr) : MetaM Bool :=
    match a with
    -- Atomic
    | Expr.bvar i ..    => return i < b.bvarIdx!
    | Expr.fvar id ..   => return Name.lt id.name b.fvarId!.name
    | Expr.mvar id ..   => return Name.lt id.name b.mvarId!.name
    | Expr.sort u ..    => return Level.normLt u b.sortLevel!
    | Expr.const n ..   => return Name.lt n b.constName! -- We igore the levels
    | Expr.lit v ..     => return Literal.lt v b.litValue!
    -- Composite
    | Expr.proj _ i e ..    => if i != b.projIdx! then return i < b.projIdx! else lt e b.projExpr!
    | Expr.app ..           => ltApp a b
    | Expr.lam _ d e ..     => ltPair d e b.bindingDomain! b.bindingBody!
    | Expr.forallE _ d e .. => ltPair d e b.bindingDomain! b.bindingBody!
    | Expr.letE _ _ v e ..  => ltPair v e b.letValue! b.letBody!
    -- See main function
    | Expr.mdata ..         => unreachable!

  allChildrenLt (a b : Expr) : MetaM Bool :=
    match a with
    | Expr.proj _ _ e ..    => lt e b
    | Expr.app ..           =>
      a.withApp fun f args => do
        let infos := (← getFunInfoNArgs f args.size).paramInfo
        for i in [:infos.size] do
          -- We ignore instance implicit arguments during comparison
          if !infos[i]!.isInstImplicit then
            if !(← lt args[i]! b) then
              return false
        for i in [infos.size:args.size] do
          if !(← lt args[i]! b) then
            return false
        return true
    | Expr.lam _ d e ..     => lt d b <&&> lt e b
    | Expr.forallE _ d e .. => lt d b <&&> lt e b
    | Expr.letE _ _ v e ..  => lt v b <&&> lt e b
    | _ => return true

  someChildGe (a b : Expr) : MetaM Bool :=
    return !(← allChildrenLt a b)

  lpo (a b : Expr) : MetaM Bool := do
    -- Case 1: `a < b` if for some child `b_i` of `b`, we have `b_i >= a`
    if (← someChildGe b a) then
      return true
    else if a.ctorWeight > b.ctorWeight then
      return false
    else
      -- Case 2: `a < b` if `a.ctorWeight < b.ctorWeight` and for all children `a_i` of `a`, `a_i < b`
      -- Case 3: `a < b` if `a` & `b` have the same ctor, and `a` is lexicographically smaller, and for all children `a_i` of a, `a_i < b`
      if !(← allChildrenLt a b) then
        return false -- Cases 2 and 3 can't be true
      else if a.ctorWeight < b.ctorWeight then
        return true -- Case 2
      else
        -- `a.ctorWeight == b.ctorWeight`
        lexSameCtor a b -- Case 3

end

end ACLt

@[implementedBy ACLt.lt]
opaque Expr.acLt : Expr → Expr → MetaM Bool

end Lean.Meta
