Primitive types
Fourteen primitives, three aliases, one literal suffix. That's every type the compiler knows about on its own, and everything else you write, including string and array<T>, is a library type.
int32 $count = 3;
usize $index = 0;
float64 $ratio = 0.5;
bool $ready = true;Types is the chapter with the reasoning. This page is the table, the literal grammar, and the exact wording of every conversion the compiler refuses.
The table
| Type | Bits | Signed | Minimum | Maximum |
|---|---|---|---|---|
int8 | 8 | yes | -128 | 127 |
int16 | 16 | yes | -32,768 | 32,767 |
int32 | 32 | yes | -2,147,483,648 | 2,147,483,647 |
int64 | 64 | yes | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
uint8 | 8 | no | 0 | 255 |
uint16 | 16 | no | 0 | 65,535 |
uint32 | 32 | no | 0 | 4,294,967,295 |
uint64 | 64 | no | 0 | 18,446,744,073,709,551,615 |
isize | pointer width | yes | same as int64 today | same as int64 today |
usize | pointer width | no | 0 | same as uint64 today |
float32 | 32 | n/a | IEEE 754 single | IEEE 754 single |
float64 | 64 | n/a | IEEE 754 double | IEEE 754 double |
bool | 1 | n/a | false | true |
void | n/a | n/a | not a value | not a value |
bool is one bit in the emitted code and one byte in memory, which is the usual arrangement and never something you have to think about.
Ask for a size yourself and you get the same numbers:
echo mem::size<int32>(); // 4
echo mem::size<bool>(); // 1
echo mem::size<usize>(); // 8The largest value, by name
You will want the top of a count more often than you want to type eighteen digits. Integer primitives answer min() and max() as statics, folded from the same widths the table above uses:
echo usize::max(); // 18446744073709551615
echo int32::min(); // -2147483648
echo uint8::max(); // 255std::math::MAX_USIZE is the same number as a constant. Reach for the type form when you are already writing usize. There is no float32::max(): floats keep the math constants.
usize and isize are pointer width, and today that is always 8
Their width is one compile-time constant, ECO_TARGET_POINTER_SIZE, and it's 8. There is no per-target logic behind it yet, so on every platform Echo currently builds for, usize is 64 bits.
What matters more than the number is that they are their own types, not aliases. Assigning a uint64 into a usize is a real conversion, not a no-op the compiler waves through:
uint64 $raw = 7;
usize $converted = $raw;
echo $converted; // 7That distinction is what keeps every length, count, capacity and index in the standard library spelled usize rather than accidentally spelled uint64 on one platform and uint32 on another.
Three aliases
int $a = 1; // int32
uint $b = 2; // uint32
float $c = 3.0f; // float32There are no others. No double, no byte, no char, no short, no long. int is int32 everywhere and is not the machine word.
numeric, integer, signed, unsigned and floating look like they belong on this list and don't. They are generic constraint aliases, usable only in a type parameter's constraint, never as the type of a variable. See Generics.
What an untyped literal decides
| Literal | Type |
|---|---|
25 | int32, promoted to int64 only if it does not fit |
0.5 | float64 |
0.5f | float32 |
true | bool |
"hi" | string |
0xFF | uint8, and the width comes from the digit count |
The last row is the one that surprises people.
A radix literal picks its width from how you wrote it
With no type in front, a 0x or 0b literal counts its digits and picks an unsigned type to fit them. Nothing about the value:
| Hex digits | Binary digits | Type |
|---|---|---|
| 1 to 2 | 1 to 8 | uint8 |
| 3 to 4 | 9 to 16 | uint16 |
| 5 to 8 | 17 to 32 | uint32 |
| 9 or more | 33 or more | uint64 |
So $e = 0xFF; is a uint8 and $f = 0x00FF; is a uint16, despite being the same number. Write the zeros when you mean the width.
That's only what happens when nothing else has an opinion. A destination outranks it, exactly as it does for a decimal literal, and the range check comes with it:
int32 $x = 0xFF; // an int32 holding 255, not a uint8
uint64 $b = 0b1011; // a uint64 holding 11
echo $x;
echo $b;uint8 $x = 0xFFFF;
// error: The literal '0xFFFF' is too large for the integer type 'uint8'. The maximum value is '255'.Write hex or binary when you mean a bit pattern, and put the type in front when the width matters.
Every literal form
Exists:
| Form | Example | Notes |
|---|---|---|
| decimal integer | 25, -3 | the sign is part of the token, see below |
| decimal float | 0.5, 1. | a trailing dot gets an implicit zero, so 1. is 1.0 |
| float suffix | 3.14f | f is the only suffix in the language, and only on floats |
| hexadecimal | 0xFF, 0Xff | see the width rule above |
| binary | 0b1011, 0B1011 | same width rule as hex, counted in bits |
| string | "hi", 'hi' | " interpolates {$...}, ' is verbatim |
| bool | true, false |
Does not exist:
- Octal. No
0o17, no leading-zero form. - Digit separators.
1_000is1followed by an identifier_000. - Exponent notation.
1e9stops at thee. - Typed integer suffixes. No
25i64, no25u. Put the type in front instead. - Character literals. There is no
chartype, so there is nothing for'a'to be. It is a one-bytestring.
The minus sign glues to a digit
- immediately followed by a digit is read as part of the number, which is what makes -3 a literal rather than a negation of 3. The cost is that a binary - needs spaces around it:
echo 1 - 2; // -1Written 1-2 you get unexpected '-2' - two expressions with no operator between them.
Escape sequences
| Escape | Means |
|---|---|
\n \t \r | newline, tab, carriage return |
\0 | a nul byte |
\\ \" \' | a literal backslash or quote |
\xNN | one byte, exactly two hex digits |
\u{...} | a unicode scalar, one to six hex digits, braces required |
echo "tab:\there"; // tab: here
echo "\x41"; // A
echo "\u{1F600}"; // an emojiThere is no \a, \b, \f or \v, and no unbraced \uXXXX. Anything else after a backslash is an error, not a passthrough. String literals are validated as UTF-8 at compile time.
Conversions
There are two separate machines here, and knowing which one you are in is the whole story. A literal is checked at parse time against the destination, because the compiler can see the value. A variable is not checked at all, because it cannot.
A literal is checked
| Case | Result |
|---|---|
| fits the destination | converted at compile time, no runtime cost |
| integer literal too large | error |
| integer literal too small | error |
| negative literal into an unsigned type | error |
float64 literal into a float32 that loses bits | warning |
| float literal with a fraction into an integer type | error |
| any literal into a struct or class | error |
0 or 1 into a bool | false or true |
any other number literal into a bool, or true/false into a number | error |
| hex or binary literal | checked at its destination, like a decimal one |
The exact wording, so you can recognise it:
uint8 $no = 256;
// error: Integer overflow: The literal '256' is too large for the integer type 'uint8'.
// The maximum value is '255'.uint8 $n = -1;
// error: Invalid type conversion: The integer literal '-1' cannot be implicitly converted to an
// unsigned integer because it is negative.float64 $x = true;
// error: Invalid type conversion: a literal of type 'bool' cannot be written where a 'float64' is
// expected - Echo has no truthiness in a written literal, so say which of the two you meanttrue into a number is refused. The other direction is not quite a mirror: bool $ready = 1; is true, bool $ready = 0; is false, and bool $ready = 3; is the error above. 0 and 1 are what echo prints for a bool. 3 is not either of those.
A variable still converts either way, as the next section says. An integer into a bool is compared against zero at runtime. It is only a written literal other than 0 or 1 that has to say which of the two it meant.
The one warning:
float32 $pi = 3.14;
// warning: This operation results in a loss of precision: The literal '3.14' is stored in 32bit float
// which will result in the effctive value 3.14Yes, effctive. Writing 3.14f makes the warning go away, because then the literal was a float32 to begin with and no precision was lost on the way in.
A variable is not checked
Every conversion in this table happens silently, at runtime, between variables:
| From | To | What happens |
|---|---|---|
| narrower int | wider int | sign-extends if the source is signed, zero-extends if it is unsigned |
| wider int | narrower int | truncates |
| int | float | converted, may lose precision |
| float | int | truncates toward zero |
float64 | float32 | rounded |
bool | int | 1 or 0 |
| int or float | bool | true when the value is not zero |
int64 $big = 5000000000;
int32 $small = $big;
echo $small; // 705032704
float64 $pi = 3.14159265358979;
float32 $f = $pi;
echo $f; // 3.141593The type checker deliberately does not look at primitive-to-primitive conversions. It refuses conversions between pointers, structs, classes, interfaces, callables, C function pointers and weak handles, and waves numbers through. I am not happy with that. I'd rather narrowing needed something written down, and it is on the list. Until then the literal check is the only safety net you get.
$x as T writes the destination next to the value, for the sites that have none: a return, an operand of <, a field of a constructor about to be called. (int32)$x does not parse and int32($x) is not a function. Casts has the spelling. Narrowing at a typed destination still happens silently. as is the escape hatch, not a requirement.
The other written form is inside an unsafe block, where $bytes:$ as uint8& (or uint8&($bytes:$)) promotes raw storage to a typed borrow. That is a different operation with a similar shape.
void
void is the return type that means the function produces nothing. Saying it out loud is required:
function log(string $line) : void
{
echo $line;
}
log("done"); // doneIt is not a local, a field, a parameter, or a type argument. A boundary that can fail and has nothing to hand back returns status<E>.
ptr<void> is refused. Echo's untyped handle is ptr<Handle> over an extern struct. void[4] is refused too: an array needs a size, and void has none.
void is not "the compiler has not worked this out yet". That's a separate internal state you'll never see spelled in your program.
Next
- Types for the reasoning behind these choices, and the value/reference split.
- Expressions for what happens when you mix two of these in one expression.
- Keywords for the words that are reserved, none of which are type names.