Portable number semantics
Since: 0.1.0
This document is the normative numeric contract for Game Event Script compiler,
runtime, .gesb, and portable Conformance implementations. Every language port
must implement these results independently of its language’s
default overflow, conversion, formatting, or math-library conventions.
Conversion compatibility
Within a compatible release line from 1.0 onward, numeric conversions preserve accepted input forms, units, value interpretation, rounding/overflow rules and specified invalid-input outcomes. A compatible update cannot silently change a conversion’s meaning, such as interpreting a percentage as a different ratio. The numeric variation and text-spelling freedoms explicitly allowed by this specification remain allowed; compatibility does not turn them into bitwise or textual identity promises. See source compatibility and recompilation.
Numeric representations
- Integer values are signed 64-bit integers in
[-2^63, 2^63 - 1]. - Fractional values use IEEE 754 binary64.
.gesbstores their exact 64 bits in little-endian form; implementations must not serialize a host-language object layout. - A finite binary64 result that is exactly integral and lies in the Int64 range
is normally stored as an integer. The Int64 upper test is strictly
< 2^63, not<= (double)Int64.MaxValue, becauseInt64.MaxValuerounds to2^63in binary64. - Percentage values store their ratio as binary64. For example,
10%stores0.1. - Quantity units do not change the numeric representation.
The integer/float storage distinction is not an arbitrary-precision promise.
is integer also accepts finite, exactly integral binary64 values as described
in the language specification.
Integer arithmetic and overflow
Integer +, -, and * first compute an exact signed-64 result without using
the host language’s ambient checked/unchecked behavior. If the mathematical
result fits Int64, the result remains an integer. On overflow, both operands are
converted to binary64 and the operation is repeated as binary64. This can lose
integer precision, but it never wraps, traps, or invokes C++ signed-overflow
undefined behavior.
After binary64 fallback, normal value canonicalization still applies. Therefore
an overflow approximation that rounds to an exactly integral in-range binary64
value may be stored as an integer. This is a representation detail; all ports
must produce the same numeric value. Unary negation and abs of -2^63 likewise
produce binary64 2^63.
Compiler constant folding and VM execution must use the same helpers and yield
the same result. In particular, operands above 2^53 must not be converted to
binary64 before an exact in-range integer operation.
Binary64 special values
- A scalar NaN is the internal marker for invalid mathematics or numeric
conversion and canonicalizes to
nothingat the value boundary. It is never an observable scalar number. A conversion implementation may instead report an invalid result directly; this has the same script-visible outcome. - Positive and negative infinity are observable binary64 values. Operators use
the explicit rules in the language and bytecode specifications; scalar
/follows IEEE division, including division by zero. - Every stored numeric zero is canonicalized to positive zero. This applies to
scalar floats, percentages, vector/point components, and floating ranges.
Consequently
-0formats as0and compares equal to0. - A vector or point with a NaN component is invalid and canonicalizes to
nothing. Floating ranges already require finite bounds and step.
Conversion and rounding
An existing Int64 number cast with as :Number retains its exact value and
quantity unit. The cast must not pass that value through Binary64, including
when the result is used as a dynamic random seed. Compiler constant evaluation
and runtime conversion use the same rules.
Explicit text-to-Number conversion follows the text grammar and value rules in Text and number conversion. These rules apply equally to literals, named constants, and values received at runtime.
Binary64-to-Int64 conversion first truncates toward zero and then saturates:
| Input | Result |
|---|---|
| NaN | 0 for low-level conversion helpers; observable NaN has already become nothing |
+Infinity or value >= 2^63 |
Int64.MaxValue |
-Infinity or value < -2^63 |
Int64.MinValue |
| otherwise | truncation toward zero |
Integer-valued intrinsics use the following definitions and then the same saturating conversion:
floor: toward negative infinity.ceil: toward positive infinity.truncate: toward zero.round half even: nearest; an exact half selects the even integer.round half up: nearest; an exact half goes away from zero.round half down: nearest; an exact half goes toward zero.
These names describe the existing DSL semantics; they are not delegated to a host language’s similarly named default overload.
Text and number conversion
Accepted numeric text
An explicit as :Number or as :Percentage cast consumes one complete numeric
text token. It ignores ASCII space (U+0020) and tab (U+0009) before and after
the token.
No whitespace is allowed inside it. The grammar is:
NumericText = Padding NumberToken PaddingPadding = { " " | "\t" }NumberToken = (Finite | Infinity | "NaN") [Suffix]Finite = [Sign] (Digits | GroupedDigits) ["." Digits] [Exponent]Infinity = [Sign] "Infinity"Exponent = ("e" | "E") [Sign] DigitsSign = "+" | "-"Digits = Digit {["_"] Digit}GroupedDigits = FirstGroup "," ThreeDigits {"," ThreeDigits}FirstGroup = Digit [Digit [Digit]]ThreeDigits = Digit Digit DigitDigit = "0".."9"Suffix = "m" | "s" | "°" | "%"All digits and signs are ASCII. Special tokens and suffixes are case-sensitive.
Leading zeroes are allowed in the integer part and exponent. Both e and E,
an explicit positive exponent sign, and exponent leading zeroes are accepted.
Thus 1000, 100.2, 1.02e20, 1e03, and 1E+003 are valid inputs.
Single underscores may separate adjacent digits, matching numeric source
literals. They may occur in the integer part, fractional part, or text
exponent, without imposing a group size. They may not lead, trail, repeat, or
touch the decimal point, exponent marker, sign, or suffix. Thus 1_003.33,
1_223.32, 1000.2_5, and 1e0_3 are valid.
A comma is an invariant thousands separator in the integer part only. The
first group has one to three digits; every subsequent group has exactly three.
Thus 1,003.24 and 1,234,567.89 are valid, and 1,003 means 1003, never
1.003. Commas and underscores must not be combined in one numeric token.
Commas are not allowed in the fractional part or exponent. Separator validation
precedes removal; a parser must not simply discard all commas or underscores.
Decimal commas, malformed grouping, hexadecimal notation, non-ASCII digits,
trailing signs, multiple suffixes, unknown suffixes, and unconsumed characters
are invalid. A decimal point requires digits on both sides. An exponent
requires at least one digit. Examples of invalid input are 12,34, 100,2,
1__000, 1_.25, 1,_000, 1,000.2_5, .5, 1., 1e, 1e+, 1000-,
10 m, and 10ms.
Invalid input is a failed numeric conversion. An internal NaN or invalid-result
marker is normalized to script-visible nothing, just as invalid mathematics
is. Explicit NaN text has the same result. There is no observable scalar NaN
and no script exception for a failed text cast.
This grammar belongs to runtime text conversion. It does not extend numeric source literals, whose syntax is owned by Language.
Shared conversion and source token boundaries
Compiler literal decoding and explicit text casts must use the same decimal value, precision, and rounding rules for corresponding numeric input. A shared numeric conversion core can serve both paths; token recognition and the target value kind remain responsibilities of the caller.
The source lexer still applies the source-literal grammar before conversion.
In particular, source commas separate arguments and list items: [1,003]
contains two items, 1 and 3, whereas '1,003' as :Number yields 1003.
Source underscores follow the common digit-separator rule. Text-only padding,
signs, exponent notation, special-value spellings, and comma grouping do not
implicitly become source-token syntax. Source signs and mathematical constants
retain their expression-level meaning.
A Percentage literal constructs a Percentage value; as :Number reads the
numeric ratio as defined below. A common conversion core must preserve this
distinction. Invalid source syntax remains a structured compiler diagnostic;
the runtime cast’s nothing result must not silently replace a malformed source
literal.
Numeric value and suffix interpretation
For as :Number, the decimal significand and exponent denote one exact
mathematical decimal value before conversion to GES storage:
- With no suffix, the result is a unitless Number.
- With
m,s, or°, the result is a Number carrying that Quantity unit. The suffix neither rescales nor rounds the numeric value. - With
%, the mathematical decimal value is divided by exactly 100 before numeric conversion. The result ofas :Numberis the unitless ratio, not a Percentage value. This is the same numeric view as casting a Percentage to Number. The%suffix is responsible for the scaling.
If the resulting exact mathematical value is an integer within signed Int64,
it is parsed directly and exactly as Int64, irrespective of decimal or exponent
notation. In particular, 9007199254740993, 9007199254740993.0, and
9007199254740993e0 must all retain that integer. Binary64 is not an intermediate
representation for this case.
All other finite decimal inputs are rounded once to the nearest Binary64 value, with exact halfway cases selecting the even significand. Normal GES numeric storage canonicalization then applies. Overflow yields signed infinity; underflow follows the same rounding rule through subnormal values to zero. Zero is canonicalized to positive zero. Parsing does not saturate at Int64 bounds and does not use the runtime equality ULP tolerance.
Percentage scaling is part of the exact decimal interpretation, before that single rounding step. Parsing a percentage by first rounding its displayed number to Binary64 and then dividing by 100 is permitted only if it produces the same required result. This also avoids intermediate overflow for a finite ratio whose percentage magnitude exceeds the Binary64 range.
Infinity, +Infinity, and -Infinity produce the corresponding signed
Binary64 infinity. A Quantity suffix is retained; % leaves the infinity
unitless. NaN produces nothing with or without a suffix.
| Text input | Result of as :Number |
|---|---|
1000 or 1e03 |
Int64 1000 |
1_003.33 |
nearest Binary64 to decimal 1003.33 |
1_223.32 |
nearest Binary64 to decimal 1223.32 |
1,003.24 |
nearest Binary64 to decimal 1003.24 |
100.2 |
nearest Binary64 to decimal 100.2 |
1.02e20 |
nearest Binary64 to decimal 1.02e20 |
1000.01 or 1.00001e03 |
the same Binary64 value |
1.00001e02 |
nearest Binary64 to decimal 100.001 |
1e03m |
Int64 Quantity 1000m |
100.2s |
Binary64 Quantity with unit s |
90° |
Int64 Quantity 90° |
10% or 1e1% |
unitless Number with the same numeric value as 0.1 |
100% |
unitless Int64 1 |
Conversion to Percentage
For a numeric input, as :Percentage always interprets the finite, unitless
numeric view as a ratio and stores that ratio as Binary64. It does not divide
by 100 based on integer storage, integrality, sign, or magnitude. Ratios greater
than one and negative ratios are valid; this cast does not clamp probabilities.
An existing Percentage is preserved, including ratios outside [-1, 1].
Int64 inputs are converted to the nearest Binary64 ratio using ties to even.
Text input uses the numeric text grammar and exact decimal interpretation above:
- A token without a suffix denotes the ratio directly, just like numeric input.
- A token with
%denotes a percentage magnitude. Its exact decimal value is divided by 100 once, before rounding to the Binary64 ratio. - A Quantity suffix is invalid for this target type.
The parsed ratio is rounded once to Binary64 with ties to even, and zero is
canonicalized to positive zero. A finite ratio produces a Percentage value.
Quantity inputs, non-finite ratios, invalid numeric text, unsupported values,
and nothing produce nothing. No branch reinterprets a parsed ratio as a
percentage magnitude based on its value or storage kind.
| Expression | Percentage value |
|---|---|
0.1 as :Percentage |
10% |
1 as :Percentage |
100% |
1.02 as :Percentage |
102% |
20 as :Percentage |
2000% |
(1 / 100) as :Percentage |
1% |
(20 / 100) as :Percentage |
20% |
'0.1' as :Percentage |
10% |
'20' as :Percentage |
2000% |
'10%' as :Percentage |
10% |
'102%' as :Percentage |
102% |
102% as :Percentage |
102% |
For every Percentage value p, each of the following must recover its exact
stored Binary64 ratio and Percentage kind after zero canonicalization:
(p as :Number) as :Percentage(p as :Text) as :Percentage((p as :Text) as :Number) as :Percentage((p as :Number) as :Text) as :PercentageThis includes zero, negative values, 100%, values above 100%, subnormal
ratios, and very large finite ratios. These guarantees start with a Percentage,
whose representation is Binary64. They do not imply lossless conversion of
every arbitrary Int64 Number to Percentage and back when that integer cannot
be represented exactly in Binary64.
Numeric text output and roundtrip
Numeric as :Text output is culture-independent and must be accepted by the
numeric text grammar above. A formatter may choose decimal or exponential
notation, e or E, exponent padding, and optional insignificant zeroes.
The language does not require a shortest spelling or a particular threshold
for switching notation. Output has no surrounding whitespace, comma grouping,
or underscore separators. Numeric zero is written as 0, followed by its suffix
when present.
Infinity is written as Infinity or -Infinity, with a Quantity suffix when
present. An observable scalar NaN does not exist.
For every unitless Number or scalar Quantity n, parsing its formatted text
must recover its exact numeric value and unit after normal GES storage
canonicalization. Int64 roundtrips preserve every bit. Binary64 roundtrips
preserve every bit after zero and integral-storage canonicalization; approximate
numeric equality is insufficient. This guarantee holds when one conforming
implementation writes the text and another reads it.
Quantity formatting appends its unit suffix. Percentage formatting appends %
and expresses the stored ratio as a decimal percentage magnitude. Parsing
that text with as :Number must recover the Percentage’s numeric ratio after
normal Number canonicalization. This guarantee preserves the numeric meaning;
as :Number does not preserve the Percentage runtime kind. A formatter must
choose enough digits to satisfy this rule, including subnormal and very large
finite ratios. Rounding an intermediate Binary64 product by 100 must not break
the roundtrip. If the ratio’s Number view canonicalizes to Int64, the text must
recover that exact integer, not a nearby decimal integer that would merely
round to the same Binary64 bits.
Parsing the same formatted text with as :Percentage restores the Percentage
kind and exact ratio as defined in Conversion to Percentage.
For example, 1000.01 and 1.00001e03 are both permitted output spellings for
the same Number. Parsing 1e03 yields 1000; its subsequent text may be 1000
or an equivalent exponent spelling. There is no guarantee of retaining the
original input spelling or of producing an expanded decimal display.
A fixed implementation and version must format the same canonical value consistently across compiler evaluation, VM execution, concatenation, and numeric components of nested value formatting. Formatting must not depend on ambient locale or call history. Different implementations or runtime versions may choose different permitted spellings. Text equality, ordering, length, and indexing continue to observe the actual text exactly; they do not compare the numeric meanings of differently spelled texts.
The input grammar, separator validation, suffix interpretation, exact Int64 selection, and Binary64 rounding above are language rules on every platform. A platform library’s accepted spellings, grouping rules, locale, or default numeric representation do not define GES behavior.
The implementation may use its platform’s numeric library or a dedicated conversion algorithm for decimal/binary conversion. Either choice must enforce the complete input grammar, exact conversion and roundtrip rules, including input produced by another port. Recognizing a numeric token and converting its exact decimal value to storage are separate responsibilities; a shared literal scanner alone does not establish correctly rounded numeric conversion. The contract does not prescribe a parser or formatter implementation.
Division, modulo, and remainder
/converts scalar operands to binary64 and performs IEEE binary64 division.1 / 0is+Infinity,-1 / 0is-Infinity, and0 / 0becomesnothingthrough NaN canonicalization.divis floor division:floor(a / b). Integer operands use exact integer quotient/remainder arithmetic so values above2^53do not lose precision. The exceptional integer caseInt64.MinValue div -1produces binary642^63.remis the truncating remainder. A non-zero result has the sign of the left operand:-7 rem 3 = -1,7 rem -3 = 1.modis the floor-modulo companion ofdiv. A non-zero result has the sign of the right operand:-7 mod 3 = 2,7 mod -3 = -2.modorremby zero is invalid mathematics and becomesnothing.Int64.MinValue mod -1andInt64.MinValue rem -1are exactly integer zero; they do not overflow. Integerdivby zero uses the same infinity ornothingoutcomes as/by zero.- For binary64 operands,
remuses the IEEE/Cfmod-style truncating remainder;modadjusts a non-zero remainder once by the divisor to obtain its sign.
Comparisons
Integer-only equality is exact. Whenever binary64 participates, runtime numeric equality uses distance in the ordered IEEE 754 representation and accepts at most two ULPs. NaN is never equal; infinities are equal only to the same signed infinity; positive and negative zero are equal.
Ordering operators use ordinary IEEE comparisons after the language’s type and unit checks. They do not apply the equality ULP window.
Transcendental functions
sin, cos, tan, inverse trigonometric functions, atan2, ln, exp, and
non-integer power use the target platform’s IEEE binary64 math library after
the DSL’s unit, domain, absence, and special-value checks. Exact bit identity
between different correctly rounded or near-correctly-rounded math libraries is
not required.
Conformance expectations therefore compare finite binary64 results by ULP distance.
The default is 4096 ULPs, allowing bounded cross-library differences. A test or individual step
can set maxFloatUlps; 0 requires bit-exact finite results. NaN/nothing,
infinities, kinds, units, integers, booleans, text, and structure remain exact.
Runtime = is unaffected and always uses its two-ULP rule.
Canonical conformance scalar encoding
The YAML value schema described here is test interchange data and does not define a product wire-message format.
- Int64 and binary64 payloads are strings so YAML implementations cannot first round them through an implementation-defined numeric type.
- A finite binary64 is written as the shortest decimal that parses back to the same binary64 bits.
- Decimal point is
., exponent marker is lowercasee, a positive exponent has no+, and exponent leading zeroes are removed. - Both signed zeroes write as
0. - Infinity writes as
Infinityor-Infinity. NaNis accepted in expected YAML as the spelling of invalid mathematics and decodes tonothing; it is not an observable scalar NaN value.
These canonical spellings are the Conformance transport representation. They
do not constrain the permitted numeric as :Text output spellings defined
above. In particular, a :Text payload is ordinary exact text; the transport
must not rewrite it merely because its contents resemble a number.
Examples: 0, 0.1, 1e20, 5e-324, Infinity, -Infinity.
Port requirements
Each language port must have direct tests for:
- exact and overflowing Int64 add/subtract/multiply, including operands above
2^53; - the exclusive
2^63conversion boundary and saturation; - negative
div,mod, andremcombinations; - all midpoint rounding modes and infinities;
- ULP ordering across negative values, both zeroes, infinities, and NaN;
- decimal and exponent input, underscore separators, strict text-only comma grouping, exact Int64 text conversion, Binary64 halfway and range boundaries, Quantity suffixes, and exact Percentage scaling;
- matching source-literal and text-cast numeric conversion with source token boundaries and error handling preserved;
- ratio-based Percentage casts and exact Percentage/Number/Text roundtrips,
including integers, negative values, and percentages at or above
100%; - numeric text roundtrip with permitted output spellings and special tokens;
- canonical Conformance scalar encoding and exponent normalization.