Jump to content

Module talk:Auphen

From Yezur Wiki
Revision as of 12:53, 18 July 2026 by Khurouan (talk | contribs) (Log the ~ word boundary (D16))

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 ?n starts at char n.
  • D4 — positive-index affix insertion/deletion is not off by one.
  • D5 — circumfix A#B wraps 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] holds tʃ, 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; only the first % in a result is substituted; 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.