;;; Arl Standard Library - Type Predicates

(module types
  (export list? list-or-pair? null? nil?
          symbol? keyword? number? string? vector?
          true? false? boolean? fn? callable? procedure?
          environment? is-refclass?
          atom? empty?
          type-of
          real? complex? rational? exact? inexact?
          integer? natural?
          finite? infinite? nan?
          even? odd? zero?
          positive? negative? non-negative? non-positive?)

  (import core :refer (r-call))

  ;;' @section List and Pair Predicates

  ;;' @description Return #t if x is a proper list (R list or call), not a dotted pair.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (list? (list 1 2 3))  ; => #t
  ;;' (list? ())  ; => #t
  ;;' (list? "hello")  ; => #f
  ;;' @assert
  ;;' (assert-true (list? (list 1 2 3)))
  ;;' (assert-true (list? ()))
  ;;' (assert-false (list? "hello"))
  ;;' @seealso pair?, list-or-pair?, null?
  (define list?
    (lambda (x)
      (if (pair? x) #f
        (if (is.list x) #t (if (is.call x) #t #f)))))

  ;;' @description Return #t if x is a non-empty list or dotted pair (pairlist cell).
  ;;' @param x Value to test
  ;;' @examples
  ;;' (list-or-pair? (list 1))  ; => #t
  ;;' @assert
  ;;' (assert-true (list-or-pair? (list 1)))
  ;;' @seealso list?, pair?, null?, atom?
  (define list-or-pair?
    (lambda (x)
      (if (pair? x) #t
        (if (list? x) (if (> (length x) 0) #t #f) #f))))

  ;;' @description Return #t for empty list or #nil.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (null? #nil)  ; => #t
  ;;' (null? ())  ; => #t
  ;;' (null? (list 1))  ; => #f
  ;;' @assert
  ;;' (assert-true (null? #nil))
  ;;' (assert-true (null? ()))
  ;;' (assert-false (null? (list 1)))
  ;;' @seealso nil?, pair?, list-or-pair?
  (define null?
    (lambda (x)
      (if (is.null x)
        #t
        (if (is.list x)
          (if (== (length x) 0) #t #f)
          (if (is.call x)
            (if (== (length x) 0) #t #f)
            #f)))))

  ;;' @description Alias for null?.
  ;;' @param x Value to test
  ;;' @seealso null?
  (define nil?
    (lambda (x)
      (null? x)))

  ;;' @description Return #t if x is not a non-empty list or
  ;;' dotted pair (i.e. not list-or-pair?).
  ;;' @param x Value to test
  ;;' @examples
  ;;' (atom? 42)  ; => #t
  ;;' (atom? "hello")  ; => #t
  ;;' (atom? (list 1 2))  ; => #f
  ;;' (atom? ())  ; => #t  (empty list is atomic)
  ;;' @assert
  ;;' (assert-true (atom? 42))
  ;;' (assert-true (atom? "hello"))
  ;;' (assert-false (atom? (list 1 2)))
  ;;' (assert-true (atom? ()))
  ;;' @seealso list-or-pair?
  (define atom?
    (lambda (x)
      (not (list-or-pair? x))))

  ;;' @description Return #t if x is empty (0-length).
  ;;' (Note the empty string "" has length 1 and is not "empty"!)
  ;;' @param x Value to test
  ;;' @examples
  ;;' (empty? ())  ; => #t
  ;;' (empty? (list))  ; => #t
  ;;' (empty? (list 1))  ; => #f
  ;;' @assert
  ;;' (assert-true (empty? ()))
  ;;' (assert-true (empty? (list)))
  ;;' (assert-false (empty? (list 1)))
  ;;' @note The empty string `""` has length 1 in R and is NOT considered empty!
  ;;' @seealso null?, length= (in `sequences` module)
  (define empty?
    (lambda (x)
      (isTRUE (== (length x) 0))))

  ;;' @section Symbol Predicates

  ;;' @description Return #t if x is a symbol.
  ;;' @param x Value to test
  ;;' @signature (symbol? x)
  ;;' @examples
  ;;' (symbol? 'foo)  ; => #t
  ;;' (symbol? "foo")  ; => #f
  ;;' (symbol? 42)  ; => #f
  ;;' @assert
  ;;' (assert-true (symbol? 'foo))
  ;;' (assert-false (symbol? "foo"))
  ;;' (assert-false (symbol? 42))
  (define symbol? is.symbol)

  ;;' @description Return #t if x is an Arl keyword.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (keyword? ':foo)  ; => #t
  ;;' (keyword? 'foo)   ; => #f
  ;;' @assert
  ;;' (assert-true (keyword? ':foo))
  ;;' (assert-false (keyword? 'foo))
  (define keyword?
    (lambda (x)
      (isTRUE (inherits x "arl_keyword"))))

  ;;' @section Basic Type Predicates

  ;;' @description Return #t if x is a number (real or complex).
  ;;' @param x Value to test
  ;;' @examples
  ;;' (number? 42)  ; => #t
  ;;' @assert
  ;;' (assert-true (number? 42))
  ;;' @seealso real?, complex?, rational?, exact?, inexact?
  (define number?
    (lambda (x)
      (if (is.numeric x)
        #t
        (if (is.complex x) #t #f))))

  ;;' @description Return #t if x is character.
  ;;' @param x Value to test
  ;;' @signature (string? x)
  ;;' @examples
  ;;' (string? "hello")  ; => #t
  ;;' @assert
  ;;' (assert-true (string? "hello"))
  (define string? is.character)

  ;;' @description Return #t if x is a non-list atomic vector.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (vector? (c 1 2 3))  ; => #t
  ;;' @assert
  ;;' (assert-true (vector? (c 1 2 3)))
  ;;' @note Tests whether x is a non-list atomic vector (numeric, character, logical, etc.). R lists are NOT vectors by this predicate.
  (define vector?
    (lambda (x)
      (if (is.atomic x)
        (if (is.list x) #f #t)
        #f)))

  ;;' @description Return #t if x is a single logical value.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (boolean? #t)  ; => #t
  ;;' (boolean? #f)  ; => #t
  ;;' (boolean? TRUE)  ; => #t
  ;;' (boolean? (c TRUE FALSE))  ; => #f
  ;;' @assert
  ;;' (assert-true (boolean? #t))
  ;;' (assert-true (boolean? #f))
  ;;' (assert-true (boolean? TRUE))
  ;;' (assert-false (boolean? (c TRUE FALSE)))
  (define boolean?
    (lambda (x)
      (if (is.logical x)
        (isTRUE (== (length x) 1))
        #f)))

  ;;' @description Return #t if x is TRUE.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (true? #t)  ; => #t
  ;;' @assert
  ;;' (assert-true (true? #t))
  (define true?
    (lambda (x)
      (isTRUE x)))

  ;;' @description Return #t if x is #f.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (false? #f)  ; => #t
  ;;' @assert
  ;;' (assert-true (false? #f))
  (define false?
    (lambda (x)
      (identical x #f)))

  ;;' @section Function Predicates

  ;;' @description Return #t if x is a function.
  ;;' @param x Value to test
  ;;' @signature (fn? x)
  ;;' @examples
  ;;' (fn? car)  ; => #t
  ;;' @assert
  ;;' (assert-true (fn? car))
  ;;' @seealso callable?, procedure?
  (define fn? is.function)

  ;;' @description Alias for fn?.
  ;;' @param x Value to test
  ;;' @signature (callable? x)
  ;;' @seealso fn?
  (define callable? fn?)

  ;;' @description Alias for fn? - return #t if x is a function.
  ;;' @param x Value to test
  ;;' @seealso fn?, callable?
  (define procedure?
    (lambda (x)
      (fn? x)))

  ;;' @section Environment Predicates

  ;;' @description Test if x is an environment (including R6 objects).
  ;;' @param x Value to test
  ;;' @examples
  ;;' (environment? (baseenv))  ; => #t
  ;;' @assert
  ;;' (assert-true (environment? (baseenv)))
  ;;' @note R6 objects, dicts, and sets are all environments in R, so this predicate returns #t for them.
  (define environment?
    (lambda (x)
      (r-call "is.environment" (list x))))

  ;;' @description Test if x is a Reference Class object.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (is-refclass? (baseenv))  ; => #f
  ;;' @assert
  ;;' (assert-false (is-refclass? (baseenv)))
  (define is-refclass?
    (lambda (x)
      (and (r-call "isS4" (list x))
           (r-call "is" (list x "refClass")))))

  ;;' @section Type Introspection

  ;;' @description Return the type of value (alias for R's typeof).
  ;;' @param x Value to test
  ;;' @signature (type-of x)
  ;;' @examples
  ;;' (type-of 42)  ; => "double"
  ;;' @assert
  ;;' (assert-equal "double" (type-of 42))
  ;;' @seealso class, mode (R functions)
  (define type-of typeof)

  ;;' @section Numeric Type Predicates
  ;;' Arl implements a numeric tower adapted for R's type system:
  ;;'
  ;;' ```
  ;;' number?    (is.numeric OR is.complex)
  ;;' ├─ complex?  (is.complex)
  ;;' └─ real?     (is.numeric AND NOT is.complex)
  ;;'    ├─ ±Inf   (real but not rational)
  ;;'    └─ rational? (real? AND is.finite)
  ;;'       └─ integer? (is.finite AND is.numeric AND x == as.integer(x))
  ;;'          └─ natural? (integer? AND x >= 0)
  ;;'
  ;;' Orthogonal predicates:
  ;;' exact?   (is.integer - storage type)
  ;;' inexact? (number? AND NOT is.integer)
  ;;' ```

  ;;' @description Return #t if x is a real number (includes ±Inf, excludes complex).
  ;;' @param x Value to test
  ;;' @examples
  ;;' (real? 42)            ; => #t
  ;;' (real? 3.14)          ; => #t
  ;;' (real? Inf)           ; => #t
  ;;' (real? (make-rectangular 3 4))  ; => #f
  ;;' @assert
  ;;' (assert-true (real? 42))
  ;;' (assert-true (real? 3.14))
  ;;' (assert-true (real? Inf))
  ;;' (assert-false (real? (make-rectangular 3 4)))
  ;;' @seealso rational?, complex?, number?
  (define real?
    (lambda (x)
      (if (is.numeric x)
        (if (is.complex x) #f #t)
        #f)))

  ;;' @description Return #t if x is a complex number.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (complex? (make-rectangular 3 4))  ; => #t
  ;;' (complex? 42)         ; => #f
  ;;' @assert
  ;;' (assert-true (complex? (make-rectangular 3 4)))
  ;;' (assert-false (complex? 42))
  ;;' @seealso real?, number?, make-rectangular (in `math` module)
  (define complex?
    (lambda (x)
      (is.complex x)))

  ;;' @description Return #t if x is a finite real number (rational in R are finite floats).
  ;;' @param x Value to test
  ;;' @examples
  ;;' (rational? 42)        ; => #t
  ;;' (rational? 3.14)      ; => #t
  ;;' (rational? Inf)       ; => #f (infinities are real but not rational)
  ;;' (rational? NaN)       ; => #f
  ;;' @assert
  ;;' (assert-true (rational? 42))
  ;;' (assert-true (rational? 3.14))
  ;;' (assert-false (rational? Inf))
  ;;' (assert-false (rational? NaN))
  ;;' @note In R, all finite floating-point numbers can be represented as rationals (IEEE 754). R does not have a built-in rational or decimal type like some Schemes.
  ;;' @seealso real?, integer?, number?
  (define rational?
    (lambda (x)
      (if (real? x)
        (if (is.finite x) #t #f)
        #f)))

  ;;' @description Return #t if x is an exact number (integer storage type in R).
  ;;' @param x Value to test
  ;;' @examples
  ;;' (exact? 5L)           ; => #t (integer type)
  ;;' (exact? 5.0)          ; => #f (double type)
  ;;' (exact? (->integer 5)); => #t
  ;;' @assert
  ;;' (assert-true (exact? 5L))
  ;;' (assert-false (exact? 5.0))
  ;;' (assert-true (exact? (->integer 5)))
  ;;' @note In Scheme, exactness is a property of the number. In R (and Arl), exactness corresponds to integer storage type. All integers are exact; all doubles and complex numbers are inexact.
  ;;' @seealso inexact?, exact->inexact (in `conversions` module)
  (define exact?
    (lambda (x)
      (is.integer x)))

  ;;' @description Return #t if x is an inexact number (double or complex in R).
  ;;' @param x Value to test
  ;;' @examples
  ;;' (inexact? 5.0)        ; => #t
  ;;' (inexact? (make-rectangular 3 4))  ; => #t
  ;;' (inexact? 5L)         ; => #f
  ;;' @assert
  ;;' (assert-true (inexact? 5.0))
  ;;' (assert-true (inexact? (make-rectangular 3 4)))
  ;;' (assert-false (inexact? 5L))
  ;;' @seealso exact?, inexact->exact (in `conversions` module)
  (define inexact?
    (lambda (x)
      (if (number? x)
        (not (is.integer x))
        #f)))

  ;;' @description Return #t if x is an integer-valued number.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (integer? 42)         ; => #t
  ;;' (integer? 42.0)       ; => #t (value is integer even if storage is double)
  ;;' (integer? 3.14)       ; => #f
  ;;' (integer? Inf)        ; => #f
  ;;' @assert
  ;;' (assert-true (integer? 42))
  ;;' (assert-true (integer? 42.0))
  ;;' (assert-false (integer? 3.14))
  ;;' (assert-false (integer? Inf))
  ;;' @note Tests for integer VALUE, not storage type. Use `exact?` to test storage type.
  ;;' @seealso natural?, exact?, rational?
  (define integer?
    (lambda (x)
      (if (is.numeric x)
        (if (is.finite x)
          (isTRUE (== x (as.integer x)))
          #f)
        #f)))

  ;;' @description Return #t if x is a natural number (integer >= 0).
  ;;' @param x Value to test
  ;;' @examples
  ;;' (natural? 0)          ; => #t
  ;;' (natural? 42)         ; => #t
  ;;' (natural? -5)         ; => #f
  ;;' (natural? 3.14)       ; => #f
  ;;' @assert
  ;;' (assert-true (natural? 0))
  ;;' (assert-true (natural? 42))
  ;;' (assert-false (natural? -5))
  ;;' (assert-false (natural? 3.14))
  ;;' @seealso integer?, positive?, non-negative?
  (define natural?
    (lambda (x)
      (if (integer? x)
        (isTRUE (>= x 0))
        #f)))

  ;;' @description Return #t if x is finite.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (finite? 42)          ; => #t
  ;;' (finite? Inf)         ; => #f
  ;;' (finite? NaN)         ; => #f
  ;;' (finite? -Inf)        ; => #f
  ;;' @assert
  ;;' (assert-true (finite? 42))
  ;;' (assert-false (finite? Inf))
  ;;' (assert-false (finite? NaN))
  ;;' (assert-false (finite? -Inf))
  ;;' @seealso infinite?, nan?, rational?
  (define finite?
    (lambda (x)
      (isTRUE (is.finite x))))

  ;;' @description Return #t if x is infinite.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (infinite? Inf)       ; => #t
  ;;' (infinite? -Inf)      ; => #t
  ;;' (infinite? 42)        ; => #f
  ;;' (infinite? NaN)       ; => #f
  ;;' @assert
  ;;' (assert-true (infinite? Inf))
  ;;' (assert-true (infinite? -Inf))
  ;;' (assert-false (infinite? 42))
  ;;' (assert-false (infinite? NaN))
  ;;' @seealso finite?, nan?
  (define infinite?
    (lambda (x)
      (isTRUE (is.infinite x))))

  ;;' @description Return #t if x is NaN.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (nan? NaN)            ; => #t
  ;;' (nan? (/ 0 0))        ; => #t (0/0 = NaN)
  ;;' (nan? 42)             ; => #f
  ;;' (nan? Inf)            ; => #f
  ;;' @assert
  ;;' (assert-true (nan? NaN))
  ;;' (assert-true (nan? (/ 0 0)))
  ;;' (assert-false (nan? 42))
  ;;' (assert-false (nan? Inf))
  ;;' @seealso finite?, infinite?
  (define nan?
    (lambda (x)
      (isTRUE (is.nan x))))

  ;;' @section Value Predicates

  ;;' @description Return #t if x is an even number.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (even? 4)             ; => #t
  ;;' (even? 3)             ; => #f
  ;;' (even? 0)             ; => #t
  ;;' @assert
  ;;' (assert-true (even? 4))
  ;;' (assert-false (even? 3))
  ;;' (assert-true (even? 0))
  ;;' @seealso odd?
  (define even?
    (lambda (x)
      (isTRUE (== (%% x 2) 0))))

  ;;' @description Return #t if x is an odd number.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (odd? 3)              ; => #t
  ;;' (odd? 4)              ; => #f
  ;;' @assert
  ;;' (assert-true (odd? 3))
  ;;' (assert-false (odd? 4))
  ;;' @seealso even?
  (define odd?
    (lambda (x)
      (isTRUE (== (%% x 2) 1))))

  ;;' @description Return #t if x is zero.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (zero? 0)             ; => #t
  ;;' (zero? 0.0)           ; => #t
  ;;' (zero? 1)             ; => #f
  ;;' @assert
  ;;' (assert-true (zero? 0))
  ;;' (assert-true (zero? 0.0))
  ;;' (assert-false (zero? 1))
  ;;' @seealso positive?, negative?
  (define zero?
    (lambda (x)
      (isTRUE (== x 0))))

  ;;' @description Return #t if x is greater than zero.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (positive? 5)         ; => #t
  ;;' (positive? 0)         ; => #f
  ;;' (positive? -5)        ; => #f
  ;;' @assert
  ;;' (assert-true (positive? 5))
  ;;' (assert-false (positive? 0))
  ;;' (assert-false (positive? -5))
  ;;' @seealso negative?, non-negative?, zero?
  (define positive?
    (lambda (x)
      (isTRUE (> x 0))))

  ;;' @description Return #t if x is less than zero.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (negative? -5)        ; => #t
  ;;' (negative? 0)         ; => #f
  ;;' (negative? 5)         ; => #f
  ;;' @assert
  ;;' (assert-true (negative? -5))
  ;;' (assert-false (negative? 0))
  ;;' (assert-false (negative? 5))
  ;;' @seealso positive?, non-positive?, zero?
  (define negative?
    (lambda (x)
      (isTRUE (< x 0))))

  ;;' @description Return #t if x is greater than or equal to zero.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (non-negative? 5)     ; => #t
  ;;' (non-negative? 0)     ; => #t
  ;;' (non-negative? -5)    ; => #f
  ;;' @assert
  ;;' (assert-true (non-negative? 5))
  ;;' (assert-true (non-negative? 0))
  ;;' (assert-false (non-negative? -5))
  ;;' @seealso positive?, natural?
  (define non-negative?
    (lambda (x)
      (isTRUE (>= x 0))))

  ;;' @description Return #t if x is less than or equal to zero.
  ;;' @param x Value to test
  ;;' @examples
  ;;' (non-positive? -5)    ; => #t
  ;;' (non-positive? 0)     ; => #t
  ;;' (non-positive? 5)     ; => #f
  ;;' @assert
  ;;' (assert-true (non-positive? -5))
  ;;' (assert-true (non-positive? 0))
  ;;' (assert-false (non-positive? 5))
  ;;' @seealso negative?
  (define non-positive?
    (lambda (x)
      (isTRUE (<= x 0))))
)
