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.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- 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)
|