Atum Primitive Language Values

Here I cover the primitive language values and operations used in Atum. Although not too exciting, there are a few important points about property descriptors and how Atum handles objects.

Atum minimizes the interface between the host and hosted languages. Implementing ECMAScript in ECMAScript, this design choice results in some unnecessary code, but it also allows the the behavior of the hosted language to be easily modified.

Basic Language Values

Section 8 of ECMAScript 5.1 defines the basic ECMAScript language values types.

Types

ECMAScript language values may be one of six types: Undefined, Null, Boolean, String, Number, or Object.

Atum defines an enum of for these six types in atum::value::type. All primitive value objects have a type property set to one of these values

define("atum/value/type", ['exports'], function(exports) {

exports.UNDEFINED = 'undefined';
exports.NULL = 'null';
exports.BOOLEAN = 'boolean';
exports.STRING = 'string';
exports.NUMBER = 'number';
exports.OBJECT = 'object';

});

Base Value

All hosted language values extend Value. Atum uses Bes heavily to automate record definition.

define("atum/value/value", [
    'bes/record'],
function(
    exports
    record) {

exports.Value = record.declare(null, []);
});

Undefined

The undefined type is used for uninitialized values and has a single instance: UNDEFINED.

define("atum/value/undef", [
    'bes/record',
    'atum/value/type',
    'atum/value/value'],
function(
    exports,
    record,
    type,
    value) {

var Undefined = record.extend(value.Value, []);
Undefined.prototype.type = type.UNDEFINED;

exports.UNDEFINED = Undefined.create();
});

Null

The null type has a single instance: NULL.

define("atum/value/nil", [
    'bes/record',
    'atum/value/type',
    'atum/value/value'],
function(
    exports,
    record,
    type,
    value) {

var Null = record.extend(value.Value, []);
Null.prototype.type = type.NULL;

exports.NULL = Null.create();
});

Boolean

The boolean type has two values: TRUE and FALSE. create converts a host language boolean to a hosted boolean, while its inverse isTrue converts a hosted boolean to a host boolean.

Boolean values support a single operation, logicalNot.

define("atum/value/boolean", [
    'bes/record',
    'atum/value/type',
    'atum/value/value'],
function(
    exports,
    record,
    type,
    value) {

var Boolean = record.extend(value.Value, [
    'value']);    
Boolean.prototype.type = type.BOOLEAN;

/// Constants
exports.TRUE = Boolean.create(true);
exports.FALSE = Boolean.create(false);

/// Host Operations
exports.create = function(x) {
    reutrn (x ? TRUE : FALSE);
};

exports.isTrue = function(x) {
    return (value.isBoolean(x) && x.value);
};

/// Operations
exports.logicalNot = function(a) { 
    return create(!a.value);
};

});

Number

ECMAScript numbers are represented using the [IEEE 754][ieee754] floating point arithmetic standard. Unlike Null, Undefined, and Boolean, Atum creates a distinct value instance for every number value. Number instances store a host Javascript number and perform calculations using this number.

var HostNumber = Number;

define("atum/value/number", [
    'bes/record',
    'atum/value/type',
    'atum/value/value'],
function(
    exports,
    record,
    type,
    value) {

var Number = record.extend(value.Value, [
    'value']);
Number.prototype.type = type.NUMBER;

exports.Number = Number;

A few useful numeric constants and special values are also defined from the host language’s number.

exports.ZERO = Number.create(0);
exports.MAX_VALUE = Number.create(HostNumber.MAX_VALUE);
exports.MIN_VALUE = Number.create(HostNumber.MIN_VALUE);
exports.NaN = Number.create(HostNumber.NaN);
exports.NEGATIVE_INFINITY = Number.create(HostNumber.NEGATIVE_INFINITY);
exports.POSITIVE_INFINITY = Number.create(HostNumber.POSITIVE_INFINITY);

And a set of hosted number operation functions are defined:

/// Unary Operations
exports.negate = function(a) { return Number.create(-a.value); };
exports.bitwiseNot = function(a) { return Number.create(~a.value); };

/// Binary Numeric Operations
exports.add = function(l, r) { return Number.create(l.value + r.value); };
exports.subtract = function(l, r) { return Number.create(l.value - r.value); };
exports.multiply = function(l, r) { return Number.create(l.value * r.value); };
exports.divide = function(l, r) { return Number.create(l.value / r.value); };
exports.remainder = function(l, r) { return Number.create(l.value % r.value); };

/// Binary Bitwise Operations
exports.leftShift = function(l, r) { return Number.create(l.value << r.value); };
exports.signedRightShift = function(l, r) { return Number.create(l.value >> r.value); };
exports.unsignedRightShift = function(l, r) { return Number.create(l.value >>> r.value); };
exports.bitwiseAnd = function(l, r) { return Number.create(l.value & r.value); };
exports.bitwiseOr = function(l, r) { return Number.create(l.value | r.value); };
exports.bitwiseXor = function(l, r) { return Number.create(l.value ^ r.value); };

/// Binary Relational Operations
exports.lt = function(l, r) { return boolean.create(l.value < r.value); };
exports.lte = function(l, r) { return boolean.create(l.value <= r.value); };
exports.gt = function(l, r) { return boolean.create(l.value > r.value); };
exports.gte = function(l, r) { return boolean.create(l.value >= r.value); };
});

