Oct 11, 20265 min read/2026/10/11/a-grader-that-cant-be-fooled/

A grader that can't be fooled

The Kite.NET series, part 4 of 8. Previous: Compiling a made-up language to .NET

The whole experiment depends on one thing: every answer the model writes is graded by a machine, the same way
every time. If the grader is weak, the numbers mean nothing. This article is about what the grader checks, and
two traps that quietly switched parts of it off.

Four layers

Every answer goes through four checks, in order:

Layer Catches Ids
Kite's parser and analyzer syntax mistakes, C# written into Kite, changing a let, naming KT000-KT006
The C# compiler (Roslyn) type errors, unknown names, possible null references, unused variables CSxxxx
The .NET analyzers wasted work, dead code, lost results CAxxxx
Hidden tests wrong answers

An answer compiles if it has no errors. It's clean if it also has no warnings. It passes if it
compiles and every hidden test passes.

Kite's own rules

Some mistakes only make sense in Kite, so Roslyn can't see them. Kite checks these itself:

Id Rule
KT000 Syntax: bad indentation, a statement Kite doesn't have
KT001 Changing a let or a parameter (error)
KT002 A let mut that is never changed (warning)
KT003 Naming: functions and variables snake_case, types PascalCase
KT004 Another language written into Kite: ;, braces, var, new, &&, !,
quot;
(error)
KT006 A result thrown away: upper(name) on a line by itself

Here's a program with four mistakes:

module Mistakes

fn average(xs: list<dec>) -> dec
    let total = sum(xs)
    total = total + 0.0
    let mut count = len(xs)
    upper("unused")
    return total / count

fn first_name(name: str?) -> str
    return name.Split(' ')[0]
$ kite check mistakes.kite
mistakes.kite(5): error KT001: 'total' cannot be changed: declare it with let mut
mistakes.kite(6): warning KT002: 'count' is never changed: declare it with let, not let mut
mistakes.kite(7): warning KT006: the result of upper() is not used
mistakes.kite(11): warning CS8602: Dereference of a possibly null reference.
failed: 1 errors, 3 warnings

Three come from Kite's analyzer. The last one comes from the C# compiler: name may be none, and the code uses
it without checking. It points at line 11 of the Kite file, thanks to the #line markers from part 3.

And here's what happens when someone writes C# habits into Kite:

module Leak

fn add(a: int, b: int) -> int
    var sum = a + b;
    return sum
leak.kite(4): error KT004: 'var' is C#, not Kite
leak.kite(4): error KT004: ';' is C#, not Kite

KT004 is the most important rule for the experiment. It's how I measure whether the model is really writing
Kite or falling back to a language it already knows.

The .NET analyzers

Roslyn ships around 270 analyzer rules (Microsoft.CodeAnalysis.NetAnalyzers). By default the .NET SDK turns
on only the ones about outright bugs. For Kite I turned on a few more that a reviewer would flag: dead code
(CA1508), lost results (CA1806), and wasted LINQ (CA1829, CA1860 and friends):

module Ca

fn some(xs: list<int>) -> bool
    if xs.Count() > 0
        return true
    return xs.Any()
ca.kite(4): warning CA1829: Use the "Count" property instead of Enumerable.Count()
ca.kite(6): warning CA1860: Prefer comparing 'Count' to 0 rather than using 'Any()'

A few rules are switched off because they fight Kite's own style: CA1707 complains about underscores in names,
and Kite requires snake_case.

Trap 1: the analyzers were silently skipping everything

The first version loaded all 270 analyzers, and they reported... nothing. Not even on code that was obviously
wasteful.

The cause: I had named the in-memory C# file shop.kite.g.cs. Roslyn treats any file ending in .g.cs as
generated code, and most analyzers skip generated code on purpose. Nothing was wrong except the file name.
Renaming it to shop.kite.cs turned every rule back on.

That one is worth remembering: an analyzer that finds nothing may not be running at all. Test your grader on
code that should fail.

Trap 2: the standard library vanished

Kite's standard library is a C# file compiled into every program. To keep it out of the compiler itself, I
embedded it as a resource named Std.cs.txt. At run time it wasn't there.

MSBuild had read Std.cs.txt as "a resource for the culture cs", which is Czech, and put it in a separate
satellite assembly for Czech users. Renaming it to KiteStd.txt fixed it. It took longer to find than to fix.

Hidden tests

The last layer is the simplest and the most important. Each task has tests the model never sees:

test "word_count counts words"
    expect word_count("hello world") == 2
    expect word_count("  one   two three  ") == 3
    expect word_count("") == 0
    expect word_count("single") == 1

kite test runs each test with a 5-second limit, so an infinite loop in the model's code counts as a failure
instead of hanging the run.

Why all four layers

Each layer catches things the others can't. Tests alone would accept code that's correct but sloppy:
let mut everywhere, results thrown away. The compiler alone would accept code that's tidy but wrong. Together
they grade the way a strict reviewer would, except they never get tired and never grade the same code
differently twice.

The grader also checks the training data, which is the next article: before a task could teach the model
anything, it had to pass all four layers itself.

Next: how the tasks were made and kept.