mattress.js - radishengine/drowsy GitHub Wiki

Require Path: 'mattress'

mattress.js is intended to help out with hosting a custom runtime, particularly when interpreting code that expects certain semantics that are not trivial to get right with the standard JavaScript primitives.

(Please keep in mind that the main goal of this module is good logic/semantics, not high performance! Do not be surprised if code that uses these features is significantly slower than equivalent code that just uses plain JavaScript variables and operators. Bottom line: It is recommended to avoid using this module unless you don't have much of a choice.)

Signed 32-bit Integer Hashcodes

mattress.hashString(str) returns a hashcode for the given string.

mattress.hashValue(value) returns a hashcode for the given value. All kinds of value should be supported[1]. A basic value type like number or boolean will be converted to a string and then hashed using .hashString(). An object can specify its own hash by defining the property obj[mattress.HASH_PROP], otherwise one will be automatically generated and retained in a WeakMap. The hashcode for null and undefined is 0.

[1] Should be. But who knows what lurks in some corner of the JS ecosystem?

Hashing Multiple Values

mattress.hashList(values [, inAnyOrder] [, initialHash]) returns a hashcode for the given list of values, which should be an array (or array-like object).

If inAnyOrder is true, then the order that values come in the list does not affect the hashcode. For example, if a and b are different values:

mattress.hashList([a, b], true) === mattress.hashList([b, a], true)

...is guaranteed to be true, while

mattress.hashList([a, b]) === mattress.hashList([b, a])

...is extremely likely to be false.

inAnyOrder mode also effectively ignores any null or undefined elements in the list, and it has the quirk that if the same value appears more than once it will either be like the value only appeared once (if it appears an odd number of times), or never appeared at all (if it appears an even number of times).

The initialHash parameter allows the hash to be built up cumulatively, i.e.:

var oneShot = mattress.hashList([1, 2, 3, 4, 5, 6]);
var cumulative = mattress.hashList([1, 2, 3]);
cumulative = mattress.hashList([4, 5, 6], cumulative);
// oneShot === cumulative

HashTable

A simple hashtable implementation based on these hashcodes[2] is available:

var hashTable = new mattress.HashTable();
hashTable.set(hashTable, "that's me!");

It supports methods .set(key, value), .get(key [,defaultValue]), .contains(key), .remove(key), and .keys(). You can also override the way the hashtable tests for equality between two (non-string) keys by replacing its .testEquality(a, b) method, which is by default Object.is().

[2] Remember that this is only intended to help with hosting subsystems that really rely on hashcode-based logic. If you're looking for a true associative array where the keys can be any type (and implementation details are unimportant) take a look at Map, which has at least basic support in the major browsers.

"C-Style" 32-bit Integer Multiplication

For most 32-bit integer arithmetic operations, you can use the conventions (...) | 0 (for signed) and (...) >>> 0 (unsigned) to ensure that the result is the same as it would be anywhere. For example, a signed int32 division like this:

var a_div_b = (a / b) | 0;

...has universally consistent results for any given (valid int32) values for a and b. This is also true for addition and subtraction.

It's not always true for multiplication, though. For certain large (or negatively large) values, where the result massively overflows (or underflows), the result may be outside of the precision range for integers in JavaScript, and ultimately become (slightly) different from what you could get if you did the same integer multiplication in C.

mattress.i32_mul(a, b) and mattress.u32_mul(a, b) provide "pedantic" multiplication for signed and unsigned int32s, respectively:

var a_mul_b = mattress.i32_mul(a, b);

Note that you do not need to use this, and can use (a * b) | 0 as normal, if you can be sure that:

  • ...either a or b is within the range of about +/-2,000,000, or:
  • ...both a and b are within about +/-90,000,000, or:
  • ...you don't care about this overflow/underflow edge-case.

Boxed Value Objects

You can create a boxed value object by using .asBoxed<Type> property of a number or boolean literal:

var intX = (0).asBoxedInt32;
var booleanY = (true).asBoxedBoolean;

The basic types involved are: Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64, Float32, Float64, Boolean

For 64-bit types, use a string-enclosed number literal (if using a -/+ sign symbol, remember to keep it inside the string!). Preferably make it hexadecimal, using the 0x prefix, but decimal (and 0b binary) will work too:

var ulongZ = ('0x8888888800000000').asBoxedUint64;

Once you have a boxed value object, you can use the .set() method to replace its value:

intX.set(10);
booleanY.set(false);
longY.set('0x4242424242424242');

The .set() method will return the new value in normalized form. Note that this form may not be exactly the same as the value you passed in to .set(), i.e. if the value needs to be truncated to fit into the right number of bits, or it got converted from a signed to an unsigned integer. For the Int8, Int16, Int32, Uint8, Uint16, Uint32, Float32, Float64 and Boolean types, the normalized form will be a normal JavaScript Number or Boolean value. For Int64 and Uint64, the normalized form will be a JavaScript Number if the value can be converted without losing precision, otherwise it will be the boxed value object.

You can also mutate a boxed value by using a number of variants of the form .set_<op>():

intX.set_add(20); // add 20 to the current value
intX.set_lshift(3); // binary-shift the value, 3 bits to the left

Here is the full list of available mutate operations:

  • set_add(v): add
  • set_sub(v): subtract
  • set_mul(v): multiply
  • set_div(v): divide
  • set_mod(v): modulus
  • set_bor(v); bitwise OR
  • set_band(v): bitwise AND
  • set_bxor(v): bitwise XOR
  • set_lshift(count): left bit-shift
  • set_rshift(count): right bit-shift, logical
  • set_arshift(count): right bit-shift, arithmetic

Like .set(), these variants return the new value in normalized form.

Value Conversion and Casting

You can get the string form of any boxed number value by using .toString([radix]) in the usual way. The convention (''+boxedValue) will also work.

The normalized form of a boxed value can be accessed directly by getting its .normalized property. As mentioned earlier, this can still be the boxed value itself, in the case of BoxedInt64 and BoxedUint64.

You can cast a boxed value to the normalized form of any of the other types, by using one of the .as<Type> properties (not to be confused with .asBoxed<Type>):

var boxedInt = (0x12345).asBoxedInt32;
var unboxedByte = boxedInt.asUint8; // truncated to 0x45

This is equivalent to:

var boxedInt = (0x12345).asBoxedInt32;
var unboxedInt = boxedInt.normalized;
var boxedByte = (unboxedInt).asBoxedUint8;
var unboxedByte = boxedByte.normalized;

If you just want to get a normal JavaScript number value out of a boxed type, even if it could potentially lose some precision for 64-bit integers, use the .asFloat64 property.

If a boxed value is JSON-encoded with JSON.stringify(), it will be written out as either the contained value as a number or boolean, or a string containing the value as a hex literal (with the 0x prefix):

var intA = (0).asBoxedInt32;
var boolB = (true).asBoxedBoolean;
var longC = ('-0x1234567654321').asBoxedInt64;

var json = JSON.stringify([ intA, boolB, longC ]);
// --> [0, true, "-0x123456787654321"]
⚠️ **GitHub.com Fallback** ⚠️