Ring already checks your types.
It just throws the answer away.

Write int func Sum(int x, int y) today, on stock Ring, and it runs — Ring's own parser reads that annotation on purpose. What it does not do is remember it. ringpp check reads the same channel and remembers what Ring forgets.

What Ring already gives you, and what was missing

Ring already has this

Type annotations on parameters — int func Sum(int x, int y) — parse cleanly on stock Ring today. Mahmoud Fayed put that channel in Ring's grammar on purpose, years ago.

What was missing

Nothing ever read what the parser accepted. The annotation was real syntax with zero effect — write it correctly or write nonsense, Ring runs either one exactly the same way.

What Ring++ is

A checker that reads the same annotations, for real, before your program runs — and reports the disagreement. Your code doesn't change; what you learn about it does.

The bug it found in Ring's own standard library

Not a synthetic example — this is libraries/stdlib/stdsecurity.ring, shipped in Ring 1.27, found by running ringpp check against Ring's own source tree and confirmed by actually calling the function.

encrypt_ex calls the three-parameter std_encrypt with four arguments. It has never worked — nothing in Ring's own codebase ever called it, and nothing static was looking until now.

What ships in Ring 1.27 stdsecurity.ring
Func encrypt_ex cString,cKey,cIV,cCipher
    return std_encrypt(cString,cKey,cIV,cCipher)  # 4 args --
# std_encrypt takes 3. std_encrypt_ex, right next to it, takes 4.
ringpp check catches it before it runs
stdsecurity.ring
  52:10  error  rpp/type-arity
         std_encrypt() takes 3 argument(s), called with 4
         Error (R20) at run time (defined in stdfunctions.ring)

# running it instead:
x.encrypt_ex("secret", "key", "iv", "AES-256-CBC")
# Error (R20) : Calling function with extra number of parameters

Postscript — fixed upstream, one day later. On 25 August 2026, the day after this finding reached the Ring group, both functions were repaired in Ring's own repository — commit 7890ea5, "Reported by Mansour Ayouni". The finding did its job.

Why the checker can be certain, not just suspicious

An arity claim across files is only worth making if it cannot be wrong. Three measured Ring behaviours make it safe, not three guesses:

One definition, guaranteed

A duplicate function name is C22 at load time — the program never starts. So within one load graph, a resolving name has exactly one live definition. "Defined in another file" is not a guess.

A known resolution order

Inside a class: own method → inherited → global → builtin, arity enforced at every step. A call inside a class body is decidable whenever the whole method chain is visible.

It knows what Ring keeps

Parameter type annotations are parsed and discarded by Ring itself; a return annotation is an ordinary variable read that needs typehints.ring loaded or it raises R24. The checker knows which half of the syntax Ring actually evaluates.

Where certainty is not available — an unknown parent class, a file that did not parse, a name with two definitions — the checker refuses rather than reports. A program whose load "stdlib.ring" could not be followed gets NO VERDICT, never a false "clean".

The trip inside Ring, if you want it

Somewhere in Ring's own C source (stmt.c) sits a comment that just says "Support Type Identifier" — the exact line where the parser reads a type in front of a parameter and, deliberately, keeps parsing rather than rejecting it as a syntax error. It was never wired to anything. Not a bug, not an accident: Mahmoud Fayed left a door open in Ring's own grammar, syntax that costs nothing and does nothing, years before anyone built something to walk through it. Finding that line — and confirming, by testing it, that Ring genuinely never enforces what it accepts — is the whole reason ringpp check exists in the shape it does, rather than as a new syntax bolted onto Ring from outside. The fuller design story is its own page, alongside the other two doors and the contract that keeps this working as Ring evolves.

The 25 rules

Every one traces to a measured finding — a specific microsecond figure or a specific Ring error code, never "this might be slow".

layer rules needs
lint varptr-*, memcpy-*, undefined-function, uninitialized-variable, unknown-class, unknown-package, exit-outside-loop, exit-bad-depth, substr-in-loop, len-in-loop-header, genarray-in-loop, empty-catch, method-shadows-builtin, unparsed a parse only
advice (--advise) advise-forin, advise-patch-rebuild — places where a measured Ring++ idiom is faster. Hidden by default: opportunities are not defects, and mixing them is how a lint gets ignored. a parse only
level-1 types type-hints-missing, type-arity, type-arg-mismatch, type-return-mismatch, type-not-a-hint Ring's own annotation channel
cross-file type-duplicate-func, type-declared-conflict the load graph, --ring <dir>

What it found at scale

1,959
Ring's own files — 3 errors found, all real, 0 false positives
99
arity call sites found in Softanza's 6,012 files — 97 in dead archives
46
distinct functions those 99 call sites reached across
3
false positives an earlier version produced, and what they cost — below

What it got wrong, and what that cost

Every one was a Ring scoping rule the checker did not yet know — and not one would have been caught by a test written from imagination. All three were caught by running real corpora, not by reasoning about the rules in the abstract.

344 invented errors — a brace block like StzCharQ("x") { ? Name() } runs in the object's own scope, so Name() is a method call, not an undefined function. Would have buried the 99 real findings thirty to one.

A call inside a comment — one file that failed to parse had a call invented by the grammar's own error recovery inside a /* block comment */. Files that do not parse are now excluded from cross-file checking entirely, in both directions — costing 12 genuine duplicate definitions in a specification sketch that cannot run. A true finding in a file that never runs costs less than one false finding in a file that does.

Ring's own call keywordcall draw(oGame, self) invokes a function held in a variable. Took Ring's own libraries from 0 errors to 5 until the checker learned to recognise it.

The rule underneath all three: a checker's only asset is that it is never wrong about correct code. The full case study has the rest, including two live bugs found and fixed in Softanza.

Next

Native build — ship it with no compiler Blog