Value Semantics & References​

Every value in JaiScript is a value in the C++ sense: a number, a string, an array, a map and a class instance alike. The whole model:

  1. Assignment copies. b = a and auto b = a make a separate value. So does a container literal: [h, h] holds two copies of h.
  2. Parameters and returns copy. A by-value parameter is the callee's own copy, and a by-value return hands back a value that aliases nothing.
  3. Methods act on their receiver. p.set(42) changes p.
  4. Sharing is spelled. Two names reach one object only through a & parameter, a reference declaration (auto& r = x;), a range-for auto&, a shared_ptr<T> or cast<T&>.
  5. A copy is deep, except through a handle. Copying an object copies its value fields and elements; a shared_ptr, a coroutine handle and a lambda inside it keep pointing at what they pointed at.

A write to a copy never reaches the original. The checker refuses the plainest cases at load (lost writes); the rest is the habit of asking "is this a copy?" wherever a change does not stick.

jaiscript
class Hero {
    int hp = 10;
    void hurt(int n) { hp -= n; }
}
Hero a;
Hero b = a;                       // a copy
b.hp = 1;
a.hurt(3);                        // a method changes its receiver
auto party = [a, a];              // two more copies
party[0].hp = 0;
int peek(Hero h) { h.hp += 100; return h.hp; }   // h is the callee's copy
print("{} {} {} {} {}", peek(a), a.hp, b.hp, party[0].hp, party[1].hp);

array<int> stock = [1, 2, 3];
array<int> get_stock() { return stock; }     // by value: the caller gets a copy
array<int>& stock_ref() { return stock; }    // by reference: the caller names stock
auto mine = get_stock();
mine.push(4);
stock_ref().push(5);
print("{} {}", stock.size(), mine.size());
output
107 7 1 0 7
4 4

& parameters​

A parameter declared with & names the caller's storage: a variable, a field (h.hp), an element (arr[i]), a map entry, a vector component (v.x) or a call that returns T&. auto& takes any type.

jaiscript
void bump(int& x) { x += 1; }
void heal(auto& who) { who.hp += 5; }
class Hero { int hp = 10; }
int n = 41;
Hero h;
array<int> arr = [5, 6, 7];
bump(n);
heal(h);
bump(h.hp);                 // a field binds
bump(arr[1]);               // an element binds
print("{} {} {}", n, h.hp, arr[1]);
output
42 16 7

A T& binds only a T: an int& takes an int, never a float (a load error where the checker sees it, Cannot pass float to parameter 'x' of 'f' (declared int&) for a var holding one), and a class reference takes the class or a subclass. A field or element argument binds when the argument is evaluated (evaluation order).

What a literal or a temporary may bind is decided from the function's body, so no const is needed for the common case:

ParameterTakes a literal or temporaryWrites
T& the body only readsyes, as a temporary for the callnone
T& the body writes throughno: 'grow' writes through parameter 'xs' (line 2); pass a variable at load, Cannot pass non-lvalue to reference parameter when only the run time sees itreach the caller's variable
string&yes, even when written: the temporary is writablereach a variable; stay inside the call for a literal
const T&yesrefused
a lambda's T&, called through a variableno (a string excepted)reach the caller's variable

A write is an assignment, ++/--, a mutating method, a store through an alias of the parameter, passing it on to a writer (the checker follows the call), handing it to a constructor, a shared_ptr parameter or a function the checker cannot see into (a lambda, a host function), or returning it by reference. An override that writes counts for the base method too. So the body is part of the interface: adding a write to a function that callers pass literals to fails those calls at the next load.

jaiscript
int total(array<int>& xs) { int s = 0; for (auto x : xs) { s += x; } return s; }
string upper(string& name) { name.to_upper(); return name; }
string live = "alice";
print(total([1, 2, 3]), " ", upper("bob"), " ", upper(live), " ", live);
output
6 BOB ALICE ALICE

Reference declarations​

auto& r = x; (or Hero& r = x;, var& r = x;) declares an alias. It binds any lvalue a & parameter binds, must have an initializer (reference variable 'r' must be initialized), and stores through it obey the declared type of the storage it names: var& r = hp; over an int hp stores ints. A reference is an alias, not storage, so it cannot be a field or an element.

An element reference names an index, not an element. It is resolved through the container on each use, so it stays valid when the array reallocates. After an insert, an erase below it or a sort, it reads and writes whatever sits at that index. Shrinking the array past the index raises the catchable Reference to a removed array element, and the interrupted store writes nothing. A map entry reference names its key and follows the entry; a key with no entry raises instead of inserting.

