packages feed

phino 0.0.114 → 0.0.115

raw patch · 19 files changed

+393/−64 lines, 19 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Bytes: NfNan :: NonFinite
+ Bytes: NfNinf :: NonFinite
+ Bytes: NfPinf :: NonFinite
+ Bytes: btsToNonFinite :: Bytes -> Maybe NonFinite
+ Bytes: data NonFinite
+ Bytes: instance GHC.Classes.Eq Bytes.NonFinite
+ Bytes: instance GHC.Show.Show Bytes.NonFinite
+ Bytes: nonFiniteBts :: NonFinite -> Bytes
+ Bytes: nonFiniteName :: NonFinite -> Text
+ Bytes: nonFiniteOf :: Text -> Maybe NonFinite
+ Bytes: nonFinites :: [NonFinite]
+ Bytes: unescapeStr :: String -> String
+ CST: EX_NONFINITE :: GLOBAL -> NonFinite -> TAB -> [Argument] -> EXPRESSION
+ CST: [nonfinite] :: EXPRESSION -> NonFinite

Files

README.md view
@@ -34,7 +34,7 @@  ```bash cabal update-cabal install --overwrite-policy=always phino-0.0.113+cabal install --overwrite-policy=always phino-0.0.114 phino --version ``` @@ -520,55 +520,55 @@ === parse/phi ===   warmup:     3 iterations   batches:    10 x 1-  total:      1843412.235 μs-  avg:        184341.224 μs-  min:        170612.548 μs-  max:        215203.225 μs-  std dev:    16818.121 μs+  total:      1543475.232 μs+  avg:        154347.523 μs+  min:        142171.737 μs+  max:        181971.002 μs+  std dev:    14975.901 μs === parse/xmir ===   warmup:     3 iterations   batches:    10 x 1-  total:      7584564.832 μs-  avg:        758456.483 μs-  min:        674321.184 μs-  max:        885832.048 μs-  std dev:    58868.246 μs+  total:      7819406.638 μs+  avg:        781940.664 μs+  min:        699138.670 μs+  max:        933188.047 μs+  std dev:    65608.137 μs === rewrite/normalize ===   warmup:     3 iterations   batches:    10 x 1-  total:      530792.056 μs-  avg:        53079.206 μs-  min:        52059.402 μs-  max:        55045.049 μs-  std dev:    788.817 μs+  total:      651966.123 μs+  avg:        65196.612 μs+  min:        61413.741 μs+  max:        76515.744 μs+  std dev:    4139.047 μs === print/sweet/multiline ===   warmup:     3 iterations   batches:    10 x 1-  total:      3859233.707 μs-  avg:        385923.371 μs-  min:        366896.885 μs-  max:        399481.094 μs-  std dev:    8247.212 μs+  total:      4663282.066 μs+  avg:        466328.207 μs+  min:        449248.604 μs+  max:        488747.559 μs+  std dev:    11022.030 μs === print/sweet/flat ===   warmup:     3 iterations   batches:    10 x 1-  total:      3805981.640 μs-  avg:        380598.164 μs-  min:        363776.755 μs-  max:        397781.223 μs-  std dev:    8200.622 μs+  total:      4595372.700 μs+  avg:        459537.270 μs+  min:        419405.034 μs+  max:        505827.470 μs+  std dev:    27196.223 μs === print/salty/multiline ===   warmup:     3 iterations   batches:    10 x 1-  total:      14154416.526 μs-  avg:        1415441.653 μs-  min:        1380466.817 μs-  max:        1440553.138 μs-  std dev:    15889.438 μs+  total:      14529614.517 μs+  avg:        1452961.452 μs+  min:        1402771.195 μs+  max:        1489996.347 μs+  std dev:    26769.638 μs ```  The results were calculated in [this GHA job][benchmark-gha]-on 2026-09-02 at 14:36,+on 2026-09-03 at 07:49, on Linux with 4 CPUs.  <!-- benchmark_end -->@@ -617,4 +617,4 @@ [jna]: https://github.com/java-native-access/jna [jna-native]: https://github.com/java-native-access/jna/blob/master/src/com/sun/jna/Native.java [jeo]: https://github.com/objectionary/jeo-maven-plugin-[benchmark-gha]: https://github.com/objectionary/phino/actions/runs/33642610207+[benchmark-gha]: https://github.com/objectionary/phino/actions/runs/33729718905
phino.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: phino-version: 0.0.114+version: 0.0.115 license: MIT synopsis: Command-Line Manipulator of 𝜑-Calculus Expressions description: Please see the README on GitHub at <https://github.com/objectionary/phino#readme>
src/Bytes.hs view
@@ -12,6 +12,7 @@   , strToBts   , bytesToBts   , btsToStr+  , unescapeStr   , btsToNum   , btsToUnescapedStr   , btsAnd@@ -22,6 +23,12 @@   , btsSize   , btsSlice   , btsShift+  , nonFinites+  , nonFiniteName+  , nonFiniteBts+  , btsToNonFinite+  , nonFiniteOf+  , NonFinite (..)   ) where @@ -33,6 +40,7 @@ import Data.ByteString.Lazy (unpack) import qualified Data.ByteString.Lazy.UTF8 as U import Data.Char (chr, isDigit, isPrint, ord)+import Data.List (find) import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Word (Word64, Word8)@@ -135,6 +143,58 @@ numToBts :: Double -> Bytes numToBts num = word8ToBytes (unpack (toLazyByteString (word64BE (doubleToWord num)))) +-- The three IEEE-754 doubles that are not finite numbers. None of them has a+-- numeric literal to be written with, so the printer spells each one as a+-- dispatch off the root — 'Φ.nan', 'Φ.pinf', 'Φ.ninf' — and the parser reads+-- those names back into the very bytes they stand for (see #1065)+data NonFinite = NfNan | NfPinf | NfNinf+  deriving (Eq, Show)++-- All the non-finite doubles, in the order they are documented in+nonFinites :: [NonFinite]+nonFinites = [NfNan, NfPinf, NfNinf]++-- The attribute name the value is dispatched on+-- >>> nonFiniteName NfPinf+-- "pinf"+nonFiniteName :: NonFinite -> T.Text+nonFiniteName NfNan = "nan"+nonFiniteName NfPinf = "pinf"+nonFiniteName NfNinf = "ninf"++-- The canonical byte form of a non-finite double. The patterns are spelled out+-- instead of being derived from '0 / 0' and '1 / 0' because the sign bit and+-- the payload of a computed NaN are platform-dependent, while the printer and+-- the parser have to agree on one exact pattern+-- >>> nonFiniteBts NfNan+-- BtMany ["7F","F8","00","00","00","00","00","00"]+nonFiniteBts :: NonFinite -> Bytes+nonFiniteBts NfNan = BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"]+nonFiniteBts NfPinf = BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"]+nonFiniteBts NfNinf = BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"]++-- Which non-finite double the given bytes encode, if they encode one at all.+-- Only the three canonical patterns qualify: a NaN carrying a payload, or the+-- negative quiet NaN, has no name of its own and keeps its byte form, so that+-- printing never drops a bit+-- >>> btsToNonFinite (BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"])+-- Just NfNinf+-- >>> btsToNonFinite (BtMany ["40", "45", "00", "00", "00", "00", "00", "00"])+-- Nothing+-- >>> btsToNonFinite (BtMany ["7F", "F8", "00", "00", "00", "00", "00", "01"])+-- Nothing+btsToNonFinite :: Bytes -> Maybe NonFinite+btsToNonFinite (BtMeta _) = Nothing+btsToNonFinite bts = find (btsEqual bts . nonFiniteBts) nonFinites++-- The non-finite double the given name stands for, if it names one at all+-- >>> nonFiniteOf "ninf"+-- Just NfNinf+-- >>> nonFiniteOf "number"+-- Nothing+nonFiniteOf :: T.Text -> Maybe NonFinite+nonFiniteOf name = find ((== name) . nonFiniteName) nonFinites+ -- >>> strToBts "hello" -- BtMany ["68","65","6C","6C","6F"] -- >>> strToBts "world"@@ -197,6 +257,39 @@         escapeChar c           | isPrint c && c /= '\\' && c /= '"' = [c]           | otherwise = printf "\\x%02x" (ord c)++-- The inverse of the escaping that 'btsToStr' applies, so that a sweet string+-- literal can be turned back into the very bytes it was printed from. A+-- backslash that starts no escape 'btsToStr' can produce is kept as it stands,+-- together with the character behind it+-- >>> unescapeStr "hello"+-- "hello"+-- >>> unescapeStr "h\\\""+-- "h\""+-- >>> unescapeStr "e\\ne"+-- "e\ne"+-- >>> unescapeStr "\\\\"+-- "\\"+-- >>> unescapeStr "\\t"+-- "\t"+-- >>> unescapeStr "\\x01"+-- "\SOH"+unescapeStr :: String -> String+unescapeStr = go+  where+    go :: String -> String+    go "" = ""+    go ('\\' : 'x' : high : low : rest)+      | Just code <- hexPair high low = chr code : go rest+    go ('\\' : escaped : rest)+      | Just unescaped <- lookup escaped escapes = unescaped : go rest+    go (char : rest) = char : go rest+    hexPair :: Char -> Char -> Maybe Int+    hexPair high low = case readHex [high, low] of+      [(code, "")] -> Just code+      _ -> Nothing+    escapes :: [(Char, Char)]+    escapes = [('"', '"'), ('\\', '\\'), ('n', '\n'), ('t', '\t')]  -- >>> btsToUnescapedStr (BtMany ["01", "02"]) -- "\SOH\STX"
src/CST.hs view
@@ -11,7 +11,8 @@ module CST where  import AST-import Bytes (btsToNum, btsToStr)+import Bytes (NonFinite, btsToNonFinite, btsToNum, btsToStr)+import Data.Maybe (isJust) import qualified Data.Text as T import qualified Yaml as Y @@ -174,6 +175,7 @@   | EX_APPLICATION {expr :: EXPRESSION, space :: SPACE, eol :: EOL, tab :: TAB, argument :: APP_ARGUMENT, eol' :: EOL, tab' :: TAB, indent :: Int} -- e(...)   | EX_STRING {str :: String, tab :: TAB, rhos :: [Argument]}   | EX_NUMBER {num :: Either Int Double, tab :: TAB, rhos :: [Argument]}+  | EX_NONFINITE {global :: GLOBAL, nonfinite :: NonFinite, tab :: TAB, rhos :: [Argument]} -- Φ.nan, Φ.pinf and Φ.ninf (see #1065)   | EX_META {meta :: META}   | EX_PHI_MEET {prefix :: Maybe String, idx :: Int, expr :: EXPRESSION}   | EX_PHI_AGAIN {prefix :: Maybe String, idx :: Int, expr :: EXPRESSION}@@ -260,14 +262,17 @@ expressionToCSTFrom :: Int -> Expression -> EXPRESSION expressionToCSTFrom tabs expr = toCST expr (tabs, EOL) --- A number can be rendered with the sweet numeric literal only when it is--- finite. NaN and the infinities have no such literal — and the bare `show`--- tokens (`NaN`, `Infinity`, `-Infinity`) would collide with object/function--- names — so they are kept in their byte form instead.+-- A number can be rendered in sweet form when it is either finite, and so has+-- a numeric literal, or one of the three canonical non-finite doubles, which+-- get the root dispatches `Φ.nan`, `Φ.pinf` and `Φ.ninf` instead (the bare+-- `show` tokens `NaN`, `Infinity` and `-Infinity` would collide with+-- object/function names, hence the dispatch — see #1065). Any other non-finite+-- pattern, such as a NaN carrying a payload, is kept in its byte form so that+-- no bit of it is lost. sweetNumber :: Bytes -> Bool sweetNumber bts = case btsToNum bts of-  Right d -> not (isNaN d || isInfinite d)-  Left _ -> True+  Right dbl | isNaN dbl || isInfinite dbl -> isJust (btsToNonFinite bts)+  _ -> True  -- Whether a data object may be collapsed into its sweet literal form. sweetCollapsible :: Expression -> Bool@@ -341,10 +346,11 @@       withoutLastVoidRho [BiVoid AtRho] = []       withoutLastVoidRho (bd : bds') = bd : withoutLastVoidRho bds'   toCST (DataString bts) (tabs, _) = EX_STRING (btsToStr bts) (TAB tabs) []-  -- NaN and the infinities have no sweet numeric literal (and printing the bare-  -- `show` tokens would collide with object/function names), so they are left in-  -- their byte form `Φ.number(Φ.bytes(⟦ Δ ⤍ … ⟧))` by falling through to the-  -- generic application clause below.+  -- The three canonical non-finite doubles have no sweet numeric literal, so+  -- they become the root dispatches `Φ.nan`, `Φ.pinf` and `Φ.ninf`. Any other+  -- non-finite pattern is left in its byte form `Φ.number(Φ.bytes(⟦ Δ ⤍ … ⟧))`+  -- by falling through to the generic application clause below.+  toCST (DataNumber bts) (tabs, _) | Just nonfinite <- btsToNonFinite bts = EX_NONFINITE Φ nonfinite (TAB tabs) []   toCST (DataNumber bts) (tabs, _) | sweetNumber bts = EX_NUMBER (btsToNum bts) (TAB tabs) []   toCST (ExDispatch ExXi attr) ctx = EX_ATTR (toCST attr ctx)   toCST (ExDispatch expr attr) ctx = EX_DISPATCH (toCST expr ctx) NO_SPACE (toCST attr ctx)@@ -409,9 +415,11 @@         | otherwise = (bds, [])       withoutRhosInPrimitives _ bds = (bds, [])       applicationToPrimitive :: Expression -> Int -> [Argument] -> EXPRESSION-      applicationToPrimitive (DataNumber bts) tabs = EX_NUMBER (btsToNum bts) (TAB tabs)-      applicationToPrimitive (DataString bts) tabs = EX_STRING (btsToStr bts) (TAB tabs)-      applicationToPrimitive _ _ = error "applicationToPrimitive expects DataNumber or DataString"+      applicationToPrimitive (DataNumber bts) tabs rhos = case btsToNonFinite bts of+        Just nonfinite -> EX_NONFINITE Φ nonfinite (TAB tabs) rhos+        Nothing -> EX_NUMBER (btsToNum bts) (TAB tabs) rhos+      applicationToPrimitive (DataString bts) tabs rhos = EX_STRING (btsToStr bts) (TAB tabs) rhos+      applicationToPrimitive _ _ _ = error "applicationToPrimitive expects DataNumber or DataString"       -- Here we unroll nested application sequence into flat structure       -- The returned tuple consists of:       -- 1. deepest start expression
src/Encoding.hs view
@@ -25,6 +25,7 @@   toASCII EX_FORMATION{..} = EX_FORMATION LSB' eol tab (toASCII binding) eol' tab' RSB'   toASCII EX_DISPATCH{..} = EX_DISPATCH (toASCII expr) space (toASCII attr)   toASCII EX_APPLICATION{..} = EX_APPLICATION (toASCII expr) space eol tab (toASCII argument) eol' tab' indent+  toASCII EX_NONFINITE{..} = EX_NONFINITE Q nonfinite tab rhos   toASCII EX_META{meta = META{hd = N, ..}} = EX_META (META EXCL N' rest)   toASCII EX_META{meta = META{hd = K, ..}} = EX_META (META EXCL K' rest)   toASCII EX_META{..} = EX_META (META EXCL E' (rest meta))
src/LaTeX.hs view
@@ -25,6 +25,7 @@   ) where  import AST+import Bytes (nonFiniteName) import CST import Canonizer (canonize, canonizeExpr) import Data.List (intercalate, nub)@@ -267,6 +268,9 @@   toLaTeX EX_PHI_AGAIN{..} = EX_PHI_AGAIN prefix idx (toLaTeX expr)   toLaTeX EX_META{..} = EX_META (toLaTeX meta)   toLaTeX EX_XI{} = EX_XI XI'+  -- A non-finite double is printed as a dispatch off the root, so it becomes+  -- one here too, with its name piped the way any other label is (see #1065)+  toLaTeX EX_NONFINITE{..} = EX_DISPATCH (EX_GLOBAL global) SPACE (toLaTeX (AT_LABEL (nonFiniteName nonfinite)))   toLaTeX EX_BYTES{..} = EX_BYTES (toLaTeX bytes)   toLaTeX expr = expr 
src/Parser.hs view
@@ -22,7 +22,7 @@ where  import AST-import Bytes (numToBts, strToBts)+import Bytes (nonFiniteBts, nonFiniteOf, numToBts, strToBts) import Control.Exception (Exception) import Control.Monad (guard) import Data.Char (isAsciiLower, isDigit)@@ -193,6 +193,26 @@         )     ) +-- An expression head that starts with the root: either one of the three+-- non-finite doubles named off it — `Φ.nan`, `Φ.pinf` and `Φ.ninf`, read back+-- into the very 'DataNumber' the sweet printer collapsed, which keeps+-- print-then-parse idempotent (see #1065) — or the root itself. The label after+-- the root is parsed once, here, so an ordinary dispatch such as `Φ.number`+-- costs no more than it did before the three names existed; an attribute the+-- label parser rejects (ρ, φ, a meta) is left to 'exTail', as is any further+-- dispatch or application+root :: Parser Expression+root = do+  _ <- global+  option ExRoot (try labelled)+  where+    labelled :: Parser Expression+    labelled = do+      _ <- symbol "."+      named <$> label'+    named :: T.Text -> Expression+    named name = maybe (ExDispatch ExRoot (AtLabel name)) (DataNumber . nonFiniteBts) (nonFiniteOf name)+ quotedStr :: Parser String quotedStr = char '"' >> manyTill (choice [escapedChar, noneOf ['\\', '"']]) (char '"')   where@@ -384,7 +404,7 @@ -- head part of expression -- 1. formation -- 2. this--- 3. global+-- 3. global, or an attribute or non-finite double named off it -- 4. termination -- 5. meta expression -- 6. full attribute -> sugar for $.attr@@ -397,9 +417,7 @@     , do         _ <- choice [symbol "$", symbol "ξ"]         return ExXi-    , do-        _ <- global-        return ExRoot+    , root     , do         _ <- choice [symbol "T", symbol "⊥"]         return ExTermination
src/Render.hs view
@@ -8,6 +8,7 @@  module Render where +import Bytes (nonFiniteName) import CST import Data.Text (Text) import qualified Data.Text as T@@ -204,6 +205,7 @@   render EX_APPLICATION{..} = render expr <> render space <> "(" <> render eol <> render tab <> render argument <> render eol' <> render tab' <> ")"   render EX_STRING{..} = "\"" <> render str <> "\""   render EX_NUMBER{..} = either (T.pack . show) (T.pack . show) num+  render EX_NONFINITE{..} = render global <> "." <> nonFiniteName nonfinite   render EX_META{..} = render meta   render EX_PHI_MEET{..} = "\\phinoMeet{" <> maybe "" (\p -> T.pack p <> ":") prefix <> render idx <> "}{ " <> render expr <> " }"   render EX_PHI_AGAIN{..} = "\\phinoAgain{" <> maybe "" (\p -> T.pack p <> ":") prefix <> render idx <> "}"
src/Sugar.hs view
@@ -10,7 +10,7 @@ module Sugar (toSalty, withSugarType, withoutRho, SugarType (..), ToSalty) where  import AST-import Bytes (numToBts, strToBts)+import Bytes (nonFiniteBts, numToBts, strToBts, unescapeStr) import CST import Misc (toDouble) @@ -127,6 +127,7 @@ --  |----------------------------|-----------------------------------------------------| --  | a1 -> a2                   | a1 ↦ $.a2                                           | --  | a -> 42                    | Q.number(Q.bytes([[ D> 40-45-00-00-00-00-00-00 ]])) |+--  | a -> Q.nan                 | Q.number(Q.bytes([[ D> 7F-F8-00-00-00-00-00-00 ]])) | --  | a -> "Hey"                 | Q.number(Q.bytes([[ D> 48-65-79 ]]))                | --  | [[ B ]]                    | [[ B, ^ -> ? ]], if rho is absent in 'B'            | --  | a1(a2, a3, ...) -> [[ B ]] | a1 -> [[ a2 -> ?, a3 -> ?, ..., B ]]                |@@ -176,11 +177,18 @@       (toCST (ExFormation [BiDelta (numToBts (either toDouble id num))]) (indent + 2, EOL))       tab       rhos+  toSalty EX_NONFINITE{nonfinite, tab = tab@TAB{..}, rhos} =+    saltifyPrimitive+      (toCST (BaseObject "number") (indent + 1, EOL))+      (toCST (BaseObject "bytes") (indent + 2, EOL))+      (toCST (ExFormation [BiDelta (nonFiniteBts nonfinite)]) (indent + 2, EOL))+      tab+      rhos   toSalty EX_STRING{str, tab = tab@TAB{..}, rhos} =     saltifyPrimitive       (toCST (BaseObject "string") (indent + 1, EOL))       (toCST (BaseObject "bytes") (indent + 2, EOL))-      (toCST (ExFormation [BiDelta (strToBts str)]) (indent + 2, EOL))+      (toCST (ExFormation [BiDelta (strToBts (unescapeStr str))]) (indent + 2, EOL))       tab       rhos   toSalty EX_PHI_MEET{..} = EX_PHI_MEET prefix idx (toSalty expr)
test/BytesSpec.hs view
@@ -7,7 +7,8 @@  import AST import Bytes-  ( btsAnd+  ( NonFinite (..)+  , btsAnd   , btsConcat   , btsEqual   , btsNot@@ -15,15 +16,22 @@   , btsShift   , btsSize   , btsSlice+  , btsToNonFinite   , btsToNum   , btsToStr   , btsToUnescapedStr   , bytesToBts+  , nonFiniteBts+  , nonFiniteName+  , nonFiniteOf+  , nonFinites   , numToBts   , strToBts+  , unescapeStr   ) import Control.Exception (evaluate) import Control.Monad (forM_)+import Data.Text qualified as T import Test.Hspec (Spec, anyErrorCall, describe, it, shouldBe, shouldSatisfy, shouldThrow)  spec :: Spec@@ -53,6 +61,42 @@       ]       (\(desc, num, predicate) -> it desc (btsToNum (numToBts num) `shouldSatisfy` predicate)) +  describe "nonFiniteBts and btsToNonFinite round trip" $+    forM_+      nonFinites+      (\value -> it (T.unpack (nonFiniteName value)) (btsToNonFinite (nonFiniteBts value) `shouldBe` Just value))++  describe "btsToNonFinite tells the named patterns from every other one" $+    forM_+      [ ("a finite integral value", BtMany ["40", "45", "00", "00", "00", "00", "00", "00"], Nothing)+      , ("a NaN carrying a payload", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "01"], Nothing)+      , ("the negative quiet NaN", BtMany ["FF", "F8", "00", "00", "00", "00", "00", "00"], Nothing)+      , ("a byte array of the wrong size", BtOne "7F", Nothing)+      , ("meta bytes", BtMeta "d", Nothing)+      , ("NaN", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"], Just NfNan)+      , ("positive infinity", BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"], Just NfPinf)+      , ("negative infinity", BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"], Just NfNinf)+      ]+      (\(desc, bts, expected) -> it desc (btsToNonFinite bts `shouldBe` expected))++  describe "nonFiniteOf reads back the name the printer gives" $+    forM_+      [ ("nan", Just NfNan)+      , ("pinf", Just NfPinf)+      , ("ninf", Just NfNinf)+      , ("number", Nothing)+      , ("", Nothing)+      ]+      (\(name, expected) -> it (T.unpack name) (nonFiniteOf name `shouldBe` expected))++  describe "btsToNum on the named non-finite patterns" $+    forM_+      [ ("NaN", NfNan, either (const False) isNaN)+      , ("positive infinity", NfPinf, either (const False) (\num -> isInfinite num && num > 0))+      , ("negative infinity", NfNinf, either (const False) (\num -> isInfinite num && num < 0))+      ]+      (\(desc, value, predicate) -> it desc (btsToNum (nonFiniteBts value) `shouldSatisfy` predicate))+   describe "btsToNum with a byte array that is not 8 bytes long" $     it "errors out" $       evaluate (btsToNum (BtMany ["40", "45"])) `shouldThrow` anyErrorCall@@ -86,6 +130,40 @@       ]       ( \(desc, bts, str) ->           it desc $ btsToStr bts `shouldBe` str+      )++  describe "unescapeStr" $+    forM_+      [ ("empty", "", "")+      , ("nothing to unescape", "hello", "hello")+      , ("double quote", "h\\\"", "h\"")+      , ("backslash", "\\\\", "\\")+      , ("newline", "e\\ne", "e\ne")+      , ("tab", "\\t", "\t")+      , ("hex escape", "\\x01", "\SOH")+      , ("uppercase hex escape", "\\xFF", "\255")+      , ("backslash before an escape", "\\\\n", "\\n")+      , ("unknown escape is kept as it stands", "\\q", "\\q")+      , ("trailing backslash is kept", "a\\", "a\\")+      , ("truncated hex escape is kept", "\\x0", "\\x0")+      ]+      ( \(desc, escaped, unescaped) ->+          it desc $ unescapeStr escaped `shouldBe` unescaped+      )++  describe "btsToStr/unescapeStr round trip" $+    forM_+      [ ("empty", BtEmpty)+      , ("plain word", BtMany ["68", "65", "6C", "6C", "6F"])+      , ("double quote", BtOne "22")+      , ("backslash", BtOne "5C")+      , ("newline", BtOne "0A")+      , ("tab", BtOne "09")+      , ("non-printable", BtMany ["01", "02"])+      , ("text around a newline", BtMany ["65", "0A", "65"])+      ]+      ( \(desc, bts) ->+          it desc $ strToBts (unescapeStr (btsToStr bts)) `shouldBe` bts       )    describe "btsToUnescapedStr" $
test/CLISpec.hs view
@@ -412,6 +412,10 @@       withStdin "[[foo ↦ x]]" $         testCLISucceeded ["rewrite"] ["⟦ foo ↦ ξ.x, ρ ↦ ∅ ⟧"] +    it "keeps the bytes of a string intact while desugaring it" $+      withStdin "⟦ φ ↦ Φ.string(as-bytes ↦ Φ.bytes(data ↦ ⟦ Δ ⤍ 65-0A-65, ρ ↦ ∅ ⟧)), ρ ↦ ∅ ⟧" $+        testCLISucceeded ["rewrite", "--flat"] ["Δ ⤍ 65-0A-65"]+     it "rewrites with single rule" $       withStdin "T(x -> Q.y)" $         testCLISucceeded ["rewrite", "--rule=resources/normalize/dc.yaml"] ["⊥"]
test/CSTSpec.hs view
@@ -10,6 +10,7 @@ module CSTSpec (spec) where  import AST+import Bytes (NonFinite (..)) import CST import Control.Monad (forM_) import Data.Aeson@@ -144,9 +145,11 @@     forM_       [ ("is true for a finite integral value", BtMany ["40", "45", "00", "00", "00", "00", "00", "00"], True)       , ("is true for a finite fractional value", BtMany ["BF", "D0", "00", "00", "00", "00", "00", "00"], True)-      , ("is false for NaN", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"], False)-      , ("is false for positive infinity", BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"], False)-      , ("is false for negative infinity", BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"], False)+      , ("is true for NaN", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"], True)+      , ("is true for positive infinity", BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"], True)+      , ("is true for negative infinity", BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"], True)+      , ("is false for a NaN carrying a payload", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "01"], False)+      , ("is false for the negative quiet NaN", BtMany ["FF", "F8", "00", "00", "00", "00", "00", "00"], False)       ]       (\(desc, bts, expected) -> it desc (sweetNumber bts `shouldBe` expected)) @@ -154,7 +157,7 @@     forM_       [         ( "delegates to sweetNumber for a data number"-        , DataNumber (BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"])+        , DataNumber (BtMany ["7F", "F8", "00", "00", "00", "00", "00", "01"])         , False         )       ,@@ -182,6 +185,24 @@       ]       (\(desc, metaName, expected) -> it desc (exMetaHead metaName `shouldBe` expected)) +  describe "expressionToCST on the non-finite doubles" $+    forM_+      [ ("NaN becomes Φ.nan", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"], NfNan)+      , ("positive infinity becomes Φ.pinf", BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"], NfPinf)+      , ("negative infinity becomes Φ.ninf", BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"], NfNinf)+      ]+      ( \(desc, bts, value) ->+          it desc (expressionToCST (DataNumber bts) `shouldBe` EX_NONFINITE Φ value (TAB 0) [])+      )++  describe "expressionToCST keeps a non-canonical non-finite double in byte form" $+    it "leaves a NaN carrying a payload as an application" $ do+      let payloaded = DataNumber (BtMany ["7F", "F8", "00", "00", "00", "00", "00", "01"])+          isApplication :: EXPRESSION -> Bool+          isApplication EX_APPLICATION{} = True+          isApplication _ = False+      expressionToCST payloaded `shouldSatisfy` isApplication+   describe "expressionToCST on rendering-only and meta nodes" $     forM_       [ ("ExBytes becomes a bare EX_BYTES chain node", ExBytes (BtOne "1F"), EX_BYTES (BT_ONE "1F"))@@ -587,6 +608,7 @@               }           exString = EX_STRING{str = "hi", tab = TAB 0, rhos = []}           exNumber = EX_NUMBER{num = Left 5, tab = TAB 0, rhos = []}+          exNonFinite = EX_NONFINITE{global = Φ, nonfinite = NfNan, tab = TAB 0, rhos = []}           exMeta = EX_META{meta = metaVal}           exPhiMeet = EX_PHI_MEET{prefix = Just "p", idx = 1, expr = exGlobal}           exBytes = EX_BYTES{bytes = BT_ONE "40"}@@ -618,6 +640,10 @@       exNumber.num `shouldBe` Left 5       exNumber.tab `shouldBe` TAB 0       exNumber.rhos `shouldBe` []+      exNonFinite.global `shouldBe` Φ+      exNonFinite.nonfinite `shouldBe` NfNan+      exNonFinite.tab `shouldBe` TAB 0+      exNonFinite.rhos `shouldBe` []       exMeta.meta `shouldBe` metaVal       exPhiMeet.prefix `shouldBe` Just "p"       exPhiMeet.idx `shouldBe` 1@@ -632,6 +658,7 @@       shouldShowAndEqSelf "EX_APPLICATION" exApplication       shouldShowAndEqSelf "EX_STRING" exString       shouldShowAndEqSelf "EX_NUMBER" exNumber+      shouldShowAndEqSelf "EX_NONFINITE" exNonFinite       shouldShowAndEqSelf "EX_META" exMeta       shouldShowAndEqSelf "EX_PHI_MEET" exPhiMeet       shouldShowAndEqSelf "EX_BYTES" exBytes
test/EncodingSpec.hs view
@@ -5,6 +5,7 @@  module EncodingSpec where +import Bytes (NonFinite (..)) import CST import Control.Monad (forM_) import Encoding (Encoding (..), toASCII, withEncoding)@@ -59,6 +60,11 @@         ( "EX_META with any other head becomes E'"         , toASCII (EX_META (META NO_EXCL E "abc"))         , EX_META (META EXCL E' "abc")+        )+      ,+        ( "EX_NONFINITE switches the root to its ASCII spelling"+        , toASCII (EX_NONFINITE Φ NfNan (TAB 1) [])+        , EX_NONFINITE Q NfNan (TAB 1) []         )       ,         ( "EX_PHI_MEET recurses into its expression"
test/LaTeXSpec.hs view
@@ -119,6 +119,11 @@           expressionToLaTeX expr (adjustContext defaultLatexContext) `shouldBe` expected       ) +    it "renders a non-finite double as a piped dispatch off the root" $ do+      nan <- parseExpressionThrows "[[ x -> Q.number(Q.bytes([[ D> 7F-F8-00-00-00-00-00-00 ]])) ]]"+      expressionToLaTeX nan defaultLatexContext+        `shouldBe` "\\begin{phiquation}\n[[ |x| -> Q . |nan| ]]{.}\n\\end{phiquation}"+     it "escapes '@' and '^' in an attribute label, same as '$' and '_'" $ do       let weird = ExFormation [BiTau (AtLabel "a@b^c") ExRoot, BiVoid AtRho]       expressionToLaTeX weird defaultLatexContext
test/LiningSpec.hs view
@@ -5,6 +5,7 @@  module LiningSpec where +import Bytes (NonFinite (..)) import CST import Control.Monad (forM_) import Lining (LineFormat (..), toSingleLine, withLineFormat)@@ -60,6 +61,7 @@         , EX_TERMINATION DEAD         , EX_STRING "hi" (TAB 2) []         , EX_NUMBER (Left 5) (TAB 2) []+        , EX_NONFINITE Φ NfNan (TAB 2) []         , EX_META (META NO_EXCL E "x")         , EX_BYTES BT_EMPTY         ]
test/ParserSpec.hs view
@@ -417,6 +417,24 @@       , ("", Nothing)       ] +  describe "parse the non-finite doubles named off the root" $+    test+      parseExpression+      [ ("Q.nan", Just (DataNumber (BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"])))+      , ("Φ.nan", Just (DataNumber (BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"])))+      , ("Q.pinf", Just (DataNumber (BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"])))+      , ("Φ.pinf", Just (DataNumber (BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"])))+      , ("Q.ninf", Just (DataNumber (BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"])))+      , ("Φ.ninf", Just (DataNumber (BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"])))+      , -- only the exact names are special, everything else stays an ordinary dispatch+        ("Q.number", Just (ExDispatch ExRoot (AtLabel "number")))+      , ("Q.nanny", Just (ExDispatch ExRoot (AtLabel "nanny")))+      , ("Q.x.nan", Just (ExDispatch (ExDispatch ExRoot (AtLabel "x")) (AtLabel "nan")))+      , -- a bare name is still a ξ dispatch, as it always was+        ("nan", Just (ExDispatch ExXi (AtLabel "nan")))+      , ("$.nan", Just (ExDispatch ExXi (AtLabel "nan")))+      ]+   describe "parseExpressionThrows" $     forM_       [ ("returns expression on valid input 'T'", "T", Just ExTermination)
test/PrinterSpec.hs view
@@ -66,11 +66,31 @@     it "meta lambda becomes 𝑓" $       printExpression' (ExFormation [BiLambda (FnMeta "F")]) (SWEET, UNICODE, SINGLELINE, defaultMargin) `shouldBe` "⟦ λ ⤍ 𝑓 ⟧" -  describe "printExpression keeps special double values in byte form so they re-parse" $+  describe "printExpression names the non-finite doubles instead of spelling their bytes" $     forM_-      [ ("NaN", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"])-      , ("positive infinity", BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"])-      , ("negative infinity", BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"])+      [ ("NaN", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "00"], "Q.nan", "Φ.nan")+      , ("positive infinity", BtMany ["7F", "F0", "00", "00", "00", "00", "00", "00"], "Q.pinf", "Φ.pinf")+      , ("negative infinity", BtMany ["FF", "F0", "00", "00", "00", "00", "00", "00"], "Q.ninf", "Φ.ninf")+      ]+      ( \(desc, bts, ascii, unicode) ->+          it desc $ do+            let expr = DataNumber bts+                printed = printExpression' expr (SWEET, ASCII, SINGLELINE, defaultMargin)+            printed `shouldBe` ascii+            printExpression' expr (SWEET, UNICODE, SINGLELINE, defaultMargin) `shouldBe` unicode+            -- the name is read back into the very same number+            parseExpression printed `shouldBe` Right expr+            -- and --salty expands it back into the byte form+            let salty = printExpression' expr (SALTY, ASCII, SINGLELINE, defaultMargin)+            salty `shouldContain` "Q.number("+            salty `shouldContain` "Q.bytes("+            salty `shouldContain` printBytes bts+      )++  describe "printExpression keeps a non-finite double without a name in byte form" $+    forM_+      [ ("a NaN carrying a payload", BtMany ["7F", "F8", "00", "00", "00", "00", "00", "01"])+      , ("the negative quiet NaN", BtMany ["FF", "F8", "00", "00", "00", "00", "00", "00"])       ]       ( \(desc, bts) ->           it desc $ do
test/RenderSpec.hs view
@@ -5,6 +5,7 @@  module RenderSpec (spec) where +import Bytes (NonFinite (..)) import CST import Control.Monad (forM_) import Data.Text qualified as T@@ -215,6 +216,9 @@       , ("EX_STRING", EX_STRING "hi" (TAB 0) [], "\"hi\"")       , ("EX_NUMBER integer", EX_NUMBER (Left 42) (TAB 0) [], "42")       , ("EX_NUMBER double", EX_NUMBER (Right 3.5) (TAB 0) [], "3.5")+      , ("EX_NONFINITE nan", EX_NONFINITE Φ NfNan (TAB 0) [], "Φ.nan")+      , ("EX_NONFINITE pinf", EX_NONFINITE Φ NfPinf (TAB 0) [], "Φ.pinf")+      , ("EX_NONFINITE ninf ascii", EX_NONFINITE Q NfNinf (TAB 0) [], "Q.ninf")       , ("EX_META", EX_META (META NO_EXCL E "x"), "𝑒x")       , ("EX_PHI_MEET without prefix", EX_PHI_MEET Nothing 5 xiExpr, "\\phinoMeet{5}{ ξ }")       , ("EX_PHI_MEET with prefix", EX_PHI_MEET (Just "p") 5 xiExpr, "\\phinoMeet{p:5}{ ξ }")
test/SugarSpec.hs view
@@ -6,6 +6,7 @@ module SugarSpec (spec) where  import AST+import Bytes (NonFinite (..)) import CST import Control.Monad (forM_) import Encoding (Encoding (UNICODE))@@ -218,9 +219,39 @@         , "Φ.number(\n    as-bytes ↦ Φ.bytes(\n      data ↦ ⟦\n        Δ ⤍ 40-45-00-00-00-00-00-00,\n        ρ ↦ ∅\n      ⟧\n    )\n  )(\n    ρ ↦ ξ.y\n  )"         )       ,+        ( "EX_NONFINITE nan expands into the Q.number(Q.bytes(...)) form"+        , EX_NONFINITE Φ NfNan (TAB 1) []+        , "Φ.number(\n    as-bytes ↦ Φ.bytes(\n      data ↦ ⟦\n        Δ ⤍ 7F-F8-00-00-00-00-00-00,\n        ρ ↦ ∅\n      ⟧\n    )\n  )"+        )+      ,+        ( "EX_NONFINITE pinf expands into the Q.number(Q.bytes(...)) form"+        , EX_NONFINITE Φ NfPinf (TAB 1) []+        , "Φ.number(\n    as-bytes ↦ Φ.bytes(\n      data ↦ ⟦\n        Δ ⤍ 7F-F0-00-00-00-00-00-00,\n        ρ ↦ ∅\n      ⟧\n    )\n  )"+        )+      ,+        ( "EX_NONFINITE ninf keeps an extra rho argument carried alongside the primitive"+        , EX_NONFINITE Φ NfNinf (TAB 1) [ArTau AtRho (ExDispatch ExXi (AtLabel "y"))]+        , "Φ.number(\n    as-bytes ↦ Φ.bytes(\n      data ↦ ⟦\n        Δ ⤍ FF-F0-00-00-00-00-00-00,\n        ρ ↦ ∅\n      ⟧\n    )\n  )(\n    ρ ↦ ξ.y\n  )"+        )+      ,         ( "EX_STRING expands into the Q.string(Q.bytes(...)) form"         , EX_STRING "hi" (TAB 1) []         , "Φ.string(\n    as-bytes ↦ Φ.bytes(\n      data ↦ ⟦\n        Δ ⤍ 68-69,\n        ρ ↦ ∅\n      ⟧\n    )\n  )"+        )+      ,+        ( "EX_STRING unescapes a newline instead of taking its escape literally"+        , EX_STRING "e\\ne" (TAB 1) []+        , "Φ.string(\n    as-bytes ↦ Φ.bytes(\n      data ↦ ⟦\n        Δ ⤍ 65-0A-65,\n        ρ ↦ ∅\n      ⟧\n    )\n  )"+        )+      ,+        ( "EX_STRING unescapes a quote and a backslash into single bytes"+        , EX_STRING "\\\"\\\\" (TAB 1) []+        , "Φ.string(\n    as-bytes ↦ Φ.bytes(\n      data ↦ ⟦\n        Δ ⤍ 22-5C,\n        ρ ↦ ∅\n      ⟧\n    )\n  )"+        )+      ,+        ( "EX_STRING unescapes a hex escape back into its byte"+        , EX_STRING "\\x01" (TAB 1) []+        , "Φ.string(\n    as-bytes ↦ Φ.bytes(\n      data ↦ ⟦\n        Δ ⤍ 01-,\n        ρ ↦ ∅\n      ⟧\n    )\n  )"         )       ]       (\(desc, sweetExpr, expected) -> it desc (render (toSalty sweetExpr) `shouldBe` expected))