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 AZ / az 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 i starts a codepoint, the whole codepoint is returned.
  • If byte i is 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.