jaiscript
array<int> arr = [5, 6, 7];
auto& x = arr[1];
for (int i = 0; i < 64; ++i) { arr.push(0); }   // reallocation: x still names index 1
x = 42;
arr.insert(0, 99);                              // index 1 now holds what was arr[0]
x = 1;
print("{} {} {}", arr[0], arr[1], arr[2]);
arr.resize(1);
try { x = 2; } catch (e) { print("caught: {}", e); }
map<string, int> ages = {"ana": 31};
try { int& missing = ages["bo"]; } catch (e) { print("caught: {}", e); }
output
99 1 42
caught: Reference to a removed array element
caught: Key "bo" not found in map

const T& r = make(); binds a temporary and keeps it alive as long as r, with no copy, as in C++. A reference that is not const binds only an lvalue: Lamp& attic = make(); raises Cannot take reference of non-lvalue expression (...).

What names storage. As in C++, an lvalue is a variable, a field, an element, a vector component, a call returning T& (first() = 5), cast<T&>(e), (a = b), ++x, and a conditional whose arms are both lvalues of one type ((use_a ? a : b) = 9, auto& r = use_a ? a : b;). Anything else is a temporary, and assigning to one or binding a non-const reference to it is an error that says what was written: cannot assign to a literal, cannot assign to x++: it yields the old value, cannot assign to the result of '+': it is a temporary value, cannot assign to a call result: 'count' returns by value, cannot assign to a conditional: its arm '5' names no storage, cannot bind a reference to a conditional of 'int' and 'float': its arms must have one type, and Cannot assign to a member of a temporary for make().x = 1 (a handle result names its object, so make_shared<P>().x = 1 stores). The checker reports these at load where it sees the target; a call through a var raises the same text, capitalized, when it runs.

Range-for with auto&​

for (auto x : xs) copies each element into x; for (auto& x : xs) names the element. Write auto& whenever the body changes the element or the element is large, and the question of copies never comes up.

jaiscript
auto nums = [1, 2, 3];
int doubled = 0;
for (auto x : nums) { x *= 2; doubled += x; }   // x is a copy, read afterwards
for (auto& x : nums) { x += 10; }               // x is the element
print(nums, " ", doubled);
output
[11, 12, 13] 12

shared_ptr<T>: a handle​

When an object must be reached from several places that outlive a call (a registry, a graph, a field naming another object), hold it by handle. new T(args) and make_shared<T>(args) are the same expression, the first in JavaScript's spelling and the second in C++'s; both yield a shared_ptr<T>. Copying a handle copies the handle, in a variable, a parameter, an element, a field of a copied object or a lambda capture.

