Skip to content

Commit

Permalink
refactor: Change algo ts primitive instanceof checks to use name, and…
Browse files Browse the repository at this point in the history
… to traverse prototype chain

Plus change json representation of types to improve debug view
  • Loading branch information
tristanmenzel committed Oct 8, 2024
1 parent 99066aa commit 063134f
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 35 deletions.
5 changes: 3 additions & 2 deletions packages/algo-ts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@algorandfoundation/algorand-typescript",
"version": "0.0.1-alpha.2",
"version": "0.0.1-alpha.3",
"description": "This package contains definitions for the types which comprise Algorand TypeScript which can be compiled to run on the Algorand Virtual Machine using the Puya compiler.",
"private": false,
"main": "index.js",
Expand All @@ -17,7 +17,8 @@
"build:3-build": "rollup -c --configPlugin typescript",
"build:4-copy-pkg-json": "tstk copy-package-json -c",
"build:5-copy-readme": "copyfiles ./README.md ./dist",
"watch": "rollup -c -w --configPlugin typescript"
"watch": "rollup -c -w --configPlugin typescript",
"test": "vitest run "
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
Expand Down
98 changes: 65 additions & 33 deletions packages/algo-ts/src/impl/primitives.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { biguint, BigUintCompat, bytes, BytesCompat, uint64, Uint64Compat } from '../index'
import { DeliberateAny } from '../typescript-helpers'
import { base32ToUint8Array } from './base-32'
import { bigIntToUint8Array, uint8ArrayToBigInt, uint8ArrayToUtf8, utf8ToUint8Array } from './encoding-util'
import { bigIntToUint8Array, uint8ArrayToBigInt, uint8ArrayToHex, uint8ArrayToUtf8, utf8ToUint8Array } from './encoding-util'
import { avmError, AvmError, internalError } from './errors'
import { nameOfType } from './name-of-type'

Expand Down Expand Up @@ -71,30 +71,49 @@ export const checkBytes = (v: Uint8Array): Uint8Array => {
return v
}

