radlang — Defined Semantics (numbers, strings, bounds)
This document records the defined behavior of radlang for the edge cases that are easy to hit by accident: integer overflow, division by zero, numeric conversion, string/Unicode handling, and out-of-bounds indexing. Everything here is deliberate and tested — none of it is “undefined.”
For language fundamentals see the Overview.
Numeric semantics
Integer overflow — wraps (two’s complement)
int is a 32-bit signed integer, long is 64-bit signed. Arithmetic that
overflows wraps around, exactly like the underlying hardware:
int a = 2147483647 // INT_MAX
a = a + 1 // -2147483648 (wraps to INT_MIN)
long L = 9223372036854775807 // LONG_MAX
L = L + 1 // -9223372036854775808 There is no overflow trap. If you need saturating or checked arithmetic, guard the operands yourself.
Integer division / modulo by zero — throws
Integer division or modulo by a zero divisor throws a catchable runtime error
(division by zero), uniformly on every target. This is language-defined — it
does not depend on the hardware, so it behaves the same on Apple Silicon and
x86-64:
int bad = 10 / 0 // throws: "division by zero"
try {
int x = a / b
} catch (Error e) {
sys.output(e.message) // "division by zero"
} The compiler emits an explicit zero-divisor check ahead of the division, so the
error surfaces as a normal throw you can catch — never a hardware trap
(SIGFPE) or a silently wrong result.
Float division by zero — IEEE 754
float is an IEEE-754 double, so float division is not guarded: division by
zero produces inf / -inf, and 0.0 / 0.0 produces nan. No trap, no throw:
float a = 10.0 / 0.0 // inf
float b = -1.0 / 0.0 // -inf Float → int conversion — truncates toward zero, saturates on overflow
(float).toInt() truncates toward zero (drops the fractional part) and saturates to the target range when the value does not fit:
(3.9).toInt() // 3
(-3.9).toInt() // -3
(1.0e20).toInt() // 2147483647 (saturates to INT_MAX) String and Unicode semantics
radlang strings are UTF-8 byte sequences. String operations are byte-based, not codepoint-based — this is the defined v1 behavior. The single hard guarantee is: no string operation ever produces invalid UTF-8 from valid UTF-8 input.
Length and iteration are byte counts
"héllo".length() // 6 — 'é' is two UTF-8 bytes
"日本語".length() // 9 — three 3-byte codepoints .upper() / .lower() touch ASCII only
Only ASCII A–Z / a–z are case-folded; multibyte codepoints pass through
unchanged (so the output stays valid UTF-8):
"héllo".upper() // "HéLLO" — 'é' untouched
"日本語".upper() // "日本語" — unchanged Repetition is byte-preserving
s.repeat(n) and s * n repeat the original UTF-8 bytes without changing
their encoding. A zero or negative count returns the empty string; n * s is
also accepted.
String subtraction removes substrings
s - needle returns a new string with every non-overlapping occurrence of needle removed. An empty needle leaves the original bytes unchanged.
Indexing returns whole codepoints, never a partial byte
s[i] / s.charAt(i) index by byte offset, but always return a complete,
valid UTF-8 unit:
- If byte
istarts a codepoint, the whole codepoint is returned. - If byte
iis inside a multibyte codepoint (a continuation byte), or the sequence is truncated/malformed,U+FFFD(the replacement character�) is returned instead of a lone partial byte. - An out-of-bounds index returns the empty string
"".
"héllo"[0] // "h"
"héllo"[1] // "é" — byte 1 starts 'é', full codepoint returned
"héllo"[2] // "�" — byte 2 is 'é's continuation byte -> U+FFFD
"日本語"[0] // "日"
"日本語"[9] // "" — out of bounds Out-of-bounds indexing
Lists — throws a catchable Error
Reading a list element outside 0 .. length-1 (including a negative index)
throws a radlang Error rather than reading arbitrary memory:
int[] xs = [10, 20, 30]
xs[5] // throws Error("list index out of bounds: index 5, length 3") Catch it like any other exception:
try { int v = xs[9] }
catch (Error e) { sys.output(e.message) } // list index out of bounds: index 9, length 3 If uncaught, the program prints the message and aborts (exit 128 + SIGABRT).
There is no way to write to an arbitrary list index — lists grow only through .add() / .remove() — so out-of-bounds writes cannot occur.
Strings — return the empty string
String indexing is bounds-safe and returns "" for any out-of-range index (see
the indexing rules above). It never throws.
Constructor validation
Date(string) throws on an unrecognized format
An unparseable date string throws a catchable Error instead of silently
returning epoch 0 (which was indistinguishable from a genuine 1970 date):
Date("2020-01-15") // valid
Date("not-a-date") // throws Error("invalid date string: 'not-a-date'") Accepted formats: YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, YYYY-MM-DDTHH:MM:SS,
and Www Mmm DD YYYY HH:MM:SS.