jaiscript
class Node { int v = 1; }
class Holder { shared_ptr<Node> link = new Node(); Node own; }
shared_ptr<Node> sp = new Node();
auto alias = sp;                       // a second handle
alias.v = 99;
void poke(shared_ptr<Node> n) { n.v += 1; }
poke(sp);
Holder a;
Holder b = a;                          // b.own is a copy, b.link is a's Node
b.link.v = 5;
b.own.v = 50;
print("{} {} {} {}", sp.v, a.link.v, a.own.v, same_object(a.link, b.link));
weak_ptr<Node> w = sp;                 // observes without owning
sp.reset(new Node());                  // sp names a new object; alias keeps the old one
print("{} {} {}", sp.v, alias.v, w.lock().v);
alias.reset();
print(w.expired());
output
100 5 1 true
1 100 100
true
WrittenDoes
sp = new Node(); (a handle on the right)points sp at another object
sp = Node(); (a value on the right)assigns into the shared object: every alias sees it
sp.reset(h), sp.reset()repoints to h's object or empties sp, never writing through; a value or a primitive argument raises
Node fresh = sp;, array<Node> xs = [sp];, xs.push(sp)a handle into a value slot copies the object (C++'s *sp); an untyped container or a shared_ptr<Node> element keeps the handle
weak_ptr<Node> w = sp;only a handle seeds a weak_ptr: a value is a load error (a run-time error through a var)
shared_ptr<Base> b = derived;an upcast converts
shared_ptr<Derived> d = b;a load error: spell cast<shared_ptr<Derived>>(b). A var source is judged by its object's class when the line runs
same_object(x, y)identity, for handles, references, containers and instances; == compares values (Equality)

A shared_ptr, a weak_ptr, a function and a var can hold null; a class value never does (Classes).

cast<T&>: a reference by another type​

cast<T>(e) is a copy, cast<T&>(e) a reference to the same object and cast<shared_ptr<T>>(e) a handle sharing it. The reference form is how a base-typed slot gives up a member only the derived class declares, and it is an lvalue: it can be assigned, bound by auto& and passed to a & parameter. The value form on a handle or a reference still copies, and the checker warns (cast<C>(h) copies the instance - use cast<C&>(h) to alias it or cast<shared_ptr<C>>(h) to share it). A cast to an unrelated class raises the catchable Cannot cast 'X' to 'T'. Primitive casts are in the Type Ladder.

jaiscript
class Base { int a = 1; }
class Derived : Base { int t = 9; }
Base h = Derived();                      // a Base slot; the object inside is a Derived
cast<Derived&>(h).t = 5;                 // the same object
shared_ptr<Derived> s = cast<shared_ptr<Derived>>(h);
s.t += 70;
Derived copy = cast<Derived>(h);         // a separate object
copy.t = 0;
var n = 3;
cast<int&>(n) += 2;                      // stores an int into the var
print("{} {} {}", cast<Derived&>(h).t, copy.t, n);
output
75 0 5

Lost writes fail the load​

A write into a copy that nothing reads afterwards changes nothing, and is nearly always a loop variable or a parameter that was meant to be a reference. The checker refuses the cases it can prove:

jaiscript
class Enemy { int hp = 10; }
array<Enemy> enemies = [Enemy(), Enemy()];
for (auto e : enemies) {
    e.hp -= 1;
}
output
jaiscript: static check failed:
<script>:4:5: error: 'e.hp' writes to 'e', a copy of the element that is never read afterwards, so the write is lost; iterate with 'auto& e' to change the element
        e.hp -= 1;
        ^

The fix is the reference the code meant: for (auto& e : enemies), void hurt(Enemy& e), auto& first = enemies[0];. For a parameter the message ends take 'string& s' to change the caller's value, for a local declare 'auto& x' to change the original.

What the check covers. The write is an assignment, a compound assignment, ++/-- or a mutating builtin method. Its target is a by-value range-for variable, parameter or local of a type the checker knows to be a value (a scalar, a string, a math value, a typed container of those, a script class held by value, or an auto inferred as one of them, an array of scalar literals included), or a field or element of one. And no path reads the variable again before it dies.

What it does not cover. The check proves a write unobservable, and gives up wherever it cannot:

  • Any call after the write to something other than the engine's own print and format silences it when the copy holds class instances, because any script code may observe the copy through reflect::instances. This loads, and loses the write:
jaiscript
class Enemy { int hp = 10; }
array<Enemy> enemies = [Enemy(), Enemy()];
void log(int hp) { print("hit: {} left", hp); }
for (auto e : enemies) { e.hp -= 1; log(e.hp); }   // e is a copy
print("enemies[0].hp is still {}", enemies[0].hp);
output
hit: 9 left
hit: 9 left
enemies[0].hp is still 10
  • A var or untyped variable, a handle, and a by-value class parameter (it may share a handle argument's object) are not judged.
  • A variable that a reference binds, a lambda names, or a call takes as its receiver or argument is not judged from then on.
  • A class with a destructor, an accessor, an operator= or a host base (its own, a subclass's or a field's) is not judged.
  • A write that can itself raise (an element or character store, a store that converts, a method taking an index or count) is observable, so it is not judged.

So the check is a net under the common slip, not a guarantee. Writing auto& in every loop that changes its elements is what makes the question moot.

A container or string a function returns by value is a copy too: a.list().push(4); is push changes a call result in place and nothing reads it: 'a.list' returns by value and a.list()[0] = 7; is cannot assign to a call result: 'a.list' returns by value. Return array<int>& to change the original.

What a copy still shares​

Three kinds of value are handles, so a copy of one (or of an object holding one) reaches the same state:

  • A shared_ptr or weak_ptr, as above.
  • A coroutine handle: a copy is the same running coroutine. A method coroutine keeps the object it started on, so advancing a copied object's handle changes the original's fields. A copy that needs its own assigns a new call: copy.legs = copy.walk(); (Coroutines).
  • A lambda: its by-value captures are the lambda's own variables, kept between calls, and a copy of the lambda shares them (Functions).
jaiscript
class Walker {
    int steps = 0;
    coroutine int walk() { while (true) { steps += 1; yield steps; } }
    auto legs = walk();
}
Walker a;
Walker copy = a;
copy.legs.next();                       // a's coroutine, running on a
print("copy {}, original {}", copy.steps, a.steps);

int n = 0;
auto count = [n]() { n += 1; return n; };
auto again = count;                     // shares count's n
count(); count();
print(again());
output
copy 0, original 1
3

const: set once, then only read​

const marks a variable (const int LIMIT = 10;), a parameter, a loop variable, a structured binding or a class constant that its initializer sets and nothing writes afterwards. const alone is const auto: const limit = 10;, for (const m : pack), const [a, b] = pair;, for (const [k, v] : m). The rules are C++'s, less const methods:

  • A const value is read-only all the way down: its fields, its elements, the methods that write their receiver and the builtin methods the API reference lists as mutate. A callback method (for_each, find, reduce, ...) reads unless the callback writes through a & parameter.
  • A const value is read-only through and through (const Hero h; h.heal(); fails the load); a const handle cannot be rebound and its object stays writable (const shared_ptr<Hero> p = new Hero(); p.heal(); runs), as JavaScript's const and C++'s const shared_ptr. A copy of a const value is an ordinary value.
  • A const loop variable follows its elements: over an array<Hero>, for (const m : pack) { m.heal(); } fails the load; over an array<shared_ptr<Hero>> the same loop changes each object.
  • A const in a class body is a class constant: one value for every instance, read as Hero::MAX_HP or a bare MAX_HP in methods.
  • const is not part of a signature: f(const T&) and f(T&) are one function.
  • The checker reports a write it can see at load, naming the line that writes: cannot call 'heal' on 'h': 'h' is const and 'heal' writes its receiver (line 4), cannot assign to const 'Hero::MAX_HP', cannot call 'push' on 'xs': 'xs' is const and 'push' changes it. A path through a var is checked when it runs.
jaiscript
class Hero {
    const int MAX_HP = 100;
    int hp = 40;
    void heal() { hp = MAX_HP; }
}
int twice(const int& x) { return x * 2; }
const Hero h;                            // h.heal() would fail the load
const shared_ptr<Hero> p = new Hero();
p.heal();                                // a const handle keeps its object writable
print(h.hp, " ", p.hp, " ", Hero::MAX_HP, " ", twice(21));
const var table = [1, 2, 3];
try { table[0] = 9; } catch (e) { print("caught: ", e); }
output
40 100 100 42
caught: Cannot assign to 'table[0]': it is read-only ('table' is const)

Choosing​

UseWhenCost and limit
a value (the default)scalars, strings, small classes, anywhere a snapshot is wanteda deep copy that grows with the payload; a string copy is O(1) until written
T& / auto&a call or a loop that reads or changes the caller's objectno copy; an alias cannot be stored in a field or a container
shared_ptr<T>identity that outlives the call: registries, graphs, objects naming objectsno copy; every holder sees every change

The engine skips a copy that no code could tell from the original (a by-value parameter or local that is only read), so a read-only helper taking a class by value runs at reference speed; the semantics never change. Measurements are in Performance.

Assignment, in full​

Initialization (a declaration, a by-value parameter, a return) is copy construction and never runs operator=. An = into an existing object runs the class's operator= when one of its overloads takes the right side, and a memberwise copy otherwise (Classes). What = does to a var depends on what it holds and what is on the right:

var p holdsRight sidep = ... does
a handle (new Foo())a handle: new Foo()rebinds p; other aliases keep the old object
a Foo or subclass value: Foo()assigns into the shared object, as a shared_ptr<Foo> variable does
anything else: Bar(), 5rebinds p to that value (a typed handle would raise)
an instance by valuea value an operator= overload takesruns it; p keeps its object
anything else, a handle or nullp takes the new value

The table holds wherever a var lives (a local, a field, an array<var> element). auto q = p; of a var is a var; spell shared_ptr<Foo> q = p; to fix the type.

jaiscript
class Foo { int x = 1; }
var p = new Foo();
var alias = p;
p = Foo();                 // a value: assigns into the object alias shares
print(same_object(p, alias));
p = new Foo();             // a handle: rebinds
print(same_object(p, alias));
p = 5;                     // anything else: rebinds
print(p, " ", alias.x);
output
true
false
5 1

Bound C++ objects​

A C++ class the host binds follows every rule above: Turret b = a; runs its copy constructor, var c = tower.child(); copies what a Turret& return names while var& t = tower.child(); binds it, and = into an existing object runs the bound operator= (without one, the slot takes a copy, and assigning through a reference raises Cannot assign to a reference to 'Turret': its binding has no operator=). A type that cannot be copied, such as a host atomic<int>, raises Cannot copy C++ object of type ... and is held by reference. A host function's non-const T& parameter takes a temporary as a copy. The binding side is in dynamic_binder and Atomics.

On the C++ side the defaults are the other way round: copying a jai::script_value shares, and value.clone() is the deep copy.