Numbers
Numbers in Suji are represented as fixed-precision decimal numbers (base-10 arithmetic).
Overview
Suji has a single number type: Decimal. All numbers are decimal numbers, whether they have a fractional part or not. Integers are simply decimal numbers with no fractional component.
Key Characteristics
- Single type - All numbers are decimals (no separate integer/float types)
- Decimal arithmetic - Base-10 arithmetic, so
0.1 + 0.2 == 0.3istrue - Fixed precision - About 28–29 significant digits, with a hard maximum of
79228162514264337593543950335. This is not arbitrary precision: exceeding the range aborts the program. - No NaN/Infinity - Invalid operations raise runtime errors
- Simple literals - Only decimal digits and an optional
.(see below)
When to Use Numbers
Use numbers for:
- Counting and indexing
- Mathematical calculations
- Measurements and quantities
- Coordinates and positions
- Financial calculations (decimal arithmetic provides exact precision)
Syntax
All numbers in Suji are decimal numbers. Whether a number has a fractional part or not, it’s still the same decimal type.
Number Literals
import std:println
# Whole numbers (decimals with no fractional part)
age = 30
count = 100
negative = -42
# Decimal numbers (with fractional part)
pi = 3.14159
temperature = -40.5
tiny = 0.000001
println(age) # 30
println(negative) # -42
println(temperature) # -40.5
A number literal is only a run of decimal digits with an optional . and
fractional part. The following forms that other languages accept do not
exist in Suji, and each one is a lex or parse error:
| Not supported | Write instead |
|---|---|
Hex 0xFF, octal 0o77, binary 0b1010 | the decimal value (255, 63, 10) |
Digit separators 1_000_000 | 1000000 |
Scientific notation 3e8 | 300000000 (write the digits out) |
Negative numbers are not part of the literal syntax either: -42 is unary minus
applied to the literal 42.
Very small magnitudes are also limited: a decimal keeps at most 28 digits after
the point, so a value like 6.626e-34 simply cannot be represented. Rescale your
units instead of trying to write it.
Note: There’s no distinction between “integers” and “floats” in Suji. Both 42 and 42.0 are decimal numbers. The difference is only whether they have a fractional component.
Trailing Zeros and Scale
Every number carries a scale (the number of digits it keeps after the decimal point), and arithmetic keeps the larger scale of its operands:
import std:println
println(1.50) # 1.50 (scale preserved)
println(2.50 + 1) # 3.50
println(1.0) # 1 (a literal with only zeros normalises)
println(42.0) # 42
Arithmetic Operations
Basic Operations
import std:println
# Addition
println(5 + 3) # 8
# Subtraction
println(10 - 4) # 6
# Multiplication
println(6 * 7) # 42
# Division (always returns decimal result)
println(15 / 3) # 5
println(10 / 3) # 3.3333333333333333333333333333 (rounded at 28 digits)
# Floor division (get integer part)
println((10 / 3)::floor()) # 3
# Modulo (remainder)
println(10 % 3) # 1
# Exponentiation (the exponent must be a whole number)
println(2 ^ 10) # 1024
# For roots use the sqrt() method, not a fractional exponent:
# 4 ^ 0.5 is a runtime error, "Power exponent must be an integer"
println(4::sqrt()) # 2
^ is right-associative and binds tighter than unary minus, so 2 ^ 3 ^ 2 is
512 (not 64) and -2 ^ 2 is -4 (not 4).
Compound Assignment
import std:println
x = 10
x += 5 # x is now 15
x -= 3 # x is now 12
x *= 2 # x is now 24
x /= 4 # x is now 6
x %= 4 # x is now 2
println(x) # 2
# Postfix increment and decrement are statements, not expressions
x++
println(x) # 3
x--
println(x) # 2
Operator Precedence
import std:println
# Standard precedence (PEMDAS)
result = 2 + 3 * 4 # 14, not 20
println(result)
# Use parentheses for clarity
result = (2 + 3) * 4 # 20
println(result)
# Exponentiation has highest precedence
result = 2 * 3 ^ 2 # 18 (2 * 9)
println(result)
Comparison Operations
import std:println
# Equality
println(5 == 5) # true
println(5 == 6) # false
println(5 != 6) # true
# Relational
println(5 < 10) # true
println(5 > 10) # false
println(5 <= 5) # true
println(10 >= 10) # true
Mathematical Functions
Arithmetic that works on a single number is exposed as methods on the number
itself. The std:math module contains only the transcendental functions and two
constants.
Basic Math (number methods)
import std:println
# Absolute value
println((-5)::abs()) # 5
# Rounding
println(3.7::floor()) # 3
println(3.2::ceil()) # 4
println(3.5::round()) # 4
println(3.4::round()) # 3
# Min/Max (a method on one of the two values)
println(5::min(10)) # 5
println(5::max(10)) # 10
# Power and square root
println(2::pow(10)) # 1024
println(16::sqrt()) # 4
There is no
math:abs,math:floor,math:ceil,math:round,math:min,math:max,math:pow,math:sqrtormath:cbrt. Use the methods above. Suji has no cube-root operation at all;27 ^ (1/3)will not work either because^requires a whole-number exponent. If you need one, write a Newton-iteration helper yourself.
sqrt() returns a decimal approximation for non-perfect squares:
import std:println
println(2::sqrt()) # 1.4142135623730950488016887242
pow() requires a non-negative whole-number exponent (2::pow(-1) fails with
Negative exponents not supported).
Both ^ and pow() overflow far earlier than plain multiplication does. They
work by repeated squaring and square the base once more than the result needs, so
the intermediate value exceeds the decimal range long before the answer would:
import std:println
println(10 ^ 15) # 1000000000000000
println(2 ^ 63) # 9223372036854775808
10 ^ 16 and 2 ^ 64 both abort the program, even though both results are
comfortably inside the decimal range — 1000000000000000 * 10 * 10 computes
100000000000000000 without complaint. Overflow is not a catchable Suji error
either: it is a panic that prints Multiplication overflowed and exits with
status 101 instead of a framed diagnostic.
Trigonometry
import std:math
import std:println
# Angles in radians
println(math:sin(math:PI / 2)) # 1
println(math:cos(0)) # 1
# Trig results are approximations, so don't expect exact values
println(math:tan(math:PI / 4)) # 0.9999999956815324130588099842
# Inverse functions
println(math:asin(1)) # 1.570796326794897 (π/2)
println(math:acos(1)) # 0
println(math:atan(1)) # 0.785398163397448 (π/4)
println(math:atan2(1, 1)) # 0.785398163397448 (two-argument arctangent)
There are no hyperbolic functions (sinh, cosh, tanh) and no inverse
hyperbolics.
Logarithms and Exponentials
import std:math
import std:println
# Natural logarithm
println(math:log(math:E)) # 0.9999999999999999999998942453
# Base 10 logarithm
println(math:log10(100)) # 2
# Exponential
println(math:exp(1)) # 2.7182818261984928651595318263
# Constants (uppercase)
println(math:PI) # 3.14159265358979323846
println(math:E) # 2.71828182845904523536
The complete contents of std:math are PI, E, sin, cos, tan, asin,
acos, atan, atan2, log, log10 and exp. There is no log2, hypot,
sign, clamp, trunc or random (for randomness see
std:random).
Number Methods
Type Checking
import std:println
# Check if a number has no fractional part (is an integer)
println(42::is_int()) # true (no fractional part)
println(3.14::is_int()) # false (has fractional part)
println(42.0::is_int()) # true (no fractional part, even with .0)
# Check if a value is a number
println(42::is_number()) # true
println("text"::is_number()) # false
# Note: Suji uses decimal arithmetic, so NaN and Infinity don't exist
# Division by zero raises a runtime error instead
Conversion
import std:println
# To string
println(42::to_string()) # 42
println(3.14::to_string()) # 3.14
# Parse from string
num = "42"::to_number()
println(num) # 42
decimal = "3.14"::to_number()
println(decimal) # 3.14
to_number() raises a runtime error on input that isn’t a number, and Suji has
no way to trap a runtime error, so validate the text before converting:
import std:println
parse_or_nil = |text| {
match {
text ~ /^-?[0-9]+(\.[0-9]+)?$/ => text::to_number(),
_ => nil,
}
}
println(parse_or_nil("42")) # 42
println(parse_or_nil("abc")) # nil
Formatting
import std:println
# Rounding methods
num = 3.14159
println(num::round()) # 3
println(num::floor()) # 3
println(num::ceil()) # 4
# Note: number::to_fixed(), to_exponential() and to_precision() do not exist.
# To show a fixed number of decimals, scale, round and divide yourself:
two_dp = |x| (x * 100)::round() / 100
println(two_dp(3.14159)) # 3.14
println(two_dp(2.5)) # 2.50
println(two_dp(2)) # 2
This rounds correctly, but how many decimals get printed still depends on the
input’s scale: two_dp(2) shows 2, not 2.00. Suji has no formatting
mini-language, so pad the string yourself when you need fixed-width output.
Number Ranges
import std:println
# Range literals (list of numbers)
range1 = 0..5 # [0, 1, 2, 3, 4]
range2 = 0..=5 # [0, 1, 2, 3, 4, 5] (inclusive)
range3 = 5..0 # [5, 4, 3, 2, 1] (descending)
println(range1)
# Note: Suji doesn't support step ranges directly
# Use filter to get evens/odds:
evens = (0..10)::filter(|x| x % 2 == 0) # [0, 2, 4, 6, 8]
odds = (1..10)::filter(|x| x % 2 == 1) # [1, 3, 5, 7, 9]
println(evens)
Special Values
Division by Zero
Suji uses decimal arithmetic and raises a runtime error on division by zero:
import std:println
# Division by zero raises a runtime error
# result = 1 / 0 # Runtime error
# Check before dividing
safe_divide = |a, b| {
match b {
0 => nil,
_ => a / b,
}
}
println(safe_divide(10, 2)) # 5
println(safe_divide(10, 0)) # nil
Note: Suji does not have Infinity or NaN values. Invalid operations raise runtime errors instead.
Common Patterns
Clamping
import std:println
clamp = |value, min_val, max_val| {
match {
value < min_val => min_val,
value > max_val => max_val,
_ => value,
}
}
println(clamp(5, 0, 10)) # 5
println(clamp(-5, 0, 10)) # 0
println(clamp(15, 0, 10)) # 10
Linear Interpolation
import std:println
lerp = |a, b, t| {
a + (b - a) * t
}
# Interpolate between 0 and 100
println(lerp(0, 100, 0.0)) # 0
println(lerp(0, 100, 0.5)) # 50
println(lerp(0, 100, 1.0)) # 100
Range Mapping
import std:println
# Map value from one range to another
map_range = |value, in_min, in_max, out_min, out_max| {
(value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
}
# Map 0-100 to 0-1
println(map_range(50, 0, 100, 0, 1)) # 0.50
# Map 0-255 to 0-100 (RGB to percentage)
println(map_range(128, 0, 255, 0, 100)) # 50.196078431372549019607843137
Note the 0.50 in the first result: division keeps the scale of its operands, so
round explicitly when the exact number of decimals matters.
Averaging
import std:println
numbers = [1, 2, 3, 4, 5]
# Mean (::average() does the same thing, and returns nil for an empty list)
mean = numbers::sum() / numbers::length()
println(mean) # 3
println(numbers::average()) # 3
# Median
sorted = numbers::sort()
median = match sorted::length() % 2 {
0 => {
mid = sorted::length() / 2
(sorted[mid - 1] + sorted[mid]) / 2
},
_ => {
mid = (sorted::length() / 2)::floor()
sorted[mid]
},
}
println(median) # 3
Common Pitfalls
Pitfall 1: Decimal Precision
import std:println
# Decimal arithmetic is exact for values that fit in base 10
println(0.1 + 0.2 == 0.3) # true
println(0.1 + 0.2) # 0.3
# But precision is finite: repeating divisions are rounded to 28 digits
println(1 / 3) # 0.3333333333333333333333333333
println((1 / 3) * 3 == 1) # false
So the familiar 0.1 + 0.2 surprise is gone, but you can still lose digits.
Anything that does not terminate in base 10 is rounded, and results larger than
79228162514264337593543950335 abort the program instead of wrapping or
becoming infinity.
Pitfall 2: Division by Zero
import std:println
# Division by zero raises a runtime error (not infinity)
# result = 10 / 0 # Runtime error: "Division by zero"
# Check before dividing
divide_safe = |a, b| {
match b {
0 => nil,
_ => a / b,
}
}
println(divide_safe(10, 2)) # 5
println(divide_safe(10, 0)) # nil
Pitfall 3: Whole Number vs Decimal Division
import std:println
# Suji uses decimal division (always returns decimal result)
println(10 / 3) # 3.3333333333333333333333333333 (exact decimal)
println((10 / 3)::floor()) # 3 (get whole number part using floor)
# All numbers are decimals, so division always returns a decimal
# Use floor() if you need the whole number part
Pitfall 4: Modulo with Negatives
import std:println
# Result has sign of dividend
println(10 % 3) # 1
println(-10 % 3) # -1
println(10 % -3) # 1
Performance Tips
All Numbers Are Decimals
# All numbers are decimals, so there's no "conversion" between types
count = 0
count = count + 1
# Adding 1.0 is the same as adding 1 (both are decimals)
count = count + 1.0 # Same result
# The only difference is whether the result has a fractional part
whole = 42 # Decimal with no fractional part
decimal = 42.5 # Decimal with fractional part
# Both are the same type: Decimal
Decimal arithmetic is implemented in software rather than by the CPU’s floating-point unit, so it is exact but not free. In hot loops, hoist work that does not depend on the loop variable.
Avoid Repeated Calculations
import std:println
items = [1, 2, 3, 4]
# Recalculates the square root on every iteration
total = 0
loop through items with item {
total += item / items::length()::sqrt()
}
println(total) # 5
# Calculate once, outside the loop
sqrt_len = items::length()::sqrt()
total = 0
loop through items with item {
total += item / sqrt_len
}
println(total) # 5
Best Practices
DO:
- Handle division by zero before it happens (it raises a runtime error that cannot be caught)
- Use exact decimal comparisons for terminating values (no epsilon needed)
- Round explicitly with
round()/floor()/ceil()when a result’s scale matters - Keep magnitudes well inside the 28–29 significant digit range
- Document units and ranges
DON’T:
- Think of numbers as separate integer/float types (they’re all decimals)
- Assume arbitrary precision — overflow aborts the program
- Ignore division by zero (it raises an error, not infinity)
- Expect NaN or Infinity (invalid operations raise runtime errors)
- Reach for
math:for abs/round/sqrt/pow/min/max — those are number methods
Next Steps
- Learn about Booleans for logical values
- Explore the Math Module for trigonometry and logarithms
- Study Operators in detail