packages feed

phino 0.0.87 → 0.0.88

raw patch · 14 files changed

+186/−124 lines, 14 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ Dataize: [_shuffle] :: DataizeContext -> Bool
+ Dataize: emptyState :: State
+ Dataize: type State = String
+ Deps: type BuildTermMethodS = [ExtraArgument] -> Subst -> IO (Term, State)
+ Deps: type State = String
- CLI.Types: OptsDataize :: LogLevel -> Int -> IOFormat -> IOFormat -> SugarType -> LineFormat -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Int -> Int -> Int -> Maybe Int -> Maybe Int -> [String] -> [String] -> String -> String -> Maybe String -> Maybe String -> Maybe String -> Maybe FilePath -> Maybe FilePath -> OptsDataize
+ CLI.Types: OptsDataize :: LogLevel -> Int -> IOFormat -> IOFormat -> SugarType -> LineFormat -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Int -> Int -> Int -> Maybe Int -> Maybe Int -> [String] -> [String] -> String -> String -> Maybe String -> Maybe String -> Maybe String -> Maybe FilePath -> Maybe FilePath -> OptsDataize
- Dataize: DataizeContext :: Expression -> Int -> Int -> Bool -> BuildTermFunc -> SaveStepFunc -> DataizeContext
+ Dataize: DataizeContext :: Expression -> Int -> Int -> Bool -> Bool -> BuildTermFunc -> SaveStepFunc -> DataizeContext
- Dataize: dataize' :: Dataizable -> Expression -> DataizeContext -> IO Dataized
+ Dataize: dataize' :: Dataizable -> Expression -> State -> DataizeContext -> IO (Dataized, State)
- Dataize: morph :: Morphed -> Expression -> DataizeContext -> IO Morphed
+ Dataize: morph :: Morphed -> Expression -> State -> DataizeContext -> IO (Morphed, State)

Files

