UNPKG

@as-pect/assembly

Version:

Write AssemblyScript tests at blazing fast speeds

700 lines (633 loc) 20.5 kB
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! @external("__aspect", "attachStackTraceToReflectedValue") declare function attachStackTraceToReflectedValue(id: i32): void; // @ts-ignore: linked function decorator @external("__aspect", "createReflectedValue") 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 @external("__aspect", "createReflectedNumber") declare function createReflectedNumber( signed: bool, size: i32, reflectedTypeValue: ReflectedValueType, typeName: string, value: f64, ): i32; // @ts-ignore: linked function decorator @external("__aspect", "createReflectedLong") declare function createReflectedLong( signed: bool, size: i32, reflectedTypeValue: ReflectedValueType, typeName: string, lowValue: i32, highValue: i32, ): i32; // @ts-ignore: external declaration @external("__aspect", "pushReflectedObjectValue") @global declare function __aspectPushReflectedObjectValue( parentID: i32, value: i32, ): void; // @ts-ignore: external declaration @external("__aspect", "pushReflectedObjectKey") @global declare function __aspectPushReflectedObjectKey( parentID: i32, value: i32, ): void; @inline function sizeOf(ptr: usize): i32 { return changetype<OBJECT>(ptr - OBJECT_OVERHEAD).rtSize; } @global // @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 { @lazy export const FAILED_MATCH = 0; @lazy export const SUCCESSFUL_MATCH = 1; @lazy export const DEFER_MATCH = 2; }