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.
- tensorcode/__init__.py +84 -0
- tensorcode/actions.py +137 -0
- tensorcode/answer_type.py +222 -0
- tensorcode/awareness.py +344 -0
- tensorcode/backends/__init__.py +0 -0
- tensorcode/backends/builtin.py +167 -0
- tensorcode/backends/hf_local.py +89 -0
- tensorcode/backends/linear.py +133 -0
- tensorcode/backends/neural.py +361 -0
- tensorcode/causal.py +262 -0
- tensorcode/change.py +566 -0
- tensorcode/chunking.py +195 -0
- tensorcode/cognition.py +311 -0
- tensorcode/context.py +97 -0
- tensorcode/control.py +291 -0
- tensorcode/cues.py +192 -0
- tensorcode/expectation.py +270 -0
- tensorcode/frames.py +232 -0
- tensorcode/language/__init__.py +36 -0
- tensorcode/language/chart.py +558 -0
- tensorcode/language/discourse.py +132 -0
- tensorcode/language/domains/__init__.py +0 -0
- tensorcode/language/domains/desktop.py +552 -0
- tensorcode/language/english.py +459 -0
- tensorcode/language/features.py +112 -0
- tensorcode/language/generate.py +574 -0
- tensorcode/language/grammar.py +893 -0
- tensorcode/language/semantics.py +349 -0
- tensorcode/learning/__init__.py +30 -0
- tensorcode/learning/certificate.py +148 -0
- tensorcode/learning/induce.py +304 -0
- tensorcode/learning/library.py +217 -0
- tensorcode/learning/literals.py +126 -0
- tensorcode/learning/verify.py +253 -0
- tensorcode/memory.py +303 -0
- tensorcode/metacognition.py +351 -0
- tensorcode/ops.py +207 -0
- tensorcode/outcomes.py +99 -0
- tensorcode/permanence.py +376 -0
- tensorcode/priming.py +191 -0
- tensorcode/py.typed +0 -0
- tensorcode/quantity.py +311 -0
- tensorcode/records.py +728 -0
- tensorcode/relation.py +771 -0
- tensorcode/runtime.py +471 -0
- tensorcode/semantics_bridge.py +308 -0
- tensorcode/social.py +380 -0
- tensorcode/temporal.py +189 -0
- tensorcode/wants.py +185 -0
- tensorcode-0.1.0a1.dist-info/METADATA +196 -0
- tensorcode-0.1.0a1.dist-info/RECORD +53 -0
- tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
- tensorcode-0.1.0a1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
"""Saying a meaning out loud, with the grammar that reads it.
|
|
2
|
+
|
|
3
|
+
Generation inverts each production's semantic spec: given the mother's meaning,
|
|
4
|
+
work out what each daughter would have to mean, then realise the daughters. The
|
|
5
|
+
specs are data, so this is a search over the *same* productions the parser used
|
|
6
|
+
rather than a second grammar that has to be kept in step — the failure mode this
|
|
7
|
+
module exists to avoid is a speaker and a listener that quietly disagree.
|
|
8
|
+
|
|
9
|
+
Coverage is partial by construction: a production whose spec cannot be inverted
|
|
10
|
+
is skipped, and if nothing realises the meaning the caller gets ``None`` rather
|
|
11
|
+
than an approximation. :func:`round_trip` is the test that matters — say it, read
|
|
12
|
+
it back, and check the meaning survived.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any, Iterable, Mapping, Sequence
|
|
19
|
+
|
|
20
|
+
from .grammar import (
|
|
21
|
+
ABSENT, Ask, Attach, Build, Cat, Coord, Ent, Entry, FVar, Grammar, Head, Lit, Locative, Merge, Order, Production,
|
|
22
|
+
Qualify, Terminal, inflect,
|
|
23
|
+
)
|
|
24
|
+
from .semantics import Entity, Frame, Question, Request
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Any_:
|
|
28
|
+
"""A daughter whose meaning the mother does not constrain (a determiner, say)."""
|
|
29
|
+
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
return "ANY"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ANY = Any_()
|
|
35
|
+
|
|
36
|
+
#: features that reach the meaning through inflection or a function word, and so
|
|
37
|
+
#: must be demanded of a daughter rather than produced by a production
|
|
38
|
+
GRAMMATICAL = ("tense", "aspect", "number", "degree", "person")
|
|
39
|
+
|
|
40
|
+
#: what a subject imposes on its verb. English marks agreement on the verb, so these
|
|
41
|
+
#: are demanded of a verbal daughter and never of the noun phrase that supplied them.
|
|
42
|
+
AGREEMENT = ("number", "person")
|
|
43
|
+
|
|
44
|
+
#: categories that can express agreement. Demanding person of a name would make the
|
|
45
|
+
#: name unsayable, since no rule inflects one.
|
|
46
|
+
AGREES = ("V", "Aux", "VP")
|
|
47
|
+
|
|
48
|
+
#: The one feature a production may state without the meaning carrying it. Declarative
|
|
49
|
+
#: is the unmarked mood, so a bare frame is a statement. Every other feature a
|
|
50
|
+
#: production states — a negation, a perfect, a repair mark — must be in the meaning, or
|
|
51
|
+
#: generation invents it: that is how "no one 'm share grain" happened, and it is what
|
|
52
|
+
#: lets a production be readable but unsayable ("plenty food").
|
|
53
|
+
DEFAULTED = ("mood",)
|
|
54
|
+
|
|
55
|
+
#: features a word *asserts* by carrying them. Using a form that carries one the
|
|
56
|
+
#: meaning does not have adds to the meaning, so such a form is refused rather than
|
|
57
|
+
#: merely dispreferred: a tenseless frame said as "gave" claims a past that nobody
|
|
58
|
+
#: said, which is how "should gave food" survived a fewest-words tie against "should
|
|
59
|
+
#: give food". Number and degree are not on this list — an unmarked noun is still
|
|
60
|
+
#: singular, and nothing is claimed by saying so.
|
|
61
|
+
OVERSTATES = ("tense", "aspect", "polarity", "modality")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class Need:
|
|
66
|
+
"""What a daughter must mean, and which grammatical features it must carry."""
|
|
67
|
+
|
|
68
|
+
meaning: Any = ANY
|
|
69
|
+
features: tuple[tuple[str, Any], ...] = ()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def realize(grammar: Grammar, meaning: Any, *, cat: str | None = None, depth: int = 8) -> str | None:
|
|
73
|
+
"""The best surface string for a meaning, or ``None`` if the grammar cannot say it."""
|
|
74
|
+
cats = [cat] if cat else list(_default_cats(meaning, grammar))
|
|
75
|
+
best: _Said | None = None
|
|
76
|
+
memo: dict = {}
|
|
77
|
+
for name in cats:
|
|
78
|
+
got = _realize(grammar, name, Need(meaning), depth, memo)
|
|
79
|
+
if got is not None and (best is None or _better(got, best)):
|
|
80
|
+
best = got
|
|
81
|
+
return best.text if best else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class _Said:
|
|
86
|
+
score: float
|
|
87
|
+
tokens: int
|
|
88
|
+
text: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _better(a: _Said, b: _Said) -> bool:
|
|
92
|
+
"""Fewest words wins; ties go to the higher-scoring derivation.
|
|
93
|
+
|
|
94
|
+
Length first, not score first: production weights exist to *rank parses*, and
|
|
95
|
+
several are positive, so a score-first search is rewarded for piling on structure
|
|
96
|
+
("the field called north had fail and been..."). Every candidate that reaches here
|
|
97
|
+
already accounts for every role and feature — that is what ``_invert`` guarantees —
|
|
98
|
+
so the shortest one is the one that says exactly the meaning and nothing else.
|
|
99
|
+
"""
|
|
100
|
+
return (-a.tokens, a.score) > (-b.tokens, b.score)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _default_cats(meaning: Any, grammar: Grammar) -> Iterable[str]:
|
|
104
|
+
if isinstance(meaning, Request):
|
|
105
|
+
return ("IMP",)
|
|
106
|
+
if isinstance(meaning, Question):
|
|
107
|
+
return ("Q",)
|
|
108
|
+
if isinstance(meaning, Frame):
|
|
109
|
+
return ("S", "VP")
|
|
110
|
+
return ("NP",)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _realize(grammar: Grammar, cat: str, need: Need, depth: int,
|
|
114
|
+
memo: dict | None = None) -> _Said | None:
|
|
115
|
+
"""The best way to say ``need`` as a ``cat``, or None.
|
|
116
|
+
|
|
117
|
+
One six-word sentence asked this 24,103 times for **74** distinct (category, need)
|
|
118
|
+
pairs, because every candidate production re-explores the same daughters. So the
|
|
119
|
+
answers are memoised for the duration of one :func:`realize` call.
|
|
120
|
+
|
|
121
|
+
That memo is only sound if a result does not depend on *where* it was reached from,
|
|
122
|
+
which is why the search no longer carries a set of pairs already on the stack. That
|
|
123
|
+
set was there to stop left recursion, and the depth limit already does: depth falls
|
|
124
|
+
by one at every level, so ``VP -> VP PP`` unwinds on its own. With the stack gone,
|
|
125
|
+
an answer depends on nothing but the category, the need and the depth remaining —
|
|
126
|
+
so those three are the key, and the memo is exact rather than approximate.
|
|
127
|
+
"""
|
|
128
|
+
said, _ = _search(grammar, cat, need, depth, {} if memo is None else memo)
|
|
129
|
+
return said
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _search(grammar: Grammar, cat: str, need: Need, depth: int,
|
|
133
|
+
memo: dict) -> tuple[_Said | None, int]:
|
|
134
|
+
"""The best way to say this, and how many levels of depth that derivation used.
|
|
135
|
+
|
|
136
|
+
The depth remaining is part of what an answer depends on, so it is part of the key.
|
|
137
|
+
But an answer that used three levels is the same answer at every depth of three or
|
|
138
|
+
more, and reporting the depth *used* is what lets one computation serve them all —
|
|
139
|
+
without it the same sentence was rebuilt once per depth.
|
|
140
|
+
"""
|
|
141
|
+
if depth <= 0:
|
|
142
|
+
return None, 0
|
|
143
|
+
try:
|
|
144
|
+
base: Any = (cat, need)
|
|
145
|
+
hash(base)
|
|
146
|
+
except TypeError: # a meaning that is not hashable still needs a stable key
|
|
147
|
+
base = (cat, repr(need))
|
|
148
|
+
found = memo.get(base)
|
|
149
|
+
if found is not None and found[1] <= depth:
|
|
150
|
+
return found
|
|
151
|
+
exact = memo.get((base, depth))
|
|
152
|
+
if exact is not None:
|
|
153
|
+
return exact
|
|
154
|
+
best: _Said | None = None
|
|
155
|
+
used = 0
|
|
156
|
+
|
|
157
|
+
for score, word in _lexical(grammar, cat, need):
|
|
158
|
+
said = _Said(score, 1, word)
|
|
159
|
+
if best is None or _better(said, best):
|
|
160
|
+
best, used = said, 1
|
|
161
|
+
|
|
162
|
+
for prod in grammar.by_lhs(cat):
|
|
163
|
+
needs = _invert(prod, need, grammar)
|
|
164
|
+
if needs is None:
|
|
165
|
+
continue
|
|
166
|
+
pieces: list[str] = []
|
|
167
|
+
score, tokens, ok, deepest = prod.weight, 0, True, 0
|
|
168
|
+
for i, symbol in enumerate(prod.rhs):
|
|
169
|
+
if isinstance(symbol, Terminal):
|
|
170
|
+
pieces.append(symbol.word)
|
|
171
|
+
tokens += 1
|
|
172
|
+
continue
|
|
173
|
+
sub = needs.get(i, Need())
|
|
174
|
+
# a prohibition is checked against the daughter's *meaning*: the tense that
|
|
175
|
+
# would make "ought gave" is carried by the complement frame, not demanded
|
|
176
|
+
# of it, so refusing the demand would not have caught it
|
|
177
|
+
if any(v is ABSENT and _carries(sub.meaning, k) for k, v in symbol.features.items()):
|
|
178
|
+
ok = False
|
|
179
|
+
break
|
|
180
|
+
# the production's own category features are a constraint on the daughter,
|
|
181
|
+
# prohibitions included: a forbidden feature may sit on the *entry* rather
|
|
182
|
+
# than the meaning ("heaps" is a partitive), so the demand has to reach the
|
|
183
|
+
# lexicon rather than being checked here and dropped
|
|
184
|
+
fixed = {k: v for k, v in symbol.features.items() if not isinstance(v, FVar)}
|
|
185
|
+
if fixed:
|
|
186
|
+
sub = Need(sub.meaning, tuple(sorted({**dict(sub.features), **fixed}.items())))
|
|
187
|
+
got, sub_used = _search(grammar, symbol.name, sub, depth - 1, memo)
|
|
188
|
+
if got is None:
|
|
189
|
+
ok = False
|
|
190
|
+
break
|
|
191
|
+
deepest = max(deepest, sub_used)
|
|
192
|
+
score += got.score
|
|
193
|
+
tokens += got.tokens
|
|
194
|
+
pieces.append(got.text)
|
|
195
|
+
if ok:
|
|
196
|
+
said = _Said(score, tokens, " ".join(p for p in pieces if p))
|
|
197
|
+
if best is None or _better(said, best):
|
|
198
|
+
best, used = said, deepest + 1
|
|
199
|
+
answer = (best, used)
|
|
200
|
+
memo[(base, depth)] = answer
|
|
201
|
+
if best is not None:
|
|
202
|
+
held = memo.get(base)
|
|
203
|
+
if held is None or held[0] is None or _better(best, held[0]) or (
|
|
204
|
+
best == held[0] and used < held[1]):
|
|
205
|
+
memo[base] = answer
|
|
206
|
+
return answer
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _lexical(grammar: Grammar, cat: str, need: Need) -> list[tuple[float, str]]:
|
|
210
|
+
"""Surface forms of this category that satisfy the need, inflecting when required."""
|
|
211
|
+
wanted = dict(need.features)
|
|
212
|
+
listed: list[tuple[float, str]] = []
|
|
213
|
+
derived: list[tuple[float, str]] = []
|
|
214
|
+
candidates = (grammar.entries_by_cat(cat) if need.meaning is ANY
|
|
215
|
+
else grammar.entries_saying(cat, need.meaning))
|
|
216
|
+
for entry in candidates:
|
|
217
|
+
if need.meaning is not ANY and not _same(entry.sem, need.meaning):
|
|
218
|
+
continue
|
|
219
|
+
if any(k in entry.features and k not in wanted for k in OVERSTATES):
|
|
220
|
+
continue # the form claims more than the meaning does
|
|
221
|
+
if any(v is ABSENT and k in entry.features for k, v in wanted.items()):
|
|
222
|
+
continue # a forbidden feature: "heaps bread" needs its "of"
|
|
223
|
+
missing = {k: v for k, v in wanted.items()
|
|
224
|
+
if v is not ABSENT and entry.features.get(k) != v}
|
|
225
|
+
if not missing:
|
|
226
|
+
listed.append((entry.weight, entry.word))
|
|
227
|
+
continue
|
|
228
|
+
form = inflect(entry, missing)
|
|
229
|
+
if form is not None:
|
|
230
|
+
derived.append((entry.weight, form)) # inflection is free: it is the same word
|
|
231
|
+
# a listed form beats a derived one: the lexicon has "came", so the suffix rules
|
|
232
|
+
# are not asked to invent "comed"
|
|
233
|
+
out = listed or derived
|
|
234
|
+
out.extend(_open_class(grammar, cat, need))
|
|
235
|
+
return sorted(out, key=lambda o: (-o[0], o[1]))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _open_class(grammar: Grammar, cat: str, need: Need) -> list[tuple[float, str]]:
|
|
239
|
+
"""A name, path, literal — or a word the lexicon never had — is said by writing it.
|
|
240
|
+
|
|
241
|
+
The open-class rules that let an unknown word *in* also let it back *out*, which is
|
|
242
|
+
what keeps a village's own vocabulary sayable: a predicate like ``fail`` that no
|
|
243
|
+
lexicon lists is still inflected to "failed" by the shared suffix table.
|
|
244
|
+
"""
|
|
245
|
+
meaning = need.meaning
|
|
246
|
+
wanted = dict(need.features)
|
|
247
|
+
out: list[tuple[float, str]] = []
|
|
248
|
+
for pattern, spec in getattr(grammar, "_open", ()):
|
|
249
|
+
if spec.cat != cat:
|
|
250
|
+
continue
|
|
251
|
+
if spec.sem == "word" and isinstance(meaning, str):
|
|
252
|
+
# the bare form already carries the unmarked features (a noun is singular,
|
|
253
|
+
# a verb is tenseless), so only the rest has to be inflected — but only for
|
|
254
|
+
# what was not demanded, or the default contradicts the demand and a plural
|
|
255
|
+
# noun becomes unsayable ("the fields are empty" had no way to be said)
|
|
256
|
+
defaults = {"number": "singular"} if cat in ("N", "Adj") and "number" not in wanted else {}
|
|
257
|
+
entry = Entry(meaning, cat, {**defaults, **dict(spec.features)}, meaning, spec.weight)
|
|
258
|
+
if any(v is ABSENT and k in entry.features for k, v in wanted.items()):
|
|
259
|
+
continue
|
|
260
|
+
missing = {k: v for k, v in wanted.items()
|
|
261
|
+
if v is not ABSENT and entry.features.get(k) != v}
|
|
262
|
+
form = meaning if not missing else inflect(entry, missing)
|
|
263
|
+
if form is not None and pattern.fullmatch(form):
|
|
264
|
+
out.append((spec.weight, form))
|
|
265
|
+
continue
|
|
266
|
+
if not isinstance(meaning, Entity) or spec.kind != meaning.kind:
|
|
267
|
+
continue
|
|
268
|
+
text = f"'{meaning.text}'" if spec.kind == "literal" else meaning.text
|
|
269
|
+
if pattern.fullmatch(text):
|
|
270
|
+
out.append((spec.weight, text))
|
|
271
|
+
return out
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _agreement(value: Any) -> dict[str, Any]:
|
|
275
|
+
"""The person and number a referring expression imposes on its verb.
|
|
276
|
+
|
|
277
|
+
Parsing gets this from unification: ``NP[number=?n] VP[number=?n]`` ties the two
|
|
278
|
+
together and the words supply the value. Generation has the opposite problem — the
|
|
279
|
+
value has to come from the entity being talked about, and a name carries no
|
|
280
|
+
features at all, though it is third person singular for every purpose here.
|
|
281
|
+
"""
|
|
282
|
+
if isinstance(value, (Request, Question)):
|
|
283
|
+
value = value.frame
|
|
284
|
+
if not isinstance(value, Entity):
|
|
285
|
+
return {}
|
|
286
|
+
out = {key: value.features[key] for key in AGREEMENT if key in value.features}
|
|
287
|
+
out.setdefault("number", "singular")
|
|
288
|
+
out.setdefault("person", 3)
|
|
289
|
+
return out
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _carries(meaning: Any, key: str) -> bool:
|
|
293
|
+
"""Whether a meaning already carries a feature a daughter is forbidden to carry."""
|
|
294
|
+
if isinstance(meaning, (Request, Question)):
|
|
295
|
+
meaning = meaning.frame
|
|
296
|
+
return key in getattr(meaning, "features", {})
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _same(a: Any, b: Any) -> bool:
|
|
300
|
+
if isinstance(a, Entity) and isinstance(b, Entity):
|
|
301
|
+
return a.text.lower() == b.text.lower()
|
|
302
|
+
return a == b
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _invert(prod: Production, need: Need, grammar: Grammar) -> dict[int, Need] | None:
|
|
306
|
+
"""What each daughter must mean for this production to produce ``need``."""
|
|
307
|
+
target = need.meaning
|
|
308
|
+
sem = prod.sem
|
|
309
|
+
lift = {target_feature: (index, source) for target_feature, index, source in getattr(sem, "lift", ())}
|
|
310
|
+
needs: dict[int, Need] = {}
|
|
311
|
+
extra: dict[int, dict[str, Any]] = {}
|
|
312
|
+
|
|
313
|
+
def demand(index: int, meaning: Any = ANY, **features: Any) -> None:
|
|
314
|
+
current = needs.get(index, Need())
|
|
315
|
+
merged = dict(current.features) | extra.get(index, {}) | features
|
|
316
|
+
needs[index] = Need(meaning if meaning is not ANY else current.meaning, tuple(sorted(merged.items())))
|
|
317
|
+
|
|
318
|
+
def take_lift(features: Mapping[str, Any]) -> dict[str, Any] | None:
|
|
319
|
+
"""Route features that come from inflection to the daughter that carries them."""
|
|
320
|
+
left = dict(features)
|
|
321
|
+
for name, (index, source) in lift.items():
|
|
322
|
+
if name in left:
|
|
323
|
+
extra.setdefault(index, {})[source] = left.pop(name)
|
|
324
|
+
return left
|
|
325
|
+
|
|
326
|
+
def agree() -> None:
|
|
327
|
+
"""Route agreement along the grammar's own agreement variables.
|
|
328
|
+
|
|
329
|
+
A variable shared by two symbols is the grammar saying they agree; shared with
|
|
330
|
+
the mother, that it passes through. The whole bundle travels rather than just
|
|
331
|
+
the key the variable happens to be written on, because ``?n`` between a subject
|
|
332
|
+
and its verb is shorthand for subject-verb agreement, and English agreement is
|
|
333
|
+
person *and* number. Without this the copula was chosen alphabetically: "Nise
|
|
334
|
+
am hungry".
|
|
335
|
+
"""
|
|
336
|
+
slots: dict[str, list[int]] = {}
|
|
337
|
+
for index, symbol in enumerate(prod.rhs):
|
|
338
|
+
if isinstance(symbol, Cat):
|
|
339
|
+
for value in symbol.features.values():
|
|
340
|
+
if isinstance(value, FVar):
|
|
341
|
+
slots.setdefault(value.name, []).append(index)
|
|
342
|
+
mother = {k: v for k, v in need.features if k in AGREEMENT}
|
|
343
|
+
lhs_vars = {v.name for v in prod.lhs.features.values() if isinstance(v, FVar)}
|
|
344
|
+
for name, indices in slots.items():
|
|
345
|
+
found = dict(mother) if name in lhs_vars else {}
|
|
346
|
+
source = None
|
|
347
|
+
for index in indices:
|
|
348
|
+
got = _agreement(needs[index].meaning) if index in needs else {}
|
|
349
|
+
if got:
|
|
350
|
+
found, source = {**got, **found}, index
|
|
351
|
+
break
|
|
352
|
+
if not found:
|
|
353
|
+
continue
|
|
354
|
+
for index in indices:
|
|
355
|
+
if index == source or not isinstance(prod.rhs[index], Cat):
|
|
356
|
+
continue
|
|
357
|
+
if prod.rhs[index].name not in AGREES:
|
|
358
|
+
continue
|
|
359
|
+
current = needs.get(index, Need())
|
|
360
|
+
merged = dict(current.features) | found
|
|
361
|
+
needs[index] = Need(current.meaning, tuple(sorted(merged.items())))
|
|
362
|
+
|
|
363
|
+
def done() -> dict[int, Need]:
|
|
364
|
+
"""Materialise the demands, including features routed by ``take_lift``."""
|
|
365
|
+
for index, feats in extra.items():
|
|
366
|
+
current = needs.get(index, Need())
|
|
367
|
+
merged = dict(current.features) | feats
|
|
368
|
+
needs[index] = Need(current.meaning, tuple(sorted(merged.items())))
|
|
369
|
+
agree()
|
|
370
|
+
return needs
|
|
371
|
+
|
|
372
|
+
if target is ANY:
|
|
373
|
+
return {}
|
|
374
|
+
|
|
375
|
+
# A feature lifted from the *head* daughter is inflection ("failed"); one lifted
|
|
376
|
+
# from another daughter is a function word ("did fail"). If the meaning lacks the
|
|
377
|
+
# feature, that word would be invented — "no one 'm share grain" — so the
|
|
378
|
+
# production is refused. Inflection-carrying productions stay available, which is
|
|
379
|
+
# what a tenseless frame needs.
|
|
380
|
+
head = getattr(sem, "predicate_from", None)
|
|
381
|
+
if head is None:
|
|
382
|
+
head = getattr(sem, "index", None)
|
|
383
|
+
if lift and head is not None and any(index != head for _, (index, _) in lift.items()):
|
|
384
|
+
carried = target.frame.features if isinstance(target, (Request, Question)) else getattr(target, "features", {})
|
|
385
|
+
if not any(name in carried for name in lift):
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
if isinstance(sem, Head):
|
|
389
|
+
if isinstance(sem.index, int):
|
|
390
|
+
demand(sem.index, target, **dict(need.features))
|
|
391
|
+
return done()
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
if isinstance(sem, Lit):
|
|
395
|
+
return {} if _same(sem.value, target) else None
|
|
396
|
+
|
|
397
|
+
if isinstance(sem, Order):
|
|
398
|
+
if not isinstance(target, Request) or not isinstance(sem.index, int):
|
|
399
|
+
return None
|
|
400
|
+
demand(sem.index, Frame(target.frame.predicate, target.frame.roles,
|
|
401
|
+
{k: v for k, v in target.frame.features.items() if k != "mood"}))
|
|
402
|
+
return done()
|
|
403
|
+
|
|
404
|
+
if isinstance(sem, Ask):
|
|
405
|
+
if not isinstance(target, Question):
|
|
406
|
+
return None
|
|
407
|
+
inner = Frame(target.frame.predicate, target.frame.roles,
|
|
408
|
+
{k: v for k, v in target.frame.features.items() if k != "mood"})
|
|
409
|
+
if sem.asked_from is not None and isinstance(sem.asked_from, int):
|
|
410
|
+
demand(sem.asked_from, target.asked)
|
|
411
|
+
elif sem.asked != target.asked:
|
|
412
|
+
return None
|
|
413
|
+
if isinstance(sem.index, int):
|
|
414
|
+
demand(sem.index, inner)
|
|
415
|
+
return done()
|
|
416
|
+
nested = _invert(Production(prod.lhs, prod.rhs, sem.index, prod.weight, prod.name), Need(inner), grammar)
|
|
417
|
+
if nested is None:
|
|
418
|
+
return None
|
|
419
|
+
for i, sub in nested.items():
|
|
420
|
+
demand(i, sub.meaning, **dict(sub.features))
|
|
421
|
+
return done()
|
|
422
|
+
|
|
423
|
+
if isinstance(sem, Coord):
|
|
424
|
+
if not isinstance(target, tuple) or len(target) != len(sem.indices):
|
|
425
|
+
return None
|
|
426
|
+
for ref, value in zip(sem.indices, target):
|
|
427
|
+
if not isinstance(ref, int):
|
|
428
|
+
return None
|
|
429
|
+
demand(ref, value)
|
|
430
|
+
return done()
|
|
431
|
+
|
|
432
|
+
if isinstance(sem, Ent):
|
|
433
|
+
if not isinstance(target, Entity):
|
|
434
|
+
return None
|
|
435
|
+
features = dict(target.features)
|
|
436
|
+
for key, ref in sem.features_from:
|
|
437
|
+
if not isinstance(ref, int) or key not in features:
|
|
438
|
+
return None
|
|
439
|
+
demand(ref, features.pop(key))
|
|
440
|
+
for key, value in sem.features:
|
|
441
|
+
if features.pop(key, value if key in DEFAULTED else None) != value:
|
|
442
|
+
return None
|
|
443
|
+
left = take_lift(features)
|
|
444
|
+
if left: # features this production cannot express
|
|
445
|
+
return None
|
|
446
|
+
return done()
|
|
447
|
+
|
|
448
|
+
if isinstance(sem, Qualify):
|
|
449
|
+
if not isinstance(sem.index, int):
|
|
450
|
+
return None
|
|
451
|
+
if isinstance(target, Entity):
|
|
452
|
+
features = dict(target.features)
|
|
453
|
+
for key, ref in sem.features_from:
|
|
454
|
+
if not isinstance(ref, int) or key not in features:
|
|
455
|
+
return None
|
|
456
|
+
demand(ref, features.pop(key))
|
|
457
|
+
for key, value in sem.features:
|
|
458
|
+
if features.pop(key, value if key in DEFAULTED else None) != value:
|
|
459
|
+
return None
|
|
460
|
+
left = take_lift(features)
|
|
461
|
+
if left is None:
|
|
462
|
+
return None
|
|
463
|
+
demand(sem.index, Entity(target.kind, target.text, left, target.ref, target.candidates))
|
|
464
|
+
return done()
|
|
465
|
+
if isinstance(target, Frame):
|
|
466
|
+
features = dict(target.features)
|
|
467
|
+
for key, ref in sem.features_from:
|
|
468
|
+
if not isinstance(ref, int) or key not in features:
|
|
469
|
+
return None
|
|
470
|
+
demand(ref, features.pop(key))
|
|
471
|
+
for key, value in sem.features:
|
|
472
|
+
if features.pop(key, value if key in DEFAULTED else None) != value:
|
|
473
|
+
return None
|
|
474
|
+
roles = dict(target.roles)
|
|
475
|
+
for role, ref in sem.roles_from:
|
|
476
|
+
if not isinstance(ref, int) or role not in roles:
|
|
477
|
+
return None
|
|
478
|
+
demand(ref, roles.pop(role))
|
|
479
|
+
left = take_lift(features)
|
|
480
|
+
if left is None:
|
|
481
|
+
return None
|
|
482
|
+
demand(sem.index, Frame(target.predicate, roles, left))
|
|
483
|
+
return done()
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
if isinstance(sem, Attach):
|
|
487
|
+
if not isinstance(sem.index, int) or not isinstance(sem.modifier, int):
|
|
488
|
+
return None
|
|
489
|
+
holder = target if isinstance(target, (Frame, Entity)) else None
|
|
490
|
+
if holder is None:
|
|
491
|
+
return None
|
|
492
|
+
roles = dict(holder.roles) if isinstance(holder, Frame) else dict(holder.features)
|
|
493
|
+
markable = {e.sem for e in grammar.entries_by_cat("P")}
|
|
494
|
+
for role in sorted(roles):
|
|
495
|
+
if role in GRAMMATICAL or role in ("mood",):
|
|
496
|
+
continue
|
|
497
|
+
if role not in markable:
|
|
498
|
+
continue # no preposition marks it, so it is not a PP: "north field", not "field called north"
|
|
499
|
+
rest = {k: v for k, v in roles.items() if k != role}
|
|
500
|
+
base = (Frame(holder.predicate, rest, holder.features) if isinstance(holder, Frame)
|
|
501
|
+
else Entity(holder.kind, holder.text, rest, holder.ref, holder.candidates))
|
|
502
|
+
demand(sem.index, base)
|
|
503
|
+
demand(sem.modifier, Frame("_pp", {"role": role, "value": roles[role]}))
|
|
504
|
+
return done()
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
if isinstance(sem, Locative):
|
|
508
|
+
if not isinstance(target, Frame) or target.predicate != sem.predicate:
|
|
509
|
+
return None
|
|
510
|
+
roles = dict(target.roles)
|
|
511
|
+
theme = None
|
|
512
|
+
if sem.theme is not None:
|
|
513
|
+
if not isinstance(sem.theme, int) or sem.theme_role not in roles:
|
|
514
|
+
return None
|
|
515
|
+
theme = roles.pop(sem.theme_role)
|
|
516
|
+
demand(sem.theme, theme)
|
|
517
|
+
if len(roles) != 1 or not isinstance(sem.modifier, int):
|
|
518
|
+
return None
|
|
519
|
+
role, value = next(iter(roles.items()))
|
|
520
|
+
demand(sem.modifier, Frame("_pp", {"role": role, "value": value}))
|
|
521
|
+
if take_lift(dict(target.features)):
|
|
522
|
+
return None
|
|
523
|
+
return done()
|
|
524
|
+
|
|
525
|
+
if isinstance(sem, (Build, Merge)):
|
|
526
|
+
if not isinstance(target, Frame):
|
|
527
|
+
return None
|
|
528
|
+
roles = dict(target.roles)
|
|
529
|
+
features = {k: v for k, v in target.features.items() if k != "mood"}
|
|
530
|
+
if isinstance(sem, Build):
|
|
531
|
+
if sem.predicate is not None and sem.predicate != target.predicate:
|
|
532
|
+
return None
|
|
533
|
+
if sem.predicate_from is not None:
|
|
534
|
+
if not isinstance(sem.predicate_from, int):
|
|
535
|
+
return None
|
|
536
|
+
demand(sem.predicate_from, target.predicate)
|
|
537
|
+
else:
|
|
538
|
+
if not isinstance(sem.index, int):
|
|
539
|
+
return None
|
|
540
|
+
for role, ref in sem.roles:
|
|
541
|
+
if not isinstance(ref, int) or role not in roles:
|
|
542
|
+
return None
|
|
543
|
+
demand(ref, roles.pop(role))
|
|
544
|
+
for key, value in sem.features:
|
|
545
|
+
if features.pop(key, value if key in DEFAULTED else None) != value:
|
|
546
|
+
return None
|
|
547
|
+
# a feature demanded *of* this constituent is expressed inside it: the perfect
|
|
548
|
+
# production asks its complement for ``VP[tense=past]``, and that tense has to
|
|
549
|
+
# reach the verb rather than fall off at the mother ("has arrive")
|
|
550
|
+
for key, value in need.features:
|
|
551
|
+
if key in lift and key not in features and value is not ABSENT:
|
|
552
|
+
features[key] = value
|
|
553
|
+
left = take_lift(features)
|
|
554
|
+
if left is None:
|
|
555
|
+
return None
|
|
556
|
+
if isinstance(sem, Merge):
|
|
557
|
+
demand(sem.index, Frame(target.predicate, roles, left))
|
|
558
|
+
return done()
|
|
559
|
+
if roles or left: # a Build must account for every role and feature itself
|
|
560
|
+
return None
|
|
561
|
+
return done()
|
|
562
|
+
|
|
563
|
+
return None
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def round_trip(grammar: Grammar, meaning: Any, *, cat: str | None = None) -> tuple[str | None, Any]:
|
|
567
|
+
"""Say it, read it back: the surface string and the meaning that came back."""
|
|
568
|
+
from .chart import understand
|
|
569
|
+
|
|
570
|
+
text = realize(grammar, meaning, cat=cat)
|
|
571
|
+
if text is None:
|
|
572
|
+
return None, None
|
|
573
|
+
back = understand(grammar, text)
|
|
574
|
+
return text, (back.meanings[0] if back.meanings else None)
|