Skip to content

Commit

Permalink
feat: ARC4 Tuples and UFixedNxM
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanmenzel committed Nov 12, 2024
1 parent ce32257 commit b13fbd1
Show file tree
Hide file tree
Showing 24 changed files with 2,680 additions and 2,652 deletions.
38 changes: 30 additions & 8 deletions packages/algo-ts/src/arc4/encoded-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type NativeForArc4Int<N extends BitSize> = N extends 8 | 16 | 32 | 64 ? uint64 :
type CompatForArc4Int<N extends BitSize> = N extends 8 | 16 | 32 | 64 ? Uint64Compat : BigUintCompat

abstract class AbiEncoded implements BytesBacked {
abstract __type?: string
get bytes(): bytes {
throw new Error('todo')
}
Expand All @@ -19,23 +20,31 @@ abstract class AbiEncoded implements BytesBacked {
}

export class Str extends AbiEncoded {
__type?: 'arc4.Str'
#value: string
constructor(s?: StringCompat) {
super()
this.#value = s ?? ''
}
get native(): string {
throw new Error('TODO')
return this.#value
}
}
export class UintN<N extends BitSize> extends AbiEncoded {
__type?: `arc4.UintN<${N}>`

constructor(v?: CompatForArc4Int<N>) {
super()
}
get native(): NativeForArc4Int<N> {
throw new Error('TODO')
}
}
export class UFixedNxM<N extends BitSize, M extends number> {
constructor(v: `${number}:${number}`, n?: N, m?: M) {}
export class UFixedNxM<N extends BitSize, M extends number> extends AbiEncoded {
__type?: `arc4.UFixedNxM<${N}x${M}>`
constructor(v: `${number}.${number}`, n?: N, m?: M) {
super()
}

get native(): NativeForArc4Int<N> {
throw new Error('TODO')
Expand All @@ -48,6 +57,7 @@ export class Byte extends UintN<8> {
}
}
export class Bool {
__type?: `arc4.Bool`
#v: boolean
constructor(v?: boolean) {
this.#v = v ?? false
Expand Down Expand Up @@ -156,6 +166,7 @@ abstract class Arc4ReadonlyArray<TItem extends AbiEncoded> extends AbiEncoded {
}

export class StaticArray<TItem extends AbiEncoded, TLength extends number> extends Arc4ReadonlyArray<TItem> {
__type?: `arc4.StaticArray<${TItem['__type']}, ${TLength}>`
constructor()
constructor(...items: TItem[] & { length: TLength })
constructor(...items: TItem[])
Expand All @@ -169,6 +180,7 @@ export class StaticArray<TItem extends AbiEncoded, TLength extends number> exten
}

export class DynamicArray<TItem extends AbiEncoded> extends Arc4ReadonlyArray<TItem> {
__type?: `arc4.DynamicArray<${TItem['__type']}>`
constructor(...items: TItem[]) {
super(items)
}
Expand All @@ -194,17 +206,26 @@ export class DynamicArray<TItem extends AbiEncoded> extends Arc4ReadonlyArray<TI
return new DynamicArray<TItem>(...this.items)
}
}
type ExpandTupleType<T extends AbiEncoded[]> = T extends [infer T1 extends AbiEncoded, ...infer TRest extends AbiEncoded[]]
? TRest extends []
? `${T1['__type']}`
: `${T1['__type']},${ExpandTupleType<TRest>}`
: ''

type ItemAt<TTuple extends unknown[], TIndex extends number> = undefined extends TTuple[TIndex] ? never : TTuple[TIndex]

export class Tuple<TTuple extends unknown[]> {
export class Tuple<TTuple extends [AbiEncoded, ...AbiEncoded[]]> extends AbiEncoded {
__type?: `arc4.Tuple<${ExpandTupleType<TTuple>}>`
#items: TTuple
constructor(...items: TTuple) {
super()
this.#items = items
}

at<TIndex extends number>(index: TIndex): ItemAt<TTuple, TIndex> {
return (this.#items[index] ?? err('Index out of bounds')) as ItemAt<TTuple, TIndex>
at<TIndex extends keyof TTuple>(index: TIndex): TTuple[TIndex] {
return this.#items[index] ?? err('Index out of bounds')
}

get length(): TTuple['length'] & uint64 {
return this.#items.length
}

get native(): TTuple {
Expand All @@ -213,6 +234,7 @@ export class Tuple<TTuple extends unknown[]> {
}

export class Address extends Arc4ReadonlyArray<Byte> {
__type?: `arc4.Address`
constructor(value?: Account | string | bytes) {
let byteValues: Uint8Array
if (value === undefined) {
Expand Down
6 changes: 3 additions & 3 deletions src/awst/node-factory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CodeError } from '../errors'
import type { DeliberateAny, Props } from '../typescript-helpers'
import { codeInvariant, invariant } from '../util'
import { codeInvariant, instanceOfAny, invariant } from '../util'
import type { Expression, Statement } from './nodes'
import {
AssignmentExpression,
Expand Down Expand Up @@ -239,8 +239,8 @@ const explicitNodeFactory = {
},
tupleItemExpression(props: Omit<Props<TupleItemExpression>, 'wtype'>) {
invariant(
props.base.wtype instanceof wtypes.WTuple && props.base.wtype.types.length > Number(props.index),
'expr.base must be WTuple with length greater than index',
instanceOfAny(props.base.wtype, wtypes.WTuple, wtypes.ARC4Tuple) && props.base.wtype.types.length > Number(props.index),
'expr.base must be tuple type with length greater than index',
)
return new TupleItemExpression({
...props,
Expand Down
2 changes: 1 addition & 1 deletion src/awst/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class DecimalConstant extends Expression {
this.value = props.value
}
declare wtype: wtypes.ARC4UFixedNxM
value: number
value: string
accept<T>(visitor: ExpressionVisitor<T>): T {
return visitor.visitDecimalConstant(this)
}
Expand Down
2 changes: 1 addition & 1 deletion src/awst/to-code-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class ToCodeVisitor
return `${expression.value}`
}
visitDecimalConstant(expression: nodes.DecimalConstant): string {
throw new TodoError('Method not implemented.', { sourceLocation: expression.sourceLocation })
return `${expression.value}m`
}
visitBoolConstant(expression: nodes.BoolConstant): string {
return expression.value ? 'True' : 'False'
Expand Down
21 changes: 14 additions & 7 deletions src/awst/wtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,15 @@ export namespace wtypes {
})

export class ARC4Type extends WType {
readonly puyaTypeName: string = 'ARC4Type'
readonly nativeType: WType | null
readonly arc4Name: string
readonly otherEncodeableTypes: WType[]
constructor({
nativeType,
arc4Name,
otherEncodeableTypes,
...rest
}: {
nativeType: WType | null
arc4Name: string
otherEncodeableTypes?: WType[]
name: string
immutable?: boolean
scalarType?: AVMType | null
Expand All @@ -116,7 +112,6 @@ export namespace wtypes {
super({ ...rest, scalarType: rest.scalarType ?? AVMType.bytes })
this.arc4Name = arc4Name
this.nativeType = nativeType
this.otherEncodeableTypes = otherEncodeableTypes ?? []
}
}

Expand Down Expand Up @@ -230,13 +225,25 @@ export namespace wtypes {
scalarType: AVMType.bytes,
nativeType: n <= 64 ? uint64WType : biguintWType,
arc4Name: arc4Name ?? `uint${n}`,
otherEncodeableTypes: [uint64WType, biguintWType, boolWType],
})
this.n = n
}
}

export class ARC4UFixedNxM extends ARC4Type {}
export class ARC4UFixedNxM extends ARC4Type {
readonly n: bigint
readonly m: bigint
constructor({ n, m }: { n: bigint; m: bigint }) {
super({
name: `arc4.ufixed${n}x${m}`,
scalarType: AVMType.bytes,
nativeType: n <= 64 ? uint64WType : biguintWType,
arc4Name: `ufixed${n}x${m}`,
})
this.n = n
this.m = m
}
}

export class ARC4Struct extends ARC4Type {
fields: Record<string, ARC4Type>
Expand Down
4 changes: 4 additions & 0 deletions src/awst_build/base-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export abstract class BaseVisitor implements Visitor<Expressions, NodeBuilder> {
}

visitNumericLiteral(node: ts.NumericLiteral): InstanceBuilder {
codeInvariant(
!node.text.includes('.'),
'Literals with decimal points are not supported. Use a string literal to capture decimal values',
)
const literalValue = BigInt(node.text)
const ptype = this.context.getPTypeForNode(node)
invariant(ptype instanceof TransientType, 'Literals should resolve to transient PTypes')
Expand Down
6 changes: 4 additions & 2 deletions src/awst_build/eb/arc4/arrays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { logger } from '../../../logger'

import { base32ToUint8Array, codeInvariant, invariant } from '../../../util'
import type { PType } from '../../ptypes'
import { accountPType, bytesPType, IterableIteratorType, NumericLiteralPType, stringPType, TuplePType, uint64PType } from '../../ptypes'
import { accountPType, bytesPType, IterableIteratorGeneric, NumericLiteralPType, stringPType, TuplePType, uint64PType } from '../../ptypes'
import {
AddressClass,
arc4AddressAlias,
Expand Down Expand Up @@ -234,7 +234,9 @@ class EntriesFunctionBuilder extends FunctionBuilder {

call(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation): NodeBuilder {
parseFunctionArgs({ args, typeArgs, callLocation: sourceLocation, argSpec: (_) => [], genericTypeArgs: 0, funcName: 'entries' })
const iteratorType = IterableIteratorType.parameterise([new TuplePType({ items: [uint64PType, this.arrayBuilder.ptype.elementType] })])
const iteratorType = IterableIteratorGeneric.parameterise([
new TuplePType({ items: [uint64PType, this.arrayBuilder.ptype.elementType] }),
])
return new IterableIteratorExpressionBuilder(
nodeFactory.enumeration({
expr: this.arrayBuilder.iterate(),
Expand Down
120 changes: 120 additions & 0 deletions src/awst_build/eb/arc4/tuple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { nodeFactory } from '../../../awst/node-factory'
import type { Expression } from '../../../awst/nodes'
import type { SourceLocation } from '../../../awst/source-location'
import { logger } from '../../../logger'
import { codeInvariant, invariant } from '../../../util'
import type { PType } from '../../ptypes'
import { numberPType, TuplePType, uint64PType } from '../../ptypes'
import { ARC4EncodedType, Arc4TupleClass, ARC4TupleType } from '../../ptypes/arc4-types'
import { instanceEb } from '../../type-registry'
import type { InstanceBuilder, NodeBuilder } from '../index'
import { ClassBuilder, FunctionBuilder } from '../index'
import { requireIntegerConstant } from '../util'
import { parseFunctionArgs } from '../util/arg-parsing'
import { Arc4EncodedBaseExpressionBuilder } from './base'

export class Arc4TupleClassBuilder extends ClassBuilder {
readonly ptype = Arc4TupleClass

newCall(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation): InstanceBuilder {
const { args: tupleItems } = parseFunctionArgs({
args,
typeArgs,
genericTypeArgs: 1,
funcName: this.typeDescription,
callLocation: sourceLocation,
argSpec: (a) => [a.required(), ...args.slice(1).map(() => a.required())],
})

const expressions: Expression[] = []
const types: ARC4EncodedType[] = []
for (const item of tupleItems) {
if (item.ptype instanceof ARC4EncodedType) {
expressions.push(item.resolve())
types.push(item.ptype)
} else {
logger.error(item.sourceLocation, 'ARC4 tuple items must be ARC4 encoded types')
}
}
const tupleType = new TuplePType({
items: types,
})
const arc4TupleType = new ARC4TupleType({
types,
sourceLocation,
})
return new Arc4TupleExpressionBuilder(
nodeFactory.aRC4Encode({
value: nodeFactory.tupleExpression({
items: expressions,
wtype: tupleType.wtype,
sourceLocation,
}),
wtype: arc4TupleType.wtype,
sourceLocation,
}),
arc4TupleType,
)
}
}

export class Arc4TupleExpressionBuilder extends Arc4EncodedBaseExpressionBuilder<ARC4TupleType> {
constructor(expr: Expression, ptype: PType) {
invariant(ptype instanceof ARC4TupleType, 'ptype must be ARC4TupleType')
super(expr, ptype)
}

memberAccess(name: string, sourceLocation: SourceLocation): NodeBuilder {
switch (name) {
case 'at':
return new Arc4TupleAtFunctionBuilder(this, sourceLocation)
case 'length':
return instanceEb(
nodeFactory.uInt64Constant({
value: BigInt(this.ptype.items.length),
sourceLocation,
}),
uint64PType,
)
}
return super.memberAccess(name, sourceLocation)
}
}

class Arc4TupleAtFunctionBuilder extends FunctionBuilder {
constructor(
private builder: Arc4TupleExpressionBuilder,
sourceLocation: SourceLocation,
) {
super(sourceLocation)
}
call(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation): NodeBuilder {
const {
args: [index],
} = parseFunctionArgs({
args,
typeArgs,
genericTypeArgs: 1,
callLocation: sourceLocation,
funcName: 'at',
argSpec: (a) => [a.required(numberPType)],
})

const indexNum = requireIntegerConstant(index).value
codeInvariant(
indexNum < this.builder.ptype.items.length && indexNum >= 0,
"Index arg must be a numeric literal between 0 and the tuple's length",
)

const itemType = this.builder.ptype.items[Number(indexNum)]

return instanceEb(
nodeFactory.tupleItemExpression({
index: indexNum,
sourceLocation,
base: this.builder.resolve(),
}),
itemType,
)
}
}
Loading

0 comments on commit b13fbd1

Please sign in to comment.