Enums, Namespaces, Destructuring & Friends
The kitchen sink. This chapter collects the smaller conveniences, the things you reach for daily that don't need a chapter of their own: enums, namespaces, destructuring, the ternary operator, and null-safe access. Each behaves the way a C++ developer would guess, with the differences called out.
Enums
enum Name { a, b, c } declares sequential integer constants (0-based). They compare naturally and work as switch cases:
enum Direction { north, south, east, west }
print("east = {}", Direction.east);
auto d = Direction.south;
print("d == south: {}", d == Direction.south);
switch (d) {
case Direction.north: print("going up");
case Direction.south: print("going down");
default: print("sideways");
}
Direction.west;east = 2
d == south: true
going down
== result: 3Enum members are plain ints at runtime (Direction.east prints 2). Membership, though, is checked at load: Direction.zzz is not a run-time lookup that returns null, it fails the load before anything runs:
enum Direction { north, south, east, west }
print("{}", Direction.zzz);jaiscript: static check failed:
<script>:2:23: error: no value 'zzz' in enum 'Direction' (declared at 1:6)
print("{}", Direction.zzz);
^Members are reached with a dot (Direction.east) or, as in C++, with :: (Direction::east); both name the same constant. Values are always sequential from 0: a C++-style explicit value (enum Flag { a = 5 }) is a parse error, so spell fixed numbers as constants in a namespace (namespace Flag { int a = 5; }) when they matter.
An enum declared inside a function or a block is local to it, like a C++ local enum: its values resolve there and it vanishes with the scope. The same holds for an enum in a file that an include runs inside a function. An enum of the same name at program scope keeps its values, and the checker warns when a local declaration hides one (enum 'Color' declared in function 'reload' is local to it: the program-scope 'Color' keeps its values).
Namespaces
Namespaces group functions, variables, and classes behind ::. They are flat. namespace my::nested::deep { } is a single namespace whose name contains ::, and "nesting" blocks composes the same way. Functions overload by parameter types and count, by the rules in Functions; redefining an existing signature requires the override keyword (mirrors hot-reload discipline):
namespace utils {
auto add(a, b) { return a + b; }
auto add(a, b, c) { return a + b + c; } // overload by arity
auto VERSION = 3;
class Vec2 {
int x = 0; int y = 0;
Vec2(int px, int py) { x = px; y = py; }
auto len2() { return x * x + y * y; }
}
}
namespace my::nested::deep { // flat name with :: in it
auto answer() { return 42; }
}
print("utils::add(1, 2) = {}", utils::add(1, 2));
print("utils::add(1, 2, 3) = {}", utils::add(1, 2, 3));
print("version {}", utils::VERSION);
auto v = utils::Vec2(3, 4);
print("len2 = {}", v.len2());
my::nested::deep::answer();utils::add(1, 2) = 3
utils::add(1, 2, 3) = 6
version 3
len2 = 25
== result: 42Extra wrinkles:
- Redefining an existing signature without
overrideis an error (Function 'f' already exists in namespace 'n'. Use 'override' keyword to replace it.);auto func(x) override { ... }replaces the existing definition. Overloads that differ only by reference are an error, as for free functions. - A namespace can share a name with a class, and its functions then override the class's static methods of matching arity when they say
override(namespace Tool { int make() override { ... } }; without it the collision is an error naming the keyword).Tool::make()then runs the namespace function, and the class's other statics still answer. - A class a namespace declares is named by its qualified name, as in C++:
ns::Boxnames it from any file (one that imports the module declaring it included), and it is the nametype_ofreports and a saved$typecarries. Inside the namespace's blocks, and a nested namespace's, the bareBoxreads asns::Box(in a later reopening of the namespace too); outside, the bare name reaches it only throughusing ns::Box;. Two namespaces that each declare aBoxdeclare two classes, and a globalBoxis a third. - Keywords like
stringare usable as namespace names, and any reserved word butthisandsupernames a namespace member or an enum value, reached qualified (spider::catch(url),Mode::default; keyword member names). - Wrong arity / missing member through
::are run-time errors. The checker is lenient at every depth of a namespace path, so these are catchable, not load errors.
A namespace is a scope
A namespace variable is one variable in the namespace's own scope, layered over the globals. ns::x = v, ns::x op= v and ++ns::x store it under its declared type's rules, a function declared in the namespace reads and writes it by its bare name (a global of the same name stays separate), an initializer sees the members declared before it, and reopening the namespace redeclares in place. The host's namespace entries (string::to_lower, time::now) are read-only from script:
int hits = 100; // a global of the same name stays separate
namespace stats {
int hits = 0;
int base = 10;
int limit = base * 2; // an initializer sees the members declared before it
void record() { hits += 1; } // a namespace function reads and writes its variables by bare name
}
stats::record();
stats::record();
stats::hits += 5;
print("stats::hits {} hits {} limit {}", stats::hits, hits, stats::limit);
var bad = "many";
try { stats::hits = bad; } catch (e) { print("caught: {}", e); }
try { string::to_lower = 3; } catch (e) { print("caught: {}", e); }stats::hits 7 hits 100 limit 20
caught: Cannot assign string to 'hits' (declared int)
caught: Cannot assign to 'to_lower': it is read-onlyUsing declarations: using ns::name;
using ns::name;, or a comma list (using math::vec3, math::dot;), makes name stand for ns::name in the rest of the file it appears in, wherever the name stands: a type, a call, a value. It is a parse-time rewrite of that file's text, so the checker and both backends see only the qualified name and nothing runs for it. It is file-local and not transitive: a file that includes or imports this one does not inherit it, and a file this one includes does not see it. Any namespace works, a nested path too (using a::b::c;). There is no using namespace. After a dot, ?., :: or -> the name is a member name and stays as written. The name cannot be declared again in that file: a variable, parameter, member, function, class or enum of that name, or a second using of it, is a parse error.
namespace geo {
float scale = 2.0;
float twice(float v) { return v * scale; }
class Pt { int x = 1; int y = 2; }
}
using geo::twice, geo::Pt; // the rest of this file reads twice as geo::twice, Pt as geo::Pt
Pt p;
print("{} {}", twice(3.0), p.x + p.y);6.0 3Destructuring
auto [a, b, c] = array; unpacks positionally. Extra elements are ignored; missing ones bind null:
auto [x, y, z] = [1, 2, 3];
print("{} {} {}", x, y, z);
auto [a, b] = [10, 20, 30]; // extras ignored
print("{} {}", a, b);
auto [p, q, r] = [10, 20]; // missing become null
print("{} {} {}", p, q, r == null ? "null" : to_string(r));
auto pos = [4, 7];
auto [px, py] = pos;
px + py;1 2 3
10 20
10 20 null
== result: 11Ternary
Right-associative, so the classic cascade reads top-down without parentheses:
auto health = 40;
auto status = health > 0 ? "alive" : "dead";
auto level = health > 75 ? "healthy" :
health > 25 ? "wounded" : "critical";
print("{} and {}", status, level);
health > 50 ? 1 : 0;alive and wounded
== result: 0Null-safe access: ?.
x?.member and x?.method(...) evaluate to null when x is null instead of erroring; the call's arguments are not evaluated then. On a non-null x it is the ordinary member access, a store included (x?.hp = 5):
auto obj = null;
auto a = obj?.length(); // null-safe: short-circuits to null
print("obj?.length() = {}", a == null ? "null" : to_string(a));
auto s = "hello";
print("s?.length() = {}", s?.length());obj?.length() = null
s?.length() = 5A null in front of a ?. skips the rest of the chain, as in JavaScript and C#: a?.b.c, a?.items[0] and a?.f().g() are null when a is. Only the value in front of the ?. is tested, so a?.b.c still raises when a is not null and b is; put a ?. on every link that may be null. ?? supplies the default (Operators):
class Node { int v = 3; shared_ptr<Node> next; }
auto head = new Node();
print("{} {}", head?.v, head?.next?.v); // next is null: the second ?. yields null
shared_ptr<Node> none;
print("{}", none?.next?.v);
try { print("{}", head?.next.v); } catch (e) { print("caught: {}", e); }3 null
null
caught: Cannot access 'v' on null ('next' is null)Related conveniences documented elsewhere: weak_ptr/shared_ptr (Value Semantics), statics via :: (Classes), #include/#import hash-prefix tolerance (Modules), cast<T>(e) / cast<T&>(e) / cast<shared_ptr<T>>(e) (Type Ladder and Types That Fail Early), extern declarations and the [[warn_mode]] / [[strict_mode]] attributes (Types That Fail Early).