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 +33/−33
- phino.cabal +1/−1
- src/Bytes.hs +93/−0
- src/CST.hs +22/−14
- src/Encoding.hs +1/−0
- src/LaTeX.hs +4/−0
- src/Parser.hs +23/−5
- src/Render.hs +2/−0
- src/Sugar.hs +10/−2
- test/BytesSpec.hs +79/−1
- test/CLISpec.hs +4/−0
- test/CSTSpec.hs +31/−4
- test/EncodingSpec.hs +6/−0
- test/LaTeXSpec.hs +5/−0
- test/LiningSpec.hs +2/−0
- test/ParserSpec.hs +18/−0
- test/PrinterSpec.hs +24/−4
- test/RenderSpec.hs +4/−0
- test/SugarSpec.hs +31/−0
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))