packages feed

phino 0.0.127 → 0.0.128

raw patch · 13 files changed

+172/−85 lines, 13 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- CLI.Types: OptsMatch :: LogLevel -> Int -> SugarType -> LineFormat -> Maybe String -> Maybe String -> Maybe FilePath -> OptsMatch
+ CLI.Types: OptsMatch :: LogLevel -> Int -> SugarType -> LineFormat -> Maybe String -> Maybe String -> Maybe FilePath -> Int -> OptsMatch
- CLI.Types: OptsMerge :: LogLevel -> Int -> IOFormat -> IOFormat -> SugarType -> LineFormat -> Bool -> Bool -> Int -> Maybe FilePath -> [FilePath] -> OptsMerge
+ CLI.Types: OptsMerge :: LogLevel -> Int -> IOFormat -> IOFormat -> SugarType -> LineFormat -> Bool -> Bool -> Int -> Maybe FilePath -> [FilePath] -> Int -> OptsMerge
- CLI.Types: [_seed] :: OptsRewrite -> Int
+ CLI.Types: [_seed] :: OptsMatch -> Int

Files

README.md view
@@ -34,7 +34,7 @@  ```bash cabal update-cabal install --overwrite-policy=always phino-0.0.121+cabal install --overwrite-policy=always phino-0.0.127 phino --version ``` @@ -900,55 +900,55 @@ === parse/phi ===   warmup:     3 iterations   batches:    10 x 1-  total:      1775922.415 μs-  avg:        177592.242 μs-  min:        161735.069 μs-  max:        206519.406 μs-  std dev:    15129.161 μs+  total:      1796596.921 μs+  avg:        179659.692 μs+  min:        164919.877 μs+  max:        207942.466 μs+  std dev:    16799.590 μs === parse/xmir ===   warmup:     3 iterations   batches:    10 x 1-  total:      7571386.164 μs-  avg:        757138.616 μs-  min:        691286.480 μs-  max:        813614.260 μs-  std dev:    32034.618 μs+  total:      7612275.214 μs+  avg:        761227.521 μs+  min:        709200.274 μs+  max:        813312.545 μs+  std dev:    30787.560 μs === rewrite/normalize ===   warmup:     3 iterations   batches:    10 x 1-  total:      579674.000 μs-  avg:        57967.400 μs-  min:        56388.139 μs-  max:        61829.190 μs-  std dev:    1703.063 μs+  total:      641726.516 μs+  avg:        64172.652 μs+  min:        59940.799 μs+  max:        76191.091 μs+  std dev:    4666.243 μs === print/sweet/multiline ===   warmup:     3 iterations   batches:    10 x 1-  total:      3876263.188 μs-  avg:        387626.319 μs-  min:        373639.628 μs-  max:        405513.521 μs-  std dev:    10018.938 μs+  total:      4232626.830 μs+  avg:        423262.683 μs+  min:        402756.415 μs+  max:        452617.626 μs+  std dev:    15405.204 μs === print/sweet/flat ===   warmup:     3 iterations   batches:    10 x 1-  total:      3789406.984 μs-  avg:        378940.698 μs-  min:        367216.617 μs-  max:        395082.263 μs-  std dev:    8761.575 μs+  total:      4143560.984 μs+  avg:        414356.098 μs+  min:        398710.605 μs+  max:        431760.505 μs+  std dev:    10740.809 μs === print/salty/multiline ===   warmup:     3 iterations   batches:    10 x 1-  total:      13982361.998 μs-  avg:        1398236.200 μs-  min:        1366361.442 μs-  max:        1433666.397 μs-  std dev:    17717.682 μs+  total:      14017459.280 μs+  avg:        1401745.928 μs+  min:        1380342.970 μs+  max:        1418756.423 μs+  std dev:    14496.070 μs ```  The results were calculated in [this GHA job][benchmark-gha]-on 2026-09-09 at 15:41,+on 2026-09-11 at 08:17, on Linux with 4 CPUs.  <!-- benchmark_end -->@@ -997,4 +997,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/34371511775+[benchmark-gha]: https://github.com/objectionary/phino/actions/runs/34577695934
phino.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: phino-version: 0.0.127+version: 0.0.128 license: MIT synopsis: Command-Line Manipulator of 𝜑-Calculus Expressions description: Please see the README on GitHub at <https://github.com/objectionary/phino#readme>
src/Atoms.hs view
@@ -506,35 +506,37 @@     -- what the attribute carries, as 'ask' does, or to hand the node over as     -- it is (#1165).     referenced :: Int -> Int -> T.Text -> Bool -> IO ()-    referenced minted req name doReduce = do-      spoken' <- describe-      case spoken' of-        Left failure -> throwIO (AtomMute func described failure)-        Right value -> do-          logDebug (printf "Atom '%s' asks phino for '%s' of request %d%s as question %d" (T.unpack func) (T.unpack name) req (if doReduce then ", reduced," else ", as it is," :: String) minted)-          answer <- if doReduce then reduce value else pure value-          said (lined (object ["id" .= minted, "𝑛" .= spelled answer]))+    referenced minted req attrName doReduce = case channel of+      Closed -> throwIO (AtomMute func described "it asks phino for an attribute of a previous request, while its stdin is closed, since its entry does not say 'serve'")+      Open -> do+        spoken' <- describe+        case spoken' of+          Left failure -> throwIO (AtomMute func described failure)+          Right value -> do+            logDebug (printf "Atom '%s' asks phino for '%s' of request %d%s as question %d" (T.unpack func) (T.unpack attrName) req (if doReduce then ", reduced," else ", as it is," :: String) minted)+            answer <- if doReduce then reduce value else pure value+            said (lined (object ["id" .= minted, "𝑛" .= spelled answer]))       where         described :: String-        described = printf "{'of':%d,'attr':'%s'}" req (T.unpack name)+        described = printf "{'of':%d,'attr':'%s'}" req (T.unpack attrName)         describe :: IO (Either String Expression)         describe = do           forms <- readIORef _forms           pure $ case IM.lookup req forms of-            Nothing -> Left (printf "there is no in-flight request %d to take '%s' from" req (T.unpack name))-            Just form' -> case attributeValue name form' of-              Nothing -> Left (printf "the receiver of request %d carries no attribute '%s'" req (T.unpack name))+            Nothing -> Left (printf "there is no in-flight request %d to take '%s' from" req (T.unpack attrName))+            Just form' -> case attributeValue attrName form' of+              Nothing -> Left (printf "the receiver of request %d carries no attribute '%s'" req (T.unpack attrName))               Just value -> Right value-    attributeValue :: T.Text -> Expression -> Maybe Expression-    attributeValue name (ExFormation bds) = go bds-      where-        go :: [Binding] -> Maybe Expression-        go [] = Nothing-        go (BiTau attr value : rest)-          | T.pack (printAttribute attr) == name = Just value-          | otherwise = go rest-        go (_ : rest) = go rest-    attributeValue _ _ = Nothing+        attributeValue :: T.Text -> Expression -> Maybe Expression+        attributeValue name (ExFormation bds) = go bds+          where+            go :: [Binding] -> Maybe Expression+            go [] = Nothing+            go (BiTau attr value : rest)+              | T.pack (printAttribute attr) == name = Just value+              | otherwise = go rest+            go (_ : rest) = go rest+        attributeValue _ _ = Nothing     -- Reduce the 𝜑-expression the program asks about and say it back under     -- '𝑛', with the 'id' the question minted. A program started for the fire     -- has nothing to be answered over, since phino closed its stdin behind the
src/Bytes.hs view
@@ -419,9 +419,12 @@ -- BtMany ["00","00"] btsShift :: Int -> Bytes -> Bytes btsShift bits bts+  | magnitude >= toInteger size * 8 = word8ToBytes (replicate size 0)   | bits < 0 = word8ToBytes (map leftwards indices)   | otherwise = word8ToBytes (map rightwards indices)   where+    magnitude :: Integer+    magnitude = abs (toInteger bits)     octets :: [Word8]     octets = btsToWord8 bts     size :: Int@@ -429,9 +432,9 @@     indices :: [Int]     indices = [0 .. size - 1]     modulo :: Int-    modulo = abs bits `mod` 8+    modulo = fromInteger (magnitude `mod` 8)     offset :: Int-    offset = abs bits `div` 8+    offset = fromInteger (magnitude `div` 8)     octet :: Int -> Word8     octet index = octets !! index     rightwards :: Int -> Word8
src/CLI/Parsers.hs view
@@ -464,6 +464,7 @@             <*> optMargin             <*> optTarget             <*> many (argument str (metavar "[FILE]" <> help "Paths to input files"))+            <*> optSeed         )  matchParser :: Parser Command@@ -477,6 +478,7 @@             <*> optional (strOption (long "pattern" <> metavar "EXPRESSION" <> help "Pattern expression to match against"))             <*> optional (strOption (long "when" <> metavar "CONDITION" <> help "Predicate for matched substitutions"))             <*> argInputFile+            <*> optSeed         )  commandParser :: Parser Command
src/CLI/Runners.hs view
@@ -328,6 +328,7 @@ runMerge OptsMerge{..} = do   validateOpts   inputs' <- traverse (readInput . Just) _inputs+  setStdGen (mkStdGen _seed)   exprs <- traverse (`parseInput` _inputFormat) inputs'   expr <- merge exprs   validateXmirTopLevel _outputFormat expr@@ -364,6 +365,7 @@  runMatch :: OptsMatch -> IO () runMatch OptsMatch{..} = do+  setStdGen (mkStdGen _seed)   input <- readInput _inputFile   expr <- parseInput input PHI   if isNothing _pattern
src/CLI/Types.hs view
@@ -228,6 +228,7 @@   , _margin :: Int   , _targetFile :: Maybe FilePath   , _inputs :: [FilePath]+  , _seed :: Int   }  data OptsMatch = OptsMatch@@ -238,4 +239,5 @@   , _pattern :: Maybe String   , _when :: Maybe String   , _inputFile :: Maybe FilePath+  , _seed :: Int   }
src/Dataize.hs view
@@ -319,18 +319,18 @@ -- registry allows. Every entry is charged to the '--max-steps' budget, which -- is what bounds the walk. deepened :: Expression -> Expression -> State -> DataizeContext -> IO (Expression, State)-deepened expr univ = go ExXi expr+deepened expr univ = go Nothing ExXi expr   where     -- A term as it was written, together with what its free ξ stands for: the     -- formation the walk entered it from, without the binding it came from,     -- exactly the context the 'dot' rule hands a dispatched body. At the top     -- there is no such formation, so ξ stands for itself and contextualization     -- leaves the term alone.-    go :: Expression -> Expression -> State -> DataizeContext -> IO (Expression, State)-    go context term state' caller = do+    go :: Maybe Attribute -> Expression -> Expression -> State -> DataizeContext -> IO (Expression, State)+    go dispatched context term state' caller = do       ctx' <- deeper caller       (walked, walkedState) <- parts context term state' caller-      answer <- fired (contextualize walked context) univ walkedState ctx'+      answer <- fired dispatched (contextualize walked context) univ walkedState ctx'       maybe (pure (walked, walkedState)) pure answer     -- The parts of a term nothing fired on, walked one by one and put back     -- where they were, so the term keeps the shape it was written in.@@ -339,10 +339,10 @@       (entered, state'') <- bindings bds bds state' caller       pure (ExFormation entered, state'')     parts context (ExDispatch target attr) state' caller = do-      (entered, state'') <- go context target state' caller+      (entered, state'') <- go (Just attr) context target state' caller       pure (ExDispatch entered attr, state'')     parts context (ExApplication target arg) state' caller = do-      (entered, state'') <- go context target state' caller+      (entered, state'') <- go Nothing context target state' caller       (applied, state''') <- argument context arg state'' caller       pure (ExApplication entered applied, state''')     parts _ term state' _ = pure (term, state')@@ -354,7 +354,7 @@     bindings _ [] state' _ = pure ([], state')     bindings whole (BiTau attr body : rest) state' caller       | attr /= AtRho = do-          (entered, state'') <- go (scope attr whole) body state' caller+          (entered, state'') <- go Nothing (scope attr whole) body state' caller           (others, state''') <- bindings whole rest state'' caller           pure (BiTau attr entered : others, state''')     bindings whole (bd : rest) state' caller = do@@ -373,10 +373,10 @@     -- applies is walked by the caller and the argument it binds is walked here.     argument :: Expression -> Argument -> State -> DataizeContext -> IO (Argument, State)     argument context (ArTau attr arg) state' caller = do-      (entered, state'') <- go context arg state' caller+      (entered, state'') <- go Nothing context arg state' caller       pure (ArTau attr entered, state'')     argument context (ArAlpha alpha arg) state' caller = do-      (entered, state'') <- go context arg state' caller+      (entered, state'') <- go Nothing context arg state' caller       pure (ArAlpha alpha entered, state'')  -- Ask 𝕄 about a term and fire the λ of the formation it reaches, as long as@@ -391,16 +391,31 @@ -- compute stays as it was written with or without '_partial'; an atom that -- cannot fire deeper on the spine still fails the run, exactly as it does -- under 𝕄 alone, and '_partial' parks it. A formation still waiting for its--- arguments is left alone too (see 'saturated').-fired :: Expression -> Expression -> State -> DataizeContext -> IO (Maybe (Expression, State))-fired term univ state caller = do+-- arguments is left alone too (see 'saturated'). A term standing as the target+-- of a dispatch is where 'ml' has its say: the λ is fired only where the+-- dispatched attribute is none of the formation's own (see 'demanded').+fired :: Maybe Attribute -> Expression -> Expression -> State -> DataizeContext -> IO (Maybe (Expression, State))+fired dispatched term univ state caller = do   ctx <- deeper caller   morphed <- try (reduced ctx)   case morphed of-    Right (ExFormation bds, state') -> maybe (pure Nothing) (evaluated ctx state') (saturated bds)+    Right (ExFormation bds, state')+      | demanded bds -> maybe (pure Nothing) (evaluated ctx state') (saturated bds)     Right _ -> pure Nothing     Left failure -> parked failure   where+    -- Whether the dispatch the term stands under demands the λ of the formation+    -- 𝕄 reached. 'ml' fires that λ only where the dispatched attribute is none+    -- of the formation's own, since 'dot' resolves the dispatch before 'ml' is+    -- ever reached, and a walk firing it first answers a formation the dispatch+    -- no longer fits (#1187). A term standing anywhere else is demanded by+    -- nothing and the walk fires what 'mf' left bare, as it always has.+    demanded :: [Binding] -> Bool+    demanded bds = not (any bound bds)+      where+        bound :: Binding -> Bool+        bound (BiTau attr _) = Just attr == dispatched+        bound _ = False     -- 𝕄 takes normal forms only and a term taken from the program as it was     -- written is not necessarily one, so it is normalized against the universe     -- first, exactly as '--inside' normalizes what it is handed. Both chains@@ -421,7 +436,7 @@       Just registered -> do         answer <- fireAtom func registered self univ (reduction univ ctx)         ctx._saveEval (Evaluation func self (Just answer))-        again <- fired answer univ state' ctx+        again <- fired dispatched answer univ state' ctx         pure (Just (fromMaybe (answer, state') again))     parked :: DataizeException -> IO (Maybe a)     parked (Stuck _) | caller._partial = pure Nothing
src/XMIR.hs view
@@ -26,6 +26,7 @@ import Control.Exception (Exception (displayException), throwIO) import Control.Monad (unless) import Data.Bifunctor (bimap)+import Data.Char (isAsciiLower, isDigit) import Data.Foldable (foldlM) import Data.List (intercalate) import qualified Data.Map as M@@ -157,9 +158,7 @@ formationBinding :: Binding -> XmirContext -> IO (Maybe Node) formationBinding (BiTau (AtLabel label) expr) ctx = Just <$> namedBinding (T.unpack label) expr ctx formationBinding (BiTau AtRho expr) ctx = Just <$> namedBinding (show AtRho) expr ctx-formationBinding (BiTau AtPhi expr) ctx = do-  (base, children) <- expression expr ctx-  pure (Just (object [("name", show AtPhi), ("base", base)] children))+formationBinding (BiTau AtPhi expr) ctx = Just <$> namedBinding (show AtPhi) expr ctx formationBinding (BiDelta bytes) _ = pure (Just (NodeContent (T.pack (printBytes bytes)))) formationBinding (BiLambda (Function name)) _ = pure (Just (object [("name", show AtLambda)] [NodeContent name])) formationBinding (BiVoid AtRho) _ = pure Nothing@@ -507,7 +506,12 @@     lambdaFunction :: IO T.Text     lambdaFunction       | hasText cur = T.strip . T.pack <$> getText cur-      | otherwise = pure (T.pack (intercalate "_" ("L" : reverse fqn)))+      | otherwise = pure (T.pack (intercalate "_" ("L" : map (map spell) (reverse fqn))))+    -- A binding label admits nearly any character, while 'function' admits a+    -- digit, an ASCII lowercase letter, '_' and 'φ' only, so everything else+    -- folds into '_' and the derived name stays readable back (#1188)+    spell :: Char -> Char+    spell ch = if isDigit ch || isAsciiLower ch || ch == '_' || ch == 'φ' then ch else '_'  -- A formation keeps its Δ data in the text content of its own element, the way -- the printer emits a Δ binding, while the rest of the bindings live in the
test/BytesSpec.hs view
@@ -299,5 +299,10 @@         , BtMany ["02", "03", "00"]         )       , ("large negative shift empties out", btsShift (-2147483648) (BtMany ["BF", "F0"]), BtMany ["00", "00"])+      ,+        ( "minimum Int shift empties out"+        , btsShift (minBound :: Int) (BtMany ["BF", "F0"])+        , BtMany ["00", "00"]+        )       ]       (\(desc, actual, expected) -> it desc (actual `shouldBe` expected))
test/CLISpec.hs view
@@ -2085,6 +2085,18 @@         ["merge", resource "desugar.phi", "--output=xmir"]         ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<listing>⟦ foo ↦ ξ.x, ρ ↦ ∅ ⟧</listing>", "<o base=\"ξ.x\" name=\"foo\"/>"] +    it "reproduces the same output for the same --seed" $ do+      let args =+            [ "merge"+            , "--seed=42"+            , "--sweet"+            , resource "number.phi"+            , resource "bytes.phi"+            ]+      (firstRun, _) <- withStdout (runCLI args)+      (secondRun, _) <- withStdout (runCLI args)+      firstRun `shouldBe` secondRun+   describe "match" $ do     it "prints help" $       testCLISucceeded@@ -2102,13 +2114,22 @@       withStdin "[[]]" $         testCLISucceeded ["match", "--log-level=debug"] ["[DEBUG]: The --pattern is not provided, no substitutions are built"] -    it "prints one substitution" $-      withStdin "[[ x -> Q.x ]]" $-        testCLISucceeded ["match", "--pattern=Q.!t"] ["t >> x"]--    it "does not accept a --seed flag (matching has nothing random)" $-      withStdin "[[ x -> Q.x ]]" $-        testCLIFailed ["match", "--seed=3", "--pattern=Q.!t"] ["Invalid option `--seed=3'"]+    it "reproduces the same output for the same --seed" $ do+      dir <- getTemporaryDirectory+      let file = dir ++ "/phino-match-seed-test.phi"+      writeFile file "[[ x -> Q.x, y -> Q.y, z -> Q.z ]]"+      let args =+            [ "match"+            , "--seed=42"+            , "--sweet"+            , "--flat"+            , "--pattern=Q.!t"+            , file+            ]+      (firstRun, _) <- withStdout (runCLI args)+      (secondRun, _) <- withStdout (runCLI args)+      firstRun `shouldBe` secondRun+      removeFile file      it "prints many substitutions" $       withStdin "[[ x -> Q.x, y -> Q.y ]]" $
test/DataizeSpec.hs view
@@ -7,7 +7,7 @@ module DataizeSpec (spec) where  import AST-import Atoms (Registry, emptyRegistry)+import Atoms (Registry, emptyRegistry, readRegistry) import Control.Exception (SomeException) import Control.Monad import Data.IORef (modifyIORef', newIORef, readIORef)@@ -16,7 +16,7 @@ import Data.Maybe (fromMaybe, isJust) import Dataize (DataizeContext (..), Outcome (..), Steps (..), dataize, dataize', emptyState, execBuildTerm, insideUniverse, morph, morph') import Deps (Evaluation (..), Term (TeExpression), dontSaveEval, dontSaveStep)-import Fixtures (fixtureRegistry, withNode)+import Fixtures (fixtureRegistry, withNode, withServing, withShell) import Functions (buildTerm) import Matcher (substEmpty) import Parser (parseExpressionThrows)@@ -286,6 +286,18 @@         expr <- stuck         (morphed, _) <- morph expr (defaultDataizeContext ExRoot){_deep = True, _partial = True}         morphed `shouldBe` expr++    -- The walk enters a dispatch through its target and fires the box it finds+    -- there before 𝕄 is ever asked about the dispatch, while 'ml' demands that+    -- λ only where the dispatched attribute is none of the box's own (#1187)+    describe "a dispatch naming an attribute of the formation it stands on" $+      it "cannot fire the λ the dispatch does not demand" $+        withShell $+          withServing "printf '{\"id\": %s, \"𝑛\": \"⟦ Δ ⤍ FF- ⟧\"}\\n' \"$id\"" $ \path -> do+            box <- readRegistry path+            world <- parseExpressionThrows "[[ foo -> [[ f -> [[ a -> ?, @ -> $.a, L> L_answer ]] ]], x -> Q.foo.f( a -> [[ D> 01- ]] ).@ ]]"+            (morphed, _) <- morph world (withAtoms box (defaultDataizeContext ExRoot)){_deep = True}+            morphed `shouldBe` world    describe "morph'" $     test'
test/XMIRSpec.hs view
@@ -20,6 +20,7 @@ import Files (allPathsIn) import GHC.Generics (Generic) import Parser (parseExpressionThrows)+import Printer (printExpression) import System.FilePath (makeRelative) import Test.Hspec (Spec, anyException, describe, expectationFailure, it, runIO, shouldBe, shouldContain, shouldNotContain, shouldReturn, shouldThrow) import Text.XML (Document (..), Element (..), Node (NodeElement), Prologue (..))@@ -224,6 +225,14 @@           back `shouldBe` expr       ) +  -- A λ marker with no text is named after the enclosing bindings, whose+  -- labels admit characters the 'function' parser refuses (#1188)+  describe "derived λ function name" $+    it "spells itself in the alphabet the parser accepts" $ do+      doc <- parseXMIRThrows "<object><o name=\"foo\"><o name=\"l🌵ab12\"><o base=\"∅\" name=\"v0\"/><o name=\"λ\"/></o></o></object>"+      expr <- xmirToPhi doc+      parseExpressionThrows (printExpression expr) `shouldReturn` expr+   describe "--hide-rho in XMIR" $     it "drops every bound ρ from the printed document" $ do       expr <- parseExpressionThrows "[[ x -> 4, ^ -> [[ y -> 5 ]] ]]"@@ -272,6 +281,7 @@       , ("keeps Δ data bound to a named attribute", "[[ k -> [[ a -> [[ D> 01-02 ]], ^ -> [[ D> 03-04 ]] ]] ]]")       , ("keeps Δ data in a dispatched formation", "[[ k -> [[ D> 01-02 ]].plus ]]")       , ("keeps a bare 'Q' bound to a named attribute", "[[ x -> Q ]]")+      , ("keeps a formation bound to φ", "[[ k -> [[ @ -> [[ L> S8 ]] ]] ]]")       ]       ( \(desc, source) -> it desc $ do           expr <- parseExpressionThrows source@@ -388,6 +398,15 @@               (\cur -> C.attribute (toName "name") cur == ["φ"] && C.attribute (toName "base") cur == ["∅"])               nested       length phiVoid `shouldBe` 1++    it "renders a formation bound to φ without an empty @base (#1189)" $ do+      expr <- parseExpressionThrows "[[ x -> [[ @ -> [[ L> S8 ]] ]] ]]"+      xmir' <- expressionToXMIR expr defaultXmirContext+      let phis =+            filter+              (\cur -> C.attribute (toName "name") cur == ["φ"] && null (C.attribute (toName "base") cur))+              (C.fromDocument xmir' C.$/ C.element (toName "o") C.&/ C.element (toName "o"))+      length phis `shouldBe` 1      it "renders a bare global reference as the top-level value" $ do       expr <- parseExpressionThrows "[[ x -> Q ]]"