Skip to content

Commit

Permalink
feat: Allow stateTotals to be specified statically in contract options
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanmenzel committed Dec 7, 2024
1 parent a11c599 commit d9e3bfe
Show file tree
Hide file tree
Showing 29 changed files with 6,671 additions and 19 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dev:examples": "tsx src/cli.ts build examples --output-awst --output-awst-json",
"dev:approvals": "rimraf tests/approvals/out && tsx src/cli.ts build tests/approvals --dry-run",
"dev:expected-output": "tsx src/cli.ts build tests/expected-output --dry-run",
"dev:testing": "tsx src/cli.ts build tests/approvals/avm11.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --out-dir out/[name] --optimization-level=0",
"dev:testing": "tsx src/cli.ts build tests/approvals/state-totals.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --out-dir out/[name] --optimization-level=0",
"audit": "better-npm-audit audit",
"format": "prettier --write .",
"lint": "eslint \"src/**/*.ts\"",
Expand Down
4 changes: 2 additions & 2 deletions packages/algo-ts/src/base-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ type ContractOptions = {
* Allows defining what values should be used for global and local uint and bytes storage
* values when creating a contract. Used when outputting ARC-32 application.json schemas.
*
* If let unspecified, the totals will be determined by the compiler based on state
* If left unspecified, the totals will be determined by the compiler based on state
* variables assigned to `this`.
*
* This setting is not inherited, and only applies to the exact `Contract` it is specified
* on. If a base class does specify this setting, and a derived class does not, a warning
* will be emitted for the derived class. To resolve this warning, `state_totals` must be
* will be emitted for the derived class. To resolve this warning, `stateTotals` must be
* specified. An empty object may be provided in order to indicate that this contract should
* revert to the default behaviour
*/
Expand Down
26 changes: 22 additions & 4 deletions src/awst_build/eb/contract-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Constants } from '../../constants'
import { CodeError } from '../../errors'
import { codeInvariant, invariant } from '../../util'
import type { AwstBuildContext } from '../context/awst-build-context'
import type { ContractOptionsDecoratorData } from '../models/decorator-data'
import type { PType } from '../ptypes'
import {
arc4BaseContractType,
Expand All @@ -23,7 +24,7 @@ import { instanceEb } from '../type-registry'
import { BaseContractMethodExpressionBuilder, ContractMethodExpressionBuilder } from './free-subroutine-expression-builder'
import type { NodeBuilder } from './index'
import { DecoratorDataBuilder, FunctionBuilder, InstanceBuilder } from './index'
import { requireStringConstant } from './util'
import { requireIntegerConstant, requireStringConstant } from './util'
import { parseFunctionArgs } from './util/arg-parsing'
import { requireAvmVersion } from './util/avm-version'
import { VoidExpressionBuilder } from './void-expression-builder'
Expand Down Expand Up @@ -126,7 +127,7 @@ export class ContractOptionsDecoratorBuilder extends FunctionBuilder {
readonly ptype = contractOptionsDecorator
call(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation): NodeBuilder {
const {
args: [{ avmVersion, name }],
args: [{ avmVersion, name, stateTotals }],
} = parseFunctionArgs({
args,
typeArgs,
Expand All @@ -145,9 +146,26 @@ export class ContractOptionsDecoratorBuilder extends FunctionBuilder {

return new DecoratorDataBuilder(sourceLocation, {
type: 'contract',
avmVersion: avmVersion ? requireAvmVersion(avmVersion) : undefined,
name: name ? requireStringConstant(name).value : undefined,
avmVersion: avmVersion && requireAvmVersion(avmVersion),
name: name && requireStringConstant(name).value,
stateTotals: stateTotals && buildStateTotals(stateTotals),
sourceLocation,
})
}
}

function buildStateTotals(builder: NodeBuilder): ContractOptionsDecoratorData['stateTotals'] {
function tryGetProp(name: string): bigint | undefined {
if (builder.hasProperty(name)) {
return requireIntegerConstant(builder.memberAccess(name, builder.sourceLocation)).value
}
return undefined
}

return {
globalBytes: tryGetProp('globalBytes'),
globalUints: tryGetProp('globalUints'),
localBytes: tryGetProp('localBytes'),
localUints: tryGetProp('localUints'),
}
}
23 changes: 19 additions & 4 deletions src/awst_build/models/contract-class-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,23 @@ export class ContractClassModel {
this.options = props.options
}

hasExplicitStateTotals() {
return this.options?.stateTotals !== undefined
}

buildContract(compilationSet: CompilationSet): awst.Contract {
let approvalProgram: ContractMethod | null = this.approvalProgram
let clearProgram: ContractMethod | null = this.clearProgram
const methods: ContractMethod[] = [...this.methods, this.ctor]
const methodResolutionOrder: ContractReference[] = []

let firstBaseWithStateTotals: ContractClassModel | undefined = undefined
for (const baseType of this.type.allBases()) {
const cref = ContractReference.fromPType(baseType)
const baseClass = compilationSet.getContractClass(cref)
if (baseClass.hasExplicitStateTotals() && firstBaseWithStateTotals === undefined) {
firstBaseWithStateTotals = baseClass
}
methodResolutionOrder.push(cref)
approvalProgram ??= baseClass.approvalProgram
clearProgram ??= baseClass.clearProgram
Expand Down Expand Up @@ -91,7 +100,13 @@ export class ContractClassModel {
codeInvariant(approvalProgram, 'must have approval')
codeInvariant(clearProgram, 'must have clear')

// TODO: Tally from bases
if (!this.hasExplicitStateTotals && firstBaseWithStateTotals) {
logger.warn(
this.options?.sourceLocation ?? this.sourceLocation,
`Contract extends base contract ${firstBaseWithStateTotals.id} with explicit stateTotals, but does not define its own stateTotals. This could result in insufficient reserved state at run time. An empty object may be provided in order to indicate that this contract should revert to the default behaviour`,
)
}

const stateTotals = new StateTotals({
globalBytes: this.options?.stateTotals?.globalBytes ?? null,
globalUints: this.options?.stateTotals?.globalUints ?? null,
Expand Down Expand Up @@ -121,11 +136,11 @@ export class ContractClassModel {
clearProgram,
methodResolutionOrder,
methods,
appState: this.appState, // TODO: Tally from base
stateTotals: stateTotals, // TODO: Tally
appState: this.appState,
stateTotals,
reservedScratchSpace: reservedScratchSpace,
sourceLocation: this.sourceLocation,
avmVersion: this.options?.avmVersion ?? null, // TODO: Allow this to be set with class decorator
avmVersion: this.options?.avmVersion ?? null,
})
}

Expand Down
123 changes: 123 additions & 0 deletions tests/approvals/out/state-totals/BaseWithState.approval.teal
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#pragma version 10

tests/approvals/state-totals.algo.ts::BaseWithState.approvalProgram:
intcblock 0 1
bytecblock "oneGlobal"
txn ApplicationID
bnz main_after_if_else@2
callsub constructor

main_after_if_else@2:
callsub __puya_arc4_router__
return


// tests/approvals/state-totals.algo.ts::BaseWithState.constructor() -> void:
constructor:
// tests/approvals/state-totals.algo.ts:4
// export class BaseWithState extends Contract {
proto 0 0
// tests/approvals/state-totals.algo.ts:5
// oneGlobal = GlobalState({ initialValue: Uint64(1) })
bytec_0 // "oneGlobal"
intc_1 // 1
app_global_put
retsub


// tests/approvals/state-totals.algo.ts::BaseWithState.__puya_arc4_router__() -> uint64:
__puya_arc4_router__:
// tests/approvals/state-totals.algo.ts:4
// export class BaseWithState extends Contract {
proto 0 1
txn NumAppArgs
intc_0 // 0
!=
bz __puya_arc4_router___bare_routing@5
txna ApplicationArgs 0
pushbytes 0x2cd95aa1 // method "setState(uint64)void"
swap
match __puya_arc4_router___setState_route@2
b __puya_arc4_router___switch_case_default@3

__puya_arc4_router___setState_route@2:
// tests/approvals/state-totals.algo.ts:9
// setState(n: uint64) {
txn OnCompletion
intc_0 // NoOp
==
assert // OnCompletion is not NoOp
txn ApplicationID
intc_0 // 0
!=
assert // can only call when not creating
// tests/approvals/state-totals.algo.ts:4
// export class BaseWithState extends Contract {
txna ApplicationArgs 1
btoi
// tests/approvals/state-totals.algo.ts:9
// setState(n: uint64) {
callsub setState
intc_1 // 1
retsub

__puya_arc4_router___switch_case_default@3:
b __puya_arc4_router___after_if_else@9

__puya_arc4_router___bare_routing@5:
// tests/approvals/state-totals.algo.ts:4
// export class BaseWithState extends Contract {
txn OnCompletion
intc_0 // 0
swap
match __puya_arc4_router_____algots__.defaultCreate@6
b __puya_arc4_router___switch_case_default@7

__puya_arc4_router_____algots__.defaultCreate@6:
// tests/approvals/state-totals.algo.ts:4
// export class BaseWithState extends Contract {
txn ApplicationID
intc_0 // 0
==
assert // can only call when creating
callsub __algots__.defaultCreate
intc_1 // 1
retsub

__puya_arc4_router___switch_case_default@7:

__puya_arc4_router___after_if_else@9:
// tests/approvals/state-totals.algo.ts:4
// export class BaseWithState extends Contract {
intc_0 // 0
retsub


// tests/approvals/state-totals.algo.ts::BaseWithState.setState(n: uint64) -> void:
setState:
// tests/approvals/state-totals.algo.ts:9
// setState(n: uint64) {
proto 1 0
// tests/approvals/state-totals.algo.ts:5
// oneGlobal = GlobalState({ initialValue: Uint64(1) })
bytec_0 // "oneGlobal"
// tests/approvals/state-totals.algo.ts:10
// this.oneGlobal.value = n
frame_dig -1
app_global_put
// tests/approvals/state-totals.algo.ts:6
// twoGlobal = GlobalState<uint64>()
pushbytes "twoGlobal"
// tests/approvals/state-totals.algo.ts:11
// this.twoGlobal.value = n
frame_dig -1
app_global_put
retsub


// tests/approvals/state-totals.algo.ts::BaseWithState.__algots__.defaultCreate() -> void:
__algots__.defaultCreate:
// tests/approvals/state-totals.algo.ts:4
// export class BaseWithState extends Contract {
proto 0 0
retsub
69 changes: 69 additions & 0 deletions tests/approvals/out/state-totals/BaseWithState.arc32.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"hints": {
"setState(uint64)void": {
"call_config": {
"no_op": "CALL"
}
}
},
"source": {
"approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgp0ZXN0cy9hcHByb3ZhbHMvc3RhdGUtdG90YWxzLmFsZ28udHM6OkJhc2VXaXRoU3RhdGUuYXBwcm92YWxQcm9ncmFtOgogICAgaW50Y2Jsb2NrIDAgMQogICAgYnl0ZWNibG9jayAib25lR2xvYmFsIgogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGJueiBtYWluX2FmdGVyX2lmX2Vsc2VAMgogICAgY2FsbHN1YiBjb25zdHJ1Y3RvcgoKbWFpbl9hZnRlcl9pZl9lbHNlQDI6CiAgICBjYWxsc3ViIF9fcHV5YV9hcmM0X3JvdXRlcl9fCiAgICByZXR1cm4KCgovLyB0ZXN0cy9hcHByb3ZhbHMvc3RhdGUtdG90YWxzLmFsZ28udHM6OkJhc2VXaXRoU3RhdGUuY29uc3RydWN0b3IoKSAtPiB2b2lkOgpjb25zdHJ1Y3RvcjoKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czo0CiAgICAvLyBleHBvcnQgY2xhc3MgQmFzZVdpdGhTdGF0ZSBleHRlbmRzIENvbnRyYWN0IHsKICAgIHByb3RvIDAgMAogICAgLy8gdGVzdHMvYXBwcm92YWxzL3N0YXRlLXRvdGFscy5hbGdvLnRzOjUKICAgIC8vIG9uZUdsb2JhbCA9IEdsb2JhbFN0YXRlKHsgaW5pdGlhbFZhbHVlOiBVaW50NjQoMSkgfSkKICAgIGJ5dGVjXzAgLy8gIm9uZUdsb2JhbCIKICAgIGludGNfMSAvLyAxCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgcmV0c3ViCgoKLy8gdGVzdHMvYXBwcm92YWxzL3N0YXRlLXRvdGFscy5hbGdvLnRzOjpCYXNlV2l0aFN0YXRlLl9fcHV5YV9hcmM0X3JvdXRlcl9fKCkgLT4gdWludDY0OgpfX3B1eWFfYXJjNF9yb3V0ZXJfXzoKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czo0CiAgICAvLyBleHBvcnQgY2xhc3MgQmFzZVdpdGhTdGF0ZSBleHRlbmRzIENvbnRyYWN0IHsKICAgIHByb3RvIDAgMQogICAgdHhuIE51bUFwcEFyZ3MKICAgIGludGNfMCAvLyAwCiAgICAhPQogICAgYnogX19wdXlhX2FyYzRfcm91dGVyX19fYmFyZV9yb3V0aW5nQDUKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDAKICAgIHB1c2hieXRlcyAweDJjZDk1YWExIC8vIG1ldGhvZCAic2V0U3RhdGUodWludDY0KXZvaWQiCiAgICBzd2FwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRTdGF0ZV9yb3V0ZUAyCiAgICBiIF9fcHV5YV9hcmM0X3JvdXRlcl9fX3N3aXRjaF9jYXNlX2RlZmF1bHRAMwoKX19wdXlhX2FyYzRfcm91dGVyX19fc2V0U3RhdGVfcm91dGVAMjoKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czo5CiAgICAvLyBzZXRTdGF0ZShuOiB1aW50NjQpIHsKICAgIHR4biBPbkNvbXBsZXRpb24KICAgIGludGNfMCAvLyBOb09wCiAgICA9PQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGludGNfMCAvLyAwCiAgICAhPQogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czo0CiAgICAvLyBleHBvcnQgY2xhc3MgQmFzZVdpdGhTdGF0ZSBleHRlbmRzIENvbnRyYWN0IHsKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czo5CiAgICAvLyBzZXRTdGF0ZShuOiB1aW50NjQpIHsKICAgIGNhbGxzdWIgc2V0U3RhdGUKICAgIGludGNfMSAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX3N3aXRjaF9jYXNlX2RlZmF1bHRAMzoKICAgIGIgX19wdXlhX2FyYzRfcm91dGVyX19fYWZ0ZXJfaWZfZWxzZUA5CgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdANToKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czo0CiAgICAvLyBleHBvcnQgY2xhc3MgQmFzZVdpdGhTdGF0ZSBleHRlbmRzIENvbnRyYWN0IHsKICAgIHR4biBPbkNvbXBsZXRpb24KICAgIGludGNfMCAvLyAwCiAgICBzd2FwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19fX2FsZ290c19fLmRlZmF1bHRDcmVhdGVANgogICAgYiBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zd2l0Y2hfY2FzZV9kZWZhdWx0QDcKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX19fYWxnb3RzX18uZGVmYXVsdENyZWF0ZUA2OgogICAgLy8gdGVzdHMvYXBwcm92YWxzL3N0YXRlLXRvdGFscy5hbGdvLnRzOjQKICAgIC8vIGV4cG9ydCBjbGFzcyBCYXNlV2l0aFN0YXRlIGV4dGVuZHMgQ29udHJhY3QgewogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGludGNfMCAvLyAwCiAgICA9PQogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBjcmVhdGluZwogICAgY2FsbHN1YiBfX2FsZ290c19fLmRlZmF1bHRDcmVhdGUKICAgIGludGNfMSAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX3N3aXRjaF9jYXNlX2RlZmF1bHRANzoKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX2FmdGVyX2lmX2Vsc2VAOToKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czo0CiAgICAvLyBleHBvcnQgY2xhc3MgQmFzZVdpdGhTdGF0ZSBleHRlbmRzIENvbnRyYWN0IHsKICAgIGludGNfMCAvLyAwCiAgICByZXRzdWIKCgovLyB0ZXN0cy9hcHByb3ZhbHMvc3RhdGUtdG90YWxzLmFsZ28udHM6OkJhc2VXaXRoU3RhdGUuc2V0U3RhdGUobjogdWludDY0KSAtPiB2b2lkOgpzZXRTdGF0ZToKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czo5CiAgICAvLyBzZXRTdGF0ZShuOiB1aW50NjQpIHsKICAgIHByb3RvIDEgMAogICAgLy8gdGVzdHMvYXBwcm92YWxzL3N0YXRlLXRvdGFscy5hbGdvLnRzOjUKICAgIC8vIG9uZUdsb2JhbCA9IEdsb2JhbFN0YXRlKHsgaW5pdGlhbFZhbHVlOiBVaW50NjQoMSkgfSkKICAgIGJ5dGVjXzAgLy8gIm9uZUdsb2JhbCIKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czoxMAogICAgLy8gdGhpcy5vbmVHbG9iYWwudmFsdWUgPSBuCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyB0ZXN0cy9hcHByb3ZhbHMvc3RhdGUtdG90YWxzLmFsZ28udHM6NgogICAgLy8gdHdvR2xvYmFsID0gR2xvYmFsU3RhdGU8dWludDY0PigpCiAgICBwdXNoYnl0ZXMgInR3b0dsb2JhbCIKICAgIC8vIHRlc3RzL2FwcHJvdmFscy9zdGF0ZS10b3RhbHMuYWxnby50czoxMQogICAgLy8gdGhpcy50d29HbG9iYWwudmFsdWUgPSBuCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyB0ZXN0cy9hcHByb3ZhbHMvc3RhdGUtdG90YWxzLmFsZ28udHM6OkJhc2VXaXRoU3RhdGUuX19hbGdvdHNfXy5kZWZhdWx0Q3JlYXRlKCkgLT4gdm9pZDoKX19hbGdvdHNfXy5kZWZhdWx0Q3JlYXRlOgogICAgLy8gdGVzdHMvYXBwcm92YWxzL3N0YXRlLXRvdGFscy5hbGdvLnRzOjQKICAgIC8vIGV4cG9ydCBjbGFzcyBCYXNlV2l0aFN0YXRlIGV4dGVuZHMgQ29udHJhY3QgewogICAgcHJvdG8gMCAwCiAgICByZXRzdWIK",
"clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgp0ZXN0cy9hcHByb3ZhbHMvc3RhdGUtdG90YWxzLmFsZ28udHM6OkJhc2VXaXRoU3RhdGUuY2xlYXJTdGF0ZVByb2dyYW06CiAgICBwdXNoaW50IDEgLy8gMQogICAgcmV0dXJuCg=="
},
"state": {
"global": {
"num_byte_slices": 0,
"num_uints": 2
},
"local": {
"num_byte_slices": 1,
"num_uints": 0
}
},
"schema": {
"global": {
"declared": {
"oneGlobal": {
"type": "uint64",
"key": "oneGlobal"
},
"twoGlobal": {
"type": "uint64",
"key": "twoGlobal"
}
},
"reserved": {}
},
"local": {
"declared": {
"oneLocalBytes": {
"type": "bytes",
"key": "oneLocalBytes"
}
},
"reserved": {}
}
},
"contract": {
"name": "BaseWithState",
"methods": [
{
"name": "setState",
"args": [
{
"type": "uint64",
"name": "n"
}
],
"readonly": false,
"returns": {
"type": "void"
}
}
],
"networks": {}
},
"bare_call_config": {
"no_op": "CREATE"
}
}
Loading

0 comments on commit d9e3bfe

Please sign in to comment.