Keywords
Echo reserves thirty-eight words. The list is short on purpose. There is no new, no this, no import, no switch, no try, no throw and no sizeof.
function greet(string $name) : void
{
echo $name;
}
greet("Echo"); // EchoSix words in that program and two of them are reserved. Below is the full list, the words that look reserved and aren't, and every token that means something on its own.
The reserved words
Reserved means the lexer takes the word before anything else gets a look at it, so it can't be a function name, a struct name or a member name. Variables are safe either way, because a variable always carries a $.
Matching is whole-word. forward, classify, enumerate, nullable and constructor are all ordinary identifiers, and nothing about them collides with for, class, enum, null or const.
Declarations
| Word | What it does | Covered in |
|---|---|---|
function | declares a function, a method or a constructor | Functions |
return | leaves a function with a value | Functions |
struct | declares a value type | Structs |
class | declares a reference-counted heap type | Classes |
interface | declares a set of requirements a type can answer | Interfaces |
destructor | declares the method that runs when a value dies | Structs |
operator | declares an operator, infix, prefix or [] | Operators |
namespace | puts the rest of the file in a namespace | Namespaces |
use | binds a shorter name for a namespace, type, function or constant, for this file | Namespaces |
extern | a C surface: extern { } of functions and incomplete types, extern struct Name;, or the C function-pointer type extern function<R(P...)> | C interop |
const | a read-only variable, a constant, or a const if | Constants |
private | narrows a declaration to its file, or a member to its own type | Visibility |
internal | narrows a declaration to its module, which is also the default | Visibility |
public | widens a declaration to every module | Visibility |
static | a method or property that belongs to the type, not to a value | Structs |
test | declares a test block, compiled only by echoc test | Testing |
enum | declares a set of cases | Enums |
case | declares one case of an enum | Enums |
Control flow
| Word | What it does | Covered in |
|---|---|---|
if / else | the ordinary branch | Control flow |
while | loop while a condition holds | Control flow |
for | initializer, condition, step | Control flow |
foreach | walk anything iterable, with as | Iteration |
as | a written destination ($x as T), and the binding half of a foreach | Casts, Iteration |
break / continue | leave the loop, or jump to its next step | Control flow |
guard | unwrap, optionally bind, or leave the scope | Nullability |
Values
| Word | What it does | Covered in |
|---|---|---|
true / false | the two bool literals | Types |
null | the absent value, only where a T? admits one | Nullability |
mv | move out of a variable, or declare a parameter that takes ownership | Ownership |
strong | turn a weak<T> back into a handle, if the object is still there | Ownership |
instanceof | ask whether an interface value holds a particular type | Interfaces |
Types
| Word | What it does | Covered in |
|---|---|---|
ptr | the raw pointer type ptr<T>, and the ptr(...) conversion | Pointers |
weak | the non-owning class handle weak<T> | Ownership |
Statements
| Word | What it does | Covered in |
|---|---|---|
match | read which case an enum is holding. An expression, and a statement when its arms are blocks | Enums |
echo | print one value and a newline. A statement, not a function | Your first program |
unsafe | open a block where raw storage may be promoted to a typed borrow | Unsafe |
Words that look reserved and are not
die, assert, dprint and exit read like keywords and are ordinary functions. They are declared in Echo, in the standard library, and the compiler answers them at the call site instead of emitting a call:
assert(1 + 1 == 2);
echo "still here"; // still hereThe full set of names the compiler answers this way: size_of, align_of, is_trivially_copyable, needs_destruction, take, init, die, assert, ref_count, weak_count, dprint, alloc_bytes, realloc_bytes, free_bytes, live_allocations, process_argc, process_argv, process_envp and exit. Nothing reserves them, so you can declare a function called assert and shadow it. I would not.
There is no print. echo is the only output statement in the language.
constructor is the other one, and its asymmetry with destructor is deliberate rather than an oversight:
struct Point
{
int32 $x;
constructor(int32 $x) { $this->x = $x; }
destructor() { }
}
echo Point(3)->x; // 3destructor is a real keyword so that no member can be named destructor and collide with the mangled name of the actual one. constructor is an ordinary identifier the type parser recognises by value, which means a struct can have a property called constructor and nothing breaks.
init in a type body is the same kind of word. init { $this->encoded = ...; } is recognised only there, and mem::init is still a function. Structs is the chapter.
A few more words are contextual in the same way, recognised only in one position and ordinary identifiers everywhere else:
type, inside an interface body, declaring an associated type.map, inside an enum body, or at file scope asmap dhd : int32 for Glyph { ... }. It names a second encoding of the cases.map<K, V>is still the stdlib type. Enums is the chapter.self, inself::NAME, reaching a constant on the enclosing type.leftandright, inside anoperator(45, left)precedence clause.numeric,integer,signed,unsignedandfloating, as generic constraint aliases.
Primitive type names are not keywords either
int32, usize, bool and the rest are plain identifiers that the type parser matches by string. That's why they can never be shadowed (the primitive is checked first) and also why they don't appear in the tables above. Primitive types has the full list.
string, array, map, slice and range aren't even that. They are structs in stdlib/core/, and with --no-stdlib they simply don't exist.
The tokens
| Token | Means | Covered in |
|---|---|---|
:: | namespace separator, nested type, self::NAME | Namespaces |
-> | member and method access | Structs |
?-> | member access that stops at a null | Nullability |
?? | value on the left, or the right if it is absent | Nullability |
? | nullable suffix on a type, as in int32? | Nullability |
=> | the key half of foreach ($m as $k => $v) | Maps |
: | return type, attribute value, generic constraint | Functions |
:$ | reach the pointer itself rather than what it points at | Pointers |
& | a reference, or bitwise and. Whitespace decides, see below | Pointers |
! | negation, and the presence test on a nullable | Expressions |
** | exponentiation | Expressions |
<< >> | shift left, shift right | Expressions |
&& || | logical and, logical or | Expressions |
| ^ | bitwise or, bitwise xor | Expressions |
~ | bitwise not, prefix-only like ! | Expressions |
< > | comparison, and the brackets around type arguments | Generics |
# [ ] | an attribute is # followed by a bracketed value | Attributes |
// /* */ | comments. No doc-comment form |
Three things are missing from that table on purpose.
There is no ternary operator. ? is the nullable suffix and nothing else. ?? covers the case people usually reach for a ternary to write.
$x as T is a written destination. (int32)$x does not parse and int32($x) is not a function. See Casts.
.. and ..= are not tokens. They are ordinary operator declarations in stdlib/core/range.eco:
foreach (0 .. 3 as $i) {
echo $i; // 0, then 1, then 2
}0 .. 3 lexes as an integer, a dot, a dot and an integer, and the parser puts them back together the same way it does for any symbol a user declares. That's not a trick played for ranges. Declare <=> yourself and it goes through the identical path.
& is the one place whitespace changes meaning
& glued to what follows is a reference. & with a space after it is the bitwise operator:
uint32 $h = 12;
uint32 $mask = 7;
echo $h & $mask; // 4, bitwise andThe rule is exact rather than a matter of taste: & is a reference when the very next character is $ or any of A-Z, a-z, 0-9, _. Otherwise it is bitwise and.
The consequence to remember is that a digit counts. $mask &1 is not "bitwise-and one", it is a reference to something starting with 1, and the message you get will be about the wrong thing entirely. Put spaces around a binary &.
Unpicking this would be a lexer change, not a parser one, and it would change what &$a[$i] means everywhere. I'd rather write the space.
Two more places spacing matters
A binary - needs spaces, because - glues to a following digit and becomes part of the literal:
echo 1 - 2; // -1Written 1-2 that is two integer literals in a row, and the compiler says unexpected '-2' - two expressions with no operator between them. Which is technically accurate and takes a moment to read.
#[name: $value] needs the space after the colon, because :$ is a single token that will eat both characters. #[cache:$ttl] loses its value silently. This one is a deliberate decision rather than an accident: :$ is worth more than the compact attribute spelling.
Next
- Primitive types for the type names that are not keywords.
- Attributes for what goes inside
#[ ]. - What is missing for the holes that are left.