Types
Every value in Echo has exactly one type, and the compiler knows it before your program runs. There is no mixed, no union type and no runtime type juggling.
What might surprise you is how few types the compiler actually knows about:
int32 $count = 3;
string $name = "Echo";One of those is built into the language. The other is a struct in the standard library, written in Echo, with no special treatment at all. The compiler ships a handful of primitives and everything else is a library.
The primitives
These are the types the compiler understands on its own. Nothing has to be imported and nothing can shadow them.
| Type | Size | Range |
|---|---|---|
int8 | 1 byte | -128 to 127 |
int16 | 2 bytes | -32,768 to 32,767 |
int32 | 4 bytes | -2,147,483,648 to 2,147,483,647 |
int64 | 8 bytes | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
uint8 | 1 byte | 0 to 255 |
uint16 | 2 bytes | 0 to 65,535 |
uint32 | 4 bytes | 0 to 4,294,967,295 |
uint64 | 8 bytes | 0 to 18,446,744,073,709,551,615 |
usize | pointer width | 0 to whatever fits |
isize | pointer width | signed, same width |
float32 | 4 bytes | IEEE 754 single |
float64 | 8 bytes | IEEE 754 double |
bool | 1 byte | true or false |
void | n/a | not a value; a function return that produces nothing |
An inline array is a type constructor, like ptr<T>: int32[4] is four int32s in a row. It is storage. The collection with methods, iteration and a destination-typed literal is fixed_array<T, N>. The growable cousins are array<T> and stable_array<T>.
A vector is a different constructor: simd<T, N> is N packed lanes, not N slots you index. A lane is an integer, a float or a bool. N is a power of two. The whole thing is at most 16 bytes.
You can ask for a size yourself, which is occasionally handy and is the same number the table claims:
echo mem::size<int32>(); // 4
echo mem::size<usize>(); // 8A sentinel count is a name on the type, not eighteen digits from memory. Integer primitives answer min() and max() as statics:
echo usize::max(); // 18446744073709551615
echo int32::min(); // -2147483648std::math::MAX_USIZE is the same number as a constant. Primitive types has the rest of the table.
void is the return type that means the function produces nothing. A function that returns nothing still has to say : void out loud. It is not a local, a field, a parameter, or a type argument. ptr<void> is not C's untyped pointer. That is ptr<Handle> over an extern struct.
A function that can fail and has nothing to hand back on success returns status<E>, not result<bool, E>.
See Functions.
Three spellings
int $a = 1; // int32
uint $b = 2; // uint32
float $c = 3.0f; // float32That's the whole list. int is not the machine word. It's int32 on every platform, the same way long should have been. If you want a pointer-width integer, usize and isize exist for exactly that.
There is no double. float64 is spelled float64.
Untyped literals
Leave the type off and the literal picks:
$a = 25; // int32
$b = 0.5; // float64
$c = true; // bool
$d = "hi"; // string
$e = 0xFF; // uint8, and it is 255
$g = 0b1011; // uint8 too, and it is 11
$f = 3.14f; // float32, and that is the f talkingAn untyped integer literal is an int32. An untyped float literal is a float64. Those two don't agree about width, and that's deliberate: an integer that needs more than 32 bits is unusual, and a float that wants less precision than a float64 is a decision you should have to write down.
An integer that doesn't fit an int32 goes to int64, and past that to uint64. The sign counts, so -3000000000 is an int64 too. Bigger than every integer type is a compile error.
A 0x or 0b literal is the exception. With nothing else to go on it takes an unsigned type sized to the number of digits you wrote, so 0xFF is a uint8 and 0x00FF is a uint16 despite being the same number. Write the zeros when you mean the width. Primitive types has the table.
Writing it down usually means putting the type in front:
int64 $big = 25;
uint8 $small = 255;Floats get a second way, and it's the last line of the first example.
The f suffix
3.14 and 3.14f are two different literals, not one literal spelled two ways. The f is part of the literal itself, so it settles the type on its own, with no type in front and nothing left to infer:
$a = 0.5; // float64, 8 bytes
$b = 0.5f; // float32, 4 bytes
float32 $c = 3.14f; // type and suffix agree, which is the boring caseThe suffix is also how you say you meant it. A float64 literal landing in a float32 warns, because the number in the program is not quite the number you typed:
float32 $pi = 3.14; // warning: results in a loss of precisionWrite 3.14f and the warning goes away. Variables has the rest of the narrowing rules.
usize
usize is a pointer-width unsigned integer, and it is not an alias for uint64. It's its own type with its own identity.
That matters because every length, count, capacity and index in the standard library is spelled with it:
array<int32> $numbers = [1, 2, 3];
usize $n = $numbers->count();
echo $n; // 3If usize were an alias, moving to a target with a different pointer width would silently change every one of those signatures. As its own type, the signature stays usize and the width is a detail underneath it.
The practical consequence: a count is unsigned, so subtracting past zero wraps rather than going negative. $numbers->count() - 5 is a very large number, not -2.
bool
This is the one that will genuinely hurt you if you treat bool as a small integer.
bool is its own type. It is not an integer that happens to hold 0 or 1:
bool $ready = true;
echo $ready; // 1
echo !$ready; // 0echo prints a bool as 1 or 0, because echo prints numbers and that's the number. Those two integers are also what a bool destination accepts as a literal: 1 is true, 0 is false.
bool $on = 1;
bool $off = 0;
echo $on; // 1
echo $off; // 0Anything else is not a bool. 3 is not true. There is no truthiness, no zero-is-false for the rest of the integers, no empty-string-is-false, none of it:
bool $ready = 3;
// error: a literal of type 'int32' cannot be written where a 'bool' is expected - Echo has no
// truthiness in a written literal, so say which of the two you meantThe other direction is refused outright. There is no number true means:
int32 $n = true;
// error: a literal of type 'bool' cannot be written where a 'int32' is expected - Echo has no
// truthiness in a written literal, so say which of the two you meantA condition has to actually be a bool too:
$count = 5;
if ($count > 0) { // fine, the comparison produces a bool
echo "some";
}Writing if ($count) with an integer is not valid Echo: an if condition has to be a bool. Compare it against something rather than branching on the value itself.
A variable still converts either way, as Primitive types sets out. An int32 into a bool is compared against zero at runtime. It's only a written literal other than 0 or 1 that has to say which of the two it meant.
$x as T
A destination usually decides the type. Sometimes there is no destination. Then you write it next to the value:
function wide(uint8 $b) : int32
{
return $b as int32;
}
echo (200 as int32) > 57; // 1(int32)$x does not parse, and int32($x) is not a function. Casts is the page: what it will convert, what it will not (no unwrap, no strong), and why foreach still owns as.
Values and references
Every type in Echo is one of two things, and the difference is where it lives.
A value type lives where you put it. Primitives are value types, and so is every struct. Assigning one copies it:
struct GateAddress
{
int32 $destination;
int32 $origin;
}
GateAddress $dialled = GateAddress(27, 1);
GateAddress $backup = $dialled; // a copy, not a second name for the same address
$backup->destination = 99;
echo $dialled->destination; // 27A reference type lives on the heap and is reference counted. Every class is one. Assigning one hands out another reference to the same object:
class Stargate
{
int32 $chevrons_locked;
}
Stargate $sgc = Stargate(0);
Stargate $embarkation_room = $sgc; // both names, one gate
$embarkation_room->chevrons_locked = 7;
echo $sgc->chevrons_locked; // 7Same declaration syntax, opposite semantics, one keyword apart. Structs and Classes each get a chapter, because the choice between them is the most consequential one in the language.
The standard library
string, array<T>, map<K, V>, slice<T>, range<T>, box<T>: none of these are compiler types. They are structs written in Echo, in stdlib/core/, using the same features your code has:
array<int32> $numbers = [1, 2, 3];
$numbers[] = 4;
echo $numbers->count(); // 4That [] append is an operator declaration in stdlib/core/array.eco. .. in a for range is an operator declaration in stdlib/core/range.eco. Compile with --no-stdlib and the dots stop being a symbol the program can use, because nothing declares them any more.
A small number of these types are pointed at by name so the compiler can talk about them (that's what #[echoc::core: array] does), but the shape is still the library's. There is no privileged array type you can't write yourself.
Aliases
Some types are long, and you write them over and over. type gives one a second name:
class Query<K, M>
{
public K $key;
public M $model;
static function of(K $key, M $model) : Query<K, M> { return Query<K, M>($key, $model); }
}
class Comment
{
public string $body = 'first!';
}
public type CommentQuery = Query<int64, Comment>;
CommentQuery $q = CommentQuery(5, Comment());
CommentQuery $r = CommentQuery::of(7, Comment());
echo $q->key + $r->key; // 12An alias is its target. CommentQuery and Query<int64, Comment> are one type, not two that convert. A value of either goes anywhere the other does.
That's also why it works in every place a type name does: a declaration, a parameter, a return, CommentQuery(...), CommentQuery::of(...), as, instanceof, and the .of(...) shorthand.
Generic aliases
An alias can take its own type parameters:
type Page<T> = map<string, T>;
type Ledger = Page<int64>;
Ledger $l = Page<int64>();
$l['naquadah'] = 5;
echo $l['naquadah']; // 5A generic alias always needs its arguments, the same way a generic type does. Inside a generic function it can use that function's own parameter: function total<T>(Page<T> $p).
Function types
A function type is the case it pays off most. The alternative is writing the whole signature at every use:
type Transform = function<int32(int32)>;
function apply(Transform $f, int32 $x) : int32 { return $f($x); }
echo apply((int32 $x) => $x * 2, 21); // 42Rules
An alias is declared at file or namespace scope. Inside a struct or a function body it is refused. It takes public, internal, or private like any other declaration. Without one it belongs to its module.
It can name a type declared further down, or another alias, in any file. A chain that comes back to where it started is refused.
It names one complete type. type Crate = Box; for a generic Box is refused: write Box<T> and give the alias a <T> of its own.
It cannot share a name with a type, a constant, or a function in its namespace.
Next
- Casts for
$x as T, the written destination. - Expressions for what happens when you mix these types in one expression.
- Structs and Classes for the value/reference split.
- Primitive types for the table on its own, without the prose.