Skip to content

Repository files navigation

gram.en

gram.en is an explainable grammatical analysis engine that takes a different approach from statistical or neural models for determining grammaticality. While statistical or neural models far exceed hand-written grammar rules in this engine and similar ones in terms of coverage, engines with hand-written rules earn their keep as verification tools and generators of labeled error data, as every verdict that gram.en produces also carries exactly the violated constraint, offending span, and derivable repairs all in the same computation.

It treats natural language like a compiler front end:

text → tokenizer → morphology → lexicon → parser (+ unification)
     → error detection → report

Demo

$ npm run analyze "the dog bark"

the dog bark
        ^^^^

Verdict:   ungrammatical

Violation: subject-verb agreement (number/person)
Rule:      S -> NP VP
Fix:       "the dog barks"  |  "the dog barked"  |  "the dogs bark"

Every fix shown was generated by the engine and re-parsed to verify it is actually grammatical. The same machinery, pointed at Swedish grammar data, diagnoses Swedish:

$ npm run analyze -- --lang sv "ett hund skäller"

ett hund skäller
^^^

Verdict:   ungrammatical

Violation: en/ett gender agreement (en-words vs ett-words)
Rule:      NP -> Art N
Fix:       "en hund skäller"

A failed parse is only reported as an error when the engine can localize it to a named constraint, otherwise it abstains.

$ npm run analyze "the dog glorps"
Verdict:   not analyzed -- unknown word(s): glorps

$ npm run analyze -- --lang sv "den stora hund"
Verdict:   no analysis (out of coverage; not necessarily ungrammatical)

Add --trace to any analysis to watch the morphology and Earley chart work on stderr.

There is also a browser demo (index.html): violations are underlined as you type, and clicking an underline shows the diagnosis with one-click fixes. npm run build && npm run serve, then open http://localhost:8080.

Quick start

Requires Node ≥ 22.6 (the engine runs through native TypeScript type-stripping; no build step for local use).

npm run analyze "the dog barks"   # analyze a sentence
npm test                          # unit tests + per-language regression corpora
npm run eval                      # held-out probe set (accuracy metrics)
npm run bench                     # performance benchmarks
npm run build                     # bundle the browser engine to dist/engine.js
npm run serve                     # serve the browser demo on :8080

Programmatic use:

import { load_grammar } from "./src/languages.ts";
import { analyze }      from "./src/analyze.ts";

const g = load_grammar("en");            // load once, reuse across sentences
const result = analyze(g, "the dog bark");
// result.verdict     -> "grammatical" | "ungrammatical" | "no-analysis" | "unknown-word"
// result.violations  -> [{ message, rule, span, char_span, fixes }]

How it works

Stage Responsibility
Tokenizer Splits raw text into tokens, including contractions (don't → do + n't). These are declared per grammar (language).
Morphology A trie-backed finite-state transducer maps surface words to (lemma, category, features) analyzes, with morphophonemic rewrite rules (liked → like + ed, tried → try + ed).
Lexicon Attaches categories and feature structures to closed-class words, clitics, auxiliaries, and analyzed stems.
Parser An Earley chart parser over a context-free backbone; ambiguity is kept alive until structure resolves it.
Unification Feature equations on rules enforce agreement, case, and verb-form constraints.
Diagnostics A failed unification, a relaxed constraint, or a matched mal-rule becomes the report, complete with a span and verified fixes.

Validation against Universal Dependencies

The hand-written corpus (npm test) and probe set (npm run eval) are authored: they test the engine against expectations I wrote myself as a speaker of those languages. For languages I don't speak natively, the engine is also audited against Universal Dependencies. This stays a dev-time check rather than a dependency: the treebanks are read by a small hand-written CoNLL-U parser, kept out of the repo, and never shipped with the engine.

