tensorcode 0.1.0a1__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 (53) hide show
  1. tensorcode/__init__.py +84 -0
  2. tensorcode/actions.py +137 -0
  3. tensorcode/answer_type.py +222 -0
  4. tensorcode/awareness.py +344 -0
  5. tensorcode/backends/__init__.py +0 -0
  6. tensorcode/backends/builtin.py +167 -0
  7. tensorcode/backends/hf_local.py +89 -0
  8. tensorcode/backends/linear.py +133 -0
  9. tensorcode/backends/neural.py +361 -0
  10. tensorcode/causal.py +262 -0
  11. tensorcode/change.py +566 -0
  12. tensorcode/chunking.py +195 -0
  13. tensorcode/cognition.py +311 -0
  14. tensorcode/context.py +97 -0
  15. tensorcode/control.py +291 -0
  16. tensorcode/cues.py +192 -0
  17. tensorcode/expectation.py +270 -0
  18. tensorcode/frames.py +232 -0
  19. tensorcode/language/__init__.py +36 -0
  20. tensorcode/language/chart.py +558 -0
  21. tensorcode/language/discourse.py +132 -0
  22. tensorcode/language/domains/__init__.py +0 -0
  23. tensorcode/language/domains/desktop.py +552 -0
  24. tensorcode/language/english.py +459 -0
  25. tensorcode/language/features.py +112 -0
  26. tensorcode/language/generate.py +574 -0
  27. tensorcode/language/grammar.py +893 -0
  28. tensorcode/language/semantics.py +349 -0
  29. tensorcode/learning/__init__.py +30 -0
  30. tensorcode/learning/certificate.py +148 -0
  31. tensorcode/learning/induce.py +304 -0
  32. tensorcode/learning/library.py +217 -0
  33. tensorcode/learning/literals.py +126 -0
  34. tensorcode/learning/verify.py +253 -0
  35. tensorcode/memory.py +303 -0
  36. tensorcode/metacognition.py +351 -0
  37. tensorcode/ops.py +207 -0
  38. tensorcode/outcomes.py +99 -0
  39. tensorcode/permanence.py +376 -0
  40. tensorcode/priming.py +191 -0
  41. tensorcode/py.typed +0 -0
  42. tensorcode/quantity.py +311 -0
  43. tensorcode/records.py +728 -0
  44. tensorcode/relation.py +771 -0
  45. tensorcode/runtime.py +471 -0
  46. tensorcode/semantics_bridge.py +308 -0
  47. tensorcode/social.py +380 -0
  48. tensorcode/temporal.py +189 -0
  49. tensorcode/wants.py +185 -0
  50. tensorcode-0.1.0a1.dist-info/METADATA +196 -0
  51. tensorcode-0.1.0a1.dist-info/RECORD +53 -0
  52. tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
  53. tensorcode-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,459 @@
