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:

jaiscript
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;
output
east = 2
d == south: true
going down
== result: 3

Enum 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:

jaiscript
enum Direction { north, south, east, west }
print("{}", Direction.zzz);
output
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):

jaiscript
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();
output
utils::add(1, 2) = 3
utils::add(1, 2, 3) = 6
version 3
len2 = 25
== result: 42

Extra wrinkles:

  • Redefining an existing signature without override is 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::Box names it from any file (one that imports the module declaring it included), and it is the name type_of reports and a saved $type carries. Inside the namespace's blocks, and a nested namespace's, the bare Box reads as ns::Box (in a later reopening of the namespace too); outside, the bare name reaches it only through using ns::Box;. Two namespaces that each declare a Box declare two classes, and a global Box is a third.
  • Keywords like string are usable as namespace names, and any reserved word but this and super names 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:

jaiscript
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); }
output
stats::hits 7 hits 100 limit 20
caught: Cannot assign string to 'hits' (declared int)
caught: Cannot assign to 'to_lower': it is read-only

Using 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.

jaiscript
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);
output
6.0 3

Destructuring​

auto [a, b, c] = array; unpacks positionally. Extra elements are ignored; missing ones bind null:

jaiscript
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;
output
1 2 3
10 20
10 20 null
== result: 11

Ternary​

Right-associative, so the classic cascade reads top-down without parentheses:

jaiscript
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;
output
alive and wounded
== result: 0

Null-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):

jaiscript
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());
output
obj?.length() = null
s?.length() = 5

A 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):

jaiscript
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); }
output
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).