# Specification: numbers & arithmetic.
(use ../support/harness)

(defspec "numbers / arithmetic"
  ["add"                "6"      "(+ 1 2 3)"]
  ["add zero args"      "0"      "(+)"]
  ["subtract"           "5"      "(- 10 3 2)"]
  ["negate"             "-5"     "(- 5)"]
  ["multiply"           "24"     "(* 2 3 4)"]
  ["multiply zero args" "1"      "(*)"]
  ["divide"             "2"      "(/ 10 5)"]
  ["divide to fraction" "0.5"    "(/ 1 2)"]
  ["inc"                "6"      "(inc 5)"]
  ["dec"                "4"      "(dec 5)"]
  ["quot"               "3"      "(quot 10 3)"]
  ["rem"                "1"      "(rem 10 3)"]
  ["mod"                "2"      "(mod -1 3)"]
  ["rem negative"       "-1"     "(rem -1 3)"]
  ["max"                "9"      "(max 3 9 1)"]
  ["min"                "1"      "(min 3 9 1)"]
  ["abs"                "5"      "(abs -5)"]
  ["promoting + alias"  "3"      "(+' 1 2)"]
  ["inc' alias"         "6"      "(inc' 5)"])

(defspec "numbers / comparison"
  ["less than"          "true"   "(< 1 2 3)"]
  ["less than false"    "false"  "(< 1 3 2)"]
  ["greater than"       "true"   "(> 3 2 1)"]
  ["<="                 "true"   "(<= 1 1 2)"]
  [">="                 "true"   "(>= 3 3 2)"]
  ["= numbers"          "true"   "(= 2 2)"]
  ["= different"        "false"  "(= 2 3)"]
  ["== numeric"         "true"   "(== 2 2)"]
  ["not="               "true"   "(not= 1 2)"]
  ["compare less"       "-1"     "(compare 1 2)"]
  ["compare equal"      "0"      "(compare 1 1)"]
  ["compare greater"    "1"      "(compare 2 1)"])

(defspec "numbers / predicates"
  ["zero?"              "true"   "(zero? 0)"]
  ["pos?"               "true"   "(pos? 5)"]
  ["neg?"               "true"   "(neg? -5)"]
  ["even?"              "true"   "(even? 4)"]
  ["odd?"               "true"   "(odd? 3)"]
  ["number?"            "true"   "(number? 5)"]
  ["number? false"      "false"  "(number? :a)"]
  ["int?"               "true"   "(int? 5)"]
  ["pos-int?"           "true"   "(pos-int? 5)"]
  ["neg-int?"           "true"   "(neg-int? -5)"]
  ["nat-int? zero"      "true"   "(nat-int? 0)"]
  ["nat-int? neg"       "false"  "(nat-int? -1)"]
  ["ratio? false"       "false"  "(ratio? 5)"])

# Symbolic float values and float/double predicates. NOTE: Janet represents
# integers and integer-valued doubles identically, so (float? 1.0) is false
# (1.0 is indistinguishable from 1) — a documented divergence. Fractional and
# non-finite values ARE recognized as floats.
(defspec "numbers / floats & symbolic values"
  ["read ##Inf"         "true"   "(= ##Inf (/ 1.0 0.0))"]
  ["read ##-Inf"        "true"   "(< ##-Inf 0)"]
  ["##NaN not= itself"  "true"   "(not (== ##NaN ##NaN))"]
  ["float? fractional"  "true"   "(float? 1.5)"]
  ["double? fractional" "true"   "(double? 0.25)"]
  ["float? integer"     "false"  "(float? 3)"]
  ["float? ##Inf"       "true"   "(float? ##Inf)"]
  ["double? ##NaN"      "true"   "(double? ##NaN)"]
  ["infinite? ##Inf"    "true"   "(infinite? ##Inf)"]
  ["infinite? ##-Inf"   "true"   "(infinite? ##-Inf)"]
  ["infinite? finite"   "false"  "(infinite? 1.5)"]
  ["NaN? ##NaN"         "true"   "(NaN? ##NaN)"]
  ["NaN? number"        "false"  "(NaN? 1.0)"]
  ["int? ##Inf false"   "false"  "(int? ##Inf)"]
  ["pos-int? ##Inf"     "false"  "(pos-int? ##Inf)"])

# Numeric literal syntaxes. Jolt has no true bignum/ratio/bigdec types, so the
# N (bigint) and M (bigdec) suffixes read as the plain number, ratios as the
# double quotient; radix integers (NrDDD) are parsed by base.
(defspec "numbers / literal syntax"
  ["bigint suffix N"    "42"     "42N"]
  ["bigint zero"        "0"      "0N"]
  ["bigdec suffix M"    "1.5"    "1.5M"]
  ["bigdec int M"       "0"      "0.0M"]
  ["ratio -> double"    "0.5"    "1/2"]
  ["ratio 3/4"          "0.75"   "3/4"]
  ["neg ratio"          "-0.5"   "-1/2"]
  ["radix binary"       "10"     "2r1010"]
  ["radix hex-ish"      "255"    "16rFF"]
  ["radix base36"       "35"     "36rZ"]
  ["hex"                "255"    "0xFF"]
  ["exponent"           "1000.0" "1e3"]
  ["exponent neg"       "0.015"  "1.5e-2"])

