unicode-logic-kit 0.31.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,594 @@
1
+ """Source-span tracking for :meth:`MSFLParser.parse_with_spans`.
2
+
3
+ WHY THIS IS A SIDE TABLE, NOT A NODE FIELD
4
+ -------------------------------------------
5
+ Every AST node (``_fol_nodes.py`` and the sibling ``_*_nodes.py`` modules) is a
6
+ frozen dataclass with structural equality and hashing: dedup, ``canonical_key``
7
+ caches, sets of nodes, the harvest cache in ``semantics/model_eval.py``, and
8
+ every test that compares a parsed formula to a hand-built one all depend on
9
+ that. A span FIELD would make two structurally identical formulas parsed from
10
+ different source text compare unequal and hash apart — so spans live in a
11
+ side table, never on the node itself.
12
+
13
+ WHY THE TABLE IS KEYED BY PATH, NOT BY id() (AND NOT BY THE NODE ITSELF)
14
+ --------------------------------------------------------------------------
15
+ Two occurrences of the SAME subformula are structurally equal and hash
16
+ together (``P(x)`` in ``P(x) ∧ P(x)``), so keying by node value/equality would
17
+ silently merge their two different spans into one entry — wrong by
18
+ construction. Keying by ``id()`` looks like the fix (two equal-but-distinct
19
+ objects have distinct ids) but is unstable: ``MSFLParser.parse`` runs
20
+ :func:`~unicode_logic_kit.fol._msfl_nodes.resolve_lambda_scope` (and, in modal
21
+ mode, :func:`~unicode_logic_kit.fol._modal_nodes.resolve_agent_variables`)
22
+ AFTER the Lark transform, and BOTH passes rebuild every node they touch —
23
+ :meth:`Node.map_children` always constructs a fresh ``type(self)(**new_kwargs)``,
24
+ and ``dataclasses.replace`` likewise — even when NOTHING about that node
25
+ actually changed (verified: for a lambda-free classical formula,
26
+ ``resolve_lambda_scope`` still rebuilds every node top to bottom, it just
27
+ never changes any field). So the id() a node had during transform is gone by
28
+ the time ``parse()`` returns, and an id()-keyed table built during transform
29
+ cannot be looked up against the AST the caller actually holds.
30
+
31
+ The fix is a key that names a node by its STRUCTURAL POSITION in the tree
32
+ rather than by its identity or its value: a :data:`Path` — a tuple of child
33
+ indices from the root, using the SAME deterministic child order the
34
+ traversal API (:func:`traverse`), lookup, and :func:`replace_at` all agree
35
+ on (see "PATH CONVENTION" below). A path is stable across any REBUILD that
36
+ preserves tree shape (same node type, same number and order of children at
37
+ every level) even when it changes VALUES — which is exactly what
38
+ ``resolve_lambda_scope``/``resolve_agent_variables`` do for the classical FOL
39
+ fragment (no lambda in the input ⇒ shape is always preserved, only object
40
+ identity changes) and for :mod:`unicode_logic_kit.chem.interop`'s renaming
41
+ rewrite (only an ``Atom.predicate``/``Function.name`` string changes).
42
+ :func:`project_spans` carries a path-keyed table across a rewrite explicitly,
43
+ by walking the OLD and NEW trees in lockstep; see its docstring for the one
44
+ case that is a genuine reshaping (a higher-order lambda application rewritten
45
+ into a fresh ``Application``/``LambdaVar`` chain) and how that is reported as
46
+ :data:`UNKNOWN` rather than mis-mapped.
47
+
48
+ PATH CONVENTION
49
+ ----------------
50
+ A :data:`Path` is a ``Tuple[int, ...]``; the empty tuple ``()`` addresses the
51
+ root itself. Given a node, its PATH CHILDREN — the sequence a path index 0,
52
+ 1, 2, … selects into — are:
53
+
54
+ * For every node EXCEPT :class:`~unicode_logic_kit.fol._fol_nodes.Quantifier`:
55
+ exactly :meth:`Node._child_nodes` — i.e. every dataclass field that is a
56
+ ``Node`` (in field-declaration order), with a ``Node``-list/tuple field
57
+ contributing its elements in order. This is the SAME child view
58
+ ``map_children``/``walk`` already use, so path indices for an ordinary
59
+ formula node line up with "the child at position N" the way any other
60
+ traversal over this AST already means it.
61
+ * For a ``Quantifier``: path child 0 is its ``formula`` (the matrix) and
62
+ ONLY that — the bound ``variable`` is deliberately NOT a path child (see
63
+ "WHY THE BOUND VARIABLE IS EXCLUDED" below). ``∀x P(x)`` therefore has
64
+ exactly one path child, at index 0: ``P(x)``.
65
+
66
+ This SAME rule (``_fol_nodes._path_children`` — living next to ``Node`` and
67
+ ``Quantifier`` themselves, alongside ``node_at``/``replace_at``, and
68
+ re-exported from this module) is what :func:`traverse`,
69
+ :func:`node_at`, :func:`replace_at`, and :func:`project_spans` all call, so a
70
+ path produced by one of them is always valid input to any of the others, on
71
+ the same tree — that agreement is the whole point of making paths opaque,
72
+ kit-issued values (never hand-constructed by a caller against some OTHER
73
+ child-ordering assumption). ``replace_at`` predates this module (see its own
74
+ docstring in ``_fol_nodes.py``) and was originally built against the WIDER
75
+ ``_child_nodes()`` view, flagging the Quantifier exclusion as an open
76
+ reconciliation; that reconciliation is what fixed both functions to the one
77
+ rule described here.
78
+
79
+ WHY THE BOUND VARIABLE IS EXCLUDED FROM QUANTIFIER'S PATH CHILDREN
80
+ -----------------------------------------------------------------------
81
+ A quantifier's HEAD span (see "TWO SPANS PER NODE" below) already covers the
82
+ symbol AND the bound variable together (``"∀ x"``), so the variable's source
83
+ text is already accounted for by its binder's head — exposing it AGAIN as a
84
+ separately addressable path child would double-count that occurrence and
85
+ give it two different "correct" spans (the identifier text sitting inside
86
+ the parent's head span, and its own leaf span) with no principled way to
87
+ prefer one. A term occurring in ARGUMENT position (an ``Atom``/``Function``
88
+ argument) is NOT covered by any enclosing head, so it stays a normal,
89
+ separately path-addressable child with its own leaf span — the exclusion is
90
+ specific to a BINDER's own bound-variable field, not to variables in general.
91
+ (Other binder node types — ``Count``, ``Cardinality``, ``SortedQuantifier``,
92
+ ``Lambda``, … — are outside this change's target fragment and are left on
93
+ the default, unexcluded child view; see the module docstring of
94
+ ``msflparser.py``'s span-capturing transform for the full list of what this
95
+ change does and does not cover.)
96
+
97
+ TWO SPANS PER NODE: EXTENT AND HEAD
98
+ --------------------------------------
99
+ :class:`NodeSpans` bundles both:
100
+
101
+ * ``extent`` — the minimal span covering the node's own tokens. Redundant
102
+ OUTER parentheses are never part of it: Lark's grammar wraps a formula in
103
+ parentheses via an UN-aliased, ``?``-prefixed alternative
104
+ (``?prefix: … | "(" formula ")"``) that Lark auto-inlines away whenever it
105
+ reduces to a single child, so the position Lark actually reports for the
106
+ inner node's own rule (via ``propagate_positions``) already excludes any
107
+ number of wrapping parens — ``((P(x)))`` reports the same extent as
108
+ ``P(x)`` (verified; see msflparser.py's span-capturing transform, and
109
+ ``tests/test_spans.py``).
110
+ * ``head`` — the head-TOKEN occurrence: a quantifier's symbol together with
111
+ its bound variable INCLUDING the whitespace between them (``"∀ x"``), a
112
+ negation sign, a binary connective's own occurrence, an atom's predicate
113
+ name (or its infix comparison symbol), a function's name. A leaf TERM node
114
+ (``Variable``/``Constant``/``Number``) has ``head == extent`` — it IS its
115
+ own single token.
116
+
117
+ Either field individually may be :data:`UNKNOWN` — see "UNKNOWN" below.
118
+
119
+ UNKNOWN
120
+ --------
121
+ :data:`UNKNOWN` is an explicit, documented sentinel for "this span could not
122
+ be recovered" — never a guessed or interpolated stand-in, and never silently
123
+ absent (:meth:`SpanMap.get` always returns a :class:`NodeSpans`, defaulting
124
+ individual fields to ``UNKNOWN`` rather than raising or omitting the entry).
125
+ For the pure classical FOL fragment (∀ ∃ ¬ ∧ ∨ → ↔ ⊕, predicates over
126
+ constants/variables/function terms) both fields are recovered EXACTLY for
127
+ every node reachable via :func:`traverse` — see ``tests/test_spans.py`` and
128
+ the FOLIO-corpus test for the zero-``UNKNOWN`` claim. Outside that fragment
129
+ (modal/temporal/epistemic operators, lambda, second-order, counting/
130
+ cardinality binders, sorted quantifiers, …) ``head`` may legitimately be
131
+ ``UNKNOWN`` — those operators are out of this change's target scope; two
132
+ narrow, specific, already-documented gaps besides:
133
+
134
+ * An agent prefix (``K_a``, ``B_a``, …) is lexed as a SINGLE combined token
135
+ (the agent name is sliced out of it inside a handler, never its own
136
+ token), so the agent sub-node has no token of its own — its ``Knows``/
137
+ ``Believes``/… PARENT node's own span is unaffected.
138
+ * A higher-order lambda application is rewritten, post-parse, into a fresh
139
+ ``Application``/``LambdaVar`` chain the original grammar never produced —
140
+ see :func:`project_spans`.
141
+ """
142
+
143
+ from dataclasses import dataclass
144
+ from typing import Dict, Iterator, List, Tuple
145
+
146
+ from ._fol_nodes import Node, _path_children, node_at, replace_at # noqa: F401 (node_at, replace_at re-exported)
147
+
148
+
149
+ # =========================
150
+ # Span / UNKNOWN
151
+ # =========================
152
+
153
+ @dataclass(frozen=True)
154
+ class Span:
155
+ """A slice of the ORIGINAL source text that one AST node (or one node's
156
+ head token) was parsed from.
157
+
158
+ ``start``/``end`` are character offsets into the source string, in Python
159
+ slice convention (``text[start:end] == span.text``) — and, because a
160
+ Python ``str`` already indexes by Unicode CODEPOINT (not UTF-16 unit or
161
+ byte), these offsets are codepoint indices with no further conversion
162
+ needed, satisfied by construction rather than by extra bookkeeping.
163
+ ``line``/``column`` and ``end_line``/``end_column`` are 1-based, matching
164
+ Lark's own ``Meta``/``Token`` convention: ``column`` is the 1-based
165
+ offset of ``start`` within ``line``; ``end_column`` is one past the last
166
+ character on ``end_line``. ``text`` is the exact slice, so a caller can
167
+ quote the source directly without re-slicing by hand (and risking an
168
+ off-by-one).
169
+ """
170
+
171
+ start: int
172
+ end: int
173
+ line: int
174
+ column: int
175
+ end_line: int
176
+ end_column: int
177
+ text: str
178
+
179
+
180
+ def _unknown_singleton():
181
+ """``__reduce__`` target for :data:`UNKNOWN` — always returns the one
182
+ module-level instance, so pickling (including across a process boundary,
183
+ e.g. a ``ProcessPoolExecutor`` worker) reconstructs the SAME sentinel
184
+ rather than depending on ``_UnknownSpan.__new__``'s caching to line up
185
+ with pickle's own object-construction protocol."""
186
+ return UNKNOWN
187
+
188
+
189
+ class _UnknownSpan:
190
+ """Sentinel: a span could not be recovered (see module docstring).
191
+
192
+ Falsy and distinct from every real :class:`Span`, so callers can write
193
+ ``if span:`` or ``if span is UNKNOWN:`` — never a guessed or interpolated
194
+ stand-in span.
195
+ """
196
+
197
+ _instance = None
198
+
199
+ def __new__(cls):
200
+ if cls._instance is None:
201
+ cls._instance = super().__new__(cls)
202
+ return cls._instance
203
+
204
+ def __repr__(self) -> str:
205
+ return "UNKNOWN"
206
+
207
+ def __bool__(self) -> bool:
208
+ return False
209
+
210
+ def __reduce__(self):
211
+ return (_unknown_singleton, ())
212
+
213
+
214
+ UNKNOWN = _UnknownSpan()
215
+
216
+
217
+ @dataclass(frozen=True)
218
+ class NodeSpans:
219
+ """The two spans recorded for one AST node occurrence: see the module
220
+ docstring's "TWO SPANS PER NODE". Either field may independently be
221
+ :data:`UNKNOWN`."""
222
+
223
+ extent: object # Span or UNKNOWN
224
+ head: object # Span or UNKNOWN
225
+
226
+
227
+ _UNKNOWN_NODE_SPANS = NodeSpans(UNKNOWN, UNKNOWN)
228
+
229
+
230
+ # =========================
231
+ # Path convention: traversal, lookup, replace_at all agree on this
232
+ # =========================
233
+
234
+ #: A path from a tree's root to one of its nodes: a tuple of child indices,
235
+ #: ``()`` addressing the root itself. See the module docstring's "PATH
236
+ #: CONVENTION". Opaque — always produced by :func:`traverse` or accepted back
237
+ #: by :func:`node_at`/:func:`replace_at`/:meth:`SpanMap.get`, never
238
+ #: hand-built by a caller against some other assumption about child order.
239
+ Path = Tuple[int, ...]
240
+
241
+
242
+ def traverse(root: Node) -> Iterator[Tuple[Path, Node]]:
243
+ """Yield ``(path, node)`` for every node in ``root``'s tree, pre-order.
244
+
245
+ This is the documented traversal API: it covers every node reachable via
246
+ :func:`_path_children` — every formula node AND every term node (a
247
+ variable/constant/number, and a function/atom argument in general) — but,
248
+ for a :class:`Quantifier`, NOT its bound variable a second time (see the
249
+ module docstring). A :class:`SpanMap` built by
250
+ :meth:`MSFLParser.parse_with_spans` is populated by exactly this walk, so
251
+ ``spans.get(path)`` for any ``path`` this function yields is meaningful.
252
+ """
253
+ def _walk(node: Node, path: Path) -> Iterator[Tuple[Path, Node]]:
254
+ yield path, node
255
+ for i, child in enumerate(_path_children(node)):
256
+ yield from _walk(child, path + (i,))
257
+ yield from _walk(root, ())
258
+
259
+
260
+ # node_at and replace_at themselves live in _fol_nodes.py, next to Node and
261
+ # Quantifier (the one node type the path convention special-cases) — imported
262
+ # above and re-exported here so `fol.spans.replace_at`/`fol.spans.node_at`
263
+ # work too, since a caller reasoning about spans is exactly who needs them.
264
+ # See _fol_nodes.py's own "Public tree editing (path-addressed replacement)
265
+ # and PATH CONVENTION" section for their implementation and the reasoning
266
+ # behind excluding a Quantifier's bound variable from its path children.
267
+
268
+
269
+ # =========================
270
+ # SpanMap: path -> NodeSpans, with an identity-based convenience index
271
+ # =========================
272
+
273
+ class SpanMap:
274
+ """``Path -> NodeSpans`` table returned alongside an AST by
275
+ :meth:`MSFLParser.parse_with_spans`.
276
+
277
+ The PATH-keyed table (``.get(path)``) is the primary, spec-correct
278
+ interface (see the module docstring) and is exactly what survives
279
+ pickling — see "PICKLING" below.
280
+
281
+ ``.for_node(node)`` is a CONVENIENCE lookup for a caller that is holding
282
+ an actual node object rather than a path (e.g. after some OTHER pass
283
+ found a specific node by walking the tree itself, the way
284
+ :mod:`unicode_logic_kit.semantics.model_eval`'s blame-finder does — see
285
+ :mod:`unicode_logic_kit.mcp.chem_tools`). It works by IDENTITY against
286
+ whichever tree this ``SpanMap`` was last :meth:`rebind`-ed to (a
287
+ ``SpanMap`` returned by ``parse_with_spans`` is already bound to the AST
288
+ it is returned alongside). A node that is not part of that exact tree —
289
+ even one that is structurally ``==`` to a node that IS — reports
290
+ :data:`UNKNOWN` for both fields, same as an absent path.
291
+ """
292
+
293
+ __slots__ = ("_table", "_by_id")
294
+
295
+ def __init__(self, table: Dict[Path, NodeSpans]):
296
+ self._table: Dict[Path, NodeSpans] = dict(table)
297
+ self._by_id: Dict[int, Path] = {}
298
+
299
+ # -- path-keyed access (primary) -----------------------------------
300
+
301
+ def get(self, path: Path) -> NodeSpans:
302
+ """The :class:`NodeSpans` recorded at ``path``, or a
303
+ ``NodeSpans(UNKNOWN, UNKNOWN)`` if ``path`` has no entry."""
304
+ return self._table.get(path, _UNKNOWN_NODE_SPANS)
305
+
306
+ def __getitem__(self, path: Path) -> NodeSpans:
307
+ return self.get(path)
308
+
309
+ def __contains__(self, path: Path) -> bool:
310
+ return path in self._table
311
+
312
+ def __len__(self) -> int:
313
+ return len(self._table)
314
+
315
+ def __iter__(self) -> Iterator[Path]:
316
+ return iter(self._table)
317
+
318
+ def paths(self):
319
+ """Every path with a recorded entry (dict_keys view, like a plain dict)."""
320
+ return self._table.keys()
321
+
322
+ def items(self):
323
+ """``(path, NodeSpans)`` pairs for every recorded entry (dict_items view)."""
324
+ return self._table.items()
325
+
326
+ # -- identity-based convenience -------------------------------------
327
+
328
+ def rebind(self, root: Node) -> "SpanMap":
329
+ """Return a :class:`SpanMap` with the SAME path->NodeSpans data, but
330
+ with :meth:`for_node`'s identity index rebuilt against ``root``.
331
+
332
+ Use this after a shape-preserving rewrite that produced fresh node
333
+ objects at the same paths (e.g.
334
+ :func:`unicode_logic_kit.chem.interop.rename_with_spans` calls
335
+ :func:`project_spans`, which already returns a rebound map) — or any
336
+ time a caller wants ``.for_node`` to work against a specific tree it
337
+ is holding. Cheap: one traversal of ``root`` (``O(n)``).
338
+ """
339
+ new = SpanMap(self._table)
340
+ new._by_id = {id(node): path for path, node in traverse(root)}
341
+ return new
342
+
343
+ def for_node(self, node: Node) -> NodeSpans:
344
+ """:meth:`get`, but looking ``node`` up by identity against whichever
345
+ tree this map was last :meth:`rebind`-ed to (see the class
346
+ docstring)."""
347
+ path = self._by_id.get(id(node))
348
+ if path is None:
349
+ return _UNKNOWN_NODE_SPANS
350
+ return self.get(path)
351
+
352
+ # -- pickling ---------------------------------------------------------
353
+ # Only `_table` is meaningful across a process boundary: it is keyed by
354
+ # PATH (plain tuples of int — portable data, no dependence on this
355
+ # process's object graph). `_by_id` is, by construction, scoped to
356
+ # whichever tree object this map was last rebind()ed to IN THIS PROCESS
357
+ # — those id()s mean nothing once unpickled elsewhere, so rather than
358
+ # pickle stale, silently-never-matching ids, unpickling starts with an
359
+ # empty identity index; call .rebind(root) again against the AST as
360
+ # reconstructed on the receiving end to restore .for_node().
361
+
362
+ def __getstate__(self):
363
+ return {"table": self._table}
364
+
365
+ def __setstate__(self, state):
366
+ self._table = state["table"]
367
+ self._by_id = {}
368
+
369
+
370
+ def build_span_map(root: Node, id_extent: Dict[int, Span],
371
+ id_head: Dict[int, Span]) -> SpanMap:
372
+ """Build a path-keyed :class:`SpanMap` over ``root`` from two id()-keyed
373
+ tables of (extent, head) spans captured DURING a single parse/transform.
374
+
375
+ The id()-keying of ``id_extent``/``id_head`` is a TRANSIENT bookkeeping
376
+ device local to the caller (see ``msflparser.py``'s span-capturing
377
+ transform) — object identity is only relied on for the lifetime of that
378
+ one transform call, never stored or exposed. This function is the one
379
+ place that translates it into the stable, path-keyed representation:
380
+ it walks ``root`` via :func:`traverse` (so the resulting keys are
381
+ exactly the paths :func:`traverse` yields) and looks each node's spans
382
+ up by ``id()`` while ``root`` is still the very AST the transform just
383
+ produced (ids are still live and unambiguous at this point). Returns a
384
+ map already :meth:`~SpanMap.rebind`-ed to ``root``.
385
+ """
386
+ table: Dict[Path, NodeSpans] = {}
387
+ for path, node in traverse(root):
388
+ ext = id_extent.get(id(node), UNKNOWN)
389
+ head = id_head.get(id(node), UNKNOWN)
390
+ table[path] = NodeSpans(ext, head)
391
+ return SpanMap(table).rebind(root)
392
+
393
+
394
+ def project_spans(old_root: Node, new_root: Node, old_map: SpanMap) -> SpanMap:
395
+ """Carry ``old_map`` (built over ``old_root``) forward onto ``new_root``,
396
+ the result of a rewrite over ``old_root``.
397
+
398
+ Because the table is PATH-keyed, this needs no per-node identity
399
+ tracking: a path denotes the SAME structural position in any tree with
400
+ matching shape, so a shape-preserving rewrite (:func:`resolve_lambda_scope`
401
+ for a lambda-free formula; :mod:`unicode_logic_kit.chem.interop`'s
402
+ predicate/function renaming, which only ever changes a string field) does
403
+ not need its span table "carried" at all in the old id()-based sense —
404
+ every path in ``old_map`` simply still means the same thing in
405
+ ``new_root``.
406
+
407
+ What this function actually guards against is a rewrite that is NOT
408
+ shape-preserving somewhere: :func:`resolve_lambda_scope` rewrites a
409
+ higher-order use of a lambda-bound predicate/function symbol (e.g. the
410
+ body of ``λP. P(x) ∧ Q(y)``) from an n-ary ``Atom``/``Function`` into a
411
+ fresh, LEFT-NESTED chain of ``Application`` nodes wrapping a synthesized
412
+ ``LambdaVar`` — a node whose TYPE or child count differs from what sat at
413
+ that path in ``old_root``, with no original span to carry (the original
414
+ grammar never produced these nodes at all). Walking ``old_root`` and
415
+ ``new_root`` in lockstep via :func:`_path_children`, a divergence (type
416
+ or child-count mismatch) at some path stops descent on the ``new_root``
417
+ side — that path and everything below it in ``new_root`` is left absent
418
+ from the result, which :meth:`SpanMap.get`/:meth:`SpanMap.for_node`
419
+ report as :data:`UNKNOWN`, never a guessed span. A LEAF-to-leaf step
420
+ (both sides childless) always carries its recorded spans forward even if
421
+ the node's TYPE changed (``Variable`` -> ``LambdaVar``, or a free
422
+ epistemic/doxastic agent ``Variable`` -> ``Constant`` under
423
+ :func:`~unicode_logic_kit.fol._modal_nodes.resolve_agent_variables`) — both
424
+ cover the exact same source characters, just reinterpreted.
425
+
426
+ Returns a new :class:`SpanMap`, already :meth:`~SpanMap.rebind`-ed to
427
+ ``new_root``.
428
+ """
429
+ table: Dict[Path, NodeSpans] = {}
430
+
431
+ def _walk(old: Node, new: Node, path: Path) -> None:
432
+ old_children = _path_children(old)
433
+ new_children = _path_children(new)
434
+ if not old_children and not new_children:
435
+ entry = old_map._table.get(path)
436
+ if entry is not None:
437
+ table[path] = entry
438
+ return
439
+ if type(old) is not type(new) or len(old_children) != len(new_children):
440
+ return # genuine restructuring — leave this subtree unmapped
441
+ entry = old_map._table.get(path)
442
+ if entry is not None:
443
+ table[path] = entry
444
+ for i, (oc, nc) in enumerate(zip(old_children, new_children)):
445
+ _walk(oc, nc, path + (i,))
446
+
447
+ _walk(old_root, new_root, ())
448
+ return SpanMap(table).rebind(new_root)
449
+
450
+
451
+ @dataclass(frozen=True)
452
+ class SpannedFormula:
453
+ """The result of :meth:`MSFLParser.parse_with_spans`: an AST plus its spans.
454
+
455
+ ``formula`` is exactly what :meth:`MSFLParser.parse` would have returned
456
+ for the same text (same node values, same structure) — ``parse_with_spans``
457
+ changes nothing about how the formula is built. ``spans`` maps its nodes
458
+ back to the source text; see :class:`SpanMap`.
459
+ """
460
+
461
+ formula: Node
462
+ spans: SpanMap
463
+
464
+
465
+ # =========================
466
+ # Lark meta/token -> Span
467
+ # =========================
468
+
469
+ def span_from_meta(meta, text: str) -> Span:
470
+ """Build a :class:`Span` from a Lark ``Tree.meta`` (propagate_positions=True)."""
471
+ return Span(meta.start_pos, meta.end_pos, meta.line, meta.column,
472
+ meta.end_line, meta.end_column, text[meta.start_pos:meta.end_pos])
473
+
474
+
475
+ def span_from_token(token, text: str) -> Span:
476
+ """Build a :class:`Span` from a Lark ``Token`` (position attrs are always set)."""
477
+ return Span(token.start_pos, token.end_pos, token.line, token.column,
478
+ token.end_line, token.end_column,
479
+ text[token.start_pos:token.end_pos])
480
+
481
+
482
+ def _line_col(text: str, pos: int) -> Tuple[int, int]:
483
+ """1-based ``(line, column)`` for codepoint offset ``pos`` into ``text``,
484
+ matching Lark's ``Meta``/``Token`` convention."""
485
+ line = text.count("\n", 0, pos) + 1
486
+ last_nl = text.rfind("\n", 0, pos)
487
+ column = pos - last_nl # last_nl == -1 (no preceding newline) -> pos + 1
488
+ return line, column
489
+
490
+
491
+ def make_span(text: str, start: int, end: int) -> Span:
492
+ """Build a :class:`Span` for the codepoint range ``[start, end)`` of
493
+ ``text`` directly from the text — no Lark ``Meta``/``Token`` involved.
494
+
495
+ Used for spans this module derives itself rather than reads off Lark:
496
+ a synthesized HEAD span (an operator glyph located by :func:`find_glyph`,
497
+ or a quantifier's symbol+variable) and a synthesized EXTENT for a
498
+ same-level operator-chain intermediate (see msflparser.py's
499
+ span-capturing transform).
500
+ """
501
+ l1, c1 = _line_col(text, start)
502
+ l2, c2 = _line_col(text, end)
503
+ return Span(start, end, l1, c1, l2, c2, text[start:end])
504
+
505
+
506
+ def find_glyph(text: str, start: int, end: int, glyph: str) -> Tuple[int, int]:
507
+ """Locate the exact occurrence of ``glyph`` within ``text[start:end]``.
508
+
509
+ Used to find an operator's own token position when Lark filters the
510
+ operator out of the tree as an anonymous literal (every classical
511
+ connective — ``¬ ∧ ∨ → ↔ ⊕`` — and every infix comparison — ``= < > ≤ ≥
512
+ ≠`` — is declared this way; see msflparser.py's span-capturing
513
+ transform). ``[start, end)`` is deliberately allowed to be WIDER than
514
+ the true minimal gap around the operator (e.g. it may include an
515
+ operand's own redundant wrapping parentheses, which are not part of
516
+ that operand's reported EXTENT — see this module's docstring): searching
517
+ for the operator's exact glyph text rather than "the first non-whitespace
518
+ run" is what makes that safe, since none of the punctuation the grammar
519
+ can otherwise place in that window (parentheses, whitespace) can equal
520
+ the glyph itself, and the grammar guarantees exactly one genuine
521
+ occurrence of it between two adjacent operands.
522
+
523
+ Returns ``(glyph_start, glyph_end)``. Raises ``ValueError`` if ``glyph``
524
+ does not occur in the window — a grammar/assumption violation, not a
525
+ guessable situation.
526
+ """
527
+ idx = text.find(glyph, start, end)
528
+ if idx == -1:
529
+ raise ValueError(
530
+ f"spans.find_glyph: expected to find {glyph!r} within "
531
+ f"{text[start:end]!r} (codepoints [{start}, {end})); none found")
532
+ return idx, idx + len(glyph)
533
+
534
+
535
+ def trim_ws_backward(text: str, pos: int) -> int:
536
+ """The largest index ``<= pos`` such that ``text[index:pos]`` is all
537
+ whitespace — i.e. ``pos`` scanned backward past any trailing whitespace."""
538
+ while pos > 0 and text[pos - 1].isspace():
539
+ pos -= 1
540
+ return pos
541
+
542
+
543
+ def trim_ws_forward(text: str, pos: int) -> int:
544
+ """The smallest index ``>= pos`` such that ``text[pos:index]`` is all
545
+ whitespace — i.e. ``pos`` scanned forward past any leading whitespace."""
546
+ while pos < len(text) and text[pos].isspace():
547
+ pos += 1
548
+ return pos
549
+
550
+
551
+ # =========================
552
+ # Gap filling: best-effort EXTENT fallback for a node this module has no
553
+ # precise rule for (out-of-scope operators; never used for HEAD, which is
554
+ # never fabricated — see the module docstring's UNKNOWN section)
555
+ # =========================
556
+
557
+ def fill_gap_spans(node: Node, id_extent: Dict[int, Span], text: str) -> None:
558
+ """Post-order, id()-keyed (see :func:`build_span_map`'s docstring for why
559
+ that is fine as a same-call-only bookkeeping device): derive an EXTENT
560
+ for any node this module's span-capturing transform did not directly
561
+ assign one to.
562
+
563
+ Mutates ``id_extent`` in place. The derived span is the union of the
564
+ node's OWN children's (already-known) extents — exact for a node whose
565
+ every child extent is itself exact AND whose own true extent has no
566
+ tokens outside its children's spans (true for the same-level
567
+ operator-chain case this originally targeted, where the omitted node is
568
+ itself later given a precise extent inline by the level2 fold — see
569
+ msflparser.py — so in practice this fallback now only fires for
570
+ out-of-scope node types). For anything with a mix of parenthesised and
571
+ unparenthesised children, this union CAN include stray wrapping
572
+ parentheses the same way an unprincipled union always would (see this
573
+ module's docstring on why ``find_glyph`` searches for the operator glyph
574
+ itself rather than assuming the gap is clean) — this fallback is
575
+ consequently a best-effort EXTENT only, never used for HEAD, and never
576
+ claimed exact outside the cases enumerated above.
577
+
578
+ A node with no children already in ``id_extent`` (a genuine leaf this
579
+ transform never captured — e.g. an agent variable sliced out of a
580
+ combined ``K_a``-style token) is left absent, which reports
581
+ :data:`UNKNOWN` once the id-table is turned into a path-keyed
582
+ :class:`SpanMap` by :func:`build_span_map`.
583
+ """
584
+ children = node._child_nodes()
585
+ for child in children:
586
+ fill_gap_spans(child, id_extent, text)
587
+ if id(node) in id_extent:
588
+ return
589
+ known: List[Span] = [id_extent[id(c)] for c in children if id(c) in id_extent]
590
+ if not known:
591
+ return
592
+ start = min(s.start for s in known)
593
+ end = max(s.end for s in known)
594
+ id_extent[id(node)] = make_span(text, start, end)