初始化项目,由ModelHub XC社区提供模型
Model: rpant/iolai26-solve Source: Original Platform
This commit is contained in:
198
solver/dsl/interpreter.py
Normal file
198
solver/dsl/interpreter.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Deterministic interpreter for DSL grammars: generate() (work -> task
|
||||
language) and analyze() (task -> work language).
|
||||
|
||||
The interpreter is intentionally strict and simple: it executes exactly what
|
||||
the grammar says. If a grammar needs cleverness, the proposer must encode it
|
||||
(e.g. list 'birds' as its own lexicon entry instead of relying on affix
|
||||
machinery). The verifier then selects grammars that this interpreter executes
|
||||
into correct outputs — that closed loop is the whole design.
|
||||
|
||||
Conventions the proposer prompt establishes:
|
||||
- lexicon glosses are work-language words/phrases (may be multiword);
|
||||
- affix `feature` is the work-language cue it realizes: a function word
|
||||
("the", "will", "not") or a marker name ("plural") — during generation a
|
||||
feature fires when its cue appears in the work sentence next to the stem;
|
||||
- `order` is a list of pos tags giving target-language constituent order;
|
||||
- rewrites are surface regex applied after morph concatenation (word-level).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from .grammar import Affix, Grammar, LexEntry
|
||||
|
||||
|
||||
def _words(s: str) -> List[str]:
|
||||
return [w for w in re.findall(r"[^\s]+", s.strip()) if w]
|
||||
|
||||
|
||||
def _clean(w: str) -> str:
|
||||
return w.strip(",;.!?()[]\"'«»").casefold()
|
||||
|
||||
|
||||
class Interpreter:
|
||||
def __init__(self, grammar: Grammar):
|
||||
self.g = grammar
|
||||
# gloss index: multiword glosses first (longest match wins)
|
||||
self._gloss_entries: List[Tuple[List[str], LexEntry]] = sorted(
|
||||
(( [_clean(w) for w in _words(e.gloss)], e) for e in grammar.lexicon if e.gloss),
|
||||
key=lambda t: -len(t[0]),
|
||||
)
|
||||
self._morphs: Dict[str, LexEntry] = {e.morph: e for e in grammar.lexicon}
|
||||
self._affix_by_cue: Dict[str, List[Affix]] = {}
|
||||
for a in grammar.affixes:
|
||||
self._affix_by_cue.setdefault(_clean(a.feature), []).append(a)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# generation: work-language sentence -> task-language string
|
||||
# ------------------------------------------------------------------
|
||||
def generate(self, work_sentence: str) -> Optional[str]:
|
||||
toks = [_clean(w) for w in _words(work_sentence)]
|
||||
if not toks:
|
||||
return None
|
||||
n = len(toks)
|
||||
used = [False] * n
|
||||
stems: List[Tuple[int, LexEntry]] = [] # (position of first gloss word, entry)
|
||||
|
||||
# 1. cover with lexicon glosses, longest first
|
||||
for gloss_words, entry in self._gloss_entries:
|
||||
L = len(gloss_words)
|
||||
i = 0
|
||||
while i + L <= n:
|
||||
if not any(used[i : i + L]) and toks[i : i + L] == gloss_words:
|
||||
for j in range(i, i + L):
|
||||
used[j] = True
|
||||
stems.append((i, entry))
|
||||
i += L
|
||||
else:
|
||||
i += 1
|
||||
if not stems:
|
||||
return None
|
||||
stems.sort(key=lambda t: t[0])
|
||||
|
||||
# 2. leftover tokens fire affixes on the nearest eligible stem
|
||||
pending: Dict[int, List[Affix]] = {k: [] for k in range(len(stems))}
|
||||
uncovered = [i for i in range(n) if not used[i]]
|
||||
for i in uncovered:
|
||||
cue = toks[i]
|
||||
for a in self._affix_by_cue.get(cue, []):
|
||||
k = self._nearest_stem(stems, i, a)
|
||||
if k is not None:
|
||||
pending[k].append(a)
|
||||
break
|
||||
|
||||
# 3. order stems by target constituent order if pos info available
|
||||
idx = list(range(len(stems)))
|
||||
if self.g.order and all(e.pos for _, e in stems):
|
||||
rank = {pos: r for r, pos in enumerate(self.g.order)}
|
||||
idx.sort(key=lambda k: (rank.get(stems[k][1].pos, len(rank)), stems[k][0]))
|
||||
|
||||
# 4. build surface words: affix attachment then rewrites
|
||||
out_words = []
|
||||
for k in idx:
|
||||
_, entry = stems[k]
|
||||
w = entry.morph
|
||||
for a in pending[k]:
|
||||
w = self._attach(w, a)
|
||||
w = self._apply_rewrites(w)
|
||||
out_words.append(w)
|
||||
surface = " ".join(out_words)
|
||||
return self._apply_rewrites_sentence(surface)
|
||||
|
||||
def _nearest_stem(self, stems: List[Tuple[int, LexEntry]], cue_pos: int, affix: Affix) -> Optional[int]:
|
||||
best_k, best_d = None, 10 ** 9
|
||||
for k, (pos, entry) in enumerate(stems):
|
||||
if affix.trigger and affix.trigger not in (entry.pos, entry.gloss):
|
||||
continue
|
||||
d = abs(pos - cue_pos)
|
||||
if d < best_d:
|
||||
best_k, best_d = k, d
|
||||
return best_k
|
||||
|
||||
def _attach(self, w: str, a: Affix) -> str:
|
||||
if a.position == "prefix":
|
||||
return a.form + w
|
||||
if a.position == "suffix":
|
||||
return w + a.form
|
||||
if a.position == "circumfix":
|
||||
return a.form + w + (a.form2 or a.form)
|
||||
if a.position == "infix":
|
||||
# after the first vowel-less onset (common infix site: after first C)
|
||||
m = re.match(r"^([^aeiouAEIOU]*)(.*)$", w)
|
||||
return (m.group(1) + a.form + m.group(2)) if m else w + a.form
|
||||
return w
|
||||
|
||||
def _apply_rewrites(self, w: str) -> str:
|
||||
for r in self.g.rewrites:
|
||||
w = r.apply(w)
|
||||
return w
|
||||
|
||||
def _apply_rewrites_sentence(self, s: str) -> str:
|
||||
# rewrites with explicit spaces/anchors act at sentence level too
|
||||
for r in self.g.rewrites:
|
||||
if " " in r.pattern or r.pattern.startswith("^") or r.pattern.endswith("$"):
|
||||
s = r.apply(s)
|
||||
return s
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# analysis: task-language sentence -> work-language string
|
||||
# ------------------------------------------------------------------
|
||||
def analyze(self, task_sentence: str) -> Optional[str]:
|
||||
words = [_clean(w) for w in _words(task_sentence)]
|
||||
if not words:
|
||||
return None
|
||||
rendered: List[Tuple[str, str, str]] = [] # (prefix cues, gloss, suffix cues)
|
||||
any_known = False
|
||||
for w in words:
|
||||
pre, gloss, post, known = self._analyze_word(w)
|
||||
any_known = any_known or known
|
||||
rendered.append((pre, gloss, post))
|
||||
if not any_known:
|
||||
return None
|
||||
out: List[str] = []
|
||||
for pre, gloss, post in rendered:
|
||||
for c in pre.split():
|
||||
out.append(c)
|
||||
out.append(gloss)
|
||||
for c in post.split():
|
||||
out.append(c)
|
||||
return " ".join(x for x in out if x)
|
||||
|
||||
def _analyze_word(self, w: str) -> Tuple[str, str, str, bool]:
|
||||
"""Decompose one surface word into (prefix cues, stem gloss, suffix
|
||||
cues, matched?). Tries direct lexicon hit, then affix stripping
|
||||
(longest affix first), then returns the word untouched."""
|
||||
if w in self._morphs:
|
||||
return "", self._morphs[w].gloss, "", True
|
||||
affixes = sorted(self.g.affixes, key=lambda a: -len(a.form))
|
||||
for a in affixes:
|
||||
if a.position == "prefix" and w.startswith(a.form):
|
||||
pre, gloss, post, ok = self._analyze_word(w[len(a.form):])
|
||||
if ok:
|
||||
return (self._cue(a) + " " + pre).strip(), gloss, post, True
|
||||
if a.position == "suffix" and w.endswith(a.form):
|
||||
pre, gloss, post, ok = self._analyze_word(w[: len(w) - len(a.form)])
|
||||
if ok:
|
||||
return pre, gloss, (post + " " + self._cue(a)).strip(), True
|
||||
if a.position == "circumfix" and w.startswith(a.form) and w.endswith(a.form2 or a.form):
|
||||
inner = w[len(a.form): len(w) - len(a.form2 or a.form)]
|
||||
pre, gloss, post, ok = self._analyze_word(inner)
|
||||
if ok:
|
||||
return (self._cue(a) + " " + pre).strip(), gloss, post, True
|
||||
# last resort: greedy stem containment (rewrite rules may have altered edges)
|
||||
for morph, entry in sorted(self._morphs.items(), key=lambda kv: -len(kv[0])):
|
||||
if len(morph) >= 3 and morph in w:
|
||||
return "", entry.gloss, "", True
|
||||
return "", w, "", False
|
||||
|
||||
@staticmethod
|
||||
def _cue(a: Affix) -> str:
|
||||
"""How an affix surfaces in the work-language output: function-word
|
||||
cues are emitted verbatim; abstract markers (PL, PST) are dropped —
|
||||
the proposer should prefer word cues for translatable material."""
|
||||
cue = a.feature.strip()
|
||||
if cue and cue.isalpha() and cue.casefold() == cue:
|
||||
return cue
|
||||
return ""
|
||||
Reference in New Issue
Block a user