# Strictness: numeric ops reject non-numbers like Clojure; the integer
# predicates reject non-integers; quot/rem/mod reject zero/non-finite.
(defspec "numbers / strictness (throws like Clojure)"
  ["odd? nil"           :throws  "(odd? nil)"]
  ["odd? fractional"    :throws  "(odd? 1.5)"]
  ["even? inf"          :throws  "(even? ##Inf)"]
  ["zero? nil"          :throws  "(zero? nil)"]
  ["pos? false"         :throws  "(pos? false)"]
  ["neg? keyword"       :throws  "(neg? :a)"]
  ["< nil"              :throws  "(< nil 1)"]
  ["> with nil"         :throws  "(> 1 nil)"]
  ["max non-number"     :throws  "(max 1 nil)"]
  ["quot by zero"       :throws  "(quot 10 0)"]
  ["quot inf"           :throws  "(quot ##Inf 1)"]
  ["< arity-1 any"      "true"   "(< :anything)"]
  ["odd? ok"            "true"   "(odd? 3)"]
  ["< ok"               "true"   "(< 1 2 3)"]
  ["quot ok"            "3"      "(quot 10 3)"])

(defspec "numbers / printing of inf & nan"
  ["str Infinity"       "\"Infinity\""  "(str ##Inf)"]
  ["str -Infinity"      "\"-Infinity\"" "(str ##-Inf)"]
  ["str NaN"            "\"NaN\""       "(str ##NaN)"]
  ["pr-str Infinity"    "\"Infinity\""  "(pr-str ##Inf)"]
  ["inf inside coll"    "\"[Infinity]\"" "(str [##Inf])"])

(defspec "numbers / bit-ops & math"
  ["bit-and"            "4"      "(bit-and 12 6)"]
  ["bit-or"             "14"     "(bit-or 12 6)"]
  ["bit-xor"            "10"     "(bit-xor 12 6)"]
  ["bit-shift-left"     "8"      "(bit-shift-left 1 3)"]
  ["bit-shift-right"    "2"      "(bit-shift-right 8 2)"]
  ["bit-set"            "8"      "(bit-set 0 3)"]
  ["bit-clear"          "13"     "(bit-clear 15 1)"]
  ["bit-test true"      "true"   "(bit-test 4 2)"]
  ["bigint 64-bit"      "\"9000000000\"" "(str (bigint 9000000000))"])

(defspec "numbers / random (invariants — non-deterministic)"
  ["rand-int in range"  "true" "(let [r (rand-int 5)] (and (integer? r) (>= r 0) (< r 5)))"]
  ["rand-int zero"      "0"    "(rand-int 1)"]
  ["rand in [0,1)"      "true" "(let [r (rand)] (and (>= r 0) (< r 1)))"]
  ["rand n in [0,n)"    "true" "(let [r (rand 10)] (and (>= r 0) (< r 10)))"]
  ["rand-nth member"    "true" "(contains? #{:a :b :c} (rand-nth [:a :b :c]))"]
  ["rand-nth single"    ":x"   "(rand-nth [:x])"])

# Clojure 1.11 string->scalar parsers: nil on malformed, throw on non-string.
(defspec "numbers / parse fns (1.11)"
  ["parse-long"          "42"     "(parse-long \"42\")"]
  ["parse-long negative" "-7"     "(parse-long \"-7\")"]
  ["parse-long plus"     "7"      "(parse-long \"+7\")"]
  ["parse-long float nil" "nil"   "(parse-long \"1.5\")"]
  ["parse-long hex nil"  "nil"    "(parse-long \"0x10\")"]
  ["parse-long empty nil" "nil"   "(parse-long \"\")"]
  ["parse-long junk nil" "nil"    "(parse-long \"12ab\")"]
  ["parse-long throws"   :throws  "(parse-long 42)"]
  ["parse-double"        "1.5"    "(parse-double \"1.5\")"]
  ["parse-double int"    "4.0"    "(parse-double \"4\")"]
  ["parse-double sci"    "1500.0" "(parse-double \"1.5e3\")"]
  ["parse-double neg"    "-0.5"   "(parse-double \"-0.5\")"]
  ["parse-double junk"   "nil"    "(parse-double \"abc\")"]
  ["parse-double trail"  "nil"    "(parse-double \"1.5x\")"]
  ["parse-double throws" :throws  "(parse-double :k)"]
  ["parse-boolean true"  "true"   "(parse-boolean \"true\")"]
  ["parse-boolean false" "false"  "(parse-boolean \"false\")"]
  ["parse-boolean case"  "nil"    "(parse-boolean \"True\")"]
  ["parse-boolean junk"  "nil"    "(parse-boolean \"yes\")"]
  ["parse-boolean throws" :throws "(parse-boolean true)"])

