The design underneath
the three pillars.

None of what Ring++ does is a new idea. It's three deliberate choices already built into Ring, read closely enough to use on purpose — and a promise about how Ring++ keeps working when Ring changes.

A small, opinionated surface over something vast

Ring is a small, friendly language sitting on top of a large, unforgiving one. Its own C source is not simple — hundreds of thousands of lines, memory managed by hand, every mistake a segfault with no message. Almost none of that reaches a Ring programmer. That gap — a small surface hiding something vast — is Ring's central design decision, made once, deliberately, years before Ring++ existed. Ring++ doesn't add anything new to that shape. It walks through three doors that were already left open in it.

Door one — a copy is the safe default, until it doesn't need to be

Somewhere in Ring's C source, one macro decides whether a value crossing into a function is handed over by address or copied whole: RING_VM_STACK_PUSHCVAR, vm.h:230. An object crosses by reference. A string is copied, in full, every time.

That split is not an oversight — it's the conservative choice, made correctly. Ring's objects are reference-counted: the runtime always knows how many places hold one, and can safely hand out its address because it knows exactly when the last reference goes away. A plain string carries none of that bookkeeping, so copying it is the one answer that can never surprise anyone, at the cost of a copy nobody asked for once that string gets large.

Ring++'s RppBuffer doesn't fight that rule — it moves the declaration "this needs to be held, not copied" into the machinery Ring already trusts for exactly that: an object. No VM change, no new syntax, one class. The measured difference is 75×, and where it loses is stated just as plainly.

Door two — a channel built to be extended, then left waiting

Deep in Ring's parser (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, like int func Sum(int x, int y), and keeps parsing rather than rejecting it as a syntax error. Nothing was ever wired to it. It costs nothing and does nothing, and it has been true for years.

Read as a design choice, that's a language leaving a hook for a future it doesn't have to build yet — syntax an ecosystem can grow into, without the language itself needing to change first. ringpp check is what finally walks through that specific, dated hook: it reads the same channel Ring's own scanner already validates, and treats what was silently accepted as meaning something. What it found by doing that: two functions in Ring's own shipped standard library that had never worked — since fixed upstream, because of the report.

Door three — two ways out, and one was already built

Run ring yourfile.ring -go on stock Ring today and it writes yourfile.ringo — bytecode, no C compiler involved, ever. Run ring yourfile.ringo and it just runs. This is how Ring itself starts up fast; it has always been true. Ring's own packaging tool, ring2exe, took the other fork from there — writing a C file and handing it to a real compiler.

ringpp build takes the fork that was already open: it keeps the bytecode Ring already knows how to write, and attaches a small prebuilt copy of Ring itself that knows how to run it. Nothing new was invented — one command, wrapped around a door that was standing open the whole time.

What ties the three together

Same pattern, three times: a capability Ring's own design already has, sitting unused because nothing pointed at it yet. That's the actual curriculum here — not Ring++'s implementation, but the habit of reading a language's internals closely enough to notice what it already lets you do, and building the smallest possible thing that makes doing it ordinary.

The contract — staying stable as Ring evolves

Ring++ depends on Ring's internals, and internals change — that's normal for any language under active development. The standing rule here is never fight the VM: no fork, no C patch, no shadow allocator. Instead, Ring++ writes down, in one document, the complete list of what it actually relies on — not implementation details, not struct layouts, not the pool manager. Everything outside that list stays entirely free to change without asking anyone, because Ring++ never touches it.

~20
registered functions named — the whole dependency, nothing implicit
9
observable behaviours, checked, not assumed
<1s
to verify all of them, every time ringpp.ring loads

The contract itself is machine-checked by rpp/probe.ring on every load — not "does this function exist" but "does it still behave the way Ring++ assumes." A few rows are HARD: the pointer arithmetic the fast path is entirely built on. If one of those ever changes, using it raises immediately, by name, rather than silently corrupting memory. A few are softringvm_genarray, sub-states — and degrade to a slower, pure-Ring path when they don't hold, saying so. The rest are informational.

The point isn't paranoia, it's honesty: if a future Ring changes one of these specific, named behaviours, the probe says exactly which claim broke, in words, the moment the library loads — instead of someone finding out through corrupted data three weeks later.

A contract both parties could sign

The long-term intent, drafted but deliberately not sent — findings about Ring go to the Ring Google Group, posted by the maintainer himself, after review, never as a pull request: propose this exact list as a two-way agreement. A small, named surface treated as observable-and-stable — or at least flagged in release notes before it changes — in exchange for Ring++ promising to depend on nothing outside it. No forking, no patching, no reaching past the documented API into C internals, ever.

It's the same shape as Ring's own relationship to C: a small, opinionated surface over something vast, kept honest by checking it rather than assuming it holds.

Next

Read the full contract Library reference