Mova0.6 pre-release⌕
☰
home / docs / repo-docs / The English layer

The English layer

This page covers how Mova reads and writes English. It walks through the en crate step by step: the dictionary in code, the tokenizer, the tagger, morphology, the parser, the graph it stores and the generator that writes text back. It then describes the expert system of seeds and rules that checks the annotator, the deriver that builds trees by explicit rules, and the round trip (the autoencoder) that sends a text up through every layer and back down to show what each layer keeps and loses. The crate reference is en — Mova's English graph compressor; the full round-trip reference is roundtrip — where each layer of Mova loses information.

What it is

The distributed model en.bin is trained on UD English treebanks plus the agreed school trees (Training Mova).

Every step below is real output of en explain "<sentence>"; run it on any sentence. The running example is "Should I bring an umbrella tomorrow?"

How the compressor works, step by step

Step 0. The dictionary in code

Sources: AGID paradigms (permissive licence) and frequencies from UD; the extension from OEWN (CC BY 4.0), SPECIALIST (NLM terms), the 2010 U.S. Census surnames and the Names Corpus.

Step 1. Tokenizer: a finite automaton

Every character has a class: Le (letter), Di (digit), Sp (space), Pe (period), Ap (apostrophe)… A table state × class → (command, new state) drives the automaton, one lookup per character.

"S" Le New St→Wo · "h" Le App Wo→Wo · … · " " Sp Sep Wo→St · "I" Le New St→Wo …

New starts a word, App appends a character, Sep closes the word; St is "between words", Wo "inside a word". Result: Should │ I │ bring │ an │ umbrella │ tomorrow │ ?

Dictionaries from the treebanks handle abbreviations ("Mr.", "U.S."), clitics ("did|n't", "John|'s") and hyphenated words.

Step 2. Tagger: the class of each word

Tags are Penn Treebank classes: Should MD, I PRP, bring VB, an DT, umbrella NN, tomorrow NN, ? .

Step 3. Morphology: lemma and case

The lemma is the dictionary form: brought, bringing, brings → bring. The graph stores lemma and tag; the form is rebuilt from them (bring + VBD → brought).

Step 4. Parser: a stack automaton

The parser has a stack (words waiting for a head) and a buffer (words not yet read). At each step a weight table picks SHIFT, LEFT(relation) or RIGHT(relation).

 1. stack [ROOT]                          · buffer [Should I bring an umbrella tomorrow ?] ⇒ SHIFT
 2. stack [ROOT Should]                   · buffer [I bring an umbrella tomorrow ?]        ⇒ SHIFT
 3. stack [ROOT Should I]                 · buffer [bring an umbrella tomorrow ?]          ⇒ SHIFT
 4. stack [ROOT Should I bring]           · buffer [an umbrella tomorrow ?]                ⇒ LEFT(nsubj): bring → I
 5. stack [ROOT Should bring]             · buffer [an umbrella tomorrow ?]                ⇒ LEFT(aux): bring → Should
 …
10. stack [ROOT bring umbrella tomorrow]  · buffer [?]                                     ⇒ RIGHT(nmod:unmarked): umbrella → tomorrow
11. stack [ROOT bring umbrella]           · buffer [?]                                     ⇒ RIGHT(obj): bring → umbrella
 …
14. stack [ROOT bring]                    · buffer []                                      ⇒ RIGHT(root): ROOT → bring

Here the parser makes a real error: "tomorrow" should attach to bring as a time adverbial, not to umbrella.

Features of the current state ("top of the stack is VB, the one below is PRP", "first word of the buffer is DT", and many more templates) are numbers: template number plus word and tag numbers. Each feature carries weights for the actions; the largest sum wins. The parser keeps a beam of 8 action sequences.

Training is a perceptron with early update (Zhang & Clark 2008): run through the treebank knowing the correct action sequence; when it falls out of the beam, stop, add +1 to the correct actions and −1 to the chosen wrong ones. Several passes, then the weights are averaged.

Step 5. The graph: what is stored

 #  lemma (number)    tag  case     relation       head    space
 1  should (96)       MD   AsIs     aux              3     yes
 2  I (7)             PRP  Title    nsubj            3     yes
 3  bring (303)       VB   AsIs     root             0     yes
 4  a (6)             DT   AsIs     det              5     yes
 5  umbrella (16799)  NN   AsIs     obj              3     yes
 6  tomorrow (755)    NN   AsIs     nmod:unmarked    5     no
 7  ? (32)            .    AsIs     punct            3     no
sentence bits: interrogative — yes; first word capitalized — yes

