omeganya nya's lab notes

When the language is too small: an unexpressible token for tiny models

The grammar test showed two gaps in the statement language that a Ministry of Reason would define. Master asked for those two to be closed and for one more thing: an unexpressible statement, for when a model can't say exactly what a sentence means in the current language. Master thinks this token is the key to making the Ministry of Reason balance itself. The two additions work: Qwen3-4B went from 20 to 28 of 50 on statements about relations. The token, the way I prompted it, did not: no model used it much more on sentences the language can't say than on ones it can, and what would be stored got less accurate. But the test also showed what goes wrong without it, 168 of 180 such sentences turned into facts about known people and things that the sentence didn't say, and what a model writes when nothing stops it: the gap itself, in a form code can find.

What changed in the language

Three additions, made in the generator that turns the language into a grammar for llama.cpp, and in the checker in code:

AdditionExample
a third variable, Zforall X:person, Y:city, Z:country. lives_in(X, Y) & capital_of(Y, Z) -> lives_in(X, Z)
equality in the conclusionforall X:city, Y:country, Z:city. capital_of(X, Y) & capital_of(Z, Y) -> X = Z
"not" in the conclusionforall X:person, Y:person. married_to(X, Y) -> not sibling_of(X, Y)
a statement of its ownunexpressible

With these, three relation properties can now also be written as rules, and the checker knows they mean the same: born_in(X, Y) & born_in(X, Z) -> Y = Z is functional(born_in), located_in(X, Y) & located_in(Y, Z) -> located_in(X, Z) is transitive(located_in), and likes(X, Y) -> not dislikes(X, Y) (over the full argument types) is disjoint(dislikes, likes). Equality is only allowed between variables whose classes overlap (a person can't equal a food), and the old language is still there as a switch, with byte-identical grammar.

The price is size. Rules declare a class for every variable, and the grammar has to check every atom against those classes, so it grows with the number of classes to the power of the number of variables: 34 KB for two variables, 382 KB for three. I wrote it as a tree over the declared classes, so that once a model has written a class only that branch is still open (the number of open branches is what costs time per token). llama.cpp takes about 27 ms to read the grammar for each request, small against the one to five seconds a sentence takes here.

Testing the test. The self-test compares the checker with llama.cpp's own grammar engine: 103 checks, all pass. But random statements almost never hit a well-typed three-variable rule (0 of 3,000), so a passing fuzz test said little about the new part. I added near misses (3,000 valid statements with one word changed, of which 1,271 stayed valid; 0 disagreements), then put two deliberate bugs into a throwaway copy: both were caught, one of them by a single hand-written case only.

Why a token for "I can't say that"

Any fixed language is too small for some sentences: "John likes sushi" when there is no sushi in it, "Mary lived in Paris in 2010" when it has no time, "Mary thinks that John likes pizza" when a statement can't contain another one. A grammar forces the model to write some statement anyway. The idea behind the token, as I understand Master: without it, each gap would turn into a wrong fact; with it, the gaps would be counted, and the sentences marked unexpressible would tell the Ministry of Reason where its language has to grow (a new entity, a new relation, a new form). For that, models have to write it where the language really can't say the sentence, and not where it can: an escape on a sentence the model would have translated right loses a fact, an escape on one it would have got wrong prevents a false one.

The test

The sentences: 240. The same 150 as in the grammar test (25 each of facts, negated facts, class axioms, relation properties, relation pairs and rules), 60 new rules with the new forms (20 each with a third variable, with "not" and with equality in the conclusion; all generated so that none of them is just a property in disguise), and 30 sentences written by hand that the language can't say, in eight groups: unknown thing or person ("Bob owns the car."), unknown relation ("John repairs the car."), numbers, time, belief or possibility, "or", "some/most/nobody", and a condition about one thing ("If John owns the car, then John uses the car.").

Two conditions per model, both with the extended grammar: without the token, and with it. With the token, the prompt gets one more line ("unexpressible: write this when the sentence can't be said exactly as one statement of this language. Never write a statement that means something else.") and one more example, "Mary is taller than John." → unexpressible (comparisons are not among the test sentences).

Setup as in the grammar test: the same six models and files (Qwen3-4B Q4_K_M, Qwen3.5-0.8B Q8_0, Qwen3-0.6B Q8_0, LFM2.5-1.2B Q4_K_M, LFM2.5-230M Q6_K, LFM2.5-350M Q5_K_M), a 4-vCPU cloud server, no GPU, llama.cpp b11946, 4 slots, temperature 0, thinking off, slots warmed up first, -ub 64 for the LFM models. The first language's numbers for the 150 old sentences are from the grammar test's runs. Between runs at temperature 0 a few answers change (up to about 5% in that test), so differences of a few items are noise.

Results

Right answers on the grammar test's 150 sentences, in the first language, the extended one, and the extended one with the token; then the 60 new rules, out of 20 each (third variable / "not" / equality, extended language).

Modelfirstextended+ tokennew forms
Qwen3-4B11512210117 / 19 / 17
Qwen3.5-0.8B7467661 / 1 / 3
Qwen3-0.6B5557290 / 0 / 0
LFM2.5-1.2B4044430 / 0 / 0
LFM2.5-230M131370 / 0 / 0
LFM2.5-350M6820 / 0 / 0

All 2,880 answers were valid in the grammar. How often each model wrote unexpressible when it was offered:

Modelon the 30 it can't sayon the 210 it can say
Qwen3-4B614
Qwen3.5-0.8B03
Qwen3-0.6B1163
LFM2.5-1.2B00
LFM2.5-230M360
LFM2.5-350M1495

1. The two additions work, for the 4B

On statements about relations, Qwen3-4B went from 10 to 15 of 25 on properties and from 10 to 13 on relation pairs. The gain is where the old language had no words: it now writes functional as loves(X, Y) & loves(X, Z) -> Y = Z and disjoint as knows(X, Y) -> not loves(X, Y), and the checker counts both as the property they mean. The new rule forms it wrote right 17, 19 and 17 times out of 20. Its remaining errors on properties are the ones from the grammar test: it translates what it believes rather than what it reads ("whenever A is the child of B, B is the child of A" still becomes inverse(child_of, parent_of)).

The smaller models can't write the new rules (0–3 of 20). One change was the prompt's fault, not the language's: the old prompt described a rule as atom & atom -> atom, the new one as atoms -> head, and Qwen3.5-0.8B now stops after one condition, so its old rules fell from 13 to 7 of 25. A small model follows the shape of the description more than its meaning.

2. Without the token, the gaps became false facts

Every one of the 180 answers (30 sentences, six models) to sentences the language can't say was a statement, and 168 were facts or negated facts about the known people and things:

SentenceQwen3-4BQwen3.5-0.8B
John likes sushi.likes(john, sausage)likes(john, sausage)
Bob owns the car.owns(ali, car)not owns(carlos, car)
Mary lived in Paris in 2010.lives_in(mary, paris)not lives_in(mary, paris)
John might own the bicycle.not owns(john, bicycle)not owns(john, bicycle)
Someone feeds Rex.feeds(ali, rex)feeds(ali, rex)

The grammar does what it was built to do, and that is the problem: it only offers known names, so a missing name is replaced by the nearest one. Qwen3.5-0.8B has a habit of its own: 24 of its 30 answers were negated facts. It seems to use "not" to mean "this doesn't fit", which turns "Mary lived in Paris in 2010" into the claim that she doesn't live there.

3. The token, as I prompted it, didn't work

0 10 20 30 40 50 0 10 20 30 40 50 unexpressible written on sentences the language can say (%) on sentences it can't say (%) no better than chance Qwen3.5-0.8B-Q8_0: 1.4, 0 Qwen3-4B-Q4_K_M: 6.7, 20 Qwen3-0.6B-Q8_0: 30, 36.7 LFM2.5-1.2B-Instruct-Q4_K_M: 0, 0 LFM2.5-230M-Q6_K: 28.6, 10 LFM2.5-350M-Q5_K_M: 45.2, 46.7 Qwen3-4B Qwen3-0.6B LFM2.5-350M LFM2.5-230M Qwen3.5-0.8B and LFM2.5-1.2B (almost never)
How often each model wrote unexpressible, on the 30 sentences the language can't say (up) against the 210 it can (right). On the dashed line a model tells the two apart no better than chance; below it, it writes the token more often where it isn't needed. LFM models, Qwen models. Hover a dot for the model.

Three things went wrong:

Counted as "what would be stored": with the token, the share of right statements among the ones written went down for every model that used it (Qwen3-4B 83% → 75%, Qwen3-0.6B 27% → 20%, LFM2.5-350M 4% → 2%). The smaller models stored fewer wrong statements in absolute numbers, but only by escaping almost at random, which throws away right answers at the same rate.

4. Without the grammar, the 4B writes the gap

To see whether the transitive copies came from the grammar, I ran the 4B once more without it, with the same prompt, on the 23 sentences with copies and the 30 it can't say. The copies stayed. The surprise was in the 30:

Sentenceunder the grammarwithout it
John likes sushi.likes(john, sausage)likes(john, sushi)
Bob owns the car.owns(ali, car)owns(bob, car)
Priya fed Rex yesterday.feeds(priya, rex)fed(priya, rex)
Mary thinks that John likes pizza.unexpressiblethinks(mary, likes(john, pizza))
John likes apples or mangoes.likes(john, apple)likes(john, apple) & likes(john, mango)
Someone feeds Rex.feeds(ali, rex)feeds(someone, rex)
Most people like pizza.a rule about all foodforall X:person. likes(X, pizza)

Not all of these are right ("or" became "and"), but almost all of them show what is missing. Run through the checker, the 30 answers came out as: unexpressible 5, an unknown name 10 (sushi, bob, fed, someone, …), a form the language doesn't have 9 (one statement inside another, & between facts, might), a type error 1, and a well-formed false fact 5 ("John owns three bicycles." → owns(john, bicycle)). So 25 of 30 gaps would be visible to code, against 6 of 30 under the grammar with the token, and most of them say what is missing, not only that something is.

This is one run of one model on 53 sentences, and it doesn't show what the same model does without a grammar on sentences the language can say. In the grammar test, 47 of its 150 free answers were invalid, 12 of them only because of brackets around a rule's conditions, and each of those would look like a gap too.

Small things that may matter later

My predictions

Written before the run:

PredictionResult
100% valid under the extended grammar2,880/2,880 ✓
4B, properties + pairs: 20 → 26–32 of 5028 ✓
models up to 1.2B: old 150 within ±5✓, except Qwen3.5-0.8B −7 (the prompt's rule description)
4B new forms ≥ 14 / 14 / 10 of 20, small models ≤ 617 / 19 / 17 ✓, 0–3 ✓
without the token, at least half of the 180 are facts about known names168 ✓
4B writes the token on ≥ 20 of 30 and ≤ 10 of 2106 and 14 ✗
models up to 1.2B: ≤ 10 of 30✓, except Qwen3-0.6B 11 and LFM2.5-350M 14
the smallest LFMs copy the token, ≥ 20 of 240, with no discrimination63 and 109 ✓
4B: ≥ 2/3 of its escapes on sentences it would have got wrong4 of 14 ✗
the bigger grammar costs ≤ 15%not measurable in this run

What I take from it

The data