export abstract class AlgoTsPrimitiveCls {
private readonly _type: string = AlgoTsPrimitiveCls.name

constructor(t: string) {
this._type = `${AlgoTsPrimitiveCls.name}.${t}`
}
/**
* Verifies that an object is an instance of a type based on its name rather than reference equality.
*
* This is useful in scenarios where a module loader has loaded a module twice and hence two instances of a
* type do not have reference equality on their constructors.
* @param subject The object to check
* @param typeCtor The ctor of the type
*/
function isInstanceOfTypeByName(subject: unknown, typeCtor: { name: string }): boolean {
if (subject === null || typeof subject !== 'object') return false

let ctor = subject.constructor
while (typeof ctor === 'function') {
if (ctor.name === typeCtor.name) return true
ctor = Object.getPrototypeOf(ctor)
}
return false
}

export abstract class AlgoTsPrimitiveCls {
static [Symbol.hasInstance](x: unknown): x is AlgoTsPrimitiveCls {
return x instanceof Object && '_type' in x && (x as { _type: string })['_type'].startsWith(AlgoTsPrimitiveCls.name)
return isInstanceOfTypeByName(x, AlgoTsPrimitiveCls)
}

abstract valueOf(): bigint | string | boolean
abstract valueOf(): bigint | string
abstract toBytes(): BytesCls
}

export class Uint64Cls extends AlgoTsPrimitiveCls {
public readonly value: bigint
readonly #value: bigint
constructor(value: bigint | number | string) {
super(Uint64Cls.name)
this.value = BigInt(value)
checkUint64(this.value)
super()
this.#value = BigInt(value)
checkUint64(this.#value)

Object.defineProperty(this, 'uint64', {
value: this.#value.toString(),
writable: false,
enumerable: true,
})
}
static [Symbol.hasInstance](x: unknown): x is Uint64Cls {
return x instanceof Object && '_type' in x && (x as { _type: string })['_type'].endsWith(Uint64Cls.name)
return isInstanceOfTypeByName(x, Uint64Cls)
}
static fromCompat(v: StubUint64Compat): Uint64Cls {
if (typeof v == 'boolean') return new Uint64Cls(v ? 1n : 0n)
Expand All @@ -109,73 +128,86 @@ export class Uint64Cls extends AlgoTsPrimitiveCls {
}

valueOf(): bigint {
return this.value
return this.#value
}

toBytes(isDynamic: boolean = false): BytesCls {
return new BytesCls(bigIntToUint8Array(this.value, isDynamic ? 'dynamic' : 8))
return new BytesCls(bigIntToUint8Array(this.#value, isDynamic ? 'dynamic' : 8))
}

asAlgoTs(): uint64 {
return this as unknown as uint64
}

asBigInt(): bigint {
return this.value
return this.#value
}
asNumber(): number {
if (this.value > Number.MAX_SAFE_INTEGER) {
if (this.#value > Number.MAX_SAFE_INTEGER) {
throw new AvmError('value cannot be safely converted to a number')
}
return Number(this.value)
return Number(this.#value)
}
}

export class BigUintCls extends AlgoTsPrimitiveCls {
constructor(public readonly value: bigint) {
super(BigUintCls.name)
readonly #value: bigint
constructor(value: bigint) {
super()
this.#value = value
Object.defineProperty(this, 'biguint', {
value: value.toString(),
writable: false,
enumerable: true,
})
}
valueOf(): bigint {
return this.value
return this.#value
}

toBytes(): BytesCls {
return new BytesCls(bigIntToUint8Array(this.value))
return new BytesCls(bigIntToUint8Array(this.#value))
}

asAlgoTs(): biguint {
return this as unknown as biguint
}

asBigInt(): bigint {
return this.value
return this.#value
}
asNumber(): number {
if (this.value > Number.MAX_SAFE_INTEGER) {
if (this.#value > Number.MAX_SAFE_INTEGER) {
throw new AvmError('value cannot be safely converted to a number')
}
return Number(this.value)
return Number(this.#value)
}
static [Symbol.hasInstance](x: unknown): x is BigUintCls {
return x instanceof Object && '_type' in x && (x as { _type: string })['_type'].endsWith(BigUintCls.name)
return isInstanceOfTypeByName(x, BigUintCls)
}
static fromCompat(v: StubBigUintCompat): BigUintCls {
if (typeof v == 'boolean') return new BigUintCls(v ? 1n : 0n)
if (typeof v == 'number') return new BigUintCls(BigInt(v))
if (typeof v == 'bigint') return new BigUintCls(v)
if (v instanceof Uint64Cls) return new BigUintCls(v.value)
if (v instanceof Uint64Cls) return new BigUintCls(v.valueOf())
if (v instanceof BytesCls) return v.toBigUint()
if (v instanceof BigUintCls) return v
internalError(`Cannot convert ${nameOfType(v)} to BigUint`)
}
}

export class BytesCls extends AlgoTsPrimitiveCls {
#v: Uint8Array
readonly #v: Uint8Array
constructor(v: Uint8Array) {
super(BytesCls.name)
super()
this.#v = v
checkBytes(this.#v)
// Add an enumerable property for debugging code to show
Object.defineProperty(this, 'bytes', {
value: uint8ArrayToHex(this.#v),
writable: false,
enumerable: true,
})
}

get length() {
Expand Down Expand Up @@ -247,11 +279,11 @@ export class BytesCls extends AlgoTsPrimitiveCls {
}

valueOf(): string {
return uint8ArrayToUtf8(this.#v)
return uint8ArrayToHex(this.#v)
}

static [Symbol.hasInstance](x: unknown): x is BytesCls {
return x instanceof Object && '_type' in x && (x as { _type: string })['_type'].endsWith(BytesCls.name)
return isInstanceOfTypeByName(x, BytesCls)
}

static fromCompat(v: StubBytesCompat | undefined): BytesCls {
Expand Down Expand Up @@ -293,7 +325,7 @@ export class BytesCls extends AlgoTsPrimitiveCls {
return new BigUintCls(uint8ArrayToBigInt(this.#v))
}
toString(): string {
return this.valueOf()
return uint8ArrayToUtf8(this.#v)
}

asAlgoTs(): bytes {
Expand Down
30 changes: 30 additions & 0 deletions packages/algo-ts/tests/primitive-impl.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest'
import { BigUint, Bytes, Uint64 } from '../src'
import { AlgoTsPrimitiveCls, BigUintCls, BytesCls, Uint64Cls } from '../src/impl/primitives'

describe('AlgoTsPrimitives', () => {
describe.each([
[Bytes(''), BytesCls],
[Bytes('123'), AlgoTsPrimitiveCls],
[Uint64(1), Uint64Cls],
[Uint64(43), AlgoTsPrimitiveCls],
[BigUint(1), BigUintCls],
[BigUint(43), AlgoTsPrimitiveCls],
])('Primitive value is instanceOf implementation class', (value, instanceType) => {
it(`${JSON.stringify(value)} instanceof ${instanceType.name}`, () => {
expect(value).toBeInstanceOf(instanceType)
})
})
describe.each([
[Bytes(''), Uint64Cls],
[Bytes('123'), BigUintCls],
[Uint64(1), BytesCls],
[Uint64(43), BigUintCls],
[BigUint(1), BytesCls],
[BigUint(43), Uint64Cls],
])('Primitive value is not instanceOf unrelated class', (value, instanceType) => {
it(`${JSON.stringify(value)} not instanceof ${instanceType.name}`, () => {
expect(value).not.toBeInstanceOf(instanceType)
})
})
})
10 changes: 10 additions & 0 deletions packages/algo-ts/vitest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import typescript from '@rollup/plugin-typescript'
import { defineConfig } from 'vitest/config'

export default defineConfig({
plugins: [
typescript({
tsconfig: 'tsconfig.json',
}),
],
})

0 comments on commit 063134f

Please sign in to comment.