# fetch the treebanks once (test splits; gitignored)
mkdir -p tools/ud && base=https://raw.githubusercontent.com/UniversalDependencies
curl -sf "$base/UD_English-EWT/master/en_ewt-ud-test.conllu"          -o tools/ud/en_ewt-ud-test.conllu
curl -sf "$base/UD_Swedish-Talbanken/master/sv_talbanken-ud-test.conllu" -o tools/ud/sv_talbanken-ud-test.conllu
curl -sf "$base/UD_Russian-GSD/master/ru_gsd-ud-test.conllu"          -o tools/ud/ru_gsd-ud-test.conllu

npm run ud -- --lang ru     # report: coverage + any disagreement with gold

On the tokens inside each fragment's coverage, all three agree with the gold annotations 100% (about 2150 English, 785 Swedish, and 84 Russian), and npm test checks this whenever the treebanks are present.

Writing grammars

Grammars live in .gram files, a small declarative DSL for the properties of the target natural language:

%feature num  : sg | pl
%feature pers : 1 | 2 | 3

dog : N <num>=sg <pers>=3 <lemma>=dog

%rule y:i => _ e d                # tried -> try + ed

S -> NP VP
  <NP agr> = <VP agr> ! "subject-verb agreement" fix: agree(V)

The ! annotation makes a constraint diagnosable: if strict parsing fails but relaxing that equation yields a parse, the equation becomes the explanation and the fix: strategy generates repair candidates. Feature declarations act as a load-time type discipline: <num>=sgular is rejected when the grammar loads, not silently never-unified.

Form Meaning
word : CAT <feat>=val Lexicon entry
A -> B C + <B f> = <C f> Phrase rule with feature equation
! "message" fix: ... Diagnostic attached to a constraint
%mal A -> ... Targeted error pattern (mal-rule)
%feature, %class, %rule, %lex Feature domains, symbol classes, rewrite rules, paradigms
%start, %tokenizer, %clitic Language policy, declared per grammar
%include, %import Modular grammar files, TSV lexicons

Each language is a folder under languages/ with its own manifest, regression corpus, and lexicon. The Swedish fragment (languages/swedish/sv.gram) exercises what English doesn't: two genders with adjective agreement, plus suffixal and double definiteness (hund → hunden, den stora hunden). It also skips what Swedish lacks (present-tense subject-verb agreement), all without engine changes. The Russian fragment adds case morphology (nominative and accusative), animacy, and Cyrillic, also without engine changes. This modularity between languages without changing engine infrastructure was a main concern when writing this engine.

Accuracy and performance

On a held-out probe set of 206 sentences (npm run eval):

  • 96.9% precision of the ungrammatical verdict (63 of 65 flags are real errors)
  • 100% recall on in-scope errors, every flag naming the intended constraint
  • 66.7% abstention on out-of-scope errors: the engine says "out of coverage" rather than guessing
  • 100% of out-of-vocabulary sentences routed to unknown-word

Analysis time is independent of lexicon size: with a 10k-word imported lexicon, grammatical sentences analyze in ~0.3 ms and the diagnostic path (which generates and re-parses repair candidates) stays in low single-digit milliseconds (npm run bench).

Project layout

src/        engine: tokenizer, FST morphology, Earley parser, unification, diagnostics
languages/  grammar data: english/, swedish/, russian/
test/       unit tests, regression corpora, held-out probe set (eval.en.txt)
scripts/    bench, eval, ud-check, corpus, flatten, serve
notes/      the accompanying paper (theory + design decisions)
index.html  browser demo (loads dist/engine.js)

Theory notes

The accompanying paper covers the formal background. You are not required to have any previous knowledge outside of basic computer science to understand its contents. I've tried my best to make it pedagogical for readers without prior knowledge while remaining detailed as a formal paper, and it covers everything related to the design and implementation of the engine, including formal theory, design choices, and performance metrics. Keep in mind it is not finished and is written by me as somebody who does not have a degree in linguistics or formal language theory.

Source: notes/notes.tex · Typeset PDF: notes/notes.pdf

About

Explainable grammar-analysis engine using a compiler-inspired approach for verifiable grammatical repairs of natural languages.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages