Module talk:Auphen: Difference between revisions
Correct the diagnosis and record the fix: the crash was a category cross-product blow-up in check_env, not an index error |
Report a divergence found via YAS-AI: an @-index on a category target is numbered per member, so [V]/%3@-1 marks every distinct vowel instead of the last (28/86 verbs) |
||
| Line 71: | Line 71: | ||
— [[User:Thiorosan|Thiorosan]] ([[User talk:Thiorosan|talk]]) 2026-08-19 | — [[User:Thiorosan|Thiorosan]] ([[User talk:Thiorosan|talk]]) 2026-08-19 | ||
== An @-index on a category target is numbered per member, not across the category == | |||
Found while adding [[Module:Auphen/YAS-AI]] (Airananwe). The pronunciation ruleset on that page behaves; its <code>special</code> stem-formation set disagrees with the operator's CWS output on '''28 of 86''' verbs. | |||
'''Trigger.''' The set opens | |||
é//_# | |||
[V]/%3@-1 | |||
and means "mark the '''last vowel in the word'''". CWS marks one vowel. Auphen marks one occurrence of '''every member of <code>[V]</code> that appears''', because the index is resolved after the category has been expanded into its alternatives: each literal member then carries its own instance numbering, and each is independently "the last of its kind". | |||
'''Evidence.''' Divergence tracks the number of ''distinct'' vowel letters exactly — repeats of a single letter are unaffected, which is the signature of per-member numbering. | |||
{| class="wikitable" | |||
! input !! distinct vowel letters !! CWS !! Auphen | |||
|- | |||
| <code>papaté</code> || 1 || paped || paped | |||
|- | |||
| <code>ababé</code> || 1 || abev || abev | |||
|- | |||
| <code>opité</code> || 2 || opid || '''upid''' | |||
|- | |||
| <code>apoté</code> || 2 || apud || '''epud''' | |||
|- | |||
| <code>naquité</code> || 3 || naquid || '''nequid''' | |||
|} | |||
<code>naquité</code> → ''naquid'' is ground truth: the operator confirmed it on CWS. In every one of the 28 divergent verbs the Auphen form has ablauted a vowel earlier in the word as well as the last one. | |||
'''Scope.''' Only rules carrying '''both''' an <code>@</code>-index and a category in the target are affected. The <code>pronounce</code> ruleset of [[Module:Auphen/YAS-AI]] is structurally immune: all five of its indexed rules (<code>1/3%@-2</code>, <code>1/‼%@-3</code>, <code>9/ʘ%@-1</code>, <code>é/ʘ%@-1</code>, <code>1/ʘ1@-1</code>) have literal single-character targets, so no expansion happens. The same holds for the <code>passive</code> and <code>plural</code> sets, which carry no indices at all. | |||
'''Meanwhile.''' The <code>special</code> set is left on the data page as written, since it is correct notation and the fault is in resolution order; stems needed before a fix are taken from a Python port that reproduces CWS numbering. | |||
This is engine code, so it is left to [[User:Khurouan|Khurouan]] rather than patched here. | |||
— [[User:Thiorosan|Thiorosan]] ([[User talk:Thiorosan|talk]]) 2026-08-26 | |||
Revision as of 12:09, 26 August 2026
This page records the design, provenance, and behavioural divergences of Module:Auphen — out-of-frame notes, kept off the module source.
Provenance
Auphen is a pure-Lua (Scribunto) port of a JavaScript sound-change engine, itself an implementation of ConWorkShop's PhoMo rule notation. Rule anatomy (target / change / environment / exception / else), the affix-vs-replace split, category sets, and the operator set follow PhoMo; the divergences below are deliberate. The engine is developed and tested off-wiki: a 334-case local suite covering every operation, ground-truth parity against the original JS engine, and the live YBS-PJ ruleset.
Divergences
Fixed bugs in the source JS engine (each has a proving test off-wiki):
- D1 — the ELS
@-index is parsed and used as a fallback instance filter (the JS assigned it to the CHG index, so ELS indices were dead). - D2 — ditto
"fires inside the exception as well as the environment (the JS dropped the "previous" flag). - D3 —
?-reversal indices are computed on the clean word; negatives are not off by one, and positive?nstarts at char n. - D4 — positive-index affix insertion/deletion is not off by one.
- D5 — circumfix
A#Bwraps the word (the JS concatenated A+B). - D6 — local conditions inside an affix rule are cleanly false.
- D14 — category→category mapping pairs members by declaration order, not a longest-first sort, so digraph categories map correctly (e.g. palatalization
[H]/[J]where[J]holdstʃ, dʒ: declared order gives s→ʃ, t→tʃ, g→ɣ). The JS sorted members longest-first, silently mapping s→tʃ. Longest-first is still used for matching, so multi-character phones still match greedily. - D15 —
%("same as the target") substitutes on every occurrence in a result, so%%doubles the target (e.g. gemination[C]/%%/_4). The JS replaced only the first%. A literal per-cent sign in output is written!%. - D16 (Auphen addition) —
~(tilde) is a universal word boundary. The engine splits input on it, so each part is processed as its own word (its own#edges), and the parts are joined with a space; a~produced late by a rule becomes a plain display space. It does not clash with nasal vowels (precomposed likeã, or a combining U+0303 tilde — both distinct codepoints from ASCII~).
Divergences from CWS (documented quirks fixed, not reproduced):
- D7 — a category/index resolving out of range skips the candidate (CWS deletes the target).
- D8 —
!escape makes the next character a literal. - D9 / D10 — movement
>and copy>!use clean semantics; CWS's off-by-one is not reproduced. - D11 —
-word operations are opt-in (opts.dashToSpace). - D12 — the wildcard
*in local conditions is defined and reliable. - D13 — a UTF-8 codepoint layer replaces the JS's UTF-16 indexing: combining marks are separate characters, and digraph phones work in categories (CWS bans them).
Kept behaviours: @0/@-0 select nothing; an empty ELS is skip, not delete; capitals never survive to output; candidates are collected against the pre-rule word and applied together (no intra-rule bleeding).
Performance
Category matching currently materialises the Cartesian product of the category references in a rule, so cost is (category size) to the power of (number of references) — dominated by large sets like [C]. A guard (opts.explodeCap, default 5,000,000) skips a pathological rule with a warning rather than exceeding Scribunto's CPU limit. Tracked follow-up: a streaming/positional matcher that tests membership per word-position, removing the ceiling entirely.
Crash on left-contexts that run past the start of the word
Reporting a reproducible engine crash found while adding Module:Auphen/YAQ-GA (Gaillean). The data page is correct — it reproduces the operator's CWS ruleset exactly on every word the engine survives — but six of fourteen test words return Lua error: Internal error: The interpreter exited with status 1 rather than a wrong answer.
Trigger. The Gaillean ruleset contains
ɛ/ə/[V]_[C]|[C]_[C][C][V]|[V][C]_[C][C]|[C][C]_#|ʎ_#|ʒ_#|c_#|g͡ɣ_#/qu_
Every word whose form still contains ɛ when this rule is reached crashes; every word without one is fine. The correlation was exact across twenty probes.
| input | result |
|---|---|
e, be, eb, abe, herce, qube |
crash |
ea, ab, ka, ghalla, thaembuz, mporonat |
correct output |
ea is the diagnostic case: it survives only because the preceding rule ɛ/e/_[V]# fires first and removes the ɛ, so the rule above never evaluates.
Diagnosis. The failing conditions ([V]_[C], [C][C]_#, …) all have left-hand context. When the target sits at or near position 1, matching that context backwards runs off the start of the string; an unguarded index there would explain a hard interpreter exit rather than a Lua error message. Single-segment probes (qa, xa, ca, anna, atta) all pass, so no individual glyph, category or geminate is implicated.
Meanwhile. Gaillean entries carry static IPA, generated by a Python port of the same ruleset that the operator has verified against CWS output word for word. Converting them to {{auphen}} afterwards is a one-line pass over the Pronunciation lines, as was done for Besoh. Nothing else is blocked.
This is engine code, so it is left to Khurouan rather than patched here.
Fixed 2026-08-19 — and the diagnosis above was wrong
The crash is real but it is not an out-of-range index. jchar guards negative positions correctly, and the left-context loop is safe. The correlation reported above — every word still containing ɛ when the rule fires — held, but the cause was elsewhere.
The cause. Engine:check_env resolved a local environment by calling Engine:explode_string, which builds the Cartesian product of every category in the pattern. The Gaillean rule's environment [C]_[C][C][V] is 27 × 27 × 27 × 13 = 255,879 variants, each an allocated table with a concatenated string — rebuilt at every match position in every word. The explodeCap of five million never trips, so the engine does not warn and skip; it simply runs out of memory, and the interpreter exits before it can report a Lua error. That is why the failure looked like a hard crash rather than a stack trace.
The fix. Local environments are now matched positionally: Engine:ctx_atoms parses the pattern into literal and category atoms, and ctx_match_fwd / ctx_match_back walk it against the word, testing category membership in place and trying multi-codepoint members longest-first. No enumeration, so no blow-up. Patterns containing * still take the old path, since the wildcard matcher has its own semantics.
Verification. Every {{auphen}} output for Ancient Hertic, Besoh and Pjany was captured before and after the change across 22 words: all 22 identical. Gaillean, which crashed on six of fourteen test words, now returns a pronunciation for all of them, and 19 of 19 agree exactly with the independent Python port of the same ruleset that the operator verified against CWS.
This was engine code, so it was left alone until the operator asked for it directly.
An @-index on a category target is numbered per member, not across the category
Found while adding Module:Auphen/YAS-AI (Airananwe). The pronunciation ruleset on that page behaves; its special stem-formation set disagrees with the operator's CWS output on 28 of 86 verbs.
Trigger. The set opens
é//_# [V]/%3@-1
and means "mark the last vowel in the word". CWS marks one vowel. Auphen marks one occurrence of every member of [V] that appears, because the index is resolved after the category has been expanded into its alternatives: each literal member then carries its own instance numbering, and each is independently "the last of its kind".
Evidence. Divergence tracks the number of distinct vowel letters exactly — repeats of a single letter are unaffected, which is the signature of per-member numbering.
| input | distinct vowel letters | CWS | Auphen |
|---|---|---|---|
papaté |
1 | paped | paped |
ababé |
1 | abev | abev |
opité |
2 | opid | upid |
apoté |
2 | apud | epud |
naquité |
3 | naquid | nequid |
naquité → naquid is ground truth: the operator confirmed it on CWS. In every one of the 28 divergent verbs the Auphen form has ablauted a vowel earlier in the word as well as the last one.
Scope. Only rules carrying both an @-index and a category in the target are affected. The pronounce ruleset of Module:Auphen/YAS-AI is structurally immune: all five of its indexed rules (1/3%@-2, 1/‼%@-3, 9/ʘ%@-1, é/ʘ%@-1, 1/ʘ1@-1) have literal single-character targets, so no expansion happens. The same holds for the passive and plural sets, which carry no indices at all.
Meanwhile. The special set is left on the data page as written, since it is correct notation and the fault is in resolution order; stems needed before a fix are taken from a Python port that reproduces CWS numbering.
This is engine code, so it is left to Khurouan rather than patched here.