Scarlet Industries

Numbers

A 64-bit signed integer, an IEEE-754 double, and an exact scaled decimal for money. Nothing converts implicitly.

Integers

An Int is a signed 64-bit integer. Division truncates toward zero, and % takes the sign of the dividend: it is a remainder, not a modulus.

println('7 / 2 = ${7 / 2}')
println('-7 / 2 = ${-7 / 2}')
println('7 % 3 = ${7 % 3}')
println('-7 % 3 = ${-7 % 3}')

64-bit means 64-bit: arithmetic wraps at the ends of the range rather than promoting to a wider type. int.max_value + 1 is int.min_value. int.abs(int.min_value) saturates to int.max_value, because the true magnitude is not representable. Small integers live in the value word and large ones spill to the heap; that boundary is invisible to arithmetic and printing.

Bitwise

scarlet/int carries the bitwise operations: bitwise_and, bitwise_or, bitwise_xor, bitwise_not, bitwise_shift_left, and bitwise_shift_right. They are functions rather than operators, so their precedence against == and the arithmetic operators is never a thing to remember. All six work on the full 64 bits, and all six are total: every input has a defined answer.

// A permission bitfield: set two flags, test one, clear one.
flags = int.bitwise_or(int.bitwise_shift_left(1, 3), int.bitwise_shift_left(1, 40))
println('bit 3 set: ${int.bitwise_and(flags, int.bitwise_shift_left(1, 3)) != 0}')
println(
	'bit 3 cleared: ${int.bitwise_and(flags, int.bitwise_not(int.bitwise_shift_left(1, 3)))}',
)

// WebSocket frame masking: XOR by the key byte, and XOR again to undo it.
masked = int.bitwise_xor(200, 222)
println('masked: ${masked} back: ${int.bitwise_xor(masked, 222)}')

// A count at or past the width shifts every bit out; the count is not masked.
println('1 shifted left 64: ${int.bitwise_shift_left(1, 64)}')
// The right shift is arithmetic, so the sign survives.
println('-8 shifted right 1: ${int.bitwise_shift_right(-8, 1)}')

A shift count of 64 or more shifts every bit out. Scarlet does not mask the count to 6 bits the way C and most hardware shift instructions do, so bitwise_shift_left(1, 64) is 0, not 1. A negative count shifts the other way by the same magnitude. bitwise_shift_right is arithmetic: the sign bit propagates, so bitwise_shift_right(-8, 1) is -4, a negative value never becomes positive, and a right shift past the width leaves -1 rather than 0. Mask first to get logical behaviour.

bitwise_not complements the sign bit along with the other 63, so bitwise_not(0) is -1 and bitwise_not(n) equals 0 - n - 1. An Int is signed and has no unsigned counterpart, so complementing a small non-negative number gives a negative one; mask the result with bitwise_and(bitwise_not(n), 255) to keep it inside a byte. Integer literals are decimal, so masks are written as numbers.

Floats

A Float is an IEEE-754 double. Nothing converts implicitly: float.from_int is the way in, and floor, ceil, round, and truncate are the ways out. round breaks ties away from zero. && and || short-circuit, which is what makes a guard like this safe.

fn safe_div(a Int, b Int) Bool {
	b != 0 && a / b > 1
}

Money

A double cannot represent 0.1 exactly, so money never touches Float. A Decimal from scarlet/decimal is units * 10^(-scale), an exact scaled integer: new(1999, 2) is 19.99, and 0.1 + 0.2 is 0.3. The scale is carried through arithmetic. Addition aligns scales; multiplication adds them. The only operations that discard digits are div and round, and both take an explicit target scale and rounding mode.

type Usd {
	amount Decimal
}

fn cents(n Int) Usd {
	Usd(decimal.new(n, 2))
}

// Multiply exactly, so the scale grows to hold every digit, then round back
// to cents. decimal.round is banker's rounding (half to even), the default
// throughout scarlet/decimal.
fn tax(u Usd, rate Decimal) Usd {
	Usd(decimal.round(decimal.mul(u.amount, rate), 2))
}

The six rounding modes are HalfEven, HalfUp, Down, Up, Floor, and Ceiling. HalfEven is the default: ties go to the even neighbour, so repeated rounding does not drift. Splitting a total is the standard trap; divide and multiply back and money is lost or invented. Allocate the remainder instead.

// Split a total into n shares that sum back to exactly the total: every share
// gets the rounded-down amount, and the leftover cents go one each to the
// first shares. Dividing and multiplying back would either lose or invent
// money; this remainder-preserving allocation is the standard fix.
fn split(total Usd, n Int) Array(Usd) {
	base = decimal.div_with(total.amount, decimal.from_int(n), 2, Down) or decimal.from_int(0)
	leftover = decimal.units(decimal.sub(total.amount, decimal.mul(base, decimal.from_int(n))))
	shares(base, leftover, n)
}

Built-in == compares representation, so 1.50 == 1.5 is False. Compare numerically with decimal.eq or decimal.compare, which returns an Order. parse and from_float return Err(Nil) on inputs that would overflow rather than wrapping.