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,478 @@
1
+ """Adapter for the WillowNLtoFOL dataset (Deveci, "A Diagnostic Framework for
2
+ Compositional Generalization via NL-to-FOL Translation", JLLI 2026; earlier
3
+ described in Deveci's 2024 METU master's thesis) — local JSONL only, no
4
+ network access.
5
+
6
+ Source and verified schema
7
+ ---------------------------
8
+ Source: https://huggingface.co/datasets/iedeveci/WillowNLtoFOL (unauthenticated;
9
+ verified 2026-08-12 directly via the Hugging Face datasets-server ``splits``,
10
+ ``first-rows``, and ``rows`` APIs, and by fetching the dataset's raw
11
+ ``README.md`` — https://huggingface.co/datasets/iedeveci/WillowNLtoFOL/raw/main/README.md
12
+ — for its YAML metadata and licensing prose). The ``splits`` endpoint reports
13
+ exactly two splits under config ``"default"``: ``"train"`` (16014 rows) and
14
+ ``"test"`` (90 rows). NOTE: the dataset card's prose additionally describes a
15
+ "``WillowNLtoFOL_extended``" 90-pair out-of-distribution stress-test subset —
16
+ that appears to BE the ``"test"`` split under a different name in the card's
17
+ prose than in the actual data (the ``splits``/``info`` APIs show no subset or
18
+ config named ``extended``); this loader reads whatever split you point it at
19
+ and does not depend on which of the two names is "correct".
20
+
21
+ Each verified row (``first-rows``/``rows`` APIs, both splits) is a flat JSON
22
+ object with EXACTLY two keys:
23
+
24
+ * ``"NL_sentence"`` — ``str``, one natural-language sentence.
25
+ * ``"FOL_expression"`` — ``str``, its FOL translation, written DIRECTLY in
26
+ this kit's own unicode surface syntax (``∀``/``∃``/``∧``/``∨``/``¬``/``→``/
27
+ ``↔``/``⊕`` all appear across the verified sample rows and the ``"fol"``
28
+ dialect is what ``api.parse_any`` selects for every one that parses at
29
+ all — no ASCII/LaTeX fallback notation was observed anywhere in the sample).
30
+
31
+ Like MALLS, WillowNLtoFOL has NO id field, NO premises/conclusion structure,
32
+ and NO entailment label — it is a flat bag of (sentence, formula) pairs. This
33
+ adapter maps that onto :class:`~unicode_logic_kit.eval.datasets.DatasetExample`
34
+ with the SAME convention :mod:`~unicode_logic_kit.eval.datasets.malls` uses for
35
+ MALLS (an equally-shaped translation-pair dataset):
36
+
37
+ * ``nl_conclusion`` / ``fol_conclusion`` carry ``"NL_sentence"`` /
38
+ ``"FOL_expression"``.
39
+ * ``nl_premises`` / ``fol_premises`` are always ``()``.
40
+ * ``label`` is always ``None``.
41
+
42
+ Does the gold notation actually parse under this kit? — an honest, measured
43
+ answer, not a guess
44
+ --------------------------------------------------------------------------
45
+ The dataset card states the FOL annotations were "aggressively filtered using
46
+ ``nltk.sem.logic``" to reject unparseable/unbound-variable formulas — but
47
+ ``nltk.sem.logic`` accepts several things this kit's ``fol`` grammar
48
+ deliberately does not, so "filtered by nltk" does not imply "parses here".
49
+ To answer this honestly rather than assume it, a STRATIFIED sample of 2100 of
50
+ the 16014 real ``train`` rows (row offsets 0, 100, ..., 900, then 1500, 3000,
51
+ ..., 15000, 15900 — spanning the whole file, fetched 2026-08-12 via the
52
+ ``rows`` API) was run through ``unicode_logic_kit.api.parse_any`` and, for
53
+ whatever parsed, ``unicode_logic_kit.api.check``:
54
+
55
+ * 2075 / 2100 (98.8%) parse AND pass ``check()`` (closed, arity-consistent,
56
+ lambda-free) outright.
57
+ * 25 / 2100 (1.2%) FAIL TO PARSE under every dialect ``parse_any`` tries. Two
58
+ recurring, real causes accounted for all 25:
59
+
60
+ 1. (~21/25) Mixing ``∧``/``∨`` (or ``∧``/``⊕``) at the same nesting depth
61
+ without disambiguating parentheses, e.g. row 148's
62
+ ``"∀y (Bird(y) ∧ Predator(y) ↔ (Hunt(y) ∨ EatMeat(y) ∧ ¬EatSeeds(y)))"``
63
+ (the ``Hunt(y) ∨ EatMeat(y) ∧ ¬EatSeeds(y)`` clause). This kit's ``fol``
64
+ grammar deliberately refuses to guess a precedence between ``∧``, ``∨``,
65
+ and ``⊕`` rather than silently pick one (see the parser's own
66
+ ``SYNTAX_ERROR`` hint text) — a real, structural difference from
67
+ ``nltk.sem.logic``'s (and most textbooks') convention of ``∧`` binding
68
+ tighter than ``∨``.
69
+ 2. (remaining few) A predicate identifier that does not start with an
70
+ uppercase ASCII letter, which this kit's ``fol`` grammar requires
71
+ (predicate names match ``[A-Z][a-zA-Z0-9]*``; a lower-initial identifier
72
+ is parsed as a TERM, not a predicate, so e.g. row 1554's
73
+ ``"∀x (Device(x) → (Android(x) ∨ iOS(x)))"`` fails because ``iOS`` starts
74
+ with a lowercase letter, and row 15071's ``"...∃y (Café(y) ∧ ...)"``
75
+ fails because ``é`` is outside the grammar's ASCII letter class for
76
+ predicate names.
77
+
78
+ * 1 / 2100 (0.05%) PARSES but fails ``check()``: row 15947,
79
+ ``"∃x (Chef(x) ∧ ∃y (Cook(y) ∧ ∀z (Meal(z) ∧ CookedBy(z, y) → Cook(x, z))))"``
80
+ uses the predicate ``Cook`` at TWO different arities in the same formula
81
+ (``Cook(y)`` unary, ``Cook(x, z)`` binary) — a genuine annotation defect,
82
+ not a kit limitation; ``check()`` reports it as an ``arity_conflict``.
83
+
84
+ None of this is hidden: the fixture below (``tests/fixtures/willow_mini.jsonl``)
85
+ deliberately includes real rows from BOTH failure classes above plus the one
86
+ ``check()``-only failure, and ``tests/test_datasets_willow.py`` asserts
87
+ ``audit_examples`` finds exactly them — so a caller loading the real 16014-row
88
+ file and running :func:`~unicode_logic_kit.eval.datasets.audit_examples` should
89
+ expect roughly this same ~98.8% clean rate, not 100%.
90
+
91
+ License — a discrepancy flagged, not silently resolved
92
+ --------------------------------------------------------
93
+ The iedeveci/WillowNLtoFOL Hugging Face card states, both in its YAML
94
+ frontmatter (``license: cc-by-4.0``) and in its "Licensing Information" prose
95
+ section: **CC-BY-4.0** ("You are free to share and adapt the material for any
96
+ purpose, even commercially, provided you give appropriate credit..."),
97
+ verified directly against the raw README.md on 2026-08-12.
98
+
99
+ This conflicts with a DIFFERENT, related dataset's card: fvossel/groves
100
+ (GROVES, which uses WillowNLtoFOL as one of two NL sources) states in its own
101
+ "Licensing" section that "WillowNLtoFOL ... originally licensed under
102
+ **CC BY-NC-ND 4.0**" (also verified 2026-08-12). This loader reads
103
+ iedeveci/WillowNLtoFOL directly, so :data:`~unicode_logic_kit.eval.datasets.DATASET_INFO`
104
+ below records what is VERIFIED AT THAT PRIMARY SOURCE TODAY (CC-BY-4.0) — not
105
+ the GROVES card's claim about an earlier state of it. The discrepancy (a more
106
+ restrictive license claimed elsewhere for what may be an earlier version of
107
+ the same data) is flagged here rather than silently resolved either way.
108
+ ADDITIONALLY, per this repository's author (stated 2026-08-12): explicit
109
+ usage permission for the GROVES/unicode-fol-kit use of WillowNLtoFOL (this
110
+ kit, under the name it had until 0.30.0) was
111
+ obtained from the Willow authors — so whichever of the two license readings
112
+ applies, this repository's own use (including the real-rows test fixture
113
+ below) is covered by that direct permission. Third parties redistributing
114
+ Willow data should still confirm the applicable license themselves.
115
+
116
+ This module never downloads anything — obtain the data yourself (e.g. via the
117
+ ``datasets`` library, ``load_dataset("iedeveci/WillowNLtoFOL", split="train").to_json("willow_train.jsonl")``,
118
+ which yields the one-JSON-object-per-line format this loader expects) and
119
+ pass its local JSONL path to :func:`load_willow`.
120
+ """
121
+
122
+ import json
123
+ import re
124
+ import unicodedata
125
+ from pathlib import Path
126
+ from typing import Dict, FrozenSet, Iterator, Optional, Tuple, Union
127
+
128
+ from lark import Lark, Transformer
129
+ from lark.exceptions import LarkError, VisitError
130
+
131
+ from ...fol.nodes import (
132
+ Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
133
+ Variable, Constant, Function, free_variables,
134
+ )
135
+ from ._base import DatasetExample, _register_dataset_info
136
+
137
+ __all__ = ["load_willow", "repair_willow_formula"]
138
+
139
+ _register_dataset_info(
140
+ "willow",
141
+ license=(
142
+ "CC-BY-4.0, per the iedeveci/WillowNLtoFOL Hugging Face card (raw "
143
+ "README.md frontmatter `license: cc-by-4.0` plus its 'Licensing "
144
+ "Information' section, verified 2026-08-12). NOTE: the fvossel/groves "
145
+ "dataset card (GROVES, built in part from WillowNLtoFOL) states this "
146
+ "same source was 'originally licensed under CC BY-NC-ND 4.0' -- a "
147
+ "more restrictive claim that conflicts with what iedeveci/WillowNLtoFOL "
148
+ "publishes today; this record reflects what is verified AT THAT "
149
+ "PRIMARY SOURCE, not the GROVES card's claim about an earlier state "
150
+ "of it. Explicit usage permission for the GROVES/unicode-fol-kit use "
151
+ "(this kit, under the name it had until 0.30.0) "
152
+ "was additionally obtained from the Willow authors (per this "
153
+ "repository's author, 2026-08-12). Third parties should confirm the "
154
+ "applicable license themselves before redistributing."
155
+ ),
156
+ source_url="https://huggingface.co/datasets/iedeveci/WillowNLtoFOL",
157
+ citation_hint=(
158
+ "Deveci, İbrahim Ethem. \"A Diagnostic Framework for "
159
+ "Compositional Generalization via NL-to-FOL Translation.\" Journal "
160
+ "of Logic, Language and Information (2026). "
161
+ "https://doi.org/10.1007/s10849-026-09480-0"
162
+ ),
163
+ )
164
+
165
+
166
+ # --------------------------------------------------------------------------- #
167
+ # The Willow repair dialect: an import-time grammar for the ~1.2% tail
168
+ #
169
+ # ~98.8% of WillowNLtoFOL's gold formulas already parse under this kit's own
170
+ # grammar (measured — see the module docstring), so the loader touches
171
+ # nothing for those. The measured failure tail has exactly two structural
172
+ # causes, and this dedicated grammar exists to read PRECISELY those:
173
+ #
174
+ # 1. ∧/∨/⊕ mixed at one nesting depth without parentheses. The kit's own
175
+ # grammar refuses to guess a precedence; Willow's formulas were filtered
176
+ # through ``nltk.sem.logic``, whose convention is that ∧ binds tighter
177
+ # than ∨ — so THIS grammar adopts the textbook/NLTK precedence
178
+ # ¬ > ∧ > ∨ > ⊕ > → (right-assoc) > ↔, i.e. the reading the dataset's
179
+ # own pipeline accepted. (∨-vs-⊕ mixing without parens never occurred
180
+ # in the measured sample; the relative order chosen here is documented,
181
+ # not observed.)
182
+ # 2. Predicate identifiers outside the kit's ``[A-Z][a-zA-Z0-9]*`` class
183
+ # (``iOS``, ``Café``). The converter renames minimally: NFKD
184
+ # transliteration for non-ASCII letters (``Café`` → ``Cafe``), then the
185
+ # first letter upcased (``iOS`` → ``IOS``) — injectively per namespace,
186
+ # with the mapping recorded in ``meta``.
187
+ #
188
+ # The loader (``convert_fol=True``, the default) first tries ``api.parse_any``
189
+ # — a formula the kit already accepts stays byte-for-byte verbatim — and only
190
+ # a formula the kit refuses goes through this repair grammar; whatever fails
191
+ # BOTH keeps its verbatim string plus a recorded ``fol_conversion_error``
192
+ # (e.g. a genuinely malformed formula), never a crash or a silent drop.
193
+ # --------------------------------------------------------------------------- #
194
+
195
+ _WILLOW_GRAMMAR = r"""
196
+ ?start: formula
197
+ ?formula: iff
198
+ ?iff: implies ("↔" implies)*
199
+ ?implies: xor "→" implies -> implies
200
+ | xor
201
+ ?xor: disj ("⊕" disj)*
202
+ ?disj: conj ("∨" conj)*
203
+ ?conj: unary ("∧" unary)*
204
+ ?unary: "¬" unary -> neg
205
+ | "∀" IDENT unary -> forall
206
+ | "∃" IDENT unary -> exists
207
+ | atom
208
+ | "(" formula ")"
209
+ atom: IDENT "(" term ("," term)* ")"
210
+ ?term: IDENT "(" term ("," term)* ")" -> func
211
+ | IDENT -> term_ident
212
+ IDENT: /[^\W\d]\w*/
213
+ %import common.WS
214
+ %ignore WS
215
+ """
216
+
217
+ _KIT_PREDICATE_RE = re.compile(r"[A-Z][a-zA-Z0-9]*\Z")
218
+ _KIT_CONSTANT_RE = re.compile(r"[a-z][a-zA-Z0-9]*[a-zA-Z][a-zA-Z0-9]*\Z")
219
+ _KIT_VARIABLE_RE = re.compile(r"[a-z][0-9]*\Z")
220
+
221
+
222
+ def _ascii_fold(name: str) -> str:
223
+ """NFKD-transliterate ``name`` to its ASCII-alnum core (``Café``→``Cafe``).
224
+
225
+ Raises ``ValueError`` when nothing usable survives — a name with no Latin
226
+ core has no honest ASCII rendering, and inventing one would make the
227
+ recorded mapping unreadable.
228
+ """
229
+ folded = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode()
230
+ folded = re.sub(r"[^A-Za-z0-9]", "", folded)
231
+ if not folded or folded[0].isdigit():
232
+ raise ValueError(
233
+ f"willow: identifier {name!r} has no usable ASCII-letter core "
234
+ "to transliterate to — refusing to invent a name.")
235
+ return folded
236
+
237
+
238
+ class _WillowNameConverter:
239
+ """Minimal, injective renaming into kit conventions (see dialect block)."""
240
+
241
+ def __init__(self) -> None:
242
+ self.pred_map: Dict[str, str] = {}
243
+ self.term_map: Dict[str, str] = {}
244
+ self._final: Dict[str, Dict[str, str]] = {"predicate": {}, "term": {}}
245
+
246
+ def _claim(self, namespace: str, source: str, target: str) -> str:
247
+ owner = self._final[namespace].setdefault(target, source)
248
+ if owner != source:
249
+ raise ValueError(
250
+ f"willow: {namespace} names {owner!r} and {source!r} would "
251
+ f"both end up as {target!r} — the renaming must stay "
252
+ "injective, so this formula cannot be repaired.")
253
+ return target
254
+
255
+ def predicate(self, name: str) -> str:
256
+ if _KIT_PREDICATE_RE.match(name):
257
+ return self._claim("predicate", name, name)
258
+ folded = _ascii_fold(name)
259
+ target = folded[0].upper() + folded[1:]
260
+ if not _KIT_PREDICATE_RE.match(target):
261
+ raise ValueError(
262
+ f"willow: predicate {name!r} does not repair to a legal kit "
263
+ f"predicate (got {target!r}).")
264
+ self._claim("predicate", name, target)
265
+ self.pred_map[name] = target
266
+ return target
267
+
268
+ def term_name(self, name: str) -> str:
269
+ """A constant or function name (both live in the kit's NAME space)."""
270
+ if _KIT_CONSTANT_RE.match(name) or _KIT_VARIABLE_RE.match(name):
271
+ return name
272
+ folded = _ascii_fold(name)
273
+ target = folded[0].lower() + folded[1:]
274
+ if _KIT_VARIABLE_RE.match(target):
275
+ raise ValueError(
276
+ f"willow: term name {name!r} would repair to {target!r}, "
277
+ "which the kit lexes as a VARIABLE — refusing the silent "
278
+ "semantics change.")
279
+ if not _KIT_CONSTANT_RE.match(target):
280
+ raise ValueError(
281
+ f"willow: term name {name!r} does not repair to a legal kit "
282
+ f"constant (got {target!r}).")
283
+ self._claim("term", name, target)
284
+ self.term_map[name] = target
285
+ return target
286
+
287
+ def mapping(self) -> Dict[str, Dict[str, str]]:
288
+ return {"predicates": dict(self.pred_map), "terms": dict(self.term_map)}
289
+
290
+
291
+ class _WillowTransformer(Transformer):
292
+ """Lark tree → kit :class:`~unicode_logic_kit.fol.nodes.Node`."""
293
+
294
+ def __init__(self, converter: _WillowNameConverter) -> None:
295
+ super().__init__()
296
+ self._conv = converter
297
+
298
+ def term_ident(self, children) -> Node:
299
+ name = str(children[0])
300
+ if _KIT_VARIABLE_RE.match(name):
301
+ return Variable(name)
302
+ return Constant(self._conv.term_name(name))
303
+
304
+ def func(self, children) -> Node:
305
+ name = self._conv.term_name(str(children[0]))
306
+ return Function(name, tuple(children[1:]))
307
+
308
+ def atom(self, children) -> Node:
309
+ pred = self._conv.predicate(str(children[0]))
310
+ return Atom(pred, tuple(children[1:]))
311
+
312
+ def neg(self, children) -> Node:
313
+ return Not(children[0])
314
+
315
+ def _binder(self, token) -> Variable:
316
+ name = str(token)
317
+ if not _KIT_VARIABLE_RE.match(name):
318
+ raise ValueError(
319
+ f"willow: quantified variable {name!r} is not a kit variable "
320
+ "token ([a-z][0-9]*) — refusing to rename a BOUND name.")
321
+ return Variable(name)
322
+
323
+ def forall(self, children) -> Node:
324
+ return Quantifier("∀", self._binder(children[0]), children[1])
325
+
326
+ def exists(self, children) -> Node:
327
+ return Quantifier("∃", self._binder(children[0]), children[1])
328
+
329
+ def _fold(self, children, cls) -> Node:
330
+ node = children[0]
331
+ for right in children[1:]:
332
+ node = cls(node, right)
333
+ return node
334
+
335
+ def conj(self, children) -> Node:
336
+ return self._fold(children, And)
337
+
338
+ def disj(self, children) -> Node:
339
+ return self._fold(children, Or)
340
+
341
+ def xor(self, children) -> Node:
342
+ return self._fold(children, Xor)
343
+
344
+ def implies(self, children) -> Node:
345
+ if len(children) == 1:
346
+ return children[0]
347
+ return Implies(children[0], children[1])
348
+
349
+ def iff(self, children) -> Node:
350
+ return self._fold(children, Iff)
351
+
352
+
353
+ _willow_parser = Lark(_WILLOW_GRAMMAR, parser="lalr")
354
+
355
+
356
+ def repair_willow_formula(text: str) -> Tuple[Node, Dict[str, Dict[str, str]]]:
357
+ """Parse one kit-refused Willow formula with the repair dialect grammar.
358
+
359
+ Returns ``(node, mapping)`` — the kit AST (NLTK-precedence reading, names
360
+ repaired into kit conventions) and the changed-names-only mapping. The
361
+ node is guaranteed CLOSED; a free variable raises.
362
+
363
+ Raises:
364
+ lark.exceptions.LarkError: ``text`` is outside even the repair
365
+ dialect (genuinely malformed).
366
+ ValueError: an identifier cannot be repaired without a collision or
367
+ a constant→variable semantics change, or the result is not
368
+ closed.
369
+ """
370
+ conv = _WillowNameConverter()
371
+ tree = _willow_parser.parse(text)
372
+ try:
373
+ node = _WillowTransformer(conv).transform(tree)
374
+ except VisitError as exc:
375
+ if isinstance(exc.orig_exc, ValueError):
376
+ raise exc.orig_exc from None
377
+ raise
378
+ free = free_variables(node)
379
+ if free:
380
+ raise ValueError(
381
+ f"willow: repaired formula has free variable(s) {sorted(free)!r} "
382
+ f"— refusing (source: {text!r}).")
383
+ return node, conv.mapping()
384
+
385
+
386
+ def _example_from_record(record: dict, line_no: int,
387
+ known_bad_ids: FrozenSet[str],
388
+ convert_fol: bool) -> DatasetExample:
389
+ nl = record.get("NL_sentence")
390
+ fol = record.get("FOL_expression")
391
+ # WillowNLtoFOL has no native id field (see module docstring); some
392
+ # downstream re-export might add one, so honour it opportunistically
393
+ # before falling back to a positional id (same convention as malls.py).
394
+ raw_id = record.get("id")
395
+ example_id = str(raw_id) if raw_id is not None else f"willow:{line_no}"
396
+
397
+ meta = {k: v for k, v in record.items()
398
+ if k not in ("NL_sentence", "FOL_expression", "id")}
399
+ meta["line_no"] = line_no
400
+
401
+ if convert_fol and fol is not None:
402
+ from ... import api # lazy: same cost/cycle rationale as _base
403
+
404
+ if not api.parse_any(fol).ok:
405
+ try:
406
+ node, mapping = repair_willow_formula(fol)
407
+ except (LarkError, ValueError) as exc:
408
+ # Honest per-example fallback: verbatim string kept, failure
409
+ # recorded — never swallowed, never a crash of the load.
410
+ meta["fol_conversion_error"] = f"{type(exc).__name__}: {exc}"
411
+ else:
412
+ meta["original_fol_conclusion"] = fol
413
+ meta["fol_name_mapping"] = mapping
414
+ fol = node.to_unicode_str()
415
+
416
+ return DatasetExample(
417
+ id=example_id,
418
+ nl_premises=(),
419
+ fol_premises=(),
420
+ nl_conclusion=nl,
421
+ fol_conclusion=fol,
422
+ label=None,
423
+ known_bad=example_id in known_bad_ids,
424
+ meta=meta,
425
+ )
426
+
427
+
428
+ def load_willow(path: Union[str, Path], *,
429
+ known_bad_ids: FrozenSet[str] = frozenset(),
430
+ convert_fol: bool = True) -> Iterator[DatasetExample]:
431
+ """Stream :class:`~unicode_logic_kit.eval.datasets.DatasetExample` from a
432
+ local WillowNLtoFOL JSONL file.
433
+
434
+ Args:
435
+ path: path to a local ``.jsonl`` file — one
436
+ ``{"NL_sentence": ..., "FOL_expression": ...}`` object per
437
+ non-blank line (see module docstring for converting the upstream
438
+ Hugging Face parquet distribution to this format). NEVER
439
+ downloaded by this function.
440
+ known_bad_ids: ids (the record's own ``"id"`` if present, else the
441
+ positional fallback ``f"willow:{line_no}"``) whose
442
+ ``"FOL_expression"`` translation is known to be broken. Every
443
+ yielded example with a matching id gets ``known_bad=True``.
444
+ Defaults to an empty set.
445
+ convert_fol: with the default ``True``, a formula the kit's own
446
+ grammar already accepts stays byte-for-byte verbatim, and ONLY a
447
+ kit-refused formula goes through the repair dialect grammar
448
+ (NLTK-precedence reading of unparenthesised ∧/∨/⊕ mixes, minimal
449
+ injective renaming of out-of-class identifiers — see the dialect
450
+ comment block); a repaired example records the verbatim original
451
+ in ``meta["original_fol_conclusion"]`` and the changed names in
452
+ ``meta["fol_name_mapping"]``, and a formula neither route can
453
+ read keeps its verbatim string plus
454
+ ``meta["fol_conversion_error"]``. ``False`` restores the raw
455
+ pass-through (~98.8% kit-parse rate, see module docstring).
456
+
457
+ Yields:
458
+ One :class:`~unicode_logic_kit.eval.datasets.DatasetExample` per
459
+ non-blank JSONL line, in file order, with ``nl_conclusion``/
460
+ ``fol_conclusion`` set from ``"NL_sentence"``/``"FOL_expression"`` and
461
+ ``nl_premises``/``fol_premises`` empty (see module docstring for why
462
+ this dataset maps onto the "conclusion" slot, matching
463
+ :func:`~unicode_logic_kit.eval.datasets.malls.load_malls`).
464
+
465
+ Raises:
466
+ FileNotFoundError: ``path`` does not exist.
467
+ json.JSONDecodeError: a non-blank line is not valid JSON — raised,
468
+ not swallowed (a malformed dataset file must fail loudly).
469
+ """
470
+ path = Path(path)
471
+ with path.open("r", encoding="utf-8") as fh:
472
+ for line_no, raw_line in enumerate(fh):
473
+ line = raw_line.strip()
474
+ if not line:
475
+ continue
476
+ record = json.loads(line)
477
+ yield _example_from_record(record, line_no, known_bad_ids,
478
+ convert_fol)