cwal

s2: Operators
Login

s2: Operators

(⬑Table of Contents) (⬑grammar)

Operators

Jump to...

Operators

s2 adopts C/C++ precedence rules, with only very minor changes in interpretation (namely in how it evaluates parenthesis groups, though the effect is essentially the same), and adds a few additional operators.

Note that spaces, including newlines (in most cases), are insignificant, so operators will span lines while looking for operands. A semicolon or EOF1 ends that search. s2 does not (like C does) consider "standalone" semicolons to be an error - they simply terminate an empty expression (but not all contexts allow empty expressions).

List of Operators

The operators are listed below in order of their precedence, highest to lowest. All operators grouped under the same top-level bullet point have the same precedence as each other unless noted otherwise.

Primary expressions:

Unary:

Multiplicative:

Additive:

Bitshift:

Relational:

Equivalence and Equality:

Bitwise:

Logical:

Assignment and Conditional:

Commas:

Short Circuiting

The logical operators and ternary-if support short-circuiting of their RHS. When appropriate, they will short-circuit a single expression to the right. When short-circuiting, blatant syntax errors are still caught (and cause script failure), but the code being "skipped" is not "really" evaluated, meaning that no semantics are applied to identifiers and there are no side effects such as function calls or creation of new values. e.g. unknown identifiers and illegal property accesses will not trigger errors when being skipped, but two consecutive non-keyword identifiers will (that's one of those "blatant" syntax errors mentioned above). Some examples:

assert 'hi' === (true ? 'hi' : this+error * is / skipped);
assert 1 === (false ? obj.invalidProp.x.y.z() : 1);
assert !(false && eval another, error, skipped[^5]);

Remember that comparison and logical operators have a higher precedence than ternary if, thus the extra parenthesis are needed on the first two lines. Same goes for the ! operator on the last line.

Property Access Operators

There are several options for accessing properties of values:

Most simply:

obj.propName
obj.'propName'
obj['propName']

But more properly:

obj.identifier|String|Integer|Double[^6]
obj.(expr)
obj[expr]

Examples:

assert obj.x === obj.('x');
assert obj.x === obj.'x';
assert obj.x === obj['x'];
assert 6 === [0,2,4].1*3; // theArray[1] * 3 ⇒ 2 * 3 ⇒ 6

These operators can of course be chained:

obj.prop['sub'].subsub

Except when short-circuiting, an exception is thrown when attempting to apply these operators to values/expressions which do not support them. When short-circuiting, all property access is considered legal (but always resolves to the undefined value internally).

Arrays treat integer property keys as array indexes and all others as normal (object-level) properties. Arrays throw exceptions if given negative indexes. In s2, any value type can be a property key, but there are some caveats regarding how equivalence is checked. Namely, a non-strict comparison is used, meaning (e.g.) that the integer property 1 and double property 1.0 are equivalent for most purposes, except for array's special-casing of index properties (where it only recognizes true integers). This can hypothetically (but in practice never has) lead to a minor inconsistency: an array can have an integer property 1 (array index) and a double property 1.0 (stored as an object property), whereas an in any other non-hashtable object those are equivalent property keys.

Sidebar: "arrays", in the context of integer-type property lookups, means any value which is of type Array or has such a type in its prototype chain. The above-described behaviour applies to the first Array value found in that Value's prototype chain, whether it's the first one (as will be the case for "real" Arrays) or 5 levels up. There has been some debate on whether this should only apply to Values which are themselves really arrays, but (A) there are uses (admittedly few good ones, though) for the current behaviour and (B) so far the current behaviour hasn't caused any backfires. Anyone inheriting arrays is assumed to know what they're doing.

Hidden Properties

From the C level (not script level) it is possible to make certain properties "hidden." Such properties will normally be invisible, in that they do not show up in certain uses (e.g. property iteration or JSON output), but they can be addressed by name if the name is known. (Because cwal supports using any value type for property keys, it is possible to create keys which neither script code nor external C code can know (or even be able to formulate).) Note that hidden properties are not exempt from being removed from client code, e.g. via direct assignment over them, the unset keyword, Object.unset(), or Object.clearProperties().

Operator Overloading

This support is optional - feel free to skip it. Internally we use operator overloading to implement the + and += operators for strings (and, in the meantime, several other operators), so this feature is unlikely to outright disappear, but its conventions are up for tweaking.

ACHTUNG: overloaded operators used to be passed their this value as their own first argument and their operand, if any, as the second argument. Operators no longer explicitly pass in their own this as an argument. While explicitly passing on this initially sounded like The Right Thing To Do, it turned out to just muddle up script code with unused (but required) parameters.

Why support operator overloading at all, when many programmers (not me!) despise operator overloading? Because it fits easily within the overall framework and is interesting to experiment with. It's certainly not a required feature in order to use s2 effectively, and should be considered a "syntactic sugar" feature (except that string concatenation via the binary + operator is implemented via an operator overload in the string prototype).

s2 has support for overloading the majority of the basic operators for arbitrary container values using script-side functions. s2 will in fact throw an error if the LHS of an overloadable operator (resp. the RHS for prefix operators) is not a "simple" type and does not implement that operator, under the assumption that the operation has no useful semantics. In that sense, it requires overloading if one wants to use a higher-level data type with most operators.

When and where operator overloads are checked:

The generic process for overloading is the same for all operators: assign a function to a property with the operator's name (all are listed below). Operators which can be overloaded may use that proxy under the conditions described above. The return result of an overload operator becomes the result of the operation.

Notes and caveats:

The list of overloadable operators follows. The name, up to the first parenthesis, is the operator's property name. The parameter list shows what (if anything) gets passed to the operator:

The operators:

We can use overloading to emulate, for example, C++ stream-style output stream operators. The following example uses one more layer of indirection than is strictly needed, but only for demonstration purposes:

// Some arbitrary output-streaming function...
var f = proc me(){
 return me.b.append(argv.0);
};
// A buffer for our demo:
f.b = new s2.Buffer(100);
// The operator:
f.'operator<<' = proc(arg){
 this(arg); // extra indirection only for demo purposes.
 return this; // for chaining to work
};
// C++-like output streams:
f << "a" << "bc" << "def";
assert "abcdef" === f.b.toString();
f.b.reset();
f << 1 << 2+3;
assert "15" === f.b.toString();
// Alternate (simpler) approach:
var o = {
  buf: new s2.Buffer(20),
    'operator<<': proc(arg){
    this.buf.append(arg);
    return this;
  },
  reset: proc(){
    this.buf.reset();
    return this;
 }
};

(Note that in the meantime, the Buffer class overrides operator<< to append its argument to the buffer.)

This incidentally allows simplified output of huge heredocs:

f << <<<EOF ...arbitrarily long heredoc spanning tens of
lines... EOF;

Note that a call made in that form needs no closing parenthesis at the (far-away) other end. On the other hand, it's likely only half as fast because it's got twice the function call overhead due to the extra level of indirection the overload adds.

Overload-only Operators

s2 supports the concept of "overload-only" operators, meaning that it has a slot for them in the operator table, but it does not implement them by default. Using them will cause an exception to be thrown unless the LHS (for binary) resp. RHS (for unary prefix) implements the given operator.

The following operators are currently implemented as overload-only, denoted using the same naming/parameters conventions as in the full operator list above. See the operator overview for precedence and associativity information.

s2 applies no specific semantics to these operators - those are defined by the implementor. It makes one exception for the ::, in that an identifier on the RHS of the :: operator is treated just like an RHS identifier for the dot operator. Namely, the identifier is not resolved when evaluated, but is instead passed on to the dot/:: ops as the lookup key. Contrast with the -> operator, where s2 evaluates the RHS identifier before passing its result value to the operator. In other words foo.bar, foo->'bar', and foo::bar (note that the latter has no quotes) all end up passing 'bar' to the associated operator.

Footnotes

tokenized, have a virtual EOF just before the closing character, and thus explicit end-of-expression tokens are not necessary in any such contexts (and are errors in some contexts).

than i would like. It affects all of the combo assignment ops as well and a nice solution for that particular case is not immediately clear.

the identifier x, and assignments cannot work with that.

will be recognized as a series of 5 tokens (foo . (1.2) . 3) by the tokenizer. Such usage is expected to be rare, and one can use foo[1][2][3] or (depending on intent) foo[1.2][3] instead.


  1. ^ all {}, [], and () in s2 have, because of how they are
  2. ^ It "could" be made to, but making it so is rather more intrusive
  3. ^ C99 has such support, but cwal/s2 are, so far, strictly C89.
  4. ^ Because the result of (x) is the value x refers to, instead of
  5. ^ Currently booleans, null, and undefined have no prototype but one may eventually be added, if only to add a toString() method for consistency with other types.*
  6. [^ 5 ]
    Careful - [eval](keyword-eval.md) and friends consume commas on
    the RHS!
  7. [^ 6 ]
    There is a minor ambiguity with literal *doubles*: (`foo.1.2.3`)