README.md view
@@ -272,13 +272,11 @@ $ phino explain --contextualize \begin{phinoInference}   \phinoName{cxi}-  \phinoLabel{xi}   \phinoConclusion{ \phinoContextualize{ \phiTerminal{\xi} }{ k }{ k } } \end{phinoInference} ... \begin{phinoInference}   \phinoName{cdispatch}-  \phinoLabel{disp}   \phinoPremise{ \phinoContextualize{ n }{ k }{ n_1 } }   \phinoConclusion{ \phinoContextualize{ n . \tau }{ k }{ n_1 . \tau } } \end{phinoInference}
phino.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: phino-version: 0.0.87+version: 0.0.88 license: MIT synopsis: Command-Line Manipulator of 𝜑-Calculus Expressions description: Please see the README on GitHub at <https://github.com/objectionary/phino#readme>
resources/contextualization.yaml view
@@ -17,31 +17,26 @@ # skipped only when 'match' already binds it.  - name: cglobe-  label: globe   match: Φ   c-match: 𝑘   c-result: Φ  - name: cxi-  label: xi   match: ξ   c-match: 𝑘   c-result: 𝑘  - name: cdead-  label: dead   match: ⊥   c-match: 𝑘   c-result: ⊥  - name: cprim-  label: prim   match: ⟦𝐵⟧   c-match: 𝑘   c-result: ⟦𝐵⟧  - name: cdispatch-  label: disp   match: '𝑛.𝜏'   c-match: 𝑘   c-result: '𝑛1.𝜏'@@ -52,7 +47,6 @@         - 𝑘  - name: capplication-  label: app   match: '𝑛(𝜏 ↦ 𝑒1)'   c-match: 𝑘   c-result: '𝑛1(𝜏 ↦ 𝑛2)'@@ -67,7 +61,6 @@         - 𝑘  - name: capplicationa-  label: appa   match: '𝑛(α𝑖 ↦ 𝑒1)'   c-match: 𝑘   c-result: '𝑛1(α𝑖 ↦ 𝑛2)'
resources/dataization.yaml view
@@ -2,9 +2,11 @@ # SPDX-License-Identifier: MIT --- # Dataization 𝔻 — applied top-to-bottom, first matching clause wins. It is-# binary, 𝔻(n, e): the second argument e is the global universe, the same-# expression that 𝕄 takes as its second argument; 𝔻 forwards it to 𝕄. The first-# argument is always a normal form: a non-NF expression matches no clause.+# ternary, 𝔻(n, e, s): the second argument e is the global universe, the same+# expression that 𝕄 takes as its second argument (𝔻 forwards it to 𝕄), and the+# third argument s is the mutable state, threaded through and returned possibly+# changed. Only the 'fire' rule changes it, by firing an atom through 𝔼. The+# first argument is always a normal form: a non-NF expression matches no clause. # # Each rule is an inference rule: when 'match' matches the term and 'e-match' # matches the universe (binding the meta e), the rule yields the conclusion
resources/morphing.yaml view
@@ -1,10 +1,12 @@ # SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com # SPDX-License-Identifier: MIT ----# Morphing 𝕄 — applied top-to-bottom, first matching clause wins. It is binary,-# 𝕄(n, e): n is the term being morphed and e is the fixed global universe,-# threaded unchanged through every recursive call and substituted for Φ by the-# 'root' rule (Φ, rendered Q, is just the locator of e). The first argument is+# Morphing 𝕄 — applied top-to-bottom, first matching clause wins. It is ternary,+# 𝕄(n, e, s): n is the morphed term, e is the fixed global universe and s is+# the mutable state. The universe is threaded unchanged through every recursive+# call and substituted for Φ by the 'root' rule (Φ, rendered Q, is just the+# locator of e); the state is threaded too and the new state returned. Only the+# 'lambda' rule changes it, by firing an atom through 𝔼. The first argument is # always a normal form: a non-NF expression matches no clause. 𝕄 navigates a # normal form to a formation one operation at a time, resolving Φ against e and # peeling dispatches and applications through normalization 𝒩. It never fires a
src/CLI/Parsers.hs view
@@ -258,6 +258,7 @@             <*> optSequence             <*> optCanonize             <*> optDepthSensitive+            <*> optShuffle             <*> switch (long "quiet" <> help "Don't print the result of dataization")             <*> optCompress             <*> optMaxDepth
src/CLI/Runners.hs view
@@ -161,7 +161,7 @@       validateXmirOptions _outputFormat [(_omitListing, "omit-listing"), (_omitComments, "omit-comments")] _focus       when (length _show > 1) (invalidCLIArguments "The option --show can be used only once")     context :: Expression -> PrintProgramContext -> DataizeContext-    context loc ctx = DataizeContext loc _maxDepth _maxCycles _depthSensitive buildTerm (saveStepFunc _stepsDir ctx)+    context loc ctx = DataizeContext loc _maxDepth _maxCycles _depthSensitive _shuffle buildTerm (saveStepFunc _stepsDir ctx)     printProgCtx :: Expression -> PrintProgramContext     printProgCtx focus =       PrintProgCtx
src/CLI/Types.hs view
@@ -83,6 +83,7 @@   , _sequence :: Bool   , _canonize :: Bool   , _depthSensitive :: Bool+  , _shuffle :: Bool   , _quiet :: Bool   , _compress :: Bool   , _maxDepth :: Int
src/Dataize.hs view
@@ -9,7 +9,7 @@ -- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com -- SPDX-License-Identifier: MIT -module Dataize (morph, dataize, dataize', DataizeContext (..), execBuildTerm) where+module Dataize (morph, dataize, dataize', DataizeContext (..), State, emptyState, execBuildTerm) where  import AST import Builder (buildBytesThrows, buildExpressionThrows)@@ -19,7 +19,7 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as NE import qualified Data.Text as T-import Deps (BuildTermFunc, BuildTermMethod, SaveStepFunc, Term (..))+import Deps (BuildTermFunc, BuildTermMethodS, SaveStepFunc, State, Term (..)) import Locator (locatedExpression, withLocatedExpression) import Matcher (MetaValue (..), Subst (..), combine, matchExpression', substEmpty, substSingle) import Misc@@ -36,16 +36,23 @@  type Morphed = Dataizable +-- The initial, empty state used when dataization starts. The 'State' type itself+-- lives in 'Deps' next to 'BuildTermMethod'.+emptyState :: State+emptyState = ""+ -- The evaluation context carries only configuration. Nothing global is fixed--- here: the universe (the second argument 'e' of 𝕄(n, e) and 𝔻(n, e)) is a plain--- expression threaded as an argument to 'dataize'', 'morph' and on to the atoms.--- The working program needed for normalization is taken from the head of the--- step chain, so no 'Program' is threaded around.+-- here: the universe (the second argument 'e' of 𝕄(n, e, s) and 𝔻(n, e, s)) is a+-- plain expression threaded as an argument to 'dataize'', 'morph' and on to the+-- atoms, and the state 's' is threaded the same way (see 'State'). The working+-- program needed for normalization is taken from the head of the step chain, so+-- no 'Program' is threaded around. data DataizeContext = DataizeContext   { _locator :: Expression   , _maxDepth :: Int   , _maxCycles :: Int   , _depthSensitive :: Bool+  , _shuffle :: Bool   , _buildTerm :: BuildTermFunc   , _saveStep :: SaveStepFunc   }@@ -53,11 +60,11 @@ -- Resolve formation for LAMBDA Morphing rule. -- If formation contains λ binding, the called atom result is returned. The -- universe 'univ' is forwarded to the atom.-formation :: [Binding] -> Expression -> DataizeContext -> IO (Maybe Expression)-formation bds univ ctx = do+formation :: [Binding] -> Expression -> State -> DataizeContext -> IO (Maybe (Expression, State))+formation bds univ state ctx = do   let (lambda, bds') = maybeLambda bds   case lambda of-    Just (BiLambda (Function func)) -> Just <$> atom func (ExFormation bds') univ ctx+    Just (BiLambda (Function func)) -> Just <$> atom func (ExFormation bds') univ state ctx     _ -> pure Nothing   where     maybeLambda :: [Binding] -> (Maybe Binding, [Binding])@@ -70,11 +77,13 @@             [bd] -> (Just bd, rest)             _ -> (Nothing, bds) --- The Morphing function 𝕄 maps normal forms to formations. It is binary,--- 𝕄(n, e): besides the term 'n' it takes the universe 'e' ('univ') — a plain--- expression — and matches it against the rule's 'e-match' pattern (usually the--- '𝑒' meta, which binds 'e' so the 'root' rule substitutes it, but a rule may--- pin it to a literal such as 'globe' matching Φ). It is driven by+-- The Morphing function 𝕄 maps normal forms to formations. It is ternary,+-- 𝕄(n, e, s): besides the term 'n' it takes the universe 'e' ('univ') — a plain+-- expression — and the mutable state 's', returning the morphed term together+-- with the new state. The universe is matched against the rule's 'e-match'+-- pattern (usually the '𝑒' meta, which binds 'e' so the 'root' rule substitutes+-- it, but a rule may pin it to a literal such as 'globe' matching Φ). It is+-- driven by -- the ordered rules from 'morphing.yaml': the first matching rule's premises are -- evaluated and its conclusion 'nresult' is built, always forwarding the same -- universe. The 'morph' premise that produces the conclusion is the spine: when@@ -82,8 +91,8 @@ -- argument and its individual steps (alpha, copy, dot, …) are spliced into the -- chain before morphing continues. Every other premise is a side-computation -- evaluated in isolation by 'sidePremise', its own steps discarded.-morph :: Morphed -> Expression -> DataizeContext -> IO Morphed-morph (expr, seq) univ ctx = do+morph :: Morphed -> Expression -> State -> DataizeContext -> IO (Morphed, State)+morph (expr, seq) univ state ctx = do   matched <- firstMatch Y.morphingRules   case matched of     Just (rule, subst) -> reduce rule subst@@ -107,50 +116,60 @@     -- trailing 'morph' premise (the spine); if that premise's argument is itself     -- bound by a 'normalize' premise, the normalization joins the spine and its     -- steps splice in before morphing continues.-    reduce :: Y.MorphRule -> Subst -> IO Morphed+    reduce :: Y.MorphRule -> Subst -> IO (Morphed, State)     reduce rule subst = case producer rule.nresult rule.premises of       Nothing -> do-        final <- sides rule.premises subst+        (final, state') <- sides rule.premises subst         built <- buildExpressionThrows rule.nresult final         seq' <- leadsTo seq rule.name built ctx-        pure (built, seq')+        pure ((built, seq'), state')       Just concl@(Y.Premise _ (Y.OpMorph arg)) -> case producer arg rule.premises of         Just normal@(Y.Premise _ (Y.OpNormalize inner)) -> do-          final <- sides (rule.premises `excluding` [concl, normal]) subst+          (final, state') <- sides (rule.premises `excluding` [concl, normal]) subst           built <- buildExpressionThrows inner final           labelled <- leadsTo seq rule.name built ctx           (normal', seq') <- normalized built labelled ctx-          morph (normal', seq') univ ctx+          morph (normal', seq') univ state' ctx         _ -> do-          final <- sides (rule.premises `excluding` [concl]) subst+          (final, state') <- sides (rule.premises `excluding` [concl]) subst           built <- buildExpressionThrows arg final           seq' <- leadsTo seq rule.name built ctx-          morph (built, seq') univ ctx+          morph (built, seq') univ state' ctx       Just _ -> throwIO (userError (printf "morphing rule '%s' must conclude with a 'morph' premise" rule.name))-    sides :: [Y.Premise] -> Subst -> IO Subst-    sides premises subst = foldM (sidePremise univ ctx) subst premises+    sides :: [Y.Premise] -> Subst -> IO (Subst, State)+    sides premises subst = foldM (sidePremise univ ctx) (subst, state) premises  -- Dataize the program located at '_locator'. The program's root is the universe -- (the 'e' argument) passed to 𝔻 and 𝕄. dataize :: Program -> DataizeContext -> IO Dataized dataize program@(Program univ) ctx@DataizeContext{..} = do   expr <- locatedExpression _locator program-  (bytes, seq) <- dataize' (expr, (program, Nothing) :| []) univ ctx+  -- Dataization starts from the empty state; the final state is not yet+  -- consumed by any caller, so it is discarded here.+  ((bytes, seq), _state) <- dataize' (expr, (program, Nothing) :| []) univ emptyState ctx   pure (bytes, reverse seq)  -- The Dataization function 𝔻 retrieves bytes from an expression. It is total and--- binary, 𝔻(n, e): besides the term 'n' it takes the universe 'e' ('univ'),--- which it forwards to 𝕄. It is driven by the ordered rules from--- 'dataization.yaml': 'delta' yields the asset bytes, 'none' (a formation) and--- 'bott' (⊥) yield empty bytes (--), 'box' contextualizes the φ-body and keeps--- dataizing (its step is labelled by its 'contextualize' side-computation), and--- 'norm' reduces through morphing, splicing the morphing steps into the chain.+-- ternary, 𝔻(n, e, s): besides the term 'n' it takes the universe 'e' ('univ'),+-- which it forwards to 𝕄, and the mutable state 's', returning the bytes together+-- with the new state. Its rules come from 'dataization.yaml': 'delta' yields the+-- asset bytes, 'none' (a formation) and 'bott' (⊥) yield empty bytes (--),+-- 'box' contextualizes the φ-body and keeps dataizing (its step is labelled by+-- its 'contextualize' side-computation), and 'norm' reduces through morphing,+-- splicing the morphing steps into the chain. The clauses are disjoint (see+-- #902, #905), so their declaration order must not be load-bearing; when+-- '_shuffle' is on (the '--shuffle' flag) the rules are shuffled before the+-- 'firstMatch' walk to exercise that invariant — mirroring normalization's+-- "apply until they stop matching". A genuinely order-independent step stays+-- deterministic; a hidden overlap surfaces as a nondeterministic failure rather+-- than staying silently green. -- The conclusion bytes 'dresult' are produced by a trailing 'dataize' premise; -- when its argument is bound by a 'morph' or 'normalize' premise, that step -- joins the spine, otherwise the premise is an isolated side-computation.-dataize' :: Dataizable -> Expression -> DataizeContext -> IO Dataized-dataize' (expr, seq) univ ctx = do-  matched <- firstMatch Y.dataizationRules+dataize' :: Dataizable -> Expression -> State -> DataizeContext -> IO (Dataized, State)+dataize' (expr, seq) univ state ctx = do+  rules <- if ctx._shuffle then shuffle Y.dataizationRules else pure Y.dataizationRules+  matched <- firstMatch rules   case matched of     Just (rule, subst) -> reduce rule subst     Nothing -> throwIO (userError "no dataization rule matched")@@ -164,39 +183,39 @@         [] -> firstMatch rest     asRule :: Y.DataizeRule -> Y.Rule     asRule rule = Y.Rule rule.name Nothing Nothing rule.match ExRoot rule.when Nothing Nothing-    reduce :: Y.DataizeRule -> Subst -> IO Dataized+    reduce :: Y.DataizeRule -> Subst -> IO (Dataized, State)     reduce rule subst = case bytesProducer rule.dresult rule.premises of       Nothing -> do-        final <- sides rule.premises subst+        (final, state') <- sides rule.premises subst         bts <- buildBytesThrows rule.dresult final-        pure (bts, NE.toList seq)+        pure ((bts, NE.toList seq), state')       Just concl@(Y.Premise _ (Y.OpDataize arg)) -> case producer arg rule.premises of         -- 𝔻(𝒩(e)) records the producing step (the 'box' contextualization or the         -- 'fire' λ-application), then normalizes its result back to a normal form         -- before dataizing on, so 𝔻 only ever sees normal forms.         Just normal@(Y.Premise _ (Y.OpNormalize inner)) -> do           let side = rule.premises `excluding` [concl, normal]-          final <- sides side subst+          (final, state') <- sides side subst           built <- buildExpressionThrows inner final           labelled <- leadsTo seq (labelOf side) built ctx           (normal', seq') <- normalized built labelled ctx-          dataize' (normal', seq') univ ctx+          dataize' (normal', seq') univ state' ctx         -- 𝔻(𝕄(e)) delegates to the morphing relation, splicing its steps into the         -- chain before dataizing on.         Just morphed@(Y.Premise _ (Y.OpMorph inner)) -> do-          final <- sides (rule.premises `excluding` [concl, morphed]) subst+          (final, state') <- sides (rule.premises `excluding` [concl, morphed]) subst           built <- buildExpressionThrows inner final-          (morphed', seq') <- morph (built, seq) univ ctx-          dataize' (morphed', seq') univ ctx+          ((morphed', seq'), state'') <- morph (built, seq) univ state' ctx+          dataize' (morphed', seq') univ state'' ctx         _ -> do           let side = rule.premises `excluding` [concl]-          final <- sides side subst+          (final, state') <- sides side subst           built <- buildExpressionThrows arg final           seq' <- leadsTo seq (labelOf side) built ctx-          dataize' (built, seq') univ ctx+          dataize' (built, seq') univ state' ctx       Just _ -> throwIO (userError (printf "dataization rule '%s' must conclude with a 'dataize' premise" rule.name))-    sides :: [Y.Premise] -> Subst -> IO Subst-    sides premises subst = foldM (sidePremise univ ctx) subst premises+    sides :: [Y.Premise] -> Subst -> IO (Subst, State)+    sides premises subst = foldM (sidePremise univ ctx) (subst, state) premises     -- A spliced dataization step is labelled by its first side-computation —     -- 'box' by its 'contextualize', 'fire' by its 'evaluate'; with none it is blank.     labelOf :: [Y.Premise] -> String@@ -224,14 +243,27 @@ -- Evaluate one side-computation premise — a 'morph', 'evaluate' or 'contextualize' -- of an earlier term — in isolation, binding its result meta. These never splice -- steps into the trace: 'morph' and 'evaluate' reduce on a fresh chain and discard--- it, 'contextualize' is pure.-sidePremise :: Expression -> DataizeContext -> Subst -> Y.Premise -> IO Subst-sidePremise univ ctx subst premise = do-  term <- execBuildTerm univ ctx (verb premise.operation) (verbArgs premise.operation) subst+-- it, 'contextualize' is pure. The state is threaded through: 'evaluate' (the+-- 𝔼 of the 'lambda' and 'fire' rules) takes the incoming state 𝑠1 and yields a+-- new one 𝑠2, 'morph' propagates whatever its sub-reduction produced, and every+-- other operation leaves the state untouched.+sidePremise :: Expression -> DataizeContext -> (Subst, State) -> Y.Premise -> IO (Subst, State)+sidePremise univ ctx (subst, state) premise = do+  (term, state') <- runOperation   case combine (substSingle premise.result (metaValue term)) subst of-    Just subst' -> pure subst'+    Just subst' -> pure (subst', state')     Nothing -> throwIO (userError (printf "premise meta '%s' clashes with an existing binding" (T.unpack premise.result)))   where+    -- The 𝔼 ('evaluate') and 𝕄 ('morph') operations can change the state, so they+    -- go through their state-aware builders; every other operation is stateless+    -- and the incoming state is returned unchanged.+    runOperation :: IO (Term, State)+    runOperation = case premise.operation of+      Y.OpEvaluate expr -> _evaluate univ ctx state [ArgExpression expr] subst+      Y.OpMorph expr -> _morph univ ctx state [ArgExpression expr] subst+      operation -> do+        term <- execBuildTerm univ ctx (verb operation) (verbArgs operation) subst+        pure (term, state)     metaValue :: Term -> MetaValue     metaValue (TeExpression value) = MvExpression value     metaValue (TeAttribute value) = MvAttribute value@@ -286,14 +318,14 @@ -- forms. The universe 'univ' itself is forwarded unchanged, so morphing Φ under -- this context still resolves to the true universe rather than to this -- synthetic, binding-prepended formation.-_dataize :: Expression -> Expression -> DataizeContext -> IO Bytes-_dataize expr univ ctx@DataizeContext{_buildTerm = buildTerm} = case univ of+_dataize :: Expression -> Expression -> State -> DataizeContext -> IO (Bytes, State)+_dataize expr univ state ctx@DataizeContext{_buildTerm = buildTerm} = case univ of   ExFormation bds -> do     (TeAttribute attr) <- buildTerm "random-tau" [] substEmpty     let prog = Program (ExFormation (BiTau attr expr : bds))     (normal, seq) <- normalized expr ((prog, Nothing) :| []) ctx-    (bts, _) <- dataize' (normal, seq) univ ctx-    pure bts+    ((bts, _), state') <- dataize' (normal, seq) univ state ctx+    pure (bts, state')   _ -> throwIO (userError "Can't call _dataize from atoms with non-formation universe")  -- A number atom only operates on numeric data. Empty bytes (the result of@@ -303,58 +335,64 @@ asNumber BtEmpty = Nothing asNumber bts = Just (either toDouble id (btsToNum bts)) -atom :: T.Text -> Expression -> Expression -> DataizeContext -> IO Expression-atom "L_number_plus" self univ ctx = do-  left <- _dataize (ExDispatch self (AtLabel "x")) univ ctx-  right <- _dataize (ExDispatch self AtRho) univ ctx+atom :: T.Text -> Expression -> Expression -> State -> DataizeContext -> IO (Expression, State)+atom "L_number_plus" self univ state ctx = do+  (left, lstate) <- _dataize (ExDispatch self (AtLabel "x")) univ state ctx+  (right, rstate) <- _dataize (ExDispatch self AtRho) univ lstate ctx   case (asNumber left, asNumber right) of-    (Just first, Just second) -> pure (DataNumber (numToBts (first + second)))-    _ -> pure ExTermination-atom "L_number_times" self univ ctx = do-  left <- _dataize (ExDispatch self (AtLabel "x")) univ ctx-  right <- _dataize (ExDispatch self AtRho) univ ctx+    (Just first, Just second) -> pure (DataNumber (numToBts (first + second)), rstate)+    _ -> pure (ExTermination, rstate)+atom "L_number_times" self univ state ctx = do+  (left, lstate) <- _dataize (ExDispatch self (AtLabel "x")) univ state ctx+  (right, rstate) <- _dataize (ExDispatch self AtRho) univ lstate ctx   case (asNumber left, asNumber right) of-    (Just first, Just second) -> pure (DataNumber (numToBts (first * second)))-    _ -> pure ExTermination-atom "L_number_eq" self univ ctx = do-  x <- _dataize (ExDispatch self (AtLabel "x")) univ ctx-  rho <- _dataize (ExDispatch self AtRho) univ ctx+    (Just first, Just second) -> pure (DataNumber (numToBts (first * second)), rstate)+    _ -> pure (ExTermination, rstate)+atom "L_number_eq" self univ state ctx = do+  (x, lstate) <- _dataize (ExDispatch self (AtLabel "x")) univ state ctx+  (rho, rstate) <- _dataize (ExDispatch self AtRho) univ lstate ctx   case (asNumber x, asNumber rho) of     (Just first, Just self') ->       if self' == first-        then pure (DataNumber (numToBts first))-        else pure (ExDispatch self (AtLabel "y"))-    _ -> pure ExTermination-atom func _ _ _ = throwIO (userError (printf "Atom '%s' does not exist" (T.unpack func)))+        then pure (DataNumber (numToBts first), rstate)+        else pure (ExDispatch self (AtLabel "y"), rstate)+    _ -> pure (ExTermination, rstate)+atom func _ _ _ _ = throwIO (userError (printf "Atom '%s' does not exist" (T.unpack func)))  -- Augment the injected, context-free term builder with the dataization and -- morphing operations that need the universe: 'evaluate' applies an atom and -- 'morph' morphs a sub-expression. Both receive the universe 'univ'. Every other--- function is delegated unchanged.+-- function is delegated unchanged. This is the matcher's condition path (guards+-- in 'when'/'having'), which has no state to thread, so 𝔼 ('evaluate') and 𝕄+-- ('morph') run here on a fresh, empty state whose result is discarded; the+-- state-threading callers in 'sidePremise' use '_evaluate' and '_morph' directly. execBuildTerm :: Expression -> DataizeContext -> BuildTermFunc-execBuildTerm univ ctx "evaluate" = _evaluate univ ctx-execBuildTerm univ ctx "morph" = _morph univ ctx+execBuildTerm univ ctx "evaluate" = \args subst -> fst <$> _evaluate univ ctx emptyState args subst+execBuildTerm univ ctx "morph" = \args subst -> fst <$> _morph univ ctx emptyState args subst execBuildTerm _ ctx func = _buildTerm ctx func -_evaluate :: Expression -> DataizeContext -> BuildTermMethod-_evaluate univ ctx [ArgExpression expr] subst = do+-- The Evaluation function 𝔼(b, s): it fires the λ atom of a formation 'b' under+-- the incoming state 𝑠 and returns the atom's result together with the new state.+_evaluate :: Expression -> DataizeContext -> State -> BuildTermMethodS+_evaluate univ ctx state [ArgExpression expr] subst = do   form <- buildExpressionThrows expr subst   case form of     ExFormation bds -> do-      resolved <- formation bds univ ctx+      resolved <- formation bds univ state ctx       case resolved of-        Just obj -> pure (TeExpression obj)+        Just (obj, state') -> pure (TeExpression obj, state')         Nothing -> throwIO (userError "Function evaluate() expects a formation with a λ binding")     _ -> throwIO (userError "Function evaluate() expects a formation")-_evaluate _ _ _ _ = throwIO (userError "Function evaluate() requires exactly 1 expression argument")+_evaluate _ _ _ _ _ = throwIO (userError "Function evaluate() requires exactly 1 expression argument")  -- The Morphing function 𝕄 exposed as a build-term function so a rule can morph -- a sub-expression in its 'where' (the 'dispatch' and 'application' rules morph -- the head before re-attaching it). The step chain is discarded: the producing--- rule splices the surrounding normalization steps itself.-_morph :: Expression -> DataizeContext -> BuildTermMethod-_morph univ ctx [ArgExpression expr] subst = do+-- rule splices the surrounding normalization steps itself. The state is threaded+-- through and the new state returned alongside the morphed term.+_morph :: Expression -> DataizeContext -> State -> BuildTermMethodS+_morph univ ctx state [ArgExpression expr] subst = do   built <- buildExpressionThrows expr subst-  (morphed, _) <- morph (built, (Program univ, Nothing) :| []) univ ctx-  pure (TeExpression morphed)-_morph _ _ _ _ = throwIO (userError "Function morph() requires exactly 1 expression argument")+  ((morphed, _), state') <- morph (built, (Program univ, Nothing) :| []) univ state ctx+  pure (TeExpression morphed, state')+_morph _ _ _ _ _ = throwIO (userError "Function morph() requires exactly 1 expression argument")
src/Deps.hs view
@@ -25,6 +25,20 @@  type BuildTermMethod = [ExtraArgument] -> Subst -> IO Term +-- The state 𝑠 threaded through the Morphing 𝕄(n, e, s), Dataization 𝔻(n, e, s)+-- and Evaluation 𝔼(b, s) functions. The calculus does not yet fix what a state+-- is, so it is a plain string for now. Unlike the universe 𝑒, which is immutable+-- and threaded unchanged, the state is mutable: 𝔼 takes a state 𝑠1 and returns a+-- new one 𝑠2, and 𝕄/𝔻 propagate that change to their callers. Only the rules+-- that fire an atom — 'lambda' (morphing) and 'fire' (dataization) — can change+-- the state; every other rule threads it through untouched.+type State = String++-- Like 'BuildTermMethod', but it also takes the incoming state and returns the+-- new state alongside the term. Lives here next to 'BuildTermMethod' so the two+-- stay together.+type BuildTermMethodS = [ExtraArgument] -> Subst -> IO (Term, State)+ type BuildTermFunc = String -> BuildTermMethod  type SaveStepFunc = Program -> Int -> IO ()
src/Yaml.hs view
@@ -246,7 +246,7 @@ -- One morphing rule in inference-rule form: when 'match' matches the term and -- 'ematch' matches the universe (binding 'e'), the rule yields 'nresult' (a -- premise meta or a literal) provided 'when' holds and the ordered 'premises'--- reduce as stated. 'ematch' is the universe-argument matcher of 𝕄(n, e), in+-- reduce as stated. 'ematch' is the universe-argument matcher of 𝕄(n, e, s), in -- practice always the '𝑒' meta. data MorphRule = MorphRule   { name :: String
test/CLISpec.hs view
@@ -1153,40 +1153,33 @@         [ unlines             [ "\\begin{phinoInference}"             , "  \\phinoName{cglobe}"-            , "  \\phinoLabel{globe}"             , "  \\phinoConclusion{ \\phinoContextualize{ Q }{ k }{ Q } }"             , "\\end{phinoInference}"             , "\\begin{phinoInference}"             , "  \\phinoName{cxi}"-            , "  \\phinoLabel{xi}"             , "  \\phinoConclusion{ \\phinoContextualize{ \\phiTerminal{\\xi} }{ k }{ k } }"             , "\\end{phinoInference}"             , "\\begin{phinoInference}"             , "  \\phinoName{cdead}"-            , "  \\phinoLabel{dead}"             , "  \\phinoConclusion{ \\phinoContextualize{ T }{ k }{ T } }"             , "\\end{phinoInference}"             , "\\begin{phinoInference}"             , "  \\phinoName{cprim}"-            , "  \\phinoLabel{prim}"             , "  \\phinoConclusion{ \\phinoContextualize{ [[ B ]] }{ k }{ [[ B ]] } }"             , "\\end{phinoInference}"             , "\\begin{phinoInference}"             , "  \\phinoName{cdispatch}"-            , "  \\phinoLabel{disp}"             , "  \\phinoPremise{ \\phinoContextualize{ n }{ k }{ n_1 } }"             , "  \\phinoConclusion{ \\phinoContextualize{ n . \\tau }{ k }{ n_1 . \\tau } }"             , "\\end{phinoInference}"             , "\\begin{phinoInference}"             , "  \\phinoName{capplication}"-            , "  \\phinoLabel{app}"             , "  \\phinoPremise{ \\phinoContextualize{ n }{ k }{ n_1 } }"             , "  \\phinoPremise{ \\phinoContextualize{ e_1 }{ k }{ n_2 } }"             , "  \\phinoConclusion{ \\phinoContextualize{ n ( \\tau -> e_1 ) }{ k }{ n_1 ( \\tau -> n_2 ) } }"             , "\\end{phinoInference}"             , "\\begin{phinoInference}"             , "  \\phinoName{capplicationa}"-            , "  \\phinoLabel{appa}"             , "  \\phinoPremise{ \\phinoContextualize{ n }{ k }{ n_1 } }"             , "  \\phinoPremise{ \\phinoContextualize{ e_1 }{ k }{ n_2 } }"             , "  \\phinoConclusion{ \\phinoContextualize{ n ( \\phiTerminal{\\alpha_{i}} -> e_1 ) }{ k }{ n_1 ( \\phiTerminal{\\alpha_{i}} -> n_2 ) } }"
test/DataizeSpec.hs view
@@ -12,7 +12,7 @@ import Data.List (find, isInfixOf, nub) import Data.List.NonEmpty (NonEmpty (..)) import Data.Maybe (fromMaybe)-import Dataize (DataizeContext (DataizeContext), dataize, dataize', execBuildTerm, morph)+import Dataize (DataizeContext (DataizeContext), dataize, dataize', emptyState, execBuildTerm, morph) import Deps (dontSaveStep) import Functions (buildTerm) import Matcher (substEmpty)@@ -22,23 +22,26 @@ import Test.Hspec import Yaml qualified +-- Shuffle is enabled so the suite exercises the order-independence of the+-- dataization rules (#909): a hidden overlap surfaces as a nondeterministic+-- failure instead of staying silently green. defaultDataizeContext :: Expression -> DataizeContext-defaultDataizeContext loc = DataizeContext loc 25 25 False buildTerm dontSaveStep+defaultDataizeContext loc = DataizeContext loc 25 25 False True buildTerm dontSaveStep -test :: (Eq a, Show a) => ((Expression, NonEmpty Rewritten) -> Expression -> DataizeContext -> IO (a, [Rewritten])) -> [(String, Expression, Expression, a)] -> Spec+test :: (Eq a, Show a) => ((Expression, NonEmpty Rewritten) -> Expression -> String -> DataizeContext -> IO ((a, [Rewritten]), String)) -> [(String, Expression, Expression, a)] -> Spec test func useCases =   forM_ useCases $ \(desc, input, expr, output) ->     it desc $ do       let prog = Program expr-      (res, _) <- func (input, (prog, Nothing) :| []) expr (defaultDataizeContext ExRoot)+      ((res, _), _) <- func (input, (prog, Nothing) :| []) expr emptyState (defaultDataizeContext ExRoot)       res `shouldBe` output -test' :: (Eq a, Show a) => ((Expression, NonEmpty Rewritten) -> Expression -> DataizeContext -> IO (a, NonEmpty Rewritten)) -> [(String, Expression, Expression, a)] -> Spec+test' :: (Eq a, Show a) => ((Expression, NonEmpty Rewritten) -> Expression -> String -> DataizeContext -> IO ((a, NonEmpty Rewritten), String)) -> [(String, Expression, Expression, a)] -> Spec test' func useCases =   forM_ useCases $ \(desc, input, expr, output) ->     it desc $ do       let prog = Program expr-      (res, _) <- func (input, (prog, Nothing) :| []) expr (defaultDataizeContext ExRoot)+      ((res, _), _) <- func (input, (prog, Nothing) :| []) expr emptyState (defaultDataizeContext ExRoot)       res `shouldBe` output  testDataize :: [(String, String, String, Bytes)] -> Spec@@ -92,7 +95,7 @@     it "drills a chained λ-formation dispatch down to the base 'lambda'" $ do       let base = ExFormation [BiLambda (Function "F")]           chain = ExDispatch (ExDispatch (ExDispatch base (AtLabel "a")) (AtLabel "b")) (AtLabel "c")-      morph (chain, (Program ExRoot, Nothing) :| []) ExRoot (defaultDataizeContext ExRoot)+      morph (chain, (Program ExRoot, Nothing) :| []) ExRoot emptyState (defaultDataizeContext ExRoot)         `shouldThrow` (\e -> "Atom 'F' does not exist" `isInfixOf` show (e :: SomeException))    -- 'norm' matches the bare meta 𝑛, which unifies with any expression, so it is
test/YamlSpec.hs view
@@ -1,3 +1,6 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE NamedFieldPuns #-}+ -- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com -- SPDX-License-Identifier: MIT @@ -5,14 +8,15 @@  import Control.Exception (Exception (displayException), SomeException) import Control.Monad-import Data.List (isInfixOf)+import Data.List (isInfixOf, nub, (\\))+import Data.Maybe (fromMaybe) import Data.Text qualified as T import Data.Text.Encoding (encodeUtf8) import Data.Yaml qualified as Yaml import Misc import System.FilePath-import Test.Hspec (Spec, describe, it, runIO, shouldReturn, shouldSatisfy, shouldThrow)-import Yaml (ContextualizeRule, DataizeRule, MorphRule, yamlRule)+import Test.Hspec (Spec, describe, it, runIO, shouldBe, shouldReturn, shouldSatisfy, shouldThrow)+import Yaml (ContextualizeRule (..), DataizeRule (..), MorphRule (..), contextualizationRules, dataizationRules, morphingRules, yamlRule)  spec :: Spec spec = do@@ -56,3 +60,16 @@     it "in a contextualization rule" $       (decodeYaml "name: cxi\nlabel: cxi\nmatch: ξ\nc-match: 𝑘\nc-result: 𝑘" :: Either Yaml.ParseException ContextualizeRule)         `shouldSatisfy` failsAsRedundant++  describe "keeps effective labels unique across rule sets" $+    -- The effective label of a rule is its 'label' when present, else its+    -- 'name'. 'explain' typesets that label as the rule's token, so two rules+    -- sharing an effective label become indistinguishable. Collect every+    -- effective label from the three embedded rule sets and assert no repeats.+    it "across morphing, dataization and contextualization rules" $ do+      let labels :: [String]+          labels =+            map (\MorphRule{name, label} -> fromMaybe name label) morphingRules+              ++ map (\DataizeRule{name, label} -> fromMaybe name label) dataizationRules+              ++ map (\ContextualizeRule{name, label} -> fromMaybe name label) contextualizationRules+      (labels \\ nub labels) `shouldBe` []