String

String values are also stored in distinct instances based on the host string:

define(['exports',
        'bes/record',
        'atum/value/number',
        'atum/value/type',
        'atum/value/value'],
function(exports,
        record,
        number,
        type,
        value) {

var String = record.extend(value.Value, [
    'value']);

String.prototype.type = type.STRING;

exports.String = String;

exports.EMPTY = String.create('');

String values are immutable so all string operations return copies of the string:

/// Joins two strings into a new string.
exports.concat = function(l, r) {
    return String.create(l.value + r.value);
};

/// Get the length of a string.
exports.length = function(s) {
    return number.Number.create(s.value.length);
};

/// Get the character at `index` in string `s`
exports.charAt = function(s, index) {
    return String.create(s.value.charAt(index));
};

/// Get the character code of the character at `index` in string `s`
exports.charCodeAt = function(s, index) {
    return number.Number.create(s.value.charCodeAt(index));
};

});

Objects

Objects are a special case in Atum. Internally, ECMAScript objects consist of:

Atum meta object are simply subclasses of Object that implement the required interface. The properties are stored in a map of host strings to property descriptors.

Object Value

The base Atum Object record contains fairly little logic.

var getOwnPropertyNames = Object.getOwnPropertyNames; 

define("atum/value/Object", [
    'exports',
    'bes/record',
    'atum/value/type',
    'atum/value/value'],
function(
    exports,
    record,
    type,
    value) {

var Object = record.extend(value.Value, [
    'proto',
    'properties',
    'extensible']);
Object.prototype.type = type.OBJECT;

exports.Object = Object;

In order to keep all reference related computation logic out of the primitive value implementation, atum::value::Object only defines operations for its own properties and does not touch the prototype. A meta object, which will be detailed in a later post, provides the complete ECMAScript Object implementation.

/// Get a list of names for all properties this object has.
Object.prototype.getOwnPropertyNames = function(propertyName) {
    return getOwnPropertyNames(this.properties);
};

/// Does this object have own property `name`?
Object.prototype.hasOwnProperty = function(name) {
    return (this.getOwnPropertyNames().indexOf(name) >= 0);
};

/// Returns the property descriptor for property 'name'.
Object.prototype.getOwnProperty = function(propertyName) {
    return (this.hasOwnProperty(propertyName) ?
        this.properties[propertyName] :
        null);
};

/// Get a list of all enumerable property names for this object.
Object.prototype.getEnumerableProperties = function() {
    var self = this;
    return this.getOwnPropertyNames().filter(function(x) {
        return self.getOwnProperty(x).enumerable;
    });
};
});

Property Descriptors

ECMAScript objects store their properties using property descriptors (ECMAScript5.1 8.10). A property descriptor may hold a property’s value, but it also manages property metadata and can can define accessors that decouple a property from a memory value .

