import Lean
import Base.Utils

namespace Primitives

-------------------------------
-- Tuple field access syntax --
-------------------------------
-- Declare new syntax `a.#i` for accessing the `i`-th term in a tuple
-- The `noWs` parser is used to ensure there is no whitespace.
-- We use the maximum precedence to make the syntax work with function calls.
-- Ex.: `f (0, 1).#0`
syntax:max term noWs ".#" noWs num : term

open Lean Meta Elab Term

-- Auxliary function for computing the number of elements in a tuple (`Prod`) type.
def getArity (type : Expr) : Nat :=
  match type with
  | .app (.app (.const ``Prod _) _) as => getArity as + 1
  | _ => 1 -- It is not product

-- Given a `tuple` of size `n`, construct a term that for accessing the `i`-th element
def mkGetIdx (tuple : Expr) (n : Nat) (i : Nat) : MetaM Expr := do
  match i with
  | 0 => mkAppM ``Prod.fst #[tuple]
  | i+1 =>
    if n = 2 then
      -- If the tuple has only two elements and `i` is not `0`,
      -- we just return the second element.
      mkAppM ``Prod.snd #[tuple]
    else
      -- Otherwise, we continue with the rest of the tuple.
      let tuple ← mkAppM ``Prod.snd #[tuple]
      mkGetIdx tuple (n-1) i

-- Now, we define the elaboration function for the new syntax `a#i`
elab_rules : term
| `($a:term.#$i:num) => do
  -- Convert `i : Syntax` into a natural number
  let i := i.getNat
  -- Return error if it is 0.
  unless i ≥ 0 do
    throwError "tuple index must be greater or equal to 0"
  -- Convert `a : Syntax` into an `tuple : Expr` without providing expected type
  let tuple ← elabTerm a none
  let type ← inferType tuple
  -- Instantiate assigned metavariable occurring in `type`
  let type ← instantiateMVars type
  /- In case we are indexing into a type abbreviation, we need to unfold the type.

     TODO: we have to be careful about not unfolding too much,
     for instance because of the following code:
     ```
     def Pair T U := T × U
     def Tuple T U V := T × Pair U V
     ```
     We have to make sure that, given `x : Tuple T U V`, `x.1` evaluates
     to the pair (an element of type `Pair T U`), not to the first field
     of the pair (an element of type `T`).

     We have a similar issue below if we generate code from the following Rust definition:
     ```
     struct Tuple(u32, (u32, u32));
     ```
     The issue is that in Rust, field 1 of `Tuple` is a pair `(u32, u32)`, but
     in Lean there is no difference between `A × B × C` and `A × (B × C)`.

     In case such situations happen we probably need to resort to chaining
     the pair projectors, like in: `x.snd.fst`.
   -/
  let type ← whnf type
  -- Ensure `tuple`'s type is a `Prod`uct.
  unless type.isAppOf ``Prod do
    throwError "tuple expected{indentExpr type}"
  let n := getArity type
  -- Ensure `i` is a valid index
  unless i < n do
    throwError "invalid tuple access at {i}, tuple has {n} elements"
  mkGetIdx tuple n i

end Primitives