1
+ """A core English grammar: function words, inflection, and the productions over them.
2
+
3
+ This is the part that is knowledge of *English* rather than of any domain. It
4
+ covers the constructions a cognitive program actually has to read:
5
+
6
+ * imperatives ("make a folder called recipes on my desktop"), questions (yes/no
7
+ and wh-), and declaratives;
8
+ * reported speech ("Anem said the north field failed"), which is the construction
9
+ that must never be flattened into the shared world;
10
+ * negation, tense and aspect, modality, quantifiers, coordination, comparatives;
11
+ * pronouns, left for :mod:`tensorcode.language.discourse` to resolve.
12
+
13
+ Content words live in the domain: a caller extends this grammar with its verbs,
14
+ nouns and names (``DESKTOP`` for the assistant; a village's drifting lexicon for
15
+ the simulation). Prepositions carry the *role* they mark as their meaning, so
16
+ "in downloads" and "to documents" share one production.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import math
22
+ from dataclasses import replace
23
+
24
+ from .grammar import (
25
+ Ask, Attach, Build, Coord, Ent, Entry, Grammar, Head, Lexicon, Lit, Locative, Merge, OpenClass, Order, Qualify,
26
+ production, words,
27
+ )
28
+ from .semantics import Entity
29
+
30
+ # --------------------------------------------------------------- function words
31
+
32
+ #: Articles, possessives and demonstratives mark definiteness, not quantity. Their
33
+ #: word contributes no meaning of its own, so no production reads their ``sem``.
34
+ DETERMINERS = (
35
+ *words("the", cat="Det", definite=True),
36
+ *words("a", "an", cat="Det", definite=False),
37
+ # marked determiners are dispreferred when saying something, so a meaning that
38
+ # only asks for definiteness comes out as "the" rather than "her"
39
+ *words("my", "your", "our", "his", "her", "their", "its", cat="Det", weight=-0.3, definite=True, possessive=True),
40
+ *words("this", "that", cat="Det", weight=-0.3, definite=True, demonstrative=True, number="singular"),
41
+ *words("these", "those", cat="Det", weight=-0.3, definite=True, demonstrative=True, number="plural"),
42
+ )
43
+
44
+ #: Quantifiers do contribute meaning: it becomes the entity's ``quantifier``.
45
+ QUANTIFIERS = (
46
+ *words("every", "each", cat="Quant", sem="all", number="singular"),
47
+ *words("all", "both", cat="Quant", sem="all", number="plural"),
48
+ *words("some", cat="Quant", sem="some"),
49
+ # "any" is polarity-sensitive — "any bread" belongs in a question or a negative — so
50
+ # it is the second way to say `some`, not the first
51
+ *words("any", cat="Quant", sem="some", weight=-0.3),
52
+ *words("no", cat="Quant", sem="none"),
53
+ *words("another", "other", cat="Quant", sem="other"),
54
+ # Amounts. Unlike `all`/`some`/`none`, these keep *one meaning per word* rather than
55
+ # a canonical one per group: which word a dialect reaches for is information its
56
+ # world uses ("heaps" against "much" is how drift shows up), and a speaker asked for
57
+ # "heaps" must not be given "much". Normalising them is the caller's business.
58
+ # A mass quantifier takes a singular mass noun ("much bread"), a count one a plural
59
+ # ("many loaves"), and the partitives need "of" — which is what `partitive` marks.
60
+ # ``amount`` marks the ones that need "of" to reach a determiner — "much of the
61
+ # food", never "much the food", where "all the food" is fine. ``partitive`` marks
62
+ # the stronger case: those cannot stand in front of a noun at all without "of".
63
+ *words("much", cat="Quant", sem="much", number="singular", amount=True),
64
+ *words("little", cat="Quant", sem="little", number="singular", amount=True),
65
+ *words("many", cat="Quant", sem="many", number="plural", amount=True),
66
+ *words("few", cat="Quant", sem="few", number="plural", amount=True),
67
+ *words("full", cat="Quant", sem="full", amount=True, weight=-0.1),
68
+ *words("plenty", cat="Quant", sem="plenty", amount=True, partitive=True),
69
+ *words("heaps", cat="Quant", sem="heaps", amount=True, partitive=True),
70
+ *words("lots", cat="Quant", sem="lots", amount=True, partitive=True),
71
+ *words("loads", cat="Quant", sem="loads", amount=True, partitive=True),
72
+ )
73
+
74
+ PRONOUNS = (
75
+ Entry("i", "Pron", {"person": 1, "number": "singular"}, Entity("pronoun", "i", {"person": 1})),
76
+ Entry("me", "Pron", {"person": 1, "number": "singular"}, Entity("pronoun", "me", {"person": 1})),
77
+ Entry("we", "Pron", {"person": 1, "number": "plural"}, Entity("pronoun", "we", {"person": 1})),
78
+ Entry("you", "Pron", {"person": 2}, Entity("pronoun", "you", {"person": 2})),
79
+ Entry("it", "Pron", {"person": 3, "number": "singular"}, Entity("pronoun", "it", {"animate": False})),
80
+ Entry("they", "Pron", {"person": 3, "number": "plural"}, Entity("pronoun", "they", {})),
81
+ Entry("them", "Pron", {"person": 3, "number": "plural"}, Entity("pronoun", "them", {})),
82
+ Entry("he", "Pron", {"person": 3, "number": "singular"}, Entity("pronoun", "he", {"animate": True, "gender": "m"})),
83
+ Entry("him", "Pron", {"person": 3, "number": "singular"}, Entity("pronoun", "him", {"animate": True, "gender": "m"})),
84
+ Entry("she", "Pron", {"person": 3, "number": "singular"}, Entity("pronoun", "she", {"animate": True, "gender": "f"})),
85
+ Entry("there", "Pron", {"person": 3, "locative": True}, Entity("pronoun", "there", {"locative": True})),
86
+ Entry("here", "Pron", {"person": 3, "locative": True}, Entity("pronoun", "here", {"locative": True})),
87
+ Entry("everyone", "Pron", {"person": 3, "number": "singular"}, Entity("quantified", "everyone", {"quantifier": "all", "animate": True})),
88
+ Entry("everybody", "Pron", {"person": 3, "number": "singular"}, Entity("quantified", "everybody", {"quantifier": "all", "animate": True})),
89
+ Entry("everything", "Pron", {"person": 3, "number": "singular"}, Entity("quantified", "everything", {"quantifier": "all"})),
90
+ Entry("someone", "Pron", {"person": 3, "number": "singular"}, Entity("quantified", "someone", {"quantifier": "some", "animate": True})),
91
+ Entry("something", "Pron", {"person": 3, "number": "singular"}, Entity("quantified", "something", {"quantifier": "some"})),
92
+ Entry("nobody", "Pron", {"person": 3, "number": "singular"}, Entity("quantified", "nobody", {"quantifier": "none", "animate": True})),
93
+ Entry("nothing", "Pron", {"person": 3, "number": "singular"}, Entity("quantified", "nothing", {"quantifier": "none"})),
94
+ )
95
+
96
+ AUXILIARIES = (
97
+ *words("is", "'s", cat="Aux", sem="be", tense="present", number="singular", person=3, copula=True),
98
+ *words("are", "'re", cat="Aux", sem="be", tense="present", number="plural", copula=True),
99
+ *words("am", "'m", cat="Aux", sem="be", tense="present", person=1, copula=True),
100
+ *words("was", cat="Aux", sem="be", tense="past", number="singular", copula=True),
101
+ *words("were", cat="Aux", sem="be", tense="past", number="plural", copula=True),
102
+ *words("be", cat="Aux", sem="be", copula=True),
103
+ *words("been", cat="Aux", sem="be", aspect="perfect", copula=True),
104
+ *words("do", cat="Aux", sem="do", tense="present"),
105
+ *words("does", cat="Aux", sem="do", tense="present", number="singular"),
106
+ *words("did", cat="Aux", sem="do", tense="past"),
107
+ *words("has", cat="Aux", sem="have", tense="present", number="singular", aspect="perfect"),
108
+ *words("have", cat="Aux", sem="have", tense="present", aspect="perfect"),
109
+ *words("had", cat="Aux", sem="have", tense="past", aspect="perfect"),
110
+ *words("will", "'ll", cat="Aux", sem="will", tense="future"),
111
+ *words("being", cat="Aux", sem="be", aspect="progressive", copula=True),
112
+ )
113
+
114
+ # the copular verb also appears as a main verb, which is what carries "is installed"
115
+ COPULAS = (
116
+ # "is" is precisely the third person singular present; leaving the person off it
117
+ # meant a demand for agreement could not find it, and the copula was chosen
118
+ # alphabetically ("Nise am hungry")
119
+ *words("is", "'s", cat="V", sem="be", tense="present", number="singular", person=3, copula=True),
120
+ *words("are", "'re", cat="V", sem="be", tense="present", number="plural", copula=True),
121
+ *words("am", "'m", cat="V", sem="be", tense="present", person=1, copula=True),
122
+ *words("was", cat="V", sem="be", tense="past", number="singular", copula=True),
123
+ *words("were", cat="V", sem="be", tense="past", number="plural", copula=True),
124
+ *words("be", cat="V", sem="be", copula=True),
125
+ )
126
+
127
+ #: A modal's second form *is* its past ("could" for "can"), so the tense sits on the
128
+ #: modal and the complement stays a bare infinitive — saying a past modal frame then
129
+ #: needs no past verb, which is what "ought gave" was reaching for. Only the forms
130
+ #: with a same-meaning present partner are marked: "should" has none in this lexicon,
131
+ #: so marking it would leave a plain ``modality=should`` unsayable. "will" is an
132
+ #: auxiliary above, where it carries the future. "ought" is not a word on its own: it
133
+ #: appears below as "ought to".
134
+ MODALS = (
135
+ *words("can", cat="Modal", sem="can"),
136
+ *words("could", cat="Modal", sem="can", tense="past"),
137
+ *words("may", cat="Modal", sem="may"),
138
+ *words("might", cat="Modal", sem="may", tense="past"),
139
+ *words("must", cat="Modal", sem="must"),
140
+ *words("shall", cat="Modal", sem="shall"),
141
+ *words("should", cat="Modal", sem="should"),
142
+ *words("would", cat="Modal", sem="would"),
143
+ # "should" and "would" are past forms with no present partner left in the language,
144
+ # so each is listed twice: tenseless, and as its own past. The past reading is
145
+ # dispreferred for reading (the plain modality is the commoner one) and is what
146
+ # generation finds when a frame carries both a modality and a tense — otherwise a
147
+ # past modal frame has no truthful way to be said at all.
148
+ Entry("should", "Modal", {"tense": "past"}, "should", -0.1),
149
+ Entry("would", "Modal", {"tense": "past"}, "would", -0.1),
150
+ )
151
+
152
+ NEGATIONS = (
153
+ *words("not", "n't", cat="Neg", sem="negative"),
154
+ # "never" reads as negation but does not *mean* only that — it quantifies over
155
+ # time — so it is the second spelling when saying a plain negative polarity
156
+ *words("never", cat="Neg", sem="negative", weight=-0.2),
157
+ )
158
+
159
+ CONJUNCTIONS = (
160
+ *words("and", cat="Conj", sem="and"),
161
+ *words("or", cat="Conj", sem="or"),
162
+ )
163
+
164
+ COMPLEMENTISERS = (
165
+ *words("that", "whether", "if", cat="Comp", sem=None),
166
+ )
167
+
168
+ #: A preposition's meaning *is* the role it marks.
169
+ PREPOSITIONS = (
170
+ *words("in", "inside", "on", "at", "within", "under", cat="P", sem="location"),
171
+ *words("into", "to", "onto", cat="P", sem="destination"),
172
+ *words("from", "out", cat="P", sem="source"),
173
+ *words("of", cat="P", sem="of"),
174
+ *words("for", cat="P", sem="beneficiary"),
175
+ *words("by", cat="P", sem="agent"),
176
+ *words("than", cat="P", sem="standard"),
177
+ *words("as", cat="P", sem="as"),
178
+ *words("about", cat="P", sem="topic"),
179
+ *words("with", cat="P", sem="content"),
180
+ *words("with", cat="P", sem="instrument", weight=-0.4),
181
+ *words("without", cat="P", sem="lacking"),
182
+ *words("after", cat="P", sem="after"),
183
+ *words("before", cat="P", sem="before"),
184
+ *words("called", "named", "titled", cat="P", sem="name"),
185
+ *words("containing", "saying", cat="P", sem="content"),
186
+ )
187
+
188
+ #: A wh-word's meaning is the role it asks about.
189
+ WH_WORDS = (
190
+ *words("what", "which", cat="Wh", sem="theme"),
191
+ *words("who", "whom", cat="Wh", sem="subject"),
192
+ *words("where", cat="Wh", sem="location"),
193
+ *words("when", cat="Wh", sem="time"),
194
+ *words("why", cat="Wh", sem="reason"),
195
+ *words("how", cat="Wh", sem="manner"),
196
+ )
197
+
198
+ def _verb(lemma: str, past: str | None = None, *, sem: str | None = None, **features: Any) -> list[Entry]:
199
+ """A verb's forms, each marked for what it *is*.
200
+
201
+ Listing "came" as a bare alternate of "come" is how generation produced "snow
202
+ cames": nothing said the form was already past, so the agreement rule applied to
203
+ it. A past form carries ``tense=past`` here, which both blocks further inflection
204
+ and lets the reader see the tense it was told.
205
+ """
206
+ sem = sem or lemma
207
+ out = [Entry(lemma, "V", dict(features), sem)]
208
+ if past is not None and past != lemma:
209
+ out.append(Entry(past, "V", {**features, "tense": "past"}, sem))
210
+ return out
211
+
212
+
213
+ CORE_VERBS = (
214
+ *_verb("say", "said", reports=True),
215
+ *_verb("tell", "told", reports=True),
216
+ *_verb("think", "thought", reports=True),
217
+ *_verb("believe", reports=True),
218
+ *_verb("claim", reports=True),
219
+ *_verb("promise", reports=True),
220
+ *_verb("ask", "asked", reports=True),
221
+ *_verb("want"),
222
+ *_verb("need"),
223
+ *_verb("have", "had"),
224
+ Entry("has", "V", {"tense": "present", "number": "singular", "person": 3}, "have"),
225
+ *_verb("do"), # "did" is the auxiliary above; as a main verb it read questions as orders
226
+ *_verb("give", "gave"),
227
+ *_verb("take", "took"),
228
+ *_verb("go", "went"),
229
+ *_verb("come", "came"),
230
+ *_verb("know", "knew"),
231
+ *_verb("see", "saw"),
232
+ *_verb("help"),
233
+ )
234
+
235
+ NUMBER_WORDS = tuple(
236
+ Entry(word, "Num", {"number": "plural" if value != 1 else "singular"}, value)
237
+ for word, value in (("one", 1), ("two", 2), ("three", 3), ("four", 4), ("five", 5),
238
+ ("six", 6), ("seven", 7), ("eight", 8), ("nine", 9), ("ten", 10))
239
+ )
240
+
241
+ #: Discourse connectives are cheap to skip rather than parsed: "and then" between
242
+ #: two imperatives carries sequencing that the cover already represents.
243
+ CONNECTIVE_BACKGROUND = {
244
+ "then": math.log(0.85), "also": math.log(0.8), "next": math.log(0.7), "and": math.log(0.5),
245
+ "please": math.log(0.9), "now": math.log(0.6), "just": math.log(0.6), "ok": math.log(0.7),
246
+ "okay": math.log(0.7), "so": math.log(0.6), "afterwards": math.log(0.7), "finally": math.log(0.6),
247
+ ".": math.log(0.95), ",": math.log(0.95), "?": math.log(0.95), "!": math.log(0.95), ";": math.log(0.95),
248
+ ":": math.log(0.9), "'": math.log(0.9), "the": math.log(0.3), "of": math.log(0.3), "for": math.log(0.3),
249
+ }
250
+
251
+ def _spelling(entry: Entry) -> Entry:
252
+ """Make a contraction the second choice of two spellings of one word.
253
+
254
+ "'ll" and "n't" read as readily as "will" and "not" — that is the point of listing
255
+ them — but saying a meaning should reach for the written form, and generation
256
+ breaks ties by score. Without this the grammar said "the field 'll fail".
257
+ """
258
+ if entry.word.startswith("'") or entry.word == "n't":
259
+ return replace(entry, weight=entry.weight - 0.1)
260
+ return entry
261
+
262
+
263
+ ENGLISH_LEXICON = Lexicon(
264
+ entries={},
265
+ background=CONNECTIVE_BACKGROUND,
266
+ unseen_background=math.log(2e-3),
267
+ ).extend(*(_spelling(e) for e in (
268
+ *DETERMINERS, *QUANTIFIERS, *PRONOUNS, *AUXILIARIES, *COPULAS, *MODALS, *NEGATIONS, *CONJUNCTIONS,
269
+ *COMPLEMENTISERS, *PREPOSITIONS, *WH_WORDS, *CORE_VERBS, *NUMBER_WORDS,
270
+ )))
271
+
272
+
273
+ # ---------------------------------------------------------------- productions
274
+
275
+ #: Multiword function words: "no one" is one pronoun, not a number called "one".
276
+ MULTIWORD_FUNCTION = [
277
+ production('Pron -> "no" "one"', Lit(Entity("quantified", "no one", {"quantifier": "none", "animate": True})), weight=0.5),
278
+ production('Pron -> "every" "one"', Lit(Entity("quantified", "everyone", {"quantifier": "all", "animate": True})), weight=0.5),
279
+ production('Pron -> "any" "one"', Lit(Entity("quantified", "anyone", {"quantifier": "some", "animate": True})), weight=0.5),
280
+ # "ought" is only a modal with its "to"; without it there is no such word, which is
281
+ # why generation must not be able to reach for one
282
+ production('Modal -> "ought" "to"', Lit("should"), weight=0.5),
283
+ ]
284
+
285
+ NOUN_PHRASES = [
286
+ # a common noun denotes a description; adjectives and PPs refine it
287
+ production("NBAR[number=?n] -> N[number=?n]", Ent("description", words_from=(0,), features_from=(("noun", 0),), lift=(("number", 0, "number"),))),
288
+ production("NBAR[number=?n] -> Adj NBAR[number=?n]", Qualify(1, features_from=(("quality", 0),), extend_text_from=(0,))),
289
+ # a nominal compound names the thing: "recipes folder", "meeting notes.txt"
290
+ production("NBAR[number=?n] -> Name NBAR[number=?n]", Qualify(1, features_from=(("name", 0),), extend_text_from=(0,)), weight=-0.1),
291
+ # a noun can modify a noun: "north field", "grain store"
292
+ production("NBAR[number=?n] -> N NBAR[number=?n]", Qualify(1, features_from=(("quality", 0),), extend_text_from=(0,)), weight=-0.2),
293
+ production("NBAR[number=?n] -> Literal NBAR[number=?n]", Qualify(1, features_from=(("name", 0),), extend_text_from=(0,)), weight=-0.1),
294
+ # head first, name after: "folder photos", "file ideas.md"
295
+ production("NBAR[number=?n] -> NBAR[number=?n] Name", Qualify(0, features_from=(("name", 1),), extend_text_from=(1,)), weight=-0.2),
296
+ production("NBAR[number=?n] -> NBAR[number=?n] Path", Qualify(0, features_from=(("name", 1),), extend_text_from=(1,)), weight=-0.2),
297
+ production("NBAR[number=?n] -> NBAR[number=?n] PP", Attach(0, 1)),
298
+ # "my downloads folder": a place-noun modifier names the place
299
+ production("NBAR[number=?n] -> N[place=true] NBAR[number=?n]", Head(0), weight=-0.15),
300
+ production("NP[number=?n] -> NBAR[number=?n]", Head(0)),
301
+ production("NP[number=?n] -> Det[number=?n] NBAR[number=?n]", Qualify(1, lift=(("definite", 0, "definite"), ("demonstrative", 0, "demonstrative"), ("possessive", 0, "possessive")))),
302
+ production("NP[number=?n] -> Det NBAR[number=?n]", Qualify(1, lift=(("definite", 0, "definite"), ("demonstrative", 0, "demonstrative"), ("possessive", 0, "possessive"))), weight=-0.1),
303
+ # Quantifiers, in the five shapes English allows. The quantifier word stays in the
304
+ # entity's text as an adjective's does, so a caller that reads the text to find the
305
+ # amount ("much food") keeps finding it there.
306
+ production("NP[number=?n] -> Quant[number=?n,partitive=!] NBAR[number=?n]",
307
+ Qualify(1, features_from=(("quantifier", 0),), extend_text_from=(0,))),
308
+ production("NP[number=?n] -> Quant[partitive=!] NBAR[number=?n]",
309
+ Qualify(1, features_from=(("quantifier", 0),), extend_text_from=(0,)), weight=-0.1),
310
+ production("NP[number=?n] -> Quant[amount=!] Det NBAR[number=?n]",
311
+ Qualify(2, features_from=(("quantifier", 0),), extend_text_from=(0,), lift=(("definite", 1, "definite"),)), weight=-0.1),
312
+ # the partitive: "heaps of bread" said bare says nothing at all — `realize` returned
313
+ # None for it, and a dialect whose word for *much* is "heaps" fell back to "heaps
314
+ # bread". An amount reaches a determiner the same way: "much of the food".
315
+ production('NP[number=?n] -> Quant[partitive=true] "of" NBAR[number=?n]',
316
+ Qualify(2, features_from=(("quantifier", 0),), extend_text_from=(0,))),
317
+ production('NP[number=?n] -> Quant[amount=true] "of" Det NBAR[number=?n]',
318
+ Qualify(3, features_from=(("quantifier", 0),), extend_text_from=(0,), lift=(("definite", 2, "definite"),))),
319
+ # Tolerant in, strict out. "plenty food" drops an "of" and "much of food" keeps one
320
+ # it does not need; both are understood, and marked ``nonstandard`` so a hearer can
321
+ # see the reading was repaired. Generation cannot use these *because* of the mark —
322
+ # a production may not state a feature the meaning lacks — so it says "plenty of
323
+ # food" and "much food". The second shape is what a dialect canonicaliser produces
324
+ # when it swaps "heaps" for "much" and leaves the "of" behind.
325
+ production("NP[number=?n] -> Quant[partitive=true] NBAR[number=?n]",
326
+ Qualify(1, features_from=(("quantifier", 0),), features=(("nonstandard", True),),
327
+ extend_text_from=(0,)), weight=-0.4),
328
+ production('NP[number=?n] -> Quant[partitive=!,amount=true] "of" NBAR[number=?n]',
329
+ Qualify(2, features_from=(("quantifier", 0),), features=(("nonstandard", True),),
330
+ extend_text_from=(0,)), weight=-0.4),
331
+ production("NP[number=?n] -> Pron[number=?n]", Head(0)),
332
+ production("NP -> Name", Head(0)),
333
+ production("NP -> Path", Head(0)),
334
+ production("NP -> Literal", Head(0)),
335
+ production("NP -> Command", Head(0)),
336
+ production("NP[number=plural] -> Num NBAR", Qualify(1, features_from=(("count", 0),))),
337
+ production("NP -> Num", Ent("number", words_from=(0,), features_from=(("value", 0),))),
338
+ production("NP -> NP PP", Attach(0, 1), weight=-0.2),
339
+ production("NP[number=plural] -> NP Conj NP", Coord((0, 2))),
340
+ production("NP[number=plural] -> NP \",\" NP", Coord((0, 2)), weight=-0.35),
341
+ production("PP -> P NP", Build(predicate="_pp", roles=(("role", 0), ("value", 1)))),
342
+ production("PP -> P Literal", Build(predicate="_pp", roles=(("role", 0), ("value", 1)))),
343
+ ]
344
+
345
+ VERB_PHRASES = [
346
+ production("VP[number=?n] -> V[number=?n]", Build(predicate_from=0, lift=(("tense", 0, "tense"), ("aspect", 0, "aspect")))),
347
+ production("VP[number=?n] -> V[number=?n] NP", Build(predicate_from=0, roles=(("object", 1),), lift=(("tense", 0, "tense"), ("aspect", 0, "aspect")))),
348
+ # a double object is rarer than a verb with one object and a modifier, and letting
349
+ # it compete evenly turns "make a folder on my desktop" into "make the desktop a folder"
350
+ # a verb marked ditransitive takes "mv a.txt b.txt" as source and destination
351
+ production("VP[number=?n] -> V[number=?n,ditrans=true] NP NP", Build(predicate_from=0, roles=(("object", 1), ("destination", 2)), lift=(("tense", 0, "tense"),))),
352
+ production("VP[number=?n] -> V[number=?n] NP NP", Build(predicate_from=0, roles=(("recipient", 1), ("object", 2)), lift=(("tense", 0, "tense"), ("aspect", 0, "aspect"))), weight=-0.6),
353
+ production("VP -> VP PP", Attach(0, 1)),
354
+ # a bare locative pronoun modifies the verb: "put it there", "init a repo there"
355
+ production("VP -> VP Pron[locative=true]", Qualify(0, roles_from=(("location", 1),))),
356
+ # a dative "show me X" adds a recipient, and must not compete with a double object
357
+ production("VP[number=?n] -> V[number=?n] Pron[person=1] NP", Build(predicate_from=0, roles=(("recipient", 1), ("object", 2)), lift=(("tense", 0, "tense"),)), weight=-0.1),
358
+ # reported speech: the complement clause stays a frame of its own
359
+ production("VP[number=?n] -> V[number=?n,reports=true] S", Build(predicate_from=0, roles=(("content", 1),), lift=(("tense", 0, "tense"),)), weight=0.8),
360
+ production("VP[number=?n] -> V[number=?n,reports=true] Comp S", Build(predicate_from=0, roles=(("content", 2),), lift=(("tense", 0, "tense"),)), weight=0.8),
361
+ # a reporting verb with a *named* hearer is rarer than one that just reports, and
362
+ # letting them compete evenly splits "said the north field failed" into a dative
363
+ production("VP[number=?n] -> V[number=?n,reports=true] NP S", Build(predicate_from=0, roles=(("recipient", 1), ("content", 2)), lift=(("tense", 0, "tense"),)), weight=0.3),
364
+ production("VP[number=?n] -> V[number=?n,reports=true] NP Comp S", Build(predicate_from=0, roles=(("recipient", 1), ("content", 3)), lift=(("tense", 0, "tense"),)), weight=0.8),
365
+ # auxiliaries and negation
366
+ # an auxiliary is there to support a verb, so prefer that over taking a noun phrase
367
+ production("VP[number=?n] -> Aux[number=?n] VP", Qualify(1, lift=(("tense", 0, "tense"), ("aspect", 0, "aspect"))), weight=0.3),
368
+ # the perfect takes a participle: for a regular verb that is the -ed form, which is
369
+ # what lets "has arrived" be both read and said
370
+ production("VP[number=?n] -> Aux[number=?n,aspect=perfect] VP[tense=past]", Qualify(1, features=(("aspect", "perfect"),), lift=(("tense", 0, "tense"),)), weight=0.4),
371
+ production("VP[number=?n] -> Aux[number=?n] Neg VP", Qualify(2, features=(("polarity", "negative"),), lift=(("tense", 0, "tense"), ("aspect", 0, "aspect")))),
372
+ # a modal's complement is a bare infinitive: ``VP[tense=!]`` forbids the tense, so
373
+ # neither the parser nor the generator can put "ought" next to "gave". A tensed
374
+ # modal frame is said by the modal's own past form ("should give"), which is the
375
+ # second production: it lifts the tense onto the modal the way an auxiliary does.
376
+ production("VP -> Modal VP[tense=!]", Qualify(1, features_from=(("modality", 0),))),
377
+ production("VP -> Modal[tense=?t] VP[tense=!]",
378
+ Qualify(1, features_from=(("modality", 0),), lift=(("tense", 0, "tense"),)), weight=0.1),
379
+ production("VP -> Modal Neg VP[tense=!]", Qualify(2, features=(("polarity", "negative"),), features_from=(("modality", 0),))),
380
+ production("VP[number=?n] -> V[number=?n] Neg VP", Qualify(2, features=(("polarity", "negative"),), lift=(("tense", 0, "tense"),)), weight=-0.3),
381
+ # copular predication: "is installed", "is better than the south field"
382
+ production("VP[number=?n] -> Aux[number=?n,copula=true] AP", Merge(1, lift=(("tense", 0, "tense"),))),
383
+ production("VP[number=?n] -> Aux[number=?n,copula=true] Neg AP", Merge(2, features=(("polarity", "negative"),), lift=(("tense", 0, "tense"),))),
384
+ production("VP[number=?n] -> Aux[number=?n,copula=true] PP", Build(predicate="located", lift=(("tense", 0, "tense"),)), weight=-0.1),
385
+ # "is not trustworthy": the copula negates in place. Without this the only way to
386
+ # say a negated ``be(x, y)`` was to negate a *clause* — "am never be trustworthy",
387
+ # two copulas and a temporal quantifier doing the work of one "not".
388
+ production("VP[number=?n] -> V[number=?n,copula=true] Neg NP",
389
+ Build(predicate_from=0, roles=(("object", 2),), features=(("polarity", "negative"),),
390
+ lift=(("tense", 0, "tense"),)), weight=0.2),
391
+ production("VP -> VP Conj VP", Merge(0), weight=-0.6),
392
+ # an adjective phrase predicates something, and carries the degree of its adjective
393
+ production("AP -> Adj", Build(predicate_from=0, lift=(("degree", 0, "degree"),))),
394
+ production("AP -> Adj PP", Attach(Build(predicate_from=0, lift=(("degree", 0, "degree"),)), 1)),
395
+ production("AP -> AP PP", Attach(0, 1)),
396
+ ]
397
+
398
+ CLAUSES = [
399
+ production("S -> NP[number=?n] VP[number=?n]", Merge(1, roles=(("subject", 0),), features=(("mood", "declarative"),))),
400
+ production("S -> NP VP", Merge(1, roles=(("subject", 0),), features=(("mood", "declarative"),)), weight=-0.2),
401
+ production("S -> Comp S", Head(1), weight=-0.3),
402
+ # an imperative is a bare VP, so the imperative reading is *ranked* against the
403
+ # declarative one rather than chosen by a keyword
404
+ production("IMP -> VP", Order(0), weight=-0.05),
405
+ ]
406
+
407
+ #: A question is a clause with something fronted. ``QC`` is that clause, without a
408
+ #: mood, so the fronted auxiliary can contribute tense to it before it is asked.
409
+ QUESTIONS = [
410
+ production("QC[number=?n] -> NP[number=?n] VP[number=?n]", Merge(1, roles=(("subject", 0),))),
411
+ production("QC[number=?n] -> NP[number=?n] AP", Merge(1, roles=(("subject", 0),))),
412
+ production("QC -> NP PP", Locative("located", modifier=1, theme=0)),
413
+ # a bare noun phrase is a question's *subject* only after a wh-word ("who am i").
414
+ # Allowing it as a whole yes/no clause read "the field did not fail" as a polarity
415
+ # question about a thing called "fail", which asserts nothing and reifies nothing.
416
+ production("QSUBJ -> NP", Build(predicate="be", roles=(("subject", 0),))),
417
+ # yes/no questions
418
+ production("Q -> Aux QC", Ask(Qualify(1, lift=(("tense", 0, "tense"), ("aspect", 0, "aspect"))), asked="polarity")),
419
+ production("Q -> Aux Neg QC", Ask(Qualify(2, features=(("polarity", "negative"),), lift=(("tense", 0, "tense"),)), asked="polarity")),
420
+ production("Q -> Modal QC", Ask(Qualify(1, features_from=(("modality", 0),)), asked="polarity")),
421
+ # wh-questions: the wh-word names the role being asked about
422
+ production("Q -> Wh VP", Ask(1, asked_from=0), weight=-0.05),
423
+ production("Q -> Wh Aux QC", Ask(Qualify(2, lift=(("tense", 1, "tense"),)), asked_from=0), weight=-0.05),
424
+ production("Q -> Wh Aux QSUBJ", Ask(Qualify(2, lift=(("tense", 1, "tense"),)), asked_from=0), weight=-0.1),
425
+ production("Q -> Wh Aux PP", Ask(Locative("located", modifier=2, lift=(("tense", 1, "tense"),)), asked_from=0)),
426
+ production("Q -> Wh Aux NP PP", Ask(Locative("located", modifier=3, theme=2, lift=(("tense", 1, "tense"),)), asked_from=0), weight=-0.1),
427
+ production("Q -> Wh NP VP", Ask(Merge(2, roles=(("subject", 1),)), asked_from=0), weight=-0.15),
428
+ production("Q -> Wh Num NBAR PP", Ask(Locative("located", modifier=3, theme=Ent("description", words_from=(2,), features_from=(("noun", 2), ("count", 1)))), asked="count"), weight=-0.2),
429
+ ]
430
+
431
+ ENGLISH = Grammar(
432
+ productions=tuple(MULTIWORD_FUNCTION + NOUN_PHRASES + VERB_PHRASES + CLAUSES + QUESTIONS),
433
+ lexicon=ENGLISH_LEXICON,
434
+ start=("S", "Q", "IMP"),
435
+ open_class=(
436
+ OpenClass(r"~(?:/.*)?|/[\w.@+\-/]+|[\w@+\-.]+/[\w./@+\-]*", "Path", "path"),
437
+ OpenClass(r"[\w@+\-]+\.[A-Za-z0-9]{1,8}", "Path", "path"),
438
+ OpenClass(r"`[^`]*`", "Command", "command"),
439
+ OpenClass(r"'[^']*'|\"[^\"]*\"|“[^”]*”|‘[^’]*’", "Literal", "literal"),
440
+ OpenClass(r"\d+(?:\.\d+)?", "Num", "number"),
441
+ # Any unknown word may be a name — a folder or a person can be called
442
+ # anything — and, productively, a noun, a verb or an adjective. Without the
443
+ # last three an unseen content word has nowhere to go and takes its whole
444
+ # clause with it: no fixed lexicon holds the words villagers use. The weights
445
+ # keep every real entry ahead of a guess, and each guess is marked.
446
+ # A capitalised unknown word is probably a proper name; a lowercase one is
447
+ # more likely a common noun or a verb, and treating it as a name first is what
448
+ # made "the north field failed" parse as one long noun phrase.
449
+ OpenClass(r"[A-Z][\w'\-]*", "Name", "name", weight=-0.2),
450
+ OpenClass(r"[a-z][\w'\-]*", "Name", "name", weight=-0.9),
451
+ OpenClass(r"[A-Za-z][\w'\-]*", "N", weight=-1.0, sem="word", morphology=True, bare_weight=-1.1),
452
+ # a verb is cheap when its suffix says so ("failed") and dear when nothing
453
+ # marks it ("field"), which is how the clause finds its verb
454
+ OpenClass(r"[A-Za-z][\w'\-]*", "V", weight=-0.9, sem="word", morphology=True, bare_weight=-2.2),
455
+ OpenClass(r"[A-Za-z][\w'\-]*", "Adj", weight=-1.0, sem="word", morphology=True, bare_weight=-1.4),
456
+ ),
457
+ name="english-core",
458
+ )
459
+ """The core grammar. Domains extend it; nothing here names a domain."""
@@ -0,0 +1,112 @@
1
+ """Feature structures and unification: the one mechanism the grammar agrees on.
2
+
3
+ A category is a name plus a feature structure. Agreement, subcategorisation,
4
+ gaps and semantic plumbing are all expressed by unifying those structures, so
5
+ the parser needs exactly one operation and the grammar needs no side channels.
6
+
7
+ Values are deliberately small: atoms (``str``, ``int``, ``bool``, ``None``),
8
+ variables, tuples, and nested structures. No lists, no mutation, no cyclic
9
+ terms, so unification terminates without an occurs check on recursive bindings
10
+ and a bound structure can be hashed and cached.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from typing import Any, Mapping
17
+
18
+ Bindings = dict[str, Any]
19
+
20
+
21
+ @dataclass(frozen=True, order=True)
22
+ class FVar:
23
+ """A feature variable, written ``?name`` in a production string."""
24
+
25
+ name: str
26
+
27
+ def __str__(self) -> str:
28
+ return f"?{self.name}"
29
+
30
+
31
+ def unify(a: Any, b: Any, bindings: Bindings | None = None) -> Bindings | None:
32
+ """Unify two values under ``bindings``; ``None`` means they cannot agree.
33
+
34
+ ``bindings`` is never mutated: a caller can try an alternative without
35
+ undoing anything.
36
+ """
37
+ out = dict(bindings or {})
38
+ return out if _unify_into(a, b, out) else None
39
+
40
+
41
+ def _unify_into(a: Any, b: Any, out: Bindings) -> bool:
42
+ # ``type(x) is dict`` rather than ``isinstance(x, Mapping)``: the typing ABC check
43
+ # cost ~2 s of a 4.8 s benchmark run, and every feature structure here is a dict.
44
+ if type(a) is FVar:
45
+ a = resolve(a, out)
46
+ if type(b) is FVar:
47
+ b = resolve(b, out)
48
+ if type(a) is FVar:
49
+ out[a.name] = b
50
+ return True
51
+ if type(b) is FVar:
52
+ out[b.name] = a
53
+ return True
54
+ if type(a) is dict and type(b) is dict:
55
+ if not a or not b:
56
+ return True
57
+ for key, value in a.items():
58
+ other = b.get(key, _ABSENT)
59
+ if other is not _ABSENT and not _unify_into(value, other, out):
60
+ return False
61
+ return True
62
+ if type(a) is tuple and type(b) is tuple:
63
+ return len(a) == len(b) and all(_unify_into(x, y, out) for x, y in zip(a, b))
64
+ return a == b
65
+
66
+
67
+ class _Absent:
68
+ __slots__ = ()
69
+
70
+
71
+ _ABSENT = _Absent()
72
+
73
+
74
+ def resolve(value: Any, bindings: Bindings) -> Any:
75
+ """Follow variable bindings one value deep (chains are followed to the end)."""
76
+ if type(value) is not FVar or not bindings:
77
+ return value
78
+ seen: set[str] = set()
79
+ while type(value) is FVar and value.name in bindings and value.name not in seen:
80
+ seen.add(value.name)
81
+ value = bindings[value.name]
82
+ return value
83
+
84
+
85
+ def ground(value: Any, bindings: Bindings) -> Any:
86
+ """Apply bindings throughout a value, leaving unbound variables in place."""
87
+ if not bindings:
88
+ return value
89
+ value = resolve(value, bindings)
90
+ if type(value) is dict:
91
+ return {k: ground(v, bindings) for k, v in value.items()}
92
+ if type(value) is tuple:
93
+ return tuple(ground(v, bindings) for v in value)
94
+ return value
95
+
96
+
97
+ def merge(base: Mapping[str, Any], extra: Mapping[str, Any]) -> dict[str, Any]:
98
+ """``extra`` wins; used to specialise a lexical entry or a category."""
99
+ out = dict(base)
100
+ out.update(extra)
101
+ return out
102
+
103
+
104
+ def rename(value: Any, suffix: str) -> Any:
105
+ """Freshen variables so two uses of one production do not share them."""
106
+ if isinstance(value, FVar):
107
+ return FVar(f"{value.name}#{suffix}")
108
+ if isinstance(value, Mapping):
109
+ return {k: rename(v, suffix) for k, v in value.items()}
110
+ if isinstance(value, tuple):
111
+ return tuple(rename(v, suffix) for v in value)
112
+ return value