cwal

s2: Data Types
Login

s2: Data Types

(⬑Table of Contents)

Data Types

Jump to...

Per-type pages:

Introduction

s2 supports a fairly rich set of JavaScript-like data types, plus a few more exotic ones. All "basic" types are, as is conventional, immutable (their values cannot be changed), but "containers" (types which hold other values) may of course be modified (or they'd be quite useless (in this language)). All values are, in effect, passed by pointer. There is no such thing as "deep copy" in s21.

The built-in data types and constant values are...

Boolean Contexts

A "boolean context" is whenever s2 needs to reduce a problem's answer to a simple true or false. All value types can be implicitly converted to a boolean true or false, and their interpretations are set in stone at the cwal engine level, so neither scripts nor s2 may modify them:

Prototypes

s2 does not have classes, in the sense that high-level languages like C++ and Java have classes. It has a hard-coded core set of types (okay, "classes") with which clients can compose their data structures. s2 uses a prototypal inheritance model, similar to JavaScript's but has a few subtle, yet significant, semantic differences. A "class" in s2 is simply a specific use case for a specific Value, for which the language provides a very small amount of extra support. Specifically:

All of the base types except for the booleans, null, and undefined have a prototype object which provides common member methods. They are described in the sections of this document dedicated to each data type.

Clients may change the prototypes of any container-type value via assignment to the prototype pseudo-property, but properties set on an object are always set on that instance, and never its prototype. To demonstrate the implications of that:

var obj = {a:1, prototype: {b:2}};
assert 2 === obj.b;
obj.b = 3; // hides the prototype's copy:
assert 3 === obj.b;
assert 2 === obj.prototype.b;
unset obj.b; // but the prototype still has its copy:
assert 2 === obj.b;
var prototype; // it is only a keyword in property access contexts
obj.prototype.prototype = obj; // Throws exception: cyclic prototype relationship

Clients may not (for at least two long, boring reasons4) reassign the prototypes for the non-container types, and trying to do so will trigger an exception.

There is nothing specifically magical about prototypes except that the prototype property is not really a property. Rather, it is intercepted internally as needed as a proxy for the C-level prototype getter/setter APIs. This property does not show up in any property iteration, for example, and someObj.hasOwnProperty('prototype') will always return false. Prototype objects, in and of themselves, do not get any special treatment as prototypes - only access to the prototype (pseudo-)property is handled individually5.

cwal only supports container types as prototypes. Trying to set a non-container value as a prototype will fail, with two special-case exceptions (handled s2-side, not in cwal): assigning either null or undefined to anObject.prototype will remove that object's prototype. This is often useful for "plain properties objects" when a user wants to ensure that looked-up properties are not inherited from a prototype.

Unlike JavaScript, modifying prototype objects does not have any outwardly visible effects on objects which refer to them. e.g. when adding methods to the base Object prototype in JavaScript, traversal of properties in client-created objects can get very strange indeed (they suddenly include their prototype's properties when iterating, requiring the use of Object.hasOwnProperty() to work around it). To demonstrate this oddity:

js> var o = {a:1}
undefined
js> Object.prototype.foo = 1
1
js> o.foo
1
js> for(var i in o) { console.debug(i, o.hasOwnProperty(i)); }
// outputs:
a true
foo false // this is the weird bit: modifying the prototype changes
          // how iteration of all instances works.
// And yet:
js> Object.keys(o)
Array [ "a" ]

(Wha?!?!)

In s2, the effect is more intuitive: only the object being modified is modified, and iteration of other instances is most certainly not changed. Note that s2's core Object type does indeed have a hasOwnProperty() method, but it's essentially never needed in s2.

Also unlike JavaScript, the "class" and prototype of values in s2 are the same thing. In JavaScript, new types (classes) are created using Functions, and inherited properties are set on function's prototype. New instances of that function, created via new thatFunction(), inherit the properties of that function's prototype but not any properties of the Function (even though the instanceof keyword says they inherit that Function). In s2, that "extra" level of indirection is missing. That is, if a class is defined using a function, that function becomes each instance's prototype (which, in turn, makes every instance call()-able, in stark contrast to JavaScript). Whether that's a feature or bug is debatable, but it is how it is.

To briefly contrast the approaches:

const MyClass = function(){...};
MyClass.prototype.x = 1;
const m = new MyClass();
m instanceof MyClass; // true
m.__proto__ === MyClass.prototype; // true
// ^^^ why m.prototype does not work is one of life's eternal mysteries

vs.

const MyClass = {
  __new: proc(){...},
  x: 1
};
const m = new MyClass();
assert m inherits MyClass;
assert m.prototype === MyClass; 

This is not to claim that s2's approach is superior. In practice, JS's extra level of indirection is arguably more intuitive once one grasps the idea that new classes are created as functions. s2's approach, however, is (for better or worse) both memory-lighter and more flexible, in that any container type may be a prototype and any value with a __new function method can behave like a class for purposes of the new keyword. It does, however, lead to weirdness such as all containers which inherit a function become call()-able via that function:

const MyClass = proc(){s2out<<'the class\n'};
MyClass.__new = proc(){s2out<<'ctor\n'};
const m = new MyClass(/*calls the __new constructor*/);
// ^^^ outputs 'ctor'
m(); /** calls MyClass(), with m as the "this", but not for the reason
         one might think: in s2, a function/callable called with no
         property access operator, i.e. F() instead X.F(), is its own
         "this". Thus, in this unusual case, m is its own this in that
         context. */
// ^^^ outputs 'the class'

Literal Objects and Arrays

Like JavaScript, s2 supports creating arrays and plain objects via an "inlined" form:

var a1 = []; // empty array
var a2 = [1, 2, 3];
var o1 = {}; // empty object
var o2 = {
  a: 1, b: 'hi',
  c: 52.3
};
var o3 = {x:1, a1, a2}; // ⇒ {a1: a1, a2: a2, x:1} (JS-like, [as of 20171201](/cwal/info/c2ef5a9c88de652a))
var key1 = "hi", key2 = ", world";
var o4 = {[key1+key2]: 2}; // ⇒ [expression-as-a-key (EaaK)] support [as of 20191117](/cwal/info/37b82ddc38241caa)
assert 2 === o4."hi, world"; // or o4["hi, world"] or o4.("hi, world")

Anywhere where a value is expected, the [] and {} constructs are interpreted as an array resp. object literal, with one exception: [...] is treated as an expression block if (and only if) it appears as an object key in an object literal. All block constructs (including scopes) which use {} as bodies are prefixed with a keyword, so there is never a semantic ambiguity.

New object and array instances may optionally be created via their respective prototype's constructor, but doing so is much less efficient than simply using a literal:

const Object = {}.prototype;
var o = new Object(); // (no arguments allowed) ⇒ {}
const Array = [].prototype;
var a = new Array(1,2,3); // ⇒ [1, 2, 3]

These constructors are provided only for completeness - they are not used in practice, nor is their use recommended.

The body of an object or array literal is evaluated in a new scope, meaning, for example, that one may use local variables inside the body of an object:

{
  dirs: typeinfo(isstring var tmp = s2.getenv('SEARCH_PATH'))
    ? tmp.split(':') : ['/foo', '/bar'],
  extensions: typeinfo(isstring tmp = s2.getenv('SEARCH_EXT'))
    ? tmp.split(':') : ['.txt', '.html']
}

Notice how the second property can access the var declared in the definition of the first property. The tmp variable will go out of scope at the end of the object's body. typeinfo() is a function-like keyword, not a function, and does not start a new scope in its argument list like a function call does, so the var declaration is made in the same scope as the typeinfo() call (which is a scope created for parsing the object literal which contains the properties being defined). Object and array literals push an implicit scope while parsing the body so that temporaries created while assigning properties, as well as vars (like the one shown above), do not become part of the scope on whose behalf the object/array is being created.

Footnotes


  1. ^ But making deep copies via intermediate expressions is often possible. e.g. (var a=300, b = a*2/2) ends up with two unique copies of integers with the value 300.
  2. ^ In 30 years(!) of programming, i've never once needed scientific notation numbers.
  3. ^ We should arguably eval empty buffers as false, seeing as they are basically used like mutable strings.
  4. ^ 1) cwal only supports a single shared prototype for non-containers because doing otherwise would cost more memory for all values just to cover an as-yet-unneeded use case. 2) Sanity's sake.
  5. ^ Minor internal exception: for C-level prototypes we have to place the instances somewhere with an "indefinite" lifetime, to avoid that they get GC'd before they are used or when the last "subclass" instance goes out of scope. Yes, prototypes are subject to normal lifetime rules.