Ring's issues that Ring++ solves

Each issue below is a small Ring program, run on official Ring 1.27 and on Ring++ (16 October 2026). You read the program first, then what each runtime printed. Nothing here is a promise: it is what the two printed.

Programs that die

An object taken from a list ends the program, with no message

SolvedF-100C-24

Ring 1.27 Adding two objects through their operator method works -- unless the right one comes from a list: then the program stops, exit code 1, no error printed, and no try can catch it.

Ring++ The operator is called, and the program goes on.

# Two prices: one in a variable, one inside a list
oPrice  = new Money { nAmount = 10 }
aPrices = [ new Money { nAmount = 5 } ]

# Add them: the class tells Ring how to add two Money objects
oTotal = oPrice + aPrices[1]
? oTotal.nAmount
#--> Ring 1.27: nothing at all -- the program ends here, exit code 1,
#    with no error message, and no try/catch can stop it
#--> Ring++: 15

? "done"

class Money
	nAmount = 0

	func operator cOp, oOther
		oSum = new Money
		oSum.nAmount = nAmount + oOther.nAmount
		return oSum
Ring 1.27 printed
(nothing)
(the program stops, exit code 1)
Ring++ printed
15
done

Empty catch blocks fill the stack until the program dies

SolvedF-16

Ring 1.27 Each empty catch leaves one slot on the VM's stack. A loop that expects failures dies with Stack Overflow.

Ring++ Nothing is left behind; the loop runs to the end.

# Try something that fails, 3,000 times, and ignore the failure
for nTry = 1 to 3000
	try
		raise("a failure we expect")
	catch
		# nothing to do: the failure is expected
	done
next

? "3,000 failures handled"
#--> Ring 1.27: Error (R4) : Stack Overflow -- every empty catch
#    leaves one slot behind, until there is no room left
#--> Ring++: 3,000 failures handled
Ring 1.27 printed
Line 2 Error (R4) : Stack Overflow
(the program stops, exit code 1)
Ring++ printed
3,000 failures handled

Wrong answers, given silently

Reading a missing key adds it

SolvedC-13F-49

Ring 1.27 Asking a hash list for a key it does not have inserts that key. A read changes the data it reads.

Ring++ A read never writes.

# A hash list with two entries
aHash = [ :a = 1, :b = 2 ]

# Read an entry that does not exist
nValue = aHash[:c]

# How many entries are there now?
? len(aHash)
#--> Ring 1.27: 3 -- reading :c silently ADDED it to the list
#--> Ring++: 2 -- a read never writes
Ring 1.27 printed
3
Ring++ printed
2

A method overwrites the program's variable

SolvedC-2

Ring 1.27 A method that assigns a name the program also uses writes the program's variable, silently. Softanza spells its own variables _name_ at 175,797 places to avoid it.

Ring++ A method's variables are its own. A global is written $name.

# A variable of the program
cName = "Mansour"

# An object whose method uses a variable with the same name
oGreeter = new Greeter
oGreeter.Greet()

# Is the program's variable unchanged?
? cName
#--> Ring 1.27: a visitor -- the method silently overwrote it
#--> Ring++: Mansour -- a method's variables are its own

class Greeter
	def Greet()
		cName = "a visitor"
Ring 1.27 printed
a visitor
Ring++ printed
Mansour

switch ignores the second value of an on

Solved

Ring 1.27 on 6, 7 is accepted without a word, but only 6 is ever compared.

Ring++ on 6, 7 matches either value.

nDay = 7

switch nDay
on 6, 7
	? "weekend"
other
	? "weekday"
off
#--> Ring 1.27: weekday -- `on 6, 7` is accepted,
#    but only its first value is ever compared
#--> Ring++: weekend
Ring 1.27 printed
weekday
Ring++ printed
weekend

A range of two strings answers a string

Solved

Ring 1.27 "1":"12" is neither a list nor an error: it is a string, and the program goes on with it.

Ring++ An operator that cannot do what it is asked says so.

# A range of numbers is a list
aNumbers = 1:3
? type(aNumbers)
#--> LIST, on both

# A range of two strings that are not single letters
xRange = "1":"12"
? type(xRange)
#--> Ring 1.27: STRING -- no list, and no error
#--> Ring++: Bad parameter type! -- the operator says it cannot do this
Ring 1.27 printed
LIST
STRING
Ring++ printed
LIST
Line 7 Bad parameter type!
(the program stops, exit code 1)

The language changed under you

TRUE can be reassigned

Refused

Ring 1.27 After TRUE = 0, every if TRUE in the program takes the other branch.

Ring++ Assigning a constant of the language is refused: the line is not compiled, and the program stops there and says why.

# TRUE is a constant of the language... or is it?
TRUE = 0

if TRUE
	? "TRUE is true"
else
	? "TRUE is false now"
ok
#--> Ring 1.27: TRUE is false now
#--> Ring++: refuses the first line, before anything runs
Ring 1.27 printed
TRUE is false now
Ring++ printed
Line 2 is refused: TRUE = 0
(the line is not compiled; the program stops there)

A program can rename the language's keywords

RefusedC-28

Ring 1.27 ChangeRingKeyword renames a keyword from that line on, silently, for whoever reads the file next.

