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.)
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?
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
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.
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
orb
is within the range of about +/-2,000,000, or: - ...both
a
andb
are within about +/-90,000,000, or: - ...you don't care about this overflow/underflow edge-case.
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.
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"]