Skip to content

Proposed features to implement for PuyaTS 1.0

The following outlines some proposed changes to address feedback from the community about current functionality that is either too verbose, or in some cases infeasible when comparing between TEALScript and Algorand TypeScript beta.

The following will outline the potential changes (with some flexibility for what is actually needed for 1.0) and relevant examples that the changes impact.

For brevity examples 2.x and 3.x have been omitted in this document as in Puya they are additional variants of the basic data types covered by 1.x. In general Puya supports arbitrary levels of dynamic sized types, however in practice a good smart contract design would want to avoid this.

  • Allow direct mutation of struct fields
  • Allow native values (no need to convert to/from ARC-4)
  • Aliases would still require a .copy() to maintain semantic compatibility
  • Allow direct mutation of array members
  • Allow native values (no need to convert to/from ARC-4)
  • Allow other mutable native types as elements
  • Aliases of array would still require a .copy() to maintain semantic compatibility
  • Aliases of contents would still require a .copy()
  • Allow direct mutation of array members
  • Allow native values (no need to convert to/from ARC-4)
  • Allow other mutable native types as elements
  • Aliases of array would still require a .copy() to maintain semantic compatibility
  • Aliases of contents would still require a .copy()
  • Allow mutation of box values larger than 4k by directly manipulating the box
  • Efficiency will only work with statically sized types or raw Bytes
  • Dynamically sized types will fallback to current behaviour, which would fail if total size of box exceeds 4k e.g.
self.box.value.field = someValue

Will compile to box_replace instead of a box_get; <modify>; box_put

  • Get a Box from a BoxMap with a key, allows box referencing e.g.
const box = this.myMap.box(someKey)
  • Get a BoxRef from a Box, allows efficient mutation of underlying box bytes e.g.
const ref = this.someBox.ref
ref.replace(5000, someBytes)

Mutable Native Dynamic Array splice and/or pop support

Section titled “Mutable Native Dynamic Array splice and/or pop support”
  • Only support fixed size element types

Implementing this in the Puya backend in a comprehensive way (such that true reference tracking is broadly supported) is not a feasible option due to the limitations of the AVM.

The other features proposed should reduce the need for copying and/or reference tracking.

const arr: uint64[] = [1, 2, 3];
arr.pop();
arr.push(5);
return arr; // [1, 2, 5]
let arr: uint64[] = [1, 2, 3];
arr = [...arr.splice(-1, 1), 5]
return arr
const arr: uint64[] = [1, 2, 3];
arr.pop();
arr.push(5);
return arr; // [1, 2, 5]
const tuple: [uint64, uint64] = [1, 2];
tuple[0] = 5;
return tuple; // [5, 2]
let tuple: [uin64, uint64] = [1, 2]
tuple = tuple.with(0, 5);
return tuple; // [5, 2]
const tuple: [uint64, uint64] = [1, 2];
tuple[0] = 5;
return tuple; // [5, 2]

Currently, PuyaTS maps tuple types to the Puya WTuple type, which is defined as immutable Proposed solution will use the proposed mutable native struct type with synthetic compiled generated field names

