@as-pect/assembly
Version:
Write AssemblyScript tests at blazing fast speeds
700 lines (633 loc) • 20.5 kB
text/typescript
import { ArrayBufferView } from "arraybuffer";
import { Set } from "set";
import { assert } from "./assert";
import { ReflectedValueType } from "./ReflectedValueType";
import { OBJECT, OBJECT_OVERHEAD } from "rt/common";
function pairSeen(a1: usize, a2: usize, b1: usize, b2: usize): bool {
return bool(
(i32(a1 == b1) & i32(a2 == b2)) | (i32(a1 == b2) & i32(a2 == b1)),
);
}
// @ts-ignore: Decorators *are* valid here!
declare function attachStackTraceToReflectedValue(id: i32): void;
// @ts-ignore: linked function decorator
declare function createReflectedValue(
isNull: bool,
hasKeys: bool,
nullable: bool,
offset: i32,
pointer: usize,
signed: bool,
size: i32,
reflectedTypeValue: ReflectedValueType,
typeId: i32,
typeName: string,
value: usize,
hasValues: bool,
isManaged: bool,
): i32;
// @ts-ignore: linked function decorator
declare function createReflectedNumber(
signed: bool,
size: i32,
reflectedTypeValue: ReflectedValueType,
typeName: string,
value: f64,
): i32;
// @ts-ignore: linked function decorator
declare function createReflectedLong(
signed: bool,
size: i32,
reflectedTypeValue: ReflectedValueType,
typeName: string,
lowValue: i32,
highValue: i32,
): i32;
// @ts-ignore: external declaration
declare function __aspectPushReflectedObjectValue(
parentID: i32,
value: i32,
): void;
// @ts-ignore: external declaration
declare function __aspectPushReflectedObjectKey(
parentID: i32,
value: i32,
): void;
function sizeOf(ptr: usize): i32 {
return changetype<OBJECT>(ptr - OBJECT_OVERHEAD).rtSize;
}
// @ts-ignore: global decorator is allowed here
export class Reflect {
public static toReflectedValue<T>(
value: T,
seen: Map<usize, i32> = new Map<usize, i32>(),
): i32 {
// if T is a reference
if (isReference<T>()) {
// if the value is null, create a Null reflected value
if (changetype<usize>(value) == 0) {
return createReflectedValue(
true,
false,
isNullable<T>(),
0,
0,
false,
sizeof<T>(),
isFunction<T>()
? ReflectedValueType.Function
: ReflectedValueType.Class,
isManaged<T>() ? idof<T>() : 0,
isFunction<T>() ? "Function" : nameof<T>(),
0,
false,
isManaged<T>(),
);
}
// check the cache for anything that isn't a function
if (!isFunction<T>()) {
if (seen.has(changetype<usize>(value))) {
return seen.get(changetype<usize>(value));
}
}
// @ts-ignore: __aspectReflectAs() can potentially be implemented
if (isDefined(value.__aspectReflectAs())) {
// @ts-ignore: typesafe call to __aspectReflectAs()
let displayValue = value.__aspectReflectAs();
if (!isInteger(displayValue)) {
if (!isFloat(displayValue)) {
// @as-covers: ignore compile time check
if (!isManaged(displayValue)) {
ERROR(
"__aspectReflectAs() function should return a managed type or a number",
);
}
}
}
return Reflect.toReflectedValue(displayValue, seen);
}
if (value instanceof ArrayBuffer) {
let reflectedValue = createReflectedValue(
false,
false,
isNullable<T>(),
value.byteLength,
changetype<usize>(value),
false,
value.byteLength,
ReflectedValueType.ArrayBuffer,
idof<T>(),
nameof<T>(),
0,
true,
true,
);
seen.set(changetype<usize>(value), reflectedValue);
let length = value.byteLength;
for (let i = 0; i < length; i++) {
__aspectPushReflectedObjectValue(
reflectedValue,
Reflect.toReflectedValue(
load<u8>(changetype<usize>(value) + <usize>i),
seen,
),
);
}
return reflectedValue;
} else if (isFunction<T>()) {
let func = value;
let reflectedValue = createReflectedValue(
false,
false,
isNullable<T>(),
0,
// @ts-ignore has field
func.index,
false,
0,
ReflectedValueType.Function,
0,
"Function",
// @ts-ignore has field
func.index,
false,
isManaged<T>(),
);
return reflectedValue;
} else if (value instanceof Set) {
// create a Set reflected value
let reflectedObject = createReflectedValue(
false,
false, // sets don't have keys
isNullable<T>(),
0,
changetype<usize>(value),
false,
value.size,
ReflectedValueType.Set,
idof<T>(),
nameof<T>(),
0,
true, // sets have values
true,
);
// cache this value
seen.set(changetype<usize>(value), reflectedObject);
// loop over each item and push it to the Set
let values = value.values();
let length = values.length;
for (let i = 0; i < length; i++) {
let value = unchecked(values[i]);
let reflectedValueID = Reflect.toReflectedValue(value, seen);
__aspectPushReflectedObjectValue(reflectedObject, reflectedValueID);
}
return reflectedObject;
} else if (value instanceof Map) {
// create a Set reflected object
let reflectedValue = createReflectedValue(
false,
true, // maps have keys
isNullable<T>(),
0,
changetype<usize>(value),
false,
value.size,
ReflectedValueType.Map,
idof<T>(),
nameof<T>(),
0,
true, // maps have values
true,
);
// cache this value
seen.set(changetype<usize>(value), reflectedValue);
// loop over each key and push the key value pair to the reflected Map
let keys = value.keys();
let length = keys.length;
for (let i = 0; i < length; i++) {
let mapKey = unchecked(keys[i]);
let reflectedKeyID = Reflect.toReflectedValue(mapKey, seen);
__aspectPushReflectedObjectKey(reflectedValue, reflectedKeyID);
let mapValue = value.get(mapKey);
let reflectedValueID = Reflect.toReflectedValue(mapValue, seen);
__aspectPushReflectedObjectValue(reflectedValue, reflectedValueID);
}
return reflectedValue;
} else if (value instanceof ArrayBufferView) {
// @ts-ignore: has field
let length = value.length;
// create an arraylike reflected value
let reflectedValue = createReflectedValue(
false,
false, // arrays don't have keys
isNullable<T>(),
0,
changetype<usize>(value),
false,
length,
ReflectedValueType.TypedArray,
idof<T>(),
nameof<T>(),
0,
true, // maps have values
isManaged<T>(),
);
// cache this value
seen.set(changetype<usize>(value), reflectedValue);
// loop over each value and push it to the reflected value
for (let i = 0; i < length; i++) {
// @ts-ignore index signature is garunteed at this point
let arrayValue = unchecked(value[i]);
let reflectedArrayValueID = Reflect.toReflectedValue(
arrayValue,
seen,
);
__aspectPushReflectedObjectValue(
reflectedValue,
reflectedArrayValueID,
);
}
return reflectedValue;
} else if (value instanceof String) {
let reflectedStringID = createReflectedValue(
false,
false,
isNullable<T>(),
0,
changetype<usize>(value),
false,
value.length,
ReflectedValueType.String,
idof<T>(),
nameof<T>(),
changetype<usize>(value),
false,
true,
);
seen.set(changetype<usize>(value), reflectedStringID);
return reflectedStringID;
} else if (isArrayLike<T>()) {
// @ts-ignore: arraylike has length property
let length = <i32>value.length;
// create an arraylike reflected value
let reflectedValue = createReflectedValue(
false,
false, // arrays don't have keys
isNullable<T>(),
0,
changetype<usize>(value),
false,
length,
ReflectedValueType.Array,
isManaged<T>() ? idof<T>() : 0,
nameof<T>(),
0,
true, // maps have values
isManaged<T>(),
);
// cache this value
seen.set(changetype<usize>(value), reflectedValue);
// loop over each array item and push it to the reflected value
for (let i = 0; i < length; i++) {
// @ts-ignore: index signature in arraylike
let arrayValue = unchecked(value[i]);
let reflectedArrayValueID = Reflect.toReflectedValue(
arrayValue,
seen,
);
__aspectPushReflectedObjectValue(
reflectedValue,
reflectedArrayValueID,
);
}
return reflectedValue;
} else {
// generic class
let reflectedObjectID = createReflectedValue(
false,
true, // classes have keys
isNullable<T>(),
offsetof<T>(),
changetype<usize>(value),
false,
sizeOf(changetype<usize>(value)),
ReflectedValueType.Class,
isManaged<T>() ? idof<T>() : 0,
nameof<T>(),
0,
true, // classes have values
isManaged<T>(),
);
// cache this object
seen.set(changetype<usize>(value), reflectedObjectID);
// @ts-ignore: __aspectAddReflectedValueKeyValuePairs is auto-generated by the transform
value.__aspectAddReflectedValueKeyValuePairs(
reflectedObjectID,
seen,
[] as StaticArray<i64>,
);
return reflectedObjectID;
}
} else if (alignof<T>() === 3) {
if (isInteger<T>()) {
// u64, i64, isize, or usize (when targeting 64 bit WebAssembly)
// @ts-ignore: value is a number
let reflectedValue = createReflectedLong(
isSigned<T>(),
sizeof<T>(),
ReflectedValueType.Integer,
nameof<T>(),
// @ts-ignore: value is a 64 bit number
<i32>(value & 0xffffffff),
// @ts-ignore: value is a 64 bit number
<i32>(value >>> 32),
);
return reflectedValue;
}
// float64
let reflectedValue = createReflectedNumber(
isSigned<T>(),
sizeof<T>(),
ReflectedValueType.Float,
nameof<T>(),
// @ts-ignore: type is bool, i32, f64, or f32
<f64>value,
);
return reflectedValue;
} else {
// boolean, i32, u32, f32, isize, usize (when targeting 32 bit WebAssembly), or numbers with less bits
let reflectedValue = createReflectedNumber(
isSigned<T>(),
sizeof<T>(),
isBoolean<T>()
? ReflectedValueType.Boolean
: isInteger<T>()
? ReflectedValueType.Integer
: ReflectedValueType.Float,
nameof<T>(),
// @ts-ignore: type is bool, i32, f64, or f32
<f64>value,
);
return reflectedValue;
}
return 0;
}
public static equals<T>(
left: T,
right: T,
stack: usize[] = [],
cache: usize[] = [],
): i32 {
// use `==` operator to work with operator overloads and strings
if (isReference(left)) {
if (changetype<usize>(left) == changetype<usize>(right)) return Reflect.SUCCESSFUL_MATCH;
if (isNullable(left)) {
if(
i32(changetype<usize>(left) == 0) ^ i32(changetype<usize>(right) == 0)
) {
return Reflect.FAILED_MATCH;
}
if (left! == right!) return Reflect.SUCCESSFUL_MATCH;
} else {
if (left == right) return Reflect.SUCCESSFUL_MATCH;
}
} else {
if (left == right) return Reflect.SUCCESSFUL_MATCH; // works immutably for string comparison
}
// floats should equal each other
if (isFloat<T>()) {
if (i32(isNaN(left)) & i32(isNaN(right))) return Reflect.SUCCESSFUL_MATCH;
}
if (isReference<T>()) {
// T can always be null if it's a reference, emit a runtime check for it regardless of type
if (
i32(changetype<usize>(left) == 0) ^ i32(changetype<usize>(right) == 0)
) {
return Reflect.FAILED_MATCH;
}
}
// check every reference that isn't a function reference
if (isReference<T>()) {
if (!isFunction<T>()) {
if (isNullable<T>()) {
return referencesEqual(left!, right!, stack, cache);
} else {
return referencesEqual(left, right, stack, cache);
}
}
}
// value type, and strict equality cannot be asserted
return Reflect.FAILED_MATCH;
}
public static attachStackTrace(id: i32): void {
attachStackTraceToReflectedValue(id);
}
}
function referencesEqual<T>(
left: T,
right: T,
stack: usize[],
cache: usize[],
): i32 {
let a = changetype<usize>(left);
let b = changetype<usize>(right);
let cacheLength = cache.length;
// must be EVEN or there's a big problem
assert(i32((cacheLength & 0x00000001) == 0), "cacheLength should be even");
// short circuit for strings
if (left instanceof String) {
return Reflect.FAILED_MATCH;
}
// check the cache for matched pairs
for (let i = 0; i < cacheLength; i += 2) {
if (pairSeen(a, b, unchecked(cache[i]), unchecked(cache[i + 1])))
return Reflect.SUCCESSFUL_MATCH;
}
// short circuit because this pair might already be resolving
let length = stack.length;
for (let i = 0; i < length; i += 2) {
if (pairSeen(a, b, unchecked(stack[i]), unchecked(stack[i + 1])))
return Reflect.DEFER_MATCH;
}
// once we've determined we need to check the references for their values, arraybuffers
// require a memory compare
if (left instanceof ArrayBuffer) {
// @ts-ignore: typesafe access to byteLength property because T is ArrayBuffer
if (left.byteLength != right.byteLength) return Reflect.FAILED_MATCH;
let result = memory.compare(a, b, left.byteLength);
if (result == 0) {
cache.push(a);
cache.push(b);
return Reflect.SUCCESSFUL_MATCH;
} else return Reflect.FAILED_MATCH;
}
// @ts-ignore: valid index signature check
if (isDefined(left[0])) {
// test for safe indexof usage
// set match
if (left instanceof Set) {
// @ts-ignore: size is a valid property of Set
if (left.size != right.size) return Reflect.FAILED_MATCH;
stack.push(a);
stack.push(b);
// @ts-ignore: values() is a valid function of Set
let leftValues = left.values();
// @ts-ignore: values() is a valid function of Set
let rightValues = right.values();
let length = leftValues.length;
let leftoverLength = length;
for (let i = 0; i < length; i++) {
let leftItem = unchecked(leftValues[i]);
let continueOuter = false;
for (let j = 0; j < leftoverLength; j++) {
let rightItem = unchecked(rightValues[j]);
if (
Reflect.equals(leftItem, rightItem, stack, cache) !=
Reflect.FAILED_MATCH
) {
rightValues.splice(j, 1);
leftoverLength--;
continueOuter = true;
break;
}
}
if (continueOuter) continue;
stack.pop();
stack.pop();
return Reflect.FAILED_MATCH;
}
cache.push(a);
cache.push(b);
stack.pop();
stack.pop();
return Reflect.SUCCESSFUL_MATCH;
}
if (left instanceof Map) {
// @ts-ignore: size is a valid property of Map
if (left.size != right.size) return Reflect.FAILED_MATCH;
stack.push(a);
stack.push(b);
// collect all the keys and loop over each one
let leftKeys = left.keys();
// @ts-ignore: keys() is a valid function of Map
let rightKeys = right.keys();
// @ts-ignore: length is a valid property of Array
let keyLength = leftKeys.length;
let leftoverKeyLength = keyLength;
// assume we match and determine if the match was a failure
let result = Reflect.SUCCESSFUL_MATCH;
// for each key
for (let i = 0; i < keyLength; i++) {
let leftKey = unchecked(leftKeys[i]);
// assume won't find it
let found = false;
// find a matching key
for (let j = 0; j < leftoverKeyLength; j++) {
let rightKey = unchecked(rightKeys[j]);
// if the keys match, or are still being resolved
if (
Reflect.equals(leftKey, rightKey, stack, cache) !=
Reflect.FAILED_MATCH
) {
// the key potentially matches, obtain the values associated with the keys
let leftValue = left.get(leftKey);
// @ts-ignore: get() is a valid function of Map
let rightValue = right.get(rightKey);
// if the values match, or are still being resolved
if (
Reflect.equals(leftValue, rightValue, stack, cache) !=
Reflect.FAILED_MATCH
) {
leftoverKeyLength--;
rightKeys.splice(j, 1); // remove this key from the list
found = true;
break;
}
}
}
// if there was no match for this key value pair, the result is Failed
if (!found) {
result = Reflect.FAILED_MATCH;
break;
}
}
// if every key matched, result is still equal to `Reflect.MATCH`
if (result == Reflect.SUCCESSFUL_MATCH) {
cache.push(a);
cache.push(b);
}
stack.pop();
stack.pop();
return result;
}
// compile time array values should be compared over a for loop
// @ts-ignore: typesafe access to length
if (isDefined(left.length)) {
// @ts-ignore: typesafe access to length
let aLength = left.length;
// @ts-ignore: typesafe access to length
let bLength = right.length;
// assert the lengths are good
if (aLength != bLength) return Reflect.FAILED_MATCH;
// check each item
for (let i = 0; i < aLength; i++) {
let result = Reflect.equals(
// @ts-ignore: typesafe and runtime check safe array access
unchecked(left[i]),
// @ts-ignore: typesafe and runtime check safe array access
unchecked(right[i]),
stack,
cache,
);
if (result == Reflect.FAILED_MATCH) return Reflect.FAILED_MATCH;
}
// cache this result
cache.push(a);
cache.push(b);
return Reflect.SUCCESSFUL_MATCH;
}
}
// todo: handle Set<keyof<T>> and Map<keyof<T>, valueof<T>>
// we are trying to resolve this pair, push it to the stack
stack.push(a);
stack.push(b);
let result = false;
if (isNullable(left)) {
// @ts-ignore: __aspectStrictEquals is defined at this point
result = left!.__aspectStrictEquals(
<Object>right,
stack,
cache,
[] as StaticArray<i64>,
);
} else {
// @ts-ignore: __aspectStrictEquals is defined at this point
result = left.__aspectStrictEquals(
<Object>right, // here is the hat trick
stack,
cache,
[] as StaticArray<i64>,
);
}
if (result) {
cache.push(a);
cache.push(b);
}
stack.pop();
stack.pop();
return select(Reflect.SUCCESSFUL_MATCH, Reflect.FAILED_MATCH, result);
}
export namespace Reflect {
export const FAILED_MATCH = 0;
export const SUCCESSFUL_MATCH = 1;
export const DEFER_MATCH = 2;
}