define("atum/value/property", [
    'exports',
    'bes/record'],
function(
    exports
    record) {

var Property = record.declare(null, [
    'enumerable',   // bool, is this property listed during enumeration
    'configurable', // bool, can the property's descriptor be changed.
    'value',        // value, hosted value stored
    'writable',     // bool, can `value` be changed
    'get',          // value, hosted function to get property value
    'set']);        // value, hosted function to set property value
    
exports.Property = Property;

There are two general categories of property descriptors: data descriptors directly hold a value, while accessor descriptors uses getter and setter functions to access a value. Both types have enumerable and configurable flags.

var createValueProperty = function(value, enumerable, writable, configurable) {
    return Property.create(
        !!enumerable,
        !!configurable,
        value,
        !!writable,
        undefined,
        undefined);
};
var createAccessorProperty = function(getter, setter, enumerable, configurable) {
    return Property.create(
        !!enumerable,
        !!configurable,
        undefined,
        undefined,
        getter,
        setter);
};

A property descriptor instance can be a data descriptor or accessor descriptor, or it can be both, or it can be neither. The effective type of a property descriptor is determined by the flags and values it has:

var hasProperty = function(x, name) {
    return x[name] !== undefined;
};

var isAccessorDescriptor = function(desc) {
    return desc && (hasProperty(desc, 'get') || hasProperty(desc, 'set'));
};

var isDataDescriptor = function(desc) {
    return desc && (hasProperty(desc, 'value') || hasProperty(desc, 'writable'));
};

var isGenericDescriptor = function(desc) {
    return desc && !isAccessorDescriptor(desc) && !isDataDescriptor(desc);
};

exports.isAccessorDescriptor = isAccessorDescriptor;
exports.isDataDescriptor = isDataDescriptor;
exports.isGenericDescriptor = isGenericDescriptor;
};

Value Type Conversion

Section 9 defines the ECMAScript type conversions. atum::value::type_conversion only handles primitive values and not objects, since objects may need to call their member functions as part of their type conversion.

define("atum/value/type_conversion", [
    'atum/value/boolean',
    'atum/value/number',
    'atum/value/object',
    'atum/value/string',
    'atum/value/type'],
function(
    boolean,
    number,
    object,
    string,
    type) {

toBoolean converts to a boolean value. The number to boolean conversion uses C’s logic and is true for all values except 0 and NaN. The empty string is false but all other strings are true.

exports.toBoolean = function(input) {
    switch (input.type) {
    case type.BOOLEAN:
        return input;
    case type.NUMBER:
        return boolean.create(input.value !== 0 && input.value !== number.NaN.value);
    case type.STRING:
        return boolean.create(input.value.length > 0);
    case type.UNDEFINED:
    case type.NULL:
    default:
        return boolean.FALSE;
    }
};

toString converts to a string:

exports.toString = function(input) {
    switch (input.type) {
    case type.UNDEFINED:   return string.String.create("undefined");
    case type.NULL:        return string.String.create("null");
    case type.BOOLEAN:     return string.String.create(boolean.isTrue(input) ? 'true' : 'false');
    case type.NUMBER:      return string.String.create("" + input.value);
    case type.STRING:      return input;
    default:               return string.EMPTY;
    }
};

toNumber attempts a number conversation. The string to number conversion attempts to parse the string as a number, and will be NaN if the parsing fails.

exports.toNumber = function(input) {
    switch (input.type) {
    case type.NUMBER:      return input;
    case type.UNDEFINED:   return number.NaN;
    case type.BOOLEAN:     return number.Number.create(input.value ? 1 : 0);
    case type.STRING:      return number.Number.create(+input.value);
    case type.NULL:
    default:               return number.ZERO;
    }
};

Finally, ECMAScript defines a set of number precision operations that are used internally.

var sign = function(num) {
    return (num ?
        (num < 0 ? -1 : 1):
        0);
};

var toInteger = function(input) {
    var val = input.value;
    switch (val) {
    case number.NaN.value:
        return number.ZERO;
    case number.ZERO.value:
    case number.POSITIVE_INFINITY.value:
    case number.NEGATIVE_INFINITY.value:
        return input;
    default:
        return number.Number.create(sign(val) * Math.floor(Math.abs(val)));
    }
};

var toInt32 = function(num) {
    var val = num.value;
    switch (val) {
    case number.NaN.value:
    case number.ZERO.value:
    case number.POSITIVE_INFINITY.value:
    case number.NEGATIVE_INFINITY.value:
        return number.ZERO;
    default:
        var int32bit = (sign(val) * Math.floor(Math.abs(val))) % Math.pow(2, 32);
        return number.Number.create(int32bit >= Math.pow(2, 31) ?
            int32bit - Math.pow(2, 32) :
            int32bit);
    }
};

exports.toUint32 = function(num) {
    var val = num.value;
    switch (val) {
    case number.NaN.value:
    case number.ZERO.value:
    case number.POSITIVE_INFINITY.value:
    case number.NEGATIVE_INFINITY.value:
        return number.ZERO;
    default:
        return number.Number.create(
            (sign(val) * Math.floor(Math.abs(val))) % Math.pow(2, 32));
    }
};

});

Value Equality

The last important module is atum::value::compare for comparing primitive values.

define("atum/value/compare", [
    'exports',
    'atum/value/type',
    'atum/value/type_conversion'],
function(
    exports,
    type,
    type_conversion) {

One point that often trips up new Javascript programmers is the difference between ECMAScript’s strict equality === and standard equality == tests. Strict equality performs no type conversion, so the compared values must be of the same type.

var strictEqual = function(x, y) {
    if (x.type !== y.type)
        return false;
    
    switch (x.type) {
    case type.UNDEFINED:
    case type.NULL:
        return true;
    default:
        return (x.value === y.value);
    }
};

exports.strictEqual = strictEqual;

Regular equality will attempt type conversions during comparison:

var equal = function(x, y) {
    if (x.type === y.type)
        return strictEqual(x, y);
    
    if ((x.type === type.NULL && y.type === type.UNDEFINED) ||
      (x.type === type.UNDEFINED && y.type === type.NULL))
    {
        return true;
    }
    else if (x.type === type.NUMBER && y.type === type.STRING)
    {
        return equal(x, type_conversion.toNumber(y));
    }
    else if (x.type === type.STRING && y.type === type.NUMBER)
    {
        return equal(type_conversion.toNumber(x), y);
    }
    else if (x.type === type.BOOLEAN)
    {
        return equal(type_conversion.toNumber(x), y);
    }
    else if (y.type === type.BOOLEAN)
    {
        return equal(x, type_conversion.toNumber(y));
    }
    
    return false;
};
exports.equal = equal;

});