# Jolt numbers don't overflow, so the auto-promoting (') and unchecked
# variants are aliases of the checked ops (overlay defs, core/20-coll.clj).
(defspec "numbers / promoting & unchecked aliases"
  ["+'"                     "3"   "(+' 1 2)"]
  ["-'"                     "3"   "(-' 5 2)"]
  ["*'"                     "12"  "(*' 3 4)"]
  ["inc'"                   "6"   "(inc' 5)"]
  ["dec'"                   "4"   "(dec' 5)"]
  ["unchecked-add"          "5"   "(unchecked-add 2 3)"]
  ["unchecked-add-int"      "5"   "(unchecked-add-int 2 3)"]
  ["unchecked-subtract"     "3"   "(unchecked-subtract 5 2)"]
  ["unchecked-subtract-int" "3"   "(unchecked-subtract-int 5 2)"]
  ["unchecked-multiply"     "12"  "(unchecked-multiply 3 4)"]
  ["unchecked-multiply-int" "12"  "(unchecked-multiply-int 3 4)"]
  ["unchecked-negate"       "-5"  "(unchecked-negate 5)"]
  ["unchecked-negate-int"   "-5"  "(unchecked-negate-int 5)"]
  ["unchecked-inc"          "2"   "(unchecked-inc 1)"]
  ["unchecked-inc-int"      "2"   "(unchecked-inc-int 1)"]
  ["unchecked-dec"          "0"   "(unchecked-dec 1)"]
  ["unchecked-dec-int"      "0"   "(unchecked-dec-int 1)"]
  ["unchecked-divide-int"   "3"   "(unchecked-divide-int 7 2)"]
  ["unchecked-divide-int negative truncates toward zero" "-3" "(unchecked-divide-int -7 2)"]
  ["unchecked-divide-int by zero throws" :throws "(unchecked-divide-int 1 0)"]
  ["unchecked-remainder-int" "1"  "(unchecked-remainder-int 7 2)"]
  ["unchecked-remainder-int negative" "-1" "(unchecked-remainder-int -7 2)"]
  ["unchecked-int truncates" "3"  "(unchecked-int 3.7)"]
  ["unchecked-int negative"  "-3" "(unchecked-int -3.7)"]
  ["unchecked-long"          "3"  "(unchecked-long 3.7)"]
  ["int? on integer"        "true"  "(int? 5)"]
  ["int? on double"         "false" "(int? 5.5)"]
  ["int? on non-number"     "false" "(int? \"5\")"]
  ["num passes a number through" "5"   "(num 5)"]
  ["num on a double"        "5.5" "(num 5.5)"]
  ["num throws on non-number" :throws "(num \"x\")"])

# == is numeric equality: single arg is trivially true (Clojure's 1-arity
# never inspects the value); 2+ args must be numbers.
(defspec "numbers / == numeric equality"
  ["== single arg"          "true"  "(== :a)"]
  ["== equal"               "true"  "(== 2 2)"]
  ["== unequal"             "false" "(== 2 3)"]
  ["== chained"             "true"  "(== 2 2 2)"]
  ["== chained unequal"     "false" "(== 2 2 3)"]
  ["== int and double"      "true"  "(== 1 1.0)"]
  ["== throws on non-number" :throws "(== 1 :a)"]
  ["== throws on two keywords" :throws "(== :a :a)"])

# Variadic bit ops (canonical Clojure arities: binary host op folded over the
# rest). Shift/clear/set/flip/test stay 2-arg, as in Clojure.
(defspec "numbers / variadic bit ops"
  ["bit-and 2"        "4"  "(bit-and 12 6)"]
  ["bit-and 3"        "4"  "(bit-and 12 6 7)"]
  ["bit-and 4"        "0"  "(bit-and 12 6 7 3)"]
  ["bit-or 3"         "7"  "(bit-or 1 2 4)"]
  ["bit-xor 3"        "7"  "(bit-xor 1 2 4)"]
  ["bit-xor folds left" "1" "(bit-xor 5 6 2)"]
  ["bit-and-not 2"    "8"  "(bit-and-not 12 6)"]
  ["bit-and-not 3"    "8"  "(bit-and-not 12 6 3)"]
  ["bit-and single arg throws" :throws "(bit-and 5)"]
  ["bit-or keeps binary fast path" "3" "(bit-or 1 2)"])
