Skip to content

Type level nat parameter causes LH to check less #2457

@kleinreact

Description

@kleinreact

Lets say I wanna have a bounded number type with Num capabilities, then I can get that for some fixed lower bound 0 and upper bound 9 as follows:

{-@ LIQUID "" @-}

{-# OPTIONS_GHC -fplugin=LiquidHaskell #-}
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE StandaloneKindSignatures #-}

module T2457 where

import GHC.Base (Type)
import GHC.TypeNats (Nat, Natural)

{-@ type BI010 = { i : Integer | i >= 0 && i < 10 } @-}

{-@ data T = T BI010 @-}
type T :: Type
data T = T Integer

{-@ inline t2i @-}
{-@ t2i :: t : T -> { i : BI010 | t == T i } @-}
t2i :: T -> Integer
t2i (T i) = i
{-# INLINE t2i #-}

t0 :: T
t0 = T 3

-- does not LH type check, as expected
--t1 :: T
--t1 = T 10

{-@
instance Num T where
  (+) ::
    a : T ->
    { b : T | t2i a + t2i b < 10 } ->
    { t : T | t2i t == t2i a + t2i b }

  (-) ::
    a : T ->
    { b : T | t2i a >= t2i b } ->
    { t : T | t2i t == t2i a - t2i b }

  (*) ::
    a : T ->
    { b : T | t2i a * t2i b < 10 } ->
    { t : T | t2i t == t2i a * t2i b }

  negate :: T -> T
  abs    :: T -> T
  signum :: T -> T

  fromInteger ::
    i : BI010 ->
    { t : T | t2i t == i }
@-}
instance Num T where
  a + b        = T $ t2i a + t2i b
  a - b        = T $ t2i a - t2i b
  a * b        = T $ t2i a * t2i b
  negate       = undefined
  abs          = id
  signum       = undefined
  fromInteger  = T

t2 :: T
t2 = 3 + 3

-- does not LH type check, as expected
--t3 :: T
--t3 = 3 + 8

LH checks the bounds in this case as expected.

However, if I parameterize the bound using a type level natural instead, e.g.

{-@ LIQUID "" @-}

{-@ embed Natural as Int @-}
{-@ type BoundedInteger N = { i : Integer | N > 0 && i >= 0 && i < N } @-}

{-@ data P n = P Integer @-}
{-@ P :: forall (n :: Nat). BoundedInteger n -> P n @-}
type P :: Nat -> Type
data P n = P Integer

{-@ inline p2i @-}
{-@ p2i :: forall (n :: Nat). p : P n -> { i : BoundedInteger n | p == P i } @-}
p2i :: P n -> Integer
p2i (P i) = i
{-# INLINE p2i #-}

p0 :: P 10
p0 = P 3

-- does not LH type check, as expected
--p1 :: P 10
--p1 = P 10

{-@
instance Num (P n) where
  (+) ::
    forall (n :: Nat).
    a : P n ->
    { b : P n | p2i a + p2i b < n } ->
    { x : P n | p2i x == p2i a + p2i b }

  (-) ::
    forall (n :: Nat).
    a : P n ->
    { b : P n | p2i a >= p2i b } ->
    { x : P | p2i x == p2i a - p2i b }

  (*) ::
    forall (n :: Nat).
    a : P n ->
    { b : P n | p2i a * p2i b < n } ->
    { x : P | p2i x == p2i a * p2i b }

  negate :: forall (n :: Nat). P n -> P n
  abs    :: forall (n :: Nat). P n -> P n
  signum :: forall (n :: Nat). P n -> P n

  fromInteger ::
    forall (n :: Nat).
    i : BoundedInteger n ->
    { x : P n | p2i x == i }
@-}
instance Num (P n) where
  a + b        = P $ p2i a + p2i b
  a - b        = P $ p2i a - p2i b
  a * b        = P $ p2i a * p2i b
  negate       = undefined
  abs          = id
  signum       = undefined
  fromInteger  = P

p2 :: P 10
p2 = 3 + 3

-- LH SAFE, but 3 + 9 is no refinement in P 10!
p3 :: P 10
p3 = 3 + 9

then the last definition of p3 LH type checks, although it shouldn't. Note that only change here is the additionally introduced type level nat parameter.

I'd expect the parameterized version to behave the same as the non-parameterized one in this case.

Tested with GHC 9.10 and 8c550df.


2024-12-17: Update of the reproducer to work with 791567f.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions