Skip to content

Commit

Permalink
Support boxed primitives
Browse files Browse the repository at this point in the history
  • Loading branch information
overlookmotel committed Sep 30, 2023
1 parent 60ff5c1 commit 6ca5652
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 75 deletions.
223 changes: 165 additions & 58 deletions lib/serialize/boxed.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,82 +6,189 @@
'use strict';

// Modules
const t = require('@babel/types');
const t = require('@babel/types'),
upperFirst = require('lodash/upperFirst'),
{isNumber} = require('is-it-type');

// Imports
const {createDependency} = require('./records.js'),
{isNumberKey} = require('./utils.js');
const {
BOXED_STRING_TYPE, BOXED_BOOLEAN_TYPE, BOXED_NUMBER_TYPE, BOXED_BIGINT_TYPE, BOXED_SYMBOL_TYPE,
registerSerializer
} = require('./types.js');

// Exports

const stringToString = String.prototype.toString,
booleanValueOf = Boolean.prototype.valueOf,
numberValueOf = Number.prototype.valueOf,
bigIntValueOf = BigInt.prototype.valueOf,
symbolToPrimitive = Symbol.prototype[Symbol.toPrimitive];

module.exports = {
serializeBoxedString(str, record) {
const StringRecord = this.serializeValue(String),
len = str.length;
const node = t.newExpression(
StringRecord.varNode,
len > 0 ? [t.stringLiteral(stringToString.call(str))] : []
);
createDependency(record, StringRecord, node, 'callee');

return this.wrapWithProperties(
str, record, node, String.prototype, undefined,
(key) => {
if (key === 'length') return true;
// All numerical keys can be skipped, even ones above max safe integer key as boxed strings
// only allow writing to numerical keys within bounds of string length
if (!isNumberKey(key)) return false;
return key * 1 < len;
}
);
/**
* Trace boxed String.
* @param {Object} str - Boxed String
* @param {Object} record - Record
* @returns {number} - Type ID
*/
traceBoxedString(str, record) {
const val = stringToString.call(str),
len = val.length;
// Skip `length` property and integer-keyed properties representing characters of the string.
// These properties are all non-writable and non-configurable, so cannot be changed.
this.traceProperties(str, record, key => key === 'length' || (isNumber(key) && key < len));
record.extra = {valRecord: this.traceValue(val, null, null), len};
record.name = `boxed${upperFirst(val)}`;
return BOXED_STRING_TYPE;
},

serializeBoxedBoolean(bool, record) {
const BooleanRecord = this.serializeValue(Boolean);
const node = t.newExpression(
BooleanRecord.varNode,
booleanValueOf.call(bool) ? [t.numericLiteral(1)] : []
);
createDependency(record, BooleanRecord, node, 'callee');
return this.wrapWithProperties(bool, record, node, Boolean.prototype);
/**
* Trace boxed Boolean.
* @param {Object} bool - Boxed Boolean
* @param {Object} record - Record
* @returns {number} - Type ID
*/
traceBoxedBoolean(bool, record) {
const val = booleanValueOf.call(bool);
this.traceProperties(bool, record, null);
record.extra = {val};
record.name = `boxed${upperFirst(`${val}`)}`;
return BOXED_BOOLEAN_TYPE;
},

serializeBoxedNumber(num, record) {
const NumberRecord = this.serializeValue(Number),
unwrappedNum = numberValueOf.call(num),
numRecord = this.serializeValue(unwrappedNum);
const node = t.newExpression(
NumberRecord.varNode,
!Object.is(unwrappedNum, 0) ? [numRecord.varNode] : []
);
createDependency(record, NumberRecord, node, 'callee');
if (numRecord.node) createDependency(record, numRecord, node.arguments, 0);
return this.wrapWithProperties(num, record, node, Number.prototype);
/**
* Trace boxed Number.
* @param {Object} num - Boxed Number
* @param {Object} record - Record
* @returns {number} - Type ID
*/
traceBoxedNumber(num, record) {
const val = numberValueOf.call(num);
this.traceProperties(num, record, null);
record.extra = {val};
record.name = `boxed${val < 0 || Object.is(val, -0) ? 'Minus' : ''}${val}`;
return BOXED_NUMBER_TYPE;
},

serializeBoxedBigInt(bigInt, record) {
const ObjectRecord = this.serializeValue(Object);
const node = t.callExpression(ObjectRecord.varNode, [this.serializeValue(bigInt)]);
createDependency(record, ObjectRecord, node, 'callee');
return this.wrapWithProperties(bigInt, record, node, BigInt.prototype);
/**
* Trace boxed BigInt.
* @param {Object} bigInt - Boxed BigInt
* @param {Object} record - Record
* @returns {number} - Type ID
*/
traceBoxedBigInt(bigInt, record) {
const val = bigIntValueOf.call(bigInt),
valRecord = this.traceValue(val, null, null);
this.traceProperties(bigInt, record, null);
record.extra = {valRecord};
record.name = `boxed${upperFirst(valRecord.name)}`;
return BOXED_BIGINT_TYPE;
},

serializeBoxedSymbol(symbol, record) {
const ObjectRecord = this.serializeValue(Object),
unwrappedSymbol = symbolToPrimitive.call(symbol),
symbolRecord = this.serializeValue(unwrappedSymbol, `${record.varNode.name}Unboxed`);

const {description} = unwrappedSymbol;
if (description) record.varNode.name = `${description}Boxed`;

const node = t.callExpression(ObjectRecord.varNode, [symbolRecord.varNode]);
createDependency(record, ObjectRecord, node, 'callee');
createDependency(record, symbolRecord, node.arguments, 0);
return this.wrapWithProperties(symbol, record, node, Symbol.prototype);
/**
* Trace boxed Symbol.
* @param {Object} symbol - Boxed Symbol
* @param {Object} record - Record
* @returns {number} - Type ID
*/
traceBoxedSymbol(symbol, record) {
const val = symbolToPrimitive.call(symbol),
valRecord = this.traceDependency(val, `${record.name}Unboxed`, '<unboxed>', record);
this.traceProperties(symbol, record, null);
record.extra = {valRecord};
record.name = `boxed${upperFirst(valRecord.name)}`;
return BOXED_SYMBOL_TYPE;
}
};

/**
* Serialize boxed String.
* @this {Object} Serializer
* @param {Object} record - Record
* @param {Object} record.extra - Extra props object
* @param {Object} record.extra.valRecord - Record for boxed value
* @param {number} record.extra.len - String length
* @returns {Object} - AST node
*/
function serializeBoxedString(record) {
// `new String('xyz')` or `new String` (for empty string)
const node = t.newExpression(
this.traceAndSerializeGlobal(String),
record.extra.len > 0 ? [this.serializeValue(record.extra.valRecord)] : []
);
return this.wrapWithProperties(node, record, this.stringPrototypeRecord, null);
}
registerSerializer(BOXED_STRING_TYPE, serializeBoxedString);

/**
* Serialize boxed Boolean.
* @this {Object} Serializer
* @param {Object} record - Record
* @param {Object} record.extra - Extra props object
* @param {boolean} record.extra.val - `true` or `false`
* @returns {Object} - AST node
*/
function serializeBoxedBoolean(record) {
// `new Boolean` (for false) or `new Boolean(1)` (for true)
const node = t.newExpression(
this.traceAndSerializeGlobal(Boolean),
record.extra.val ? [this.traceAndSerializeGlobal(1)] : []
);
return this.wrapWithProperties(node, record, this.booleanPrototypeRecord, null);
}
registerSerializer(BOXED_BOOLEAN_TYPE, serializeBoxedBoolean);

/**
* Serialize boxed Number.
* @this {Object} Serializer
* @param {Object} record - Record
* @param {Object} record.extra - Extra props object
* @param {number} record.extra.val - Value
* @returns {Object} - AST node
*/
function serializeBoxedNumber(record) {
// `new Number(123)` or `new Number` (for 0)
const {val} = record.extra,
node = t.newExpression(
this.traceAndSerializeGlobal(Number),
Object.is(val, 0) ? [] : [this.traceAndSerializeGlobal(val)]
);
return this.wrapWithProperties(node, record, this.numberPrototypeRecord, null);
}
registerSerializer(BOXED_NUMBER_TYPE, serializeBoxedNumber);

/**
* Serialize boxed BigInt.
* @this {Object} Serializer
* @param {Object} record - Record
* @param {Object} record.extra - Extra props object
* @param {Object} record.extra.valRecord - Record for boxed value
* @returns {Object} - AST node
*/
function serializeBoxedBigInt(record) {
// `Object(123n)`
const node = t.callExpression(
this.traceAndSerializeGlobal(Object),
[this.serializeValue(record.extra.valRecord)]
);
return this.wrapWithProperties(node, record, this.bigIntPrototypeRecord, null);
}
registerSerializer(BOXED_BIGINT_TYPE, serializeBoxedBigInt);

/**
* Serialize boxed Symbol.
* @this {Object} Serializer
* @param {Object} record - Record
* @param {Object} record.extra - Extra props object
* @param {Object} record.extra.valRecord - Record for boxed value
* @returns {Object} - AST node
*/
function serializeBoxedSymbol(record) {
// `Object(Symbol('x'))`
const node = t.callExpression(
this.traceAndSerializeGlobal(Object),
[this.serializeValue(record.extra.valRecord)]
);
return this.wrapWithProperties(node, record, this.symbolPrototypeRecord, null);
}
registerSerializer(BOXED_SYMBOL_TYPE, serializeBoxedSymbol);
15 changes: 10 additions & 5 deletions lib/serialize/trace.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ module.exports = {
this.weakMapPrototypeRecord = this.traceValue(WeakMap.prototype, null, null);
this.urlPrototypeRecord = this.traceValue(URL.prototype, null, null);
this.urlSearchParamsPrototypeRecord = this.traceValue(URLSearchParams.prototype, null, null);
this.stringPrototypeRecord = this.traceValue(String.prototype, null, null);
this.booleanPrototypeRecord = this.traceValue(Boolean.prototype, null, null);
this.numberPrototypeRecord = this.traceValue(Number.prototype, null, null);
this.bigIntPrototypeRecord = this.traceValue(BigInt.prototype, null, null);
this.symbolPrototypeRecord = this.traceValue(Symbol.prototype, null, null);

this.minusZeroRecord = null;

Expand Down Expand Up @@ -157,11 +162,11 @@ module.exports = {
// if (typedArrayRegex.test(objType)) return this.traceBuffer(val, objType, record);
// if (objType === 'ArrayBuffer') return this.traceArrayBuffer(val, record);
// if (objType === 'SharedArrayBuffer') return this.traceSharedArrayBuffer(val, record);
// if (objType === 'String') return this.traceBoxedString(val, record);
// if (objType === 'Boolean') return this.traceBoxedBoolean(val, record);
// if (objType === 'Number') return this.traceBoxedNumber(val, record);
// if (objType === 'BigInt') return this.traceBoxedBigInt(val, record);
// if (objType === 'Symbol') return this.traceBoxedSymbol(val, record);
if (objType === 'String') return this.traceBoxedString(val, record);
if (objType === 'Boolean') return this.traceBoxedBoolean(val, record);
if (objType === 'Number') return this.traceBoxedNumber(val, record);
if (objType === 'BigInt') return this.traceBoxedBigInt(val, record);
if (objType === 'Symbol') return this.traceBoxedSymbol(val, record);
// if (objType === 'Arguments') return this.traceArguments(val, record);
throw new Error(`Cannot serialize ${objType}s`);
},
Expand Down
10 changes: 10 additions & 0 deletions lib/serialize/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ const NO_TYPE = 0,
WEAK_MAP_TYPE = OBJECT_TYPE | 7,
URL_TYPE = OBJECT_TYPE | 8,
URL_SEARCH_PARAMS_TYPE = OBJECT_TYPE | 9,
BOXED_STRING_TYPE = OBJECT_TYPE | 10,
BOXED_BOOLEAN_TYPE = OBJECT_TYPE | 11,
BOXED_NUMBER_TYPE = OBJECT_TYPE | 12,
BOXED_BIGINT_TYPE = OBJECT_TYPE | 13,
BOXED_SYMBOL_TYPE = OBJECT_TYPE | 14,
FUNCTION_TYPE = 64,
METHOD_TYPE = FUNCTION_TYPE | 1,
GLOBAL_TYPE = 128,
Expand Down Expand Up @@ -70,6 +75,11 @@ module.exports = {
WEAK_MAP_TYPE,
URL_TYPE,
URL_SEARCH_PARAMS_TYPE,
BOXED_STRING_TYPE,
BOXED_BOOLEAN_TYPE,
BOXED_NUMBER_TYPE,
BOXED_BIGINT_TYPE,
BOXED_SYMBOL_TYPE,
FUNCTION_TYPE,
METHOD_TYPE,
GLOBAL_TYPE,
Expand Down
Loading

0 comments on commit 6ca5652

Please sign in to comment.