Float

A float stores a number with a decimal point, like 3.14 or -0.001. It gives you about 15 significant digits of precision — enough for scientific work, games, and most real-world maths. When you write a number with a decimal point in Loft, it is automatically a float.

fn main() {

Write a decimal point in a literal and Loft treats the whole expression as a float. The usual arithmetic operators (+, -, *, /) all work the way you would expect.

  assert(1.5 + 0.5 == 2.0, "Float addition");
  assert(3.0 / 2.0 == 1.5, "Float division");
  assert(2.0 * 1.5 == 3.0, "Float multiplication");

Undefined results are null (float?) — but infinities are NOT undefined

Some float operations have no real answer for some inputs: sqrt of a negative, ln/log of a NEGATIVE, asin/acos outside -1..1, an out-of-domain pow, % by zero, and 0.0/0.0. Rather than crash, Loft yields null for these — so /, %, sqrt, ln, log, pow, asin and acos each produce a NULLABLE float (float?). The null then propagates: any further arithmetic on a null stays null, and ?? fallback discharges it when you need a plain float.

Read the boundary carefully, because it is not "anything unusual is null". Floats keep IEEE INFINITY, and infinity is a value, not a missing one:

1.0 / 0.0   is  inf        0.0 / 0.0   is  null

-1.0 / 0.0 is -inf 1.0 % 0.0 is null ln(0.0) is -inf ln(-1.0) is null

So a ?? fallback does NOT rescue a division by zero the way it rescues a sqrt of a negative — the infinity is not null, sails through the guard, and propagates through every later sum. When a zero divisor is possible, test the DIVISOR rather than defending the result.

  assert((sqrt(-1.0) ?? 0.0) == 0.0, "sqrt of a negative is null");
  assert(((sqrt(-1.0) + 5.0) ?? 0.0) == 0.0, "null propagates through arithmetic");
  fz = 0.0;
  fp = 1.0;
  assert("{fp / fz}" == "inf", "a non-zero over zero is an infinity, not a null");
  assert(((fp / fz) ?? -1.0) > 1000000.0, "…so `??` does not rescue it");
  assert(((fz / fz) ?? -1.0) == -1.0, "…while 0.0/0.0 really is null, and `??` does");

The f suffix selects single-precision floats, which take half the memory of a regular float but have fewer decimal digits of accuracy. This trade-off is common in graphics and audio code where exact decimal values matter less than speed or memory.

  x = 0.1f + 2 * 1.0f;
  assert(x == 2.1f, "Single precision float");

as float converts an integer to a float so you can mix them in calculations. Putting a float inside {...} converts it to text for display. You can also go the other way: "1.5" as float? parses the text back to a number. The ? is required — a bare as float on text is refused for the same reason a bare as integer is ("a text parse as float may fail, and a bare cast asserts it cannot"), because the text might not be a number.

  assert(3 as float == 3.0, "Integer to float");
  assert("1.5" as float? == 1.5, "Text to float");
  assert("{1.5}" == "1.5", "Float formatted as text");

Formatting Floats

Put a colon after the value inside {...} to control how it looks. {value:width.precision} — width is the minimum number of characters printed (padded with spaces on the left); precision fixes the number of decimal places. You can use either part on its own: {value:.2} just fixes decimal places, {value:5} just sets the minimum width.

  assert("{1.2:4.2}" == "1.20", "Float with width and precision");
  assert("{334.1:.2}" == "334.10", "Float with precision only");
  assert("{1.4:5}" == "  1.4", "Float with width only");

Math Functions

PI is a built-in constant (approximately 3.14159265358979). Multiplying it by 1000 and rounding gives 3142, which confirms the value is correct.

  assert(round(PI * 1000.0) == 3142.0, "PI constant");

pow(base, exponent) raises a number to a power — this is exponentiation. Note: ^ in Loft is bitwise XOR, NOT exponentiation. Always use pow() for powers. log(value, base) is the inverse: log(x, b) answers "b to the what power equals x?" Here: 4^5 = 1024, and log(1024, 2) = 10 because 2^10 = 1024.

  assert(log(pow(4.0, 5), 2) == 10.0, "log base 2 of 4^5");

sin and cos work in radians, not degrees. A full circle is 2*PI radians; a half circle (180 degrees) is PI radians. sin(PI) should be exactly zero but computers use approximations for decimal numbers, so you may get something like 0.0000000001 instead. We use ceil() to round it up to 0. cos(PI) is exactly -1, so the whole expression ceil(~0 + -1 * 1000) lands at -1000.

  assert(ceil(sin(PI) + cos(PI) * 1000) == -1000.0, "sin and cos");

abs() returns the absolute value — the distance from zero, always non-negative. Useful any time you care about magnitude but not direction.

  assert(abs(-2.5) == 2.5, "Absolute value of float");

round() picks the nearest whole number; a half rounds away from zero (2.5 → 3, -2.5 → -3). ceil() always rounds up to the next whole number, even for 2.01. floor() always rounds down to the previous whole number, even for 2.99. All three give back a float, not an integer — so 3.0, not 3.

  assert(round(2.6) == 3.0, "round up");
  assert(round(2.4) == 2.0, "round down");
  assert(ceil(2.1) == 3.0, "ceil");
  assert(floor(2.9) == 2.0, "floor");

Single-precision floats work inside format strings just like regular floats.

  assert("a{0.1f + 2 * 1.0f}b" == "a2.1b", "Single-precision format");
}