LEVIATHAN v962456e · 962456eee1

Types

Numeric conversions and working types

How operands of different numeric types combine, and when a value is converted on its way into a typed variable, field, parameter or return.

since 0.1.0-alpha.1linuxwindowswasm

Description

Two things decide how numbers of different types behave.

  1. The working type of an operator. The operands are joined to a common type, the operation is computed in it, and the result has that type.
  2. The storage boundary. When a value is written into a slot with an explicit numeric type (a typed variable, field, parameter, return value, array element), it is converted to that type exactly once, after the whole right-hand expression has been computed in its own working type.

var and let keep the working type of the expression they are initialized from. The order of the operands never matters.

Working types and storage boundaries

void demo() {
    var a = 10 / 4.0;
    var b = 2 + 2.0;
    bool d = 2.0 == 2;
    console.writeln(a);
    console.writeln(b);
    console.writeln(d);
    int n = 200.75 + 2000;
    float f = 5;
    console.writeln(n);
    console.writeln(f);
    n += 2.9;
    console.writeln(n);
    console.writeln((7.9).toInt() + 0.5);
}
demo();
2.500000
4.000000
true
2200
5.000000
2202
7.500000

Rules

Working type of a numeric operator.

  • If either operand is floating, the operator computes in the float format whose storage width is the wider of the two operands: float with anything is float, int16 + float8 is float16, float16 + int32 is float32.
  • Two integers of the same signedness give the wider of the two. byte + int is int, and int8 + int16 is int16.
  • A mixed signed and unsigned pair gives the smallest signed type that covers both ranges: int8 + byte is int16. uint with any signed integer is int.
  • The result is reduced to the working type, so integer operators wrap at the working width and narrow-float operators re-round in the working format. See lang.sized-integers and lang.sized-floats.
  • Comparisons convert both operands to the same working type and return bool.
  • + with a string operand is not numeric promotion. Either order converts the other operand to text and concatenates: "n=" + 5 and 5 + "n" are strings.

Storage boundaries.

  • An explicitly typed declaration, field, parameter, return value, assignment, collection element or for-in variable is a boundary. The incoming value is computed in its own working type and converted once. int n = 200.75 + 2000; computes 2200.75 as a float and stores 2200.
  • A function's declared return type is a boundary, even for a lambda whose function type is known. float half(int n) => n / 2; computes the integer division 5 / 2 = 2 first and returns 2.0.
  • The same holds for generic parameters after substitution, and when a function is called through a function-typed value.
  • Compound assignment computes the promoted result, converts it to the variable's type, and stores it: n += 2.9; is a float addition followed by a truncating store into an int.
  • The arms of a conditional expression c ? a : b are joined to one working type before the selected value leaves the conditional. An enclosing typed target is a separate final boundary.
  • Elements of an array literal are converted to the declared element type, including elements spread from a range: Array<float> ys = [1..3]; stores floats.
  • A numeric optional such as float? keeps None and converts a present value. A non-optional numeric type rejects None.

Conversion rules.

  • int to float rounds to the nearest binary64 value. float to int truncates toward zero, and throws a catchable RuntimeException for NaN, infinity or a value outside the 64-bit range.
  • A wide integer literal stored into a typed slot is converted from its own digits, not through a signed 64-bit overflow first: float f = 18446744073709551615; stores 18446744073709551616.0, not -1.0.
  • A conversion method such as f.toInt() or n.toFloat() is an explicit boundary in the middle of an expression. Parentheses alone are not: (7.9).toInt() + 0.5 is 7.500000.
  • Narrowing between sized types follows the rules in lang.sized-integers and lang.sized-floats: wrapping arithmetic never throws, a typed conversion that cannot represent the value does.

Overload selection.

  • When overloads differ by numeric parameter type, an exact match is preferred, then a lossless conversion, then a lossy contextual conversion. Only the chosen overload converts its arguments.
  • Two overloads that tie on different numeric targets make the call ambiguous, and the compiler reports "ambiguous numeric argument ... convert it explicitly". The result never depends on declaration order. Convert the argument explicitly to choose.

Examples

Boundaries on returns, arrays and optionals:

Storage boundaries

float half(int n) => n / 2;
int whole(float f) => f;

void demo() {
    bool flag = true;
    int s = flag ? 7.9 : 1;
    console.writeln(s);
    console.writeln(half(5));
    console.writeln(whole(3.99));
    Array<float> fs = [1, 2];
    Array<float> ys = [1..3];
    console.writeln(fs);
    console.writeln(ys);
    float? opt = 4;
    console.writeln(opt);
}
demo();
7
2.000000
3
[1.000000, 2.000000]
[1.000000, 2.000000, 3.000000]
4.000000

Overload selection, and string concatenation:

Overloads and string +

string pick(int x) => "int";
string pick(float x) => "float";
string size(float x) => "float";
string size(int8 x) => "int8";

console.writeln(pick(1));
console.writeln(pick(1.5));
byte b = 3;
console.writeln(size(b));
console.writeln("n=" + 5);
console.writeln(5 + "n");
console.writeln("x" + 1.5);
int
float
float
n=5
5n
x1.500000

Notes

In the last example size(b) selects size(float) because converting a byte to float loses nothing, while converting it to int8 can.

See also