type Foo = {
a: uint64;
b: uint64;
};
const foo: Foo = { a: 1, b: 2 };
foo.a = 3;
return foo; // { a: 3, b: 2 }
type Foo = {
a: uint64;
b: uint64;
};
let foo: Foo = { a: 1, b: 2 };
foo = {...foo, a: 3};
return foo; // { a: 3, b: 2 }
class Foo extends MutableObject<{
a: uint64
b: uint64
}> { }
const foo: Foo = new Foo({ a: 1, b: 2 })
foo.a = 3
return foo // { a: 3, b: 2 }
this.validatorList(poolKey.id).value.pools[poolKey.poolId - 1].totalAlgoStaked += algoToAdd
this.validatorList(poolKey.id).value.state.totalAlgoStaked += algoToAdd
this.validatorList(poolKey.id).value.state.rewardTokenHeldBack += rewardTokenAmountReserved
const validator = this.validatorList(poolKey.id).value
validator.pools[poolKey.poolId - 1].totalAlgoStaked += algoToAdd
validator.state.totalAlgoStaked += algoToAdd
validator.state.rewardTokenHeldBack += rewardTokenAmountReserved
const validator = this.validatorList(poolKey.id).value.copy()
const poolIdx: uint64 = poolKey.id.native - 1
let poolTotalAlgoStaked = validator.pools[poolIdx].totalAlgoStaked
poolTotalAlgoStaked = new arc4.UintN64(poolTotalAlgoStaked.native + algoToAdd)
this.validatorList(poolKey.id).value.pools[poolIdx].totalAlgoStaked = poolTotalAlgoStaked
this.validatorList(poolKey.id).value.state.totalAlgoStaked = new arc4.UintN64(
validator.state.totalAlgoStaked.native + algoToAdd,
)
this.validatorList(poolKey.id).value.state.rewardTokenHeldBack = new arc4.UintN64(
validator.state.rewardTokenHeldBack.native + rewardTokenAmountReserved,
)
this.totalAlgoStaked.value = new arc4.UintN64(this.totalAlgoStaked.value.native + algoToAdd)
this.validatorList(poolKey.id).value.pools[poolKey.poolId - 1].totalAlgoStaked += algoToAdd
this.validatorList(poolKey.id).value.state.totalAlgoStaked += algoToAdd
this.validatorList(poolKey.id).value.state.rewardTokenHeldBack += rewardTokenAmountReserved
const validator = this.validatorList.map(poolKey.id)
validator.value.pools[poolKey.poolId - 1].totalAlgoStaked += algoToAdd
validator.value.state.totalAlgoStaked += algoToAdd
validator.value.state.rewardTokenHeldBack += rewardTokenAmountReserved
const cardFundData = this.cardFunds(cardFund).value;
const eventData: CardFundCloseEventData = {
cardFundOwner: cardFundData.owner,
cardFund: cardFund,
partnerChannel: cardFundData.partnerChannel,
reference: cardFundData.reference
};
const partnerCardFundOwnerKeyData: PartnerCardFundData = {
partnerChannel: cardFundData.partnerChannel,
cardFundOwner: cardFundData.owner,
};
const partnerCardFundOwnerKey = sha256(rawBytes(partnerCardFundOwnerKeyData));
sendPayment({
sender: cardFund,
receiver: cardFund,
amount: 0,
closeRemainderTo: this.txn.sender,
});
const boxCost = this.getCardFundBoxMbr(cardFundData.reference);
const cardFundData = this.cardFunds(cardFund);
const eventData: CardFundCloseEventData = {
cardFundOwner: cardFundData.value.owner,
cardFund: cardFund,
partnerChannel: cardFundData.value.partnerChannel,
reference: cardFundData.value.reference
};
const partnerCardFundOwnerKeyData: PartnerCardFundData = {
partnerChannel: cardFundData.value.partnerChannel,
cardFundOwner: cardFundData.value.owner,
};
const partnerCardFundOwnerKey = sha256(arc4.encode(partnerCardFundOwnerKeyData));
sendPayment({
sender: cardFund,
receiver: cardFund,
amount: 0,
closeRemainderTo: this.txn.sender,
});
const boxCost = this.getCardFundBoxMbr(cardFundData.value.reference);
class ARC75 extends Contract {
whitelist = BoxMap<Whitelist, uint64[]>();
setAppWhitelist(appIDs: uint64[]): void {
this.whitelist(whitelist).value = appIDs;
}
deleteAppFromWhitelist(appID: uint64, index: uint64) {
const spliced = this.whitelist(whitelist).value.splice(index, 1);
assert(spliced[0] === appID);
}
}
export class ARC75 extends Contract {
whitelist = BoxMap<Whitelist, DynamicArray<UintN64>>({ keyPrefix: Bytes() });
setAppWhitelist(appIDs: uint64[]): void {
// error: Not Supported: Accessing member map on Array<uint64>
// const newWhitelist = new DynamicArray(...appIDs.map(appID => new UintN64(appID)));
const newWhitelist = new DynamicArray<UintN64>();
for (let i: uint64 = 0; i < appIDs.length; i++) {
newWhitelist.push(new UintN64(appIDs[i]));
}
this.whitelist(whitelist).value = newWhitelist.copy();
}
deleteAppFromWhitelist(index: uint64) {
// `splice` is not available in `DynamicArray`
const spliced = this.whitelist(whitelist).value.at(index);
const newWhiteList = new DynamicArray<UintN64>();
for (let i: uint64 = 0; i < this.whitelist(whitelist).value.length; i++) {
if (i !== index) {
newWhiteList.push(this.whitelist(whitelist).value.at(i));
}
}
this.whitelist(whitelist).value = newWhiteList.copy();
assert(spliced.native === appID);
}
}
export class ARC75 extends Contract {
whitelist = BoxMap<Whitelist, uint64[]>({ keyPrefix: Bytes() });
setAppWhitelist(appIDs: uint64[]): void {
this.whitelist(whitelist).value = appIDs
}
deleteAppFromWhitelist(appID: uint64, index: uint64) {
// `splice` is not available in `DynamicArray`
const spliced = this.whitelist(whitelist).value.at(index);
const newWhiteList: uint64[] = [];
for (let i: uint64 = 0; i < this.whitelist(whitelist).value.length; i++) {
if (i !== index) {
newWhiteList.push(this.whitelist(whitelist).value.at(i));
}
}
this.whitelist(whitelist).value = newWhiteList.copy();
assert(spliced === appID);
}
}
export class ARC75 extends Contract {
whitelist = BoxMap<Whitelist, uint64[]>({ keyPrefix: Bytes() });
setAppWhitelist(appIDs: uint64[]): void {
this.whitelist(whitelist).value = appIDs
}
deleteAppFromWhitelist(appID: uint64, index: uint64) {
const spliced = this.whitelist(whitelist).value.splice(index, 1);
assert(spliced[0] === appID);
}
}
export class ARC75 extends Contract {
whitelist = BoxMap<Whitelist, uint64[]>({ keyPrefix: Bytes() });
setAppWhitelist(appIDs: uint64[]): void {
this.whitelist(whitelist).value = appIDs
}
deleteAppFromWhitelist(appID: uint64, index: uint64) {
const deleted = this.whitelist(whitelist).value.pop(index);
assert(deleted === appID);
}
}

Immutable: Array elements cannot be directly mutated/changed

Mutable: Array elements can be directly mutated/changed

Native: Elements can be native (bytes, uint64) or ARC-4 (uint, byte, bool, address, string etc) values

ARC-4
Elements can be ARC-4 values (uint<N>, byte, bool, address, string etc)
Fixed
Elements cannot be added/deleted (size is fixed)
Dynamic
Elements can be added/deleted (size is dynamic)
Value
By value semantics. Memory is allocated on the stack.
Reference
By reference semantics, all references see modifications. May use scratch slots.
Immutable Native Dynamic ValueMutable Native* Dynamic ReferenceMutable Native Fixed ValueMutable Native Dynamic ValueMutable ARC4 Fixed ValueMutable ARC4 Dynamic Value
py (current)ImmutableArrayArrayNANAarc4.StaticArrayarc4.DynamicArray
py (future)ImmutableArrayReferenceArrayFixedArrayArrayarc4.StaticArrayarc4.DynamicArray
TS (current)[], ArrayMutableArrayNANAarc4.StaticArrayarc4.DynamicArray
TS (future)ImmutableArrayReferenceArrayFixedArray[], Arrayarc4.StaticArrayarc4.DynamicArray

Algorand Python

  • Array is renamed to ReferenceArray.
  • Array is repurposed to have value semantics.

Algorand Typescript

  • Array is renamed to ImmutableArray
  • Array is repurposed to be mutable.

Recommendation: 1.0

Decision: 1.0

Recommendation: 1.0

Decision: 1.0

Recommendation: 1.0

Decision: 1.0

Recommendation: 1.0

Decision: 1.0

Recommendation: 1.0

Decision: 1.0

Mutable Native Dynamic Array splice and/or pop support

Section titled “Mutable Native Dynamic Array splice and/or pop support”

Recommendation: Post 1.0

Decision: Post 1.0

Recommendation: Not Required

Decision: Not Required