Ring, faster, safer and more portable — without ever leaving Ring. A dependency-free library and a one-binary CLI, each one also a window onto how Mahmoud Fayed built a small, friendly language over a large and unforgiving one underneath.
| what it is | the number | |
|---|---|---|
| Performant code | RppBuffer / RppView — a block of bytes that crosses a function call by reference, the way every Ring object already does, instead of by copy |
75× faster — 302 ms → 4 ms, same loop, byte-identical result |
| Static analysis | ringpp check — reads the type annotations Ring's own parser already accepts and silently discards, across the whole load graph |
2 functions in Ring's own shipped stdlib that have never worked, 0 false positives across 1,959 files |
| Native build | ringpp build — Ring's own bytecode plus a prebuilt runtime stub, no C compiler involved anywhere |
1 command, 5 platforms, 0 compilers installed |
Not three separate tools — three angles on one idea: Ring already has the machinery for this, mostly unused. The library reference has every method with a real example; Start here has the install.
Either half — library or CLI — is usable alone. They meet in the middle: the checker's rules exist because the library's measurements found the traps, and the library's idioms are what the checker recommends.
Ring++ exists because of real customer-facing work, not a lab exercise. A bank in Niger, running Ring in production, liked the language but wanted more before trusting it with critical projects. A restaurant management platform in France asked the same question from a different angle — real-time, high-volume, and unwilling to leave Ring. And Softanza, the base library I've been building in Ring alongside Ring++, has grown past 300,000 lines — a codebase where the same concerns stop being optional. Ring++ is what came out of answering all three, without ever leaving Ring.
Ring++ ships as a repository, not a package. Clone it, or copy the folder
anywhere you like — the library is relocatable, and rpp/*.ring load
relative to ringpp.ring.
git clone https://github.com/mayouni/ringpp
Then, from anywhere:
load "ringpp.ring"
oBuf = new RppBuffer(1024)
oBuf.Poke(0, "hello")
? oBuf.Peek(0, 5) # --> helloThat's one method of four classes — every method on all four is on one page, each with a real example.
The same clone brings the ringpp CLI with it — one prebuilt
binary, made with Zig:
ringpp check myproject/ # type safety + the measured lint rules
ringpp why R4 # explain the Ring error you actually saw
ringpp build myproject/main.ring --out dist --target linux-x64
No C compiler, no clang, no toolchain is required or suggested — ever. The Zig source of the CLI is in the repository; only someone who wants to adapt the CLI installs the Zig compiler.
Five prebuilt binaries ship: Windows x64, Linux x64 and arm64 (static
musl — one file for any Linux), macOS x64 and arm64, all cross-built from a
single machine. bin/README.md says which is which and
how far each one is verified — Windows and Linux x64 were executed against
the full fixture set; the other three are compiled and format-checked but
have not been run, because this machine cannot run them.
Status: the library, the CLI, and eight measured examples are built
and gated. powershell -File tests\run-all.ps1 runs everything.
A site organised around the three pillars above lives in
site/ — open site/index.html directly, or enable
GitHub Pages on this repository (Settings → Pages → deploy from site/)
to put it at mayouni.github.io/ringpp. Not deployed by this commit;
deployment is a repository setting only the owner can flip.
| site/reference.html | The library, one page — every RppBuffer, RppView, RppIndexed and RppSandbox method, with a real example for each. |
| docs/CLI.md | ringpp — every command, real captured output, no compiler needed for any of it. |
| docs/CASE-TYPE-SAFETY.md | What the checker actually found — two dead functions in Ring's own standard library, two live bugs in Softanza, and the three false positives it produced along the way. |
| docs/CASE-SOFTANZA.md | A string-heavy Softanza path, measured — a 10× regression found in real code and its mechanism, a name collision that made Ring++ unloadable beside it, and the number where Ring++ itself is the wrong tool. |
| docs/ANDROID.md | Ring on a phone: a tutorial, the --target android reference, why no NDK is needed, and what three runtimes measured on the same device. |
| docs/BUILD-OPTIONS.md | The three Ring packaging tools compared from each one's own source, and which sibling project owns which target so nobody duplicates work. |
| docs/CHECKER-FRONTIER.md | Where the checker goes next — Ring's 55 runtime errors triaged for compile-time catchability, the four cost mechanisms behind every perf rule, and the rules that stay out on purpose. |
| docs/VM-CONTRACT.md | The abstract interface: exactly what Ring++ needs from the VM, as observable behaviours, probe-checked on every load — and a proposed contract both parties could agree on. |
| docs/FINDINGS.md | What the Ring VM actually does, measured. Read this first — two of its numbers killed the design I set out to write. |
| docs/DESIGN.md | The library half: what Ring++ is, the layer map, the surface, safety, upgrades, myctiger, Softanza, and the risks. |
| docs/DESIGN_TOOLCHAIN.md | The toolchain half: types, compilation, static analysis, the vendored VM, what Julia teaches and where the analogy breaks. |
| docs/PHASE_PLAN.md | Phases with gates. A plan is not done until its gate runs. |
| docs/UPSTREAM_NOTES.md | Draft text for the Ring Google Group. Not sent. |
| bench/ | The programs that produced every number. |
Ring passes lists by reference and strings by copy. That single
asymmetry — RING_VM_STACK_PUSHCVAR in vm.h:230 — is behind almost
everything that is slow about large-data work in Ring. Passing a 1 MB
string to a function costs ~750 µs; passing a handle to the same
bytes costs 0.3 µs. substr(cBig, n, 10) on a 500 KB string costs
12.5 µs because it first copies half a megabyte; ptr2str(p, n, 10)
costs 0.09 µs. Patching 2,000 offsets costs 803 ms in pure Ring
and 1 ms in place. Ring++ is the way to hold a large value still —
this is not a Ring++ invention: it's how every class instance in Ring
already behaves, extended to bytes.
What Ring++ is not is "pointers are faster." They usually are not:
building 1.6 MB from 8-byte chunks is 28× slower through memcpy
than through cOut += chunk, because Ring's string append already
doubles its capacity and every crossing into C costs ~100 ns. The
library's most common correct answer must be no, and it should be able
to say so at runtime.
Ring's parser already accepts type annotations, on purpose —
/* Support Type Identifier */, stmt.c:1217 — and
ring_state_stringtokens hands them back verbatim. int func Sum(int x, int y) runs today on stock ring.exe. Ring++ consumes a channel that
already exists and is currently thrown away — the clearest possible case
of building on Mahmoud's design rather than around it. Annotated Ring++
source always keeps running under stock Ring; the checker adds
meaning, never syntax.
(Measured before building: the two halves of an annotation are different
mechanisms — parameter types are a parser feature that costs nothing,
while a return annotation is a variable read that needs
typehints.ring loaded or it raises R24. FINDINGS
F-24 has the table; the checker knows the difference.)
On real codebases the checker found two functions in Ring's own
standard library that have never worked — encrypt_ex and
decrypt_ex call the wrong function and die with R20 on every call —
with zero false positives across Ring's 1,959 files. Both were fixed
upstream the day after the announcement —
7890ea5, "Reported
by Mansour Ayouni". See the case
study, which also records the three false
positives an earlier version did produce and what they cost.
Run ring yourfile.ring -go on stock Ring today and it writes
yourfile.ringo — compiled bytecode, no C compiler involved at any
point; that's how Ring itself starts up fast. Ring's own packaging tool,
ring2exe, takes the other door from there: it writes a C file and hands
it to Visual C++, GCC or Clang. ringpp build keeps the bytecode Ring
already writes and attaches a small prebuilt copy of Ring itself that
knows how to run it — nothing new invented, just the door that was
already open. It also refuses to package a program that reaches Ring's
Qt bridge: bundling the one library static analysis can name
(ringqt.dll) once produced a package that reported nothing missing and
crashed with no diagnostic at all, because that DLL itself links
roughly 75 further Qt libraries no .ring source ever names — on top of
Qt's own dual licence being a real business risk for anything shipped to
customers, decided against before Ring++ existed.
ringpp build app.ring --target android
One command, one signed .apk. No NDK, no Qt, no Qt Creator, no Gradle,
no JNI, no C compiler — the VM inside the APK is byte-identical to the
linux-arm64 stub every other build ships, because Ring's VM has no JIT and
so never asks Android for the executable memory its seccomp filter refuses
(F-36). The property that costs Ring speed is the one
that bought it the platform. Android is the single target that needs a
toolchain at all — an Android SDK and a JDK, since only Google's aapt2
writes a binary manifest — and the command says so and refuses rather than
half-packaging.
Verified on hardware, an Infinix X6817 (arm64, Android 12): all six of the
library's own gates pass on ARM, and ten computed results are byte-identical
with the x64 build. Benchmarking on the device then caught a bug in our own
runtime: string workloads ran 22–29× slower than desktop while compute ran
4–8×, and a profile put the interpreter at 0.44% of cycles — musl's
allocator was round-tripping every large-string copy through mmap/munmap.
The musl stubs now link mimalloc and enable Ring's own shipped computed-goto
dispatch, with not one line of Ring source changed: string workloads got
3.6–7.7× faster on the phone, and the device now tracks the desktop at a
uniform 2–7× everywhere (F-40).
docs/ANDROID.md has the full tables, including where the
library wins less on mobile and the one gap still unexplained.
Compilation is not part of the Ring++ product — no C compiler, no
toolchain is ever asked of a user. But the headroom was measured, and the
numbers are kept because they map where Ring's own boundary sits
(bench/headroom/, docs/DESIGN_TOOLCHAIN.md):
| kernel | Ring | native | ratio |
|---|---|---|---|
| scalar loop, 20 M iterations | 1151 ms | 18 ms | 64× |
| dot product, 1 M doubles | 92 ms | 0.96 ms | 96× |
| byte scan, 5 MB | 645 ms | 0.22 ms | ~3000× |
If a compiled half is ever built, it will be proposed separately, with
its own gates — and RppBuffer is already its calling convention.
ringvm_genarray() makes a permuted read of an 80,000-item list ~95×
faster. It also makes a write-heavy loop 10–16× slower, and a single
aList + x silently destroys the array. The break-even is around 10–20
random reads per mutation.
So the power was already in the language. What was missing was a way to reach it that is explicit, local, and obvious in the code — and that refuses when it would make things worse. Ring++ is meant to be a hundred decisions of that shape.
Ring 1.27.0 is required; the 1.26 install has its own pathologies.
D:\ring127\bin\ring.exe bench\07_by_value_tax.ringEach program in bench/ prints its own table. bench/safety/ contains
four programs that demonstrate the failure modes — two of them kill
the process on purpose; run each in its own shell.
- Never fight the VM. No fork, no shadow allocator, no global interception. Anything needing a VM change is a finding, reported separately.
- Survive Ring upgrades untouched. A ~30-name declared surface, one behavioural probe per name, honest degradation instead of breakage.
- No dependency the user must install. The library is pure Ring, one
load. The CLI is one shipped binary. The Zig compiler is needed only by someone adapting the CLI's source — never by a user. - Measure, never assume. Every claim is an A/B differing in one thing, and every claim ships with the pattern it hurts.
- Never open a pull request or an issue on
ring-lang/ringby default — prepare the text, don't send it. Findings go to the Ring Google Group, posted by Mansour. One exception: he asks explicitly to publish, after reviewing the finding himself.
Mahmoud Fayed — for Ring, and for the design this project is built on rather than around. The annotation channel Ring++ reads was put there deliberately; the list/string asymmetry the library exploits is a considered tradeoff, not an accident. Rule 1 above is meant literally: this is a schoolcase in his patterns of thinking, not in his implementation.
Youssef Saeed (@ysdragon) — for
tree-sitter-ring,
the Ring grammar the entire type-safety half stands on. It is vendored
here under its MIT licence (vendor/tree-sitter-ring/LICENSE) and built
into the shipped binary, so every ringpp check is running his work. It
was adopted rather than written from scratch because it independently
reached the same reading of Ring's type annotations that the measurements
here did — the strongest signal available that the reading is right.
Grammar findings from this project go back to him as
issues, never
patches around him. And when this project reported one case-sensitivity
defect in ring_state_findvar, his follow-up turned it into four —
including ring_state_newvar, whose failure mode (a variable that exists
and cannot be addressed) was worse than the one originally reported. The
fix landed as
b6aea3d,
credited to both.
The tree-sitter authors — for the runtime, also vendored under MIT
(vendor/tree-sitter/). Full third-party licence list in
LICENSE.
Andrew Kelley and the Zig team — for the
toolchain the CLI is built with. Zig compiles ringpp into one binary per
platform and is what makes ringpp build's five-platform cross-compilation
possible from a single machine. The Zig source of the CLI ships in this
repository; the Zig compiler is asked of no user — only of someone who
wants to adapt the CLI itself.