Ring++ It is refused, by name: a reader of the file sees the language it was written in.

# Rename the keyword `see` to `show`, from this line on
ChangeRingKeyword see show

show "the keyword see is now spelt show"
#--> Ring 1.27: accepts it, silently, for the rest of the file
#--> Ring++: refuses it, by name
Ring 1.27 printed
the keyword see is now spelt show
Ring++ printed
Line 2 is refused: ChangeRingKeyword see show
(the line is not compiled; the program stops there)

A loop variable keeps a value the loop never gave it

RefusedC-11

Ring 1.27 After for nIndex = 1 to 5, nIndex is 6. Four kinds of loop leave four kinds of value behind.

Ring++ The variable belongs to its loop, and reading it afterwards is refused. Run over Softanza, the rule found two real bugs.

func Main
	for nIndex = 1 to 5
		# the loop's work
	next

	# What is nIndex once the loop is over?
	nAfter = nIndex
	? nAfter
	#--> Ring 1.27: 6 -- a value the loop never took
	#--> Ring++: refuses the read -- the variable belongs to its loop
Ring 1.27 printed
6
Ring++ printed
Line 7 is refused: loop variable read after its loop: nIndex
(the line is not compiled; the program stops there)

Found only when the line runs

A missing function is found only when its line runs

SolvedF-46

Ring 1.27 The program runs up to the call, then fails -- in production, on the day that line is reached.

Ring++ rnxc check reports it, with its line, before anything runs.

? "Starting the payroll"
SendPayslips()
#--> Ring 1.27: prints the first line, then fails on the second
#--> Ring++: `rnxc check` reports it before anything runs
Ring 1.27 printed
Starting the payroll
Line 2 Error (R3) : Calling Function without definition: sendpayslips
(the program stops, exit code 1)
Ring++ printed
rnxc check p.ring, before running it:
line 2: SendPayslips() is not defined anywhere this file can reach — Error (R3) the moment this line runs

Speed

Three choices in Ring's design cost it the most: strings copied at every call, lists that are linked, and methods found by name at every call. Ring++ replaces all three. The same programs, on the same machine, minima of three runs, every answer checked equal:

the programRing 1.27Ring++why
A 1 MB string, passed to a function 2,000 times1,348 msbelow 1 msRing copies a string at every call; Ring++ passes it by reference.
Binary search: 150,000 lookups in 200,000 numbers188,741 ms93 msA Ring list is linked, so reaching item n walks to it; a Ring++ list is an array.
Merge sort of 200,000 numbers5,222 ms242 msLists, appends and calls.
Matrix multiplication, 220 by 2201,790 ms245 msNested lists and arithmetic.
Sieve of Eratosthenes up to 4,000,0001,335 ms237 msInteger arithmetic over a list.
Recursive Fibonacci of 31433 ms100 msFunction calls.

The first measurements of the VM, and what flatters them, are recorded in NOTES.md.

What Ring++ keeps from Ring, for now

Some of Ring's behaviours are kept on purpose, so that existing programs keep their meaning: a list printed one item per line, a named argument that arrives as a two-element list, decimals() as a setting for the whole program, a string measured in bytes. They are not solved, so they are not listed above. They are decisions, explained below, and the next language, Haro, is where they change.

Every decision, explained

Where Ring++ answers differently from Ring, it is never by accident. Each difference is a decision, taken for one defect at a time and written down with what it costs. There are four kinds.

Keep Ring's answer, so existing programs keep their meaning, even where Ring's choice is questionable. decimals(2) still changes every number the program prints from then on. A bare name at the top level is still a global. A load that names a missing file still stops the program before it starts, exactly as on Ring.

Fix it silently, where no correct program can notice. Reading aHash[:key] no longer inserts the key. An object taken from a list no longer ends the program when it meets an operator. Leaving a try through return also leaves its handler, from a function as from a method.

Refuse, with a rule, where Ring's behaviour is a trap. The program is refused before it runs, and rnxc check names every place the rule applies: a loop variable read after its loop, a numeric for whose bound changes while it runs.

Drop it, and say so, for what only Ring's own programs use and Softanza never does: renaming keywords, packages, raw memory pointers. Each is refused by name rather than half-built.

All 31 decisions are on record, each with its reasons and its cost, in the corrections register.

Every finding, explained

The findings are what Ring's VM was measured to do: 127 of them, each from a program that ran. They fall into four families.

Costs the source does not show. A string passed to a function is copied, every time. A loop written for i = 1 to len(s) measures the string again at every turn, which makes it quadratic. How fast a list is read depends on how it was built and on where it was read last. On identical programs, Ring measured 9 to 16 times behind Lua.

Traps. N and n are the same variable. Every func written after the first class of a file becomes a method of that class. A string has no escapes: "\n" is two characters. char(300) is char(44), silently.

Rules that Ring++ had to learn, to run Ring's programs as they are. :Name = value is a two-element list. Ring calls Main() by itself after the top level. Every operator on an object calls its operator() method. eval runs in the scope of the function that calls it.

Ring's own failures. memcpy() ending the process on a zero byte; empty catch blocks filling the stack; an object from a list ending a program without a message; the Linux build crashing as it exits.

All 127, each with the program that shows it, are in the findings.