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.
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.
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.
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.
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.
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.
An arity claim across files is only worth making if it cannot be wrong. Three measured Ring behaviours make it safe, not three guesses:
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.
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.
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".
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.
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> |
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 keyword — call 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.