diff --git a/common/collections.ts b/common/collections.ts index d3883d3..759e63d 100644 --- a/common/collections.ts +++ b/common/collections.ts @@ -18,6 +18,50 @@ export class ByteArray extends Uint8Array { return self } + /** + * Returns bytes in little-endian order. + */ + static fromU32(x: u32): ByteArray { + let self = new ByteArray(4) + self[0] = x as u8 + self[1] = (x >> 8) as u8 + self[2] = (x >> 16) as u8 + self[3] = (x >> 24) as u8 + return self + } + + /** + * Returns bytes in little-endian order. + */ + static fromI64(x: i64): ByteArray { + let self = new ByteArray(8) + self[0] = x as u8 + self[1] = (x >> 8) as u8 + self[2] = (x >> 16) as u8 + self[3] = (x >> 24) as u8 + self[4] = (x >> 32) as u8 + self[5] = (x >> 40) as u8 + self[6] = (x >> 48) as u8 + self[7] = (x >> 56) as u8 + return self + } + + /** + * Returns bytes in little-endian order. + */ + static fromU64(x: u64): ByteArray { + let self = new ByteArray(8) + self[0] = x as u8 + self[1] = (x >> 8) as u8 + self[2] = (x >> 16) as u8 + self[3] = (x >> 24) as u8 + self[4] = (x >> 32) as u8 + self[5] = (x >> 40) as u8 + self[6] = (x >> 48) as u8 + self[7] = (x >> 56) as u8 + return self + } + static empty(): ByteArray { return ByteArray.fromI32(0) } @@ -121,6 +165,81 @@ export class ByteArray extends Uint8Array { return x } + + /** + * Interprets the byte array as a little-endian I64. + * Throws in case of overflow. + */ + + toI64(): i64 { + let isNeg = this.length > 0 && this[this.length - 1] >> 7 == 1 + let padding = isNeg ? 255 : 0 + for (let i = 8; i < this.length; i++) { + if (this[i] != padding) { + assert(false, 'overflow converting ' + this.toHexString() + ' to i64') + } + } + let paddedBytes = new Bytes(8) + paddedBytes[0] = padding + paddedBytes[1] = padding + paddedBytes[2] = padding + paddedBytes[3] = padding + paddedBytes[4] = padding + paddedBytes[5] = padding + paddedBytes[6] = padding + paddedBytes[7] = padding + let minLen = paddedBytes.length < this.length ? paddedBytes.length : this.length + for (let i = 0; i < minLen; i++) { + paddedBytes[i] = this[i] + } + let x: i64 = 0 + x = (x | paddedBytes[7]) << 8 + x = (x | paddedBytes[6]) << 8 + x = (x | paddedBytes[5]) << 8 + x = (x | paddedBytes[4]) << 8 + x = (x | paddedBytes[3]) << 8 + x = (x | paddedBytes[2]) << 8 + x = (x | paddedBytes[1]) << 8 + x = x | paddedBytes[0] + return x + } + + /** + * Interprets the byte array as a little-endian U64. + * Throws in case of overflow. + */ + + toU64(): u64 { + for (let i = 8; i < this.length; i++) { + if (this[i] != 0) { + assert(false, 'overflow converting ' + this.toHexString() + ' to u64') + } + } + let paddedBytes = new Bytes(8) + paddedBytes[0] = 0 + paddedBytes[1] = 0 + paddedBytes[2] = 0 + paddedBytes[3] = 0 + paddedBytes[4] = 0 + paddedBytes[5] = 0 + paddedBytes[6] = 0 + paddedBytes[7] = 0 + let minLen = paddedBytes.length < this.length ? paddedBytes.length : this.length + for (let i = 0; i < minLen; i++) { + paddedBytes[i] = this[i] + } + let x: u64 = 0 + x = (x | paddedBytes[7]) << 8 + x = (x | paddedBytes[6]) << 8 + x = (x | paddedBytes[5]) << 8 + x = (x | paddedBytes[4]) << 8 + x = (x | paddedBytes[3]) << 8 + x = (x | paddedBytes[2]) << 8 + x = (x | paddedBytes[1]) << 8 + x = x | paddedBytes[0] + return x + } + @operator('==') equals(other: ByteArray): boolean { if (this.length != other.length) { diff --git a/common/numbers.ts b/common/numbers.ts index 80baa5c..a9f78b6 100644 --- a/common/numbers.ts +++ b/common/numbers.ts @@ -53,6 +53,21 @@ export class BigInt extends Uint8Array { return BigInt.fromByteArray(byteArray) } + static fromU32(x: u32): BigInt { + let byteArray = ByteArray.fromU32(x) + return BigInt.fromUnsignedBytes(byteArray) + } + + static fromI64(x: i64): BigInt { + let byteArray = ByteArray.fromI64(x) + return BigInt.fromByteArray(byteArray) + } + + static fromU64(x: u64): BigInt { + let byteArray = ByteArray.fromU64(x) + return BigInt.fromUnsignedBytes(byteArray) + } + static zero(): BigInt { return BigInt.fromI32(0) } @@ -74,7 +89,7 @@ export class BigInt extends Uint8Array { * `bytes` assumed to be little-endian. If your input is big-endian, call `.reverse()` first. */ - static fromUnsignedBytes(bytes: Bytes): BigInt { + static fromUnsignedBytes(bytes: ByteArray): BigInt { let signedBytes = new BigInt(bytes.length + 1) for (let i = 0; i < bytes.length; i++) { signedBytes[i] = bytes[i] @@ -105,6 +120,24 @@ export class BigInt extends Uint8Array { return byteArray.toI32() } + toU32(): u32 { + let uint8Array = changetype(this) + let byteArray = changetype(uint8Array) + return byteArray.toU32() + } + + toI64(): i64 { + let uint8Array = changetype(this) + let byteArray = changetype(uint8Array) + return byteArray.toI64() + } + + toU64(): u64 { + let uint8Array = changetype(this) + let byteArray = changetype(uint8Array) + return byteArray.toU64() + } + toBigDecimal(): BigDecimal { return new BigDecimal(this) } diff --git a/test/bigInt.ts b/test/bigInt.ts index 68f434b..2ca257e 100644 --- a/test/bigInt.ts +++ b/test/bigInt.ts @@ -79,6 +79,33 @@ export function testBigInt(): void { assert(a.toI32() == -2147483648) assert(b.toI32() == 2147483647) + a = BigInt.fromU32(U32.MIN_VALUE) + b = BigInt.fromU32(U32.MAX_VALUE) + let c = BigInt.fromU32(0) + assert(a < b && a <= b, `a: ${a.toU32()}, b: ${b.toU32()}`) + assert(b > a && b >= a, `a: ${a.toU32()}, b: ${b.toU32()}`) + assert(a.toU32() == 0, `Actual value ${a.toU32()}`) + assert(b.toU32() == 4294967295, `Actual value ${b.toU32()}`) + assert(c.toU32() == 0, `Actual value ${c.toU32()}`) + + a = BigInt.fromI64(I64.MIN_VALUE) + b = BigInt.fromI64(I64.MAX_VALUE) + c = BigInt.fromI64(0) + assert(a < b && a <= b, `a: ${a.toU64()}, b: ${b.toU64()}`) + assert(b > a && b >= a, `a: ${a.toU64()}, b: ${b.toU64()}`) + assert(a.toI64() == -9223372036854775808, `Actual value ${a.toI64()}`) + assert(b.toI64() == 9223372036854775807, `Actual value ${b.toI64()}`) + assert(c.toI64() == 0, `Actual value ${c.toI64()}`) + + a = BigInt.fromU64(U64.MIN_VALUE) + b = BigInt.fromU64(U64.MAX_VALUE) + c = BigInt.fromU64(0) + assert(a < b && a <= b, `a: ${a.toU64()}, b: ${b.toU64()}`) + assert(b > a && b >= a, `a: ${a.toU64()}, b: ${b.toU64()}`) + assert(a.toU64() == 0, `Actual value ${a.toU64()}`) + assert(b.toU64() == 18446744073709551615, `Actual value ${b.toU64()}`) + assert(c.toU64() == 0, `Actual value ${c.toU64()}`) + // This is 8071860 in binary. let blockNumber = new ByteArray(3) blockNumber[0] = 180 diff --git a/test/test.js b/test/test.js index f71151c..60e538c 100644 --- a/test/test.js +++ b/test/test.js @@ -1,108 +1,137 @@ const fs = require('fs') const asc = require('assemblyscript/cli/asc') +const path = require('path') +const { StringDecoder } = require('string_decoder') -// Copy index.ts to a temporary subdirectory so that asc doesn't put all the -// index.ts exports in the global namespace. -if (!fs.existsSync('test/temp_lib')) { - fs.mkdirSync('test/temp_lib') -} +async function main() { + // Copy index.ts to a temporary subdirectory so that asc doesn't put all the + // index.ts exports in the global namespace. + if (!fs.existsSync('test/temp_lib')) { + fs.mkdirSync('test/temp_lib') + } -if (!fs.existsSync('test/temp_out')) { - fs.mkdirSync('test/temp_out') -} + if (!fs.existsSync('test/temp_out')) { + fs.mkdirSync('test/temp_out') + } -if (!fs.existsSync('test/temp_lib/chain')) { - fs.mkdirSync('test/temp_lib/chain') -} + if (!fs.existsSync('test/temp_lib/chain')) { + fs.mkdirSync('test/temp_lib/chain') + } -if (!fs.existsSync('test/temp_lib/common')) { - fs.mkdirSync('test/temp_lib/common') -} + if (!fs.existsSync('test/temp_lib/common')) { + fs.mkdirSync('test/temp_lib/common') + } -fs.copyFileSync('common/datasource.ts', 'test/temp_lib/common/datasource.ts') -fs.copyFileSync('common/eager_offset.ts', 'test/temp_lib/common/eager_offset.ts') -fs.copyFileSync('common/json.ts', 'test/temp_lib/common/json.ts') -fs.copyFileSync('common/numbers.ts', 'test/temp_lib/common/numbers.ts') -fs.copyFileSync('common/collections.ts', 'test/temp_lib/common/collections.ts') -fs.copyFileSync('common/conversion.ts', 'test/temp_lib/common/conversion.ts') -fs.copyFileSync('common/value.ts', 'test/temp_lib/common/value.ts') -fs.copyFileSync('chain/ethereum.ts', 'test/temp_lib/chain/ethereum.ts') -fs.copyFileSync('chain/near.ts', 'test/temp_lib/chain/near.ts') -fs.copyFileSync('index.ts', 'test/temp_lib/index.ts') -let output_path = 'test/temp_out/test.wasm' - -const env = { - abort: function (message, fileName, lineNumber, columnNumber) { - console.log('aborted') - console.log('message', message) - console.log('fileName', fileName) - console.log('lineNumber', lineNumber) - console.log('columnNumber', columnNumber) - }, -} + fs.copyFileSync('common/collections.ts', 'test/temp_lib/common/collections.ts') + fs.copyFileSync('common/conversion.ts', 'test/temp_lib/common/conversion.ts') + fs.copyFileSync('common/datasource.ts', 'test/temp_lib/common/datasource.ts') + fs.copyFileSync('common/eager_offset.ts', 'test/temp_lib/common/eager_offset.ts') + fs.copyFileSync('common/json.ts', 'test/temp_lib/common/json.ts') + fs.copyFileSync('common/numbers.ts', 'test/temp_lib/common/numbers.ts') + fs.copyFileSync('common/value.ts', 'test/temp_lib/common/value.ts') + fs.copyFileSync('chain/ethereum.ts', 'test/temp_lib/chain/ethereum.ts') + fs.copyFileSync('chain/near.ts', 'test/temp_lib/chain/near.ts') + fs.copyFileSync('index.ts', 'test/temp_lib/index.ts') + + try { + const outputWasmPath = 'test/temp_out/test.wasm' -try { - testFile('test/bigInt.ts') - testFile('test/bytes.ts') - testFile('test/entity.ts') - - // Cleanup - fs.unlinkSync('test/temp_lib/index.ts') - fs.unlinkSync('test/temp_lib/common/eager_offset.ts') - fs.unlinkSync('test/temp_lib/common/numbers.ts') - fs.unlinkSync('test/temp_lib/common/collections.ts') - fs.unlinkSync('test/temp_lib/common/conversion.ts') - fs.unlinkSync('test/temp_lib/common/value.ts') - fs.unlinkSync('test/temp_lib/common/datasource.ts') - fs.unlinkSync('test/temp_lib/common/json.ts') - fs.rmdirSync('test/temp_lib/common') - fs.unlinkSync('test/temp_lib/chain/ethereum.ts') - fs.unlinkSync('test/temp_lib/chain/near.ts') - fs.rmdirSync('test/temp_lib/chain') - fs.rmdirSync('test/temp_lib') - fs.unlinkSync('test/temp_out/test.wasm') - fs.rmdirSync('test/temp_out') -} catch (e) { - console.error(e) - process.exitCode = 1 - fs.unlinkSync('test/temp_lib/index.ts') - fs.unlinkSync('test/temp_lib/common/numbers.ts') - fs.unlinkSync('test/temp_lib/common/eager_offset.ts') - fs.unlinkSync('test/temp_lib/common/collections.ts') - fs.unlinkSync('test/temp_lib/common/conversion.ts') - fs.unlinkSync('test/temp_lib/common/value.ts') - fs.unlinkSync('test/temp_lib/common/datasource.ts') - fs.unlinkSync('test/temp_lib/common/conversion.ts') - fs.rmdirSync('test/temp_lib/common') - fs.unlinkSync('test/temp_lib/chain/ethereum.ts') - fs.unlinkSync('test/temp_lib/chain/near.ts') - fs.rmdirSync('test/temp_lib/chain') - fs.rmdirSync('test/temp_lib') - fs.unlinkSync('test/temp_out/test.wasm') - fs.rmdirSync('test/temp_out') - throw e + const promises = {} + promises['bigInt'] = testFile('test/bigInt.ts', outputWasmPath) + promises['bytes'] = testFile('test/bytes.ts', outputWasmPath) + promises['entity'] = testFile('test/entity.ts', outputWasmPath) + + const entries = Object.entries(promises) + const results = await Promise.allSettled(entries.map((entry) => entry[1])) + const failures = Object.fromEntries( + results + .map((result, index) => [entries[index][0], result]) + .filter(([index, result]) => result.status === 'rejected'), + ) + + if (Object.keys(failures).length > 0) { + throw failures + } + } finally { + fs.unlinkSync('test/temp_lib/common/collections.ts') + fs.unlinkSync('test/temp_lib/common/conversion.ts') + fs.unlinkSync('test/temp_lib/common/datasource.ts') + fs.unlinkSync('test/temp_lib/common/eager_offset.ts') + fs.unlinkSync('test/temp_lib/common/json.ts') + fs.unlinkSync('test/temp_lib/common/numbers.ts') + fs.unlinkSync('test/temp_lib/common/value.ts') + fs.rmdirSync('test/temp_lib/common') + fs.unlinkSync('test/temp_lib/chain/ethereum.ts') + fs.unlinkSync('test/temp_lib/chain/near.ts') + fs.rmdirSync('test/temp_lib/chain') + fs.unlinkSync('test/temp_lib/index.ts') + fs.rmdirSync('test/temp_lib') + fs.unlinkSync('test/temp_out/test.wasm') + fs.rmdirSync('test/temp_out') + } } -function testFile(path) { - if (asc.main(['--explicitStart', '--exportRuntime', '--runtime', 'stub', path, '--lib', 'test', '-b', output_path]) != 0) { - throw Error('failed to compile') +async function testFile(sourceFile, outputWasmPath) { + console.log(`Compiling test file ${sourceFile} to WASM...`) + if ( + asc.main([ + '--explicitStart', + '--exportRuntime', + '--importMemory', + '--runtime', + 'stub', + sourceFile, + '--lib', + 'test', + '-b', + outputWasmPath, + ]) != 0 + ) { + throw Error('Failed to compile') } - let test_wasm = new Uint8Array(fs.readFileSync(output_path)) - WebAssembly.instantiate(test_wasm, { - env, + const wasmCode = new Uint8Array(fs.readFileSync(outputWasmPath)) + const memory = new WebAssembly.Memory({ initial: 1, maximum: 1 }) + const module = await WebAssembly.instantiate(wasmCode, { + env: { + memory, + abort: function (messagePtr, fileNamePtr, lineNumber, columnNumber) { + let fileSource = path.join(__dirname, '..', sourceFile) + let message = 'assertion failure' + if (messagePtr !== 0) { + message += `: ${getString(memory, messagePtr)}` + } + + throw new Error(`${message} (${fileSource}:${lineNumber}:${columnNumber})`) + }, + }, conversion: { 'typeConversion.bytesToHex': function () {}, }, - }).then((module) => { - // Call AS start explicitly - module.instance.exports._start() - - for (const [testName, testFn] of Object.entries(module.instance.exports)) { - if (typeof testFn === 'function' && testName.startsWith('test')) { - console.log(`Running "${testName}"...`) - testFn() - } - } }) + + // Call AS start explicitly + module.instance.exports._start() + + console.log(`Running "${sourceFile}" tests...`) + for (const [testName, testFn] of Object.entries(module.instance.exports)) { + if (typeof testFn === 'function' && testName.startsWith('test')) { + console.log(`Running "${testName}"...`) + testFn() + } + } +} + +function getString(memory, addr) { + let byteCount = Buffer.from(new Uint8Array(memory.buffer, addr - 4, 4)).readInt32LE() + let buffer = new Uint8Array(memory.buffer, addr, byteCount) + let encoder = new StringDecoder('utf16le') + + return encoder.write(buffer) } + +main().catch((error) => { + console.error('Test suite failed', error) + + process.exit(1) +}) diff --git a/tsconfig.json b/tsconfig.json index 3a38819..8db6e32 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "assemblyscript/std/assembly", - "include": ["index.ts", "helper-functions.ts", "global/global.ts"] + "include": ["index.ts", "helper-functions.ts", "global/global.ts", "test"] }