A node is 12 bytes and holds no strings: lemma number (4 bytes), tag (1, enum), case (1), relation (1, enum), head (2), space after (1). A word missing from the dictionary goes into the graph's local heap and gets a number with the high bit set.

Step 6. Generator: from the graph back to text

What it is for

A prompt comes in; it becomes a graph; the graph becomes typed commands for Rust code; the result is a graph; the generator writes text. No large model is called on this path: an input the parser cannot read keeps its failure. A domain builds on the same parser by self-training: the general parser annotates domain sentences, the generator is trained further on them, and a table over graph features maps a query graph to a typed command (for example, place, time, condition and unit of a weather query).

The expert system: seeds and rules

The expert system checks the annotator's output against knowledge of grammar and word structure. It learns from grammars, papers and dictionaries, not from examples. Its knowledge is written as seeds: one piece of knowledge each, with a source. A seed is the kernel a rule grows from.

How it works

  1. Seeds live in en/seeds/<section>/<name>.md. Sections: morph (inflection and word structure), nominal (the noun phrase), verbal (the verb and the clause), lexicon (dictionaries and valency), errors (typical errors of parsers, large models and learners). Indexes: seeds/.
  2. Rules are ```rule blocks in a seed, executed by a deterministic Rust engine, en::expert.
  3. Self-check against gold. Every rule runs over UD EWT. A rule that fires on gold is either wrong or has found a gold error; these cases are reviewed and the rule refined. Examples only check rules; they never generate them. Exceptions come from the seed text and its sources, not from examples.
  4. Uses: checking annotation from the teacher, from Rust and from silver; hints for the teacher in edit mode ("word 5 violates rule X"); picking sentences worth promoting to silver; hard constraints for the parser beam.

When two seeds share a rule, it lives in one seed and the other carries the line "Rule: <id> in <file>".

Seed format

# <short title>

**Gist.** 1–3 sentences: what the grammar says.
**Conditions and exceptions.** …
**Examples.** 2–4 short English examples, original or quotes of up to ~10 words.
**In UD.** How the phenomenon looks in UD (EWT) annotation: UPOS, XPOS, FEATS, relations.
**Sources.** Jespersen MEG VI §3.2; Santorini 1990, VBZ; UD `docs/_en/…`; `2023.udw-1.7`.

```rule
…
```

Rule language

rule: en.errors.singular-subject-vbp
what: singular noun subject before a VBP verb: missing -s or wrong subject
match: v[xpos=VBP]; s[rel=nsubj|nsubj:pass, head=v, upos=NOUN|PROPN, feats.Number=Sing, before=v]
require: exists c[rel=conj, head=s]
unless: s[lemma=lot|number|majority|rest|half|percent]; s[lemma=family|team|staff|government]
severity: warn
source: P17-1074 (VERB:SVA); Fowler MEU 1926, NUMBER
conditionmeaning
upos=NOUN, upos=NOUN|PROPNpart of speech (one of several)
xpos=VBZPTB tag
lemma=be, form=thatlemma, form (case-insensitive)
feats.Number=Sing, feats.Number!=Plurfeature equals / differs
feats.Number, !feats.Numberfeature present / absent
rel=nsubj:pass, rel~nsubjexact relation / relation by base (nsubj, nsubj:pass, nsubj:outer)
head=v, head=0head is node v / the root
before=v, after=v, next=v, prev=vword order; next/prev are immediate neighbours by CoNLL-U word number, punctuation included
suffix=ness, prefix=un, lemma.suffix=y, lemma.prefix=unend / start of the form or the lemma
upos!=, lemma!=lot|number, head!=0, rel!~nmod …negation of any condition ("none of the values")
feats.Person=@s, feats.Number!=@ssame feature as on node s / different
lemma=@formlemma equals form, case-insensitive

Examples:

Engine semantics:

All seeds must parse with unique rule ids: the cargo test gate seeds_all_parse.

Running it

en expert-check en/seeds <file.conllu>                 # up to three examples per rule
en expert-check en/seeds --gold <gold.conllu> <file>   # also marks each violation true or false
EN_EXPERT_LIST=1 en expert-check …                     # plus every violation: sent_id, rule, severity, nodes, text

Limits

The rules are precise but see only the ungrammatical. They catch a small part of the errors in a tree; most errors produce grammatical trees with a wrong attachment. So they serve as hints and as a filter for silver, not as full control of annotation.

Trees by logical derivation

The deriver builds dependency trees step by step, each arc from a named rule with a reason, the way the SLM solves arithmetic. It is far less accurate than the parser; its value is that every arc can be inspected, hidden rule by rule, and improved by self-play.

Learning is incremental (Ngai & Florian 2001): fix counts are updated only for sentences a rule touched, and an index from condition sets to words gives a candidate's breaks without a pass over the data. DERIVE_FULL_RESCORE=1 keeps full rescoring for comparison; equivalent rules can differ on ties. Long runs go through tools/jobs; quick checks use a prefix=1% sample of the tree store. Reference: world — worlds, schools and students.

The round trip: what each layer keeps and loses

Mova reads a text up through its layers (tokens, a dependency tree, frames, a world) and answers from the world. The autoencoder (tools/roundtrip) sends the text back down the same layers and asks at every boundary: what must be stored beside this layer for the text to come back byte for byte? That store is the layer's residual. Its size in bits says how much of the language the layer does not describe; its classes say which part.

The layers, up and down

boundaryup (reader)down (decoder)residual stores
text ↔ tokensthe tokenizer keeps the literal before every tokentokens joined by their spacinglayout (blank lines, wraps), unusual spaces
tokens ↔ treethe parser: lemma, tag, head, relationthe generator: order from the tree, forms from lemma, tag and agreementorder where the generator is wrong, forms the rules do not give, spacing
tree ↔ framesthe frames reader: each event with verb, roles, tense, negation, linksa realizer that says only what the frames keepan edit script from the realized tree to the parsed one
frames ↔ worldthe story: entities by id, the line of events, who said what, whyone clause per event, one sentence per clause unless the residual joins them; an entity by name or by the pronoun the reader would resolveframes the world dropped, field patches, sentence boundaries

Every decoder is deterministic and adds no content the layer above lacks. Choosing "she" where the reader would resolve it to Mary is wording, not new content. A realizer that invented words to sound fluent would hide what the frames lose; this one shows it.

Exact and normal modes

Residual classes and bits

Each residual item has one class: layout, spacing, punctuation, case, typography; form, source spelling; order; function words, pronoun or name, tense and modality, negation, quantity; dropped content, another content word, attachment; sentence boundaries, links (next, because, what was said), quotation, repetition.

Bits are counted by fixed rules, not an entropy coder, so runs and versions compare:

itembits
a literal string8 per byte
a dictionary word17
a function word, a negation, an auxiliary8
a punctuation mark5
a tag or a relation6
a position among n wordslog₂(n+1)
the order of a head with k dependentslog₂((k+1)!)
a flag1

Lengths and separators are not counted: the bits compare runs and rank classes; they are not a decodable code.

Blocks, not sentences

A sentence alone cannot say who "he" is. The autoencoder works on blocks: a whole lesson, a book chapter, or a run of whole chapters up to 2,500 words, cut only at chapter boundaries. One block has one discourse and one world, and the whole block must come back.

Gates and controls

When a change counts

A change counts when the residual shrinks on the development split with the gate exact and two runs identical, and a control learned on shuffled data gains clearly less, and the test split, measured once with the gate, falls too.

Training on the residual

Each run writes items and bits per block, layer and class; training reads them. The markup dictionary, the Hebrew trees' lexicon and the adapted word order are learned from the residual of the site's pages. Parallel texts (the Tanakh in many English versions, with the Hebrew) are read into one meta-world and each translation written back out of it; the bits between two translations measure their distance, and the residual says what brings them closer.

How to run it

TALES_TREEBANK, EN_MODEL and HBO_MODEL stand for your tales CoNLL-U file, en.bin and an Ancient Hebrew (hbo) model.

# from the repository root; the model is $MOVA_DATA/en/models/en.bin unless --model names another
cargo build --release -p roundtrip
R=target/release/roundtrip; OUT=$MOVA_DATA/runs/roundtrip
$R probe "The dog didn't bark. 'Let there be light,' said God."   # every layer of a text, both ways
$R run --out "$OUT/before" lessons=lessons:$MOVA_DATA/school/courses     # exact gate and residual by class
$R run --normal --drop negation --out "$OUT/control" tales=conllu:"$TALES_TREEBANK"   # negative control
$R blocks --out "$OUT/blocks" --threads 8 --model "$EN_MODEL" --hbo "$HBO_MODEL"   # whole blocks of the training mix
$R run --out "$OUT/after" lessons=lessons:$MOVA_DATA/school/courses      # after a change
$R compare "$OUT/before" "$OUT/after"                             # side by side

Limits and open work

This page in Markdown → https://md.mova.elamur.ai/free/en/docs/repo-docs/english/