Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/awst_build/arc4-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,9 @@ export function isArc4EncodableType(ptype: PType): boolean {
if (ptype.equals(accountPType)) return true
if (ptype instanceof ReadonlyTuplePType) return ptype.items.every((i) => isArc4EncodableType(i))
if (ptype instanceof MutableTuplePType) return ptype.items.every((i) => isArc4EncodableType(i))
if (ptype instanceof ImmutableObjectPType) return ptype.orderedProperties().every(([, pt]) => isArc4EncodableType(pt))
if (ptype instanceof MutableObjectPType) return ptype.orderedProperties().every(([, pt]) => isArc4EncodableType(pt))
if (ptype instanceof ImmutableObjectPType)
return !ptype.runtimeOnly && ptype.orderedProperties().every(([, pt]) => isArc4EncodableType(pt))
if (ptype instanceof MutableObjectPType) return !ptype.runtimeOnly && ptype.orderedProperties().every(([, pt]) => isArc4EncodableType(pt))
if (ptype instanceof ArrayPType || ptype instanceof FixedArrayPType || ptype instanceof ReadonlyArrayPType)
return isArc4EncodableType(ptype.elementType)
return false
Expand Down Expand Up @@ -216,15 +217,15 @@ export function ptypeToArc4EncodedType(ptype: PType, sourceLocation: SourceLocat
if (ptype instanceof MutableTuplePType)
return new ARC4TupleType({ types: ptype.items.map((i) => ptypeToArc4EncodedType(i, sourceLocation)) })

if (ptype instanceof ImmutableObjectPType)
if (ptype instanceof ImmutableObjectPType && !ptype.runtimeOnly)
return new ARC4StructType({
name: ptype.alias?.name ?? ptype.name,
module: ptype.module,
description: ptype.description,
fields: Object.fromEntries(ptype.orderedProperties().map(([p, pt]) => [p, ptypeToArc4EncodedType(pt, sourceLocation)])),
frozen: true,
})
if (ptype instanceof MutableObjectPType)
if (ptype instanceof MutableObjectPType && !ptype.runtimeOnly)
return new ARC4StructType({
name: ptype.alias?.name ?? ptype.name,
module: ptype.module,
Expand Down
11 changes: 10 additions & 1 deletion src/awst_build/context/awst-build-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ContractReference, LogicSigReference } from '../../awst/models'
import { nodeFactory } from '../../awst/node-factory'
import type { AppStorageDefinition, ARC4MethodConfig } from '../../awst/nodes'
import { SourceLocation } from '../../awst/source-location'
import { PuyaError } from '../../errors'
import { logger } from '../../logger'
import { invariant } from '../../util'
import { AbsolutePath } from '../../util/absolute-path'
Expand Down Expand Up @@ -337,7 +338,15 @@ class AwstBuildContextImpl extends AwstBuildContext {
`Redefinition of app storage member, original declared in ${declaration.sourceLocation}`,
)
}
result.set(memberName, declaration.definition)
try {
result.set(memberName, declaration.definition)
} catch (e) {
if (e instanceof PuyaError) {
logger.error(e)
} else {
throw e
}
Comment thread
iglosiggio marked this conversation as resolved.
}
}
}
return Array.from(result.values())
Expand Down
2 changes: 1 addition & 1 deletion src/awst_build/eb/arc4/struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class StructClassBuilder extends ClassBuilder {
genericTypeArgs: 1,
callLocation: sourceLocation,
funcName: this.typeDescription,
argSpec: (a) => [a.required(new ImmutableObjectPType({ properties: this.ptype.instanceType.fields }))],
argSpec: (a) => [a.required(new ImmutableObjectPType({ properties: this.ptype.instanceType.fields, runtimeOnly: false }))],
})
const initialSingle = initialValues.singleEvaluation()

Expand Down
3 changes: 2 additions & 1 deletion src/awst_build/eb/storage/box/box-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { PType } from '../../../ptypes'
import { BoxMapPType, BoxPType, bytesPType, stringPType } from '../../../ptypes'
import { FunctionBuilder, type NodeBuilder } from '../../index'
import { parseFunctionArgs } from '../../util/arg-parsing'
import { extractKey } from '../util'
import { assertCanBeUsedForStorage, extractKey } from '../util'
import { BoxProxyExpressionBuilder } from './base'
import { BoxExpressionBuilder } from './box'

Expand All @@ -26,6 +26,7 @@ export class BoxMapFunctionBuilder extends FunctionBuilder {
genericTypeArgs: 2,
argSpec: (a) => [a.obj({ keyPrefix: a.required(bytesPType, stringPType) })],
})
assertCanBeUsedForStorage(contentPType, sourceLocation)

const ptype = new BoxMapPType({ content: contentPType, keyType: keySuffixType })
return new BoxMapExpressionBuilder(extractKey(keyPrefix, wtypes.boxKeyWType, sourceLocation), ptype)
Expand Down
3 changes: 2 additions & 1 deletion src/awst_build/eb/storage/box/box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { boolPType, BoxPType, bytesPType, ReadonlyTuplePType, stringPType, uint6
import { instanceEb } from '../../../type-registry'
import { FunctionBuilder, type NodeBuilder } from '../../index'
import { parseFunctionArgs } from '../../util/arg-parsing'
import { extractKey } from '../util'
import { assertCanBeUsedForStorage, extractKey } from '../util'
import { boxExists, boxLength, BoxProxyExpressionBuilder, boxValue, BoxValueExpressionBuilder } from './base'

export class BoxFunctionBuilder extends FunctionBuilder {
Expand All @@ -25,6 +25,7 @@ export class BoxFunctionBuilder extends FunctionBuilder {
genericTypeArgs: 1,
argSpec: (a) => [a.obj({ key: a.required(bytesPType, stringPType) })],
})
assertCanBeUsedForStorage(contentPType, sourceLocation)

const ptype = new BoxPType({ content: contentPType })
return new BoxExpressionBuilder(extractKey(key, wtypes.boxKeyWType, sourceLocation), ptype)
Expand Down
3 changes: 2 additions & 1 deletion src/awst_build/eb/storage/global-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { NodeBuilder } from '../index'
import { FunctionBuilder, InstanceExpressionBuilder } from '../index'
import { parseFunctionArgs } from '../util/arg-parsing'
import { VoidExpressionBuilder } from '../void-expression-builder'
import { extractKey } from './util'
import { assertCanBeUsedForStorage, extractKey } from './util'

export class GlobalStateFunctionBuilder extends FunctionBuilder {
constructor(sourceLocation: SourceLocation) {
Expand All @@ -28,6 +28,7 @@ export class GlobalStateFunctionBuilder extends FunctionBuilder {
if (ptype.contentType.equals(numberPType)) {
logger.addCodeFix(new GlobalStateNumber({ sourceLocation }))
}
assertCanBeUsedForStorage(ptype.contentType, sourceLocation)
const {
args: [{ initialValue, key }],
} = parseFunctionArgs({
Expand Down
3 changes: 2 additions & 1 deletion src/awst_build/eb/storage/local-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { instanceEb } from '../../type-registry'
import { FunctionBuilder, InstanceBuilder, InstanceExpressionBuilder, NodeBuilder } from '../index'
import { parseFunctionArgs } from '../util/arg-parsing'
import { VoidExpressionBuilder } from '../void-expression-builder'
import { extractKey } from './util'
import { assertCanBeUsedForStorage, extractKey } from './util'

export class LocalStateFunctionBuilder extends FunctionBuilder {
constructor(sourceLocation: SourceLocation) {
Expand All @@ -23,6 +23,7 @@ export class LocalStateFunctionBuilder extends FunctionBuilder {

call(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation<ts.CallExpression>): NodeBuilder {
const [contentPType] = typeArgs
assertCanBeUsedForStorage(contentPType, sourceLocation)
const {
args: [{ key }],
} = parseFunctionArgs({
Expand Down
13 changes: 13 additions & 0 deletions src/awst_build/eb/storage/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { Expression } from '../../../awst/nodes'
import { BytesConstant } from '../../../awst/nodes'
import type { SourceLocation } from '../../../awst/source-location'
import type { wtypes } from '../../../awst/wtypes'
import { CodeError } from '../../../errors'
import type { PType } from '../../ptypes'
import { ImmutableObjectPType, MutableObjectPType, TransientType, UnsupportedType } from '../../ptypes'
import type { InstanceBuilder } from '../index'

export function extractKey(key: InstanceBuilder, keyWType: wtypes.WType, sourceLocation: SourceLocation): Expression
Expand All @@ -29,3 +32,13 @@ export function extractKey(
})
}
}

export function assertCanBeUsedForStorage(ptype: PType, sourceLocation?: SourceLocation) {
if (ptype instanceof UnsupportedType || ptype instanceof TransientType) {
throw new CodeError(`Type ${ptype} cannot be used for storage`, { sourceLocation })
}
if ((ptype instanceof MutableObjectPType || ptype instanceof ImmutableObjectPType) && ptype.runtimeOnly) {
const ptypeName = ptype.alias?.fullName || ptype.toString()
throw new CodeError(`Type ${ptypeName} cannot be used for storage`, { sourceLocation })
}
}
6 changes: 1 addition & 5 deletions src/awst_build/models/app-storage-declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { nodeFactory } from '../../awst/node-factory'
import type { AppStorageDefinition, BytesConstant } from '../../awst/nodes'
import { AppStorageKind, BytesEncoding } from '../../awst/nodes'
import type { SourceLocation } from '../../awst/source-location'
import { CodeError } from '../../errors'
import { invariant, utf8ToUint8Array } from '../../util'
import type { AppStorageType, ContractClassPType } from '../ptypes'
import { BoxMapPType, BoxPType, GlobalStateType, LocalStateType, TransientType, UnsupportedType } from '../ptypes'
import { BoxMapPType, BoxPType, GlobalStateType, LocalStateType } from '../ptypes'

export class AppStorageDeclaration {
readonly memberName: string
Expand Down Expand Up @@ -56,9 +55,6 @@ export class AppStorageDeclaration {
}

get definition(): AppStorageDefinition {
if (this.ptype.contentType instanceof UnsupportedType || this.ptype.contentType instanceof TransientType) {
throw new CodeError(`Type ${this.ptype.contentType} cannot be used for storage`, { sourceLocation: this.sourceLocation })
}
return nodeFactory.appStorageDefinition({
...this,
kind: this.kind,
Expand Down
3 changes: 2 additions & 1 deletion src/awst_build/ptypes/arc4-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export class ARC4StructType extends ARC4EncodedType {
}

get nativeType(): MutableObjectPType {
return new MutableObjectPType({ properties: this.fields })
return new MutableObjectPType({ properties: this.fields, runtimeOnly: false })
}

get wtype(): wtypes.ARC4Struct {
Expand Down Expand Up @@ -639,6 +639,7 @@ export class TypedApplicationCallResponseType extends ImmutableObjectPType {
itxn: applicationItxnType,
returnValue,
},
runtimeOnly: false,
})
this.name = `TypedApplicationCallResponseType<${returnValue.name}>`
this.returnValue = returnValue
Expand Down
58 changes: 53 additions & 5 deletions src/awst_build/ptypes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,19 +224,25 @@ export class IntersectionPType extends TransientType {
module: 'lib.d.ts',
singleton: false,
typeMessage: transientTypeErrors.intersectionTypes(name).usedAsType,
expressionMessage: transientTypeErrors.unionTypes(name).usedInExpression,
expressionMessage: transientTypeErrors.intersectionTypes(name).usedInExpression,
})
this.types = types
}

static fromTypes(types: PType[]) {
static fromTypes(types: PType[], sourceLocation: SourceLocation, description?: string, alias?: SymbolName) {
if (types.length === 0) {
throw new InternalError('Cannot create intersection of zero types')
}
const distinctTypes = types.filter(distinctByEquality((a, b) => a.equals(b))).toSorted(sortBy((t) => t.fullName))
if (distinctTypes.length === 1) {
return distinctTypes[0]
}
if (types.every((p) => p instanceof ImmutableObjectPType)) {
return objectTypeFromIntersectionParts(ImmutableObjectPType, types, sourceLocation, description, alias)
}
if (types.every((p) => p instanceof MutableObjectPType)) {
return objectTypeFromIntersectionParts(MutableObjectPType, types, sourceLocation, description, alias)
}
return new IntersectionPType({
types: distinctTypes,
})
Expand Down Expand Up @@ -932,28 +938,31 @@ abstract class ObjectPType extends PType {
readonly properties: Record<string, PType>
readonly singleton = false
readonly immutable: boolean
readonly runtimeOnly: boolean

constructor(props: {
alias?: SymbolName | null
properties: Record<string, PType>
description?: string
namePrefix: string
immutable: boolean
runtimeOnly: boolean
}) {
super()
this.name = `${props.namePrefix}${generateObjectHash(props.properties)}`
this.properties = props.properties
this.description = props.description
this.alias = props.alias ?? null
this.immutable = props.immutable
this.runtimeOnly = props.runtimeOnly
}

orderedProperties() {
return Object.entries(this.properties)
}

toString(): string {
return `{${this.orderedProperties()
return `${this.runtimeOnly ? '@runtimeOnly' : ''}{${this.orderedProperties()
.map((p) => `${this.immutable ? 'readonly ' : ''}${p[0]}:${p[1].name}`)
.join(',')}}`
}
Expand All @@ -973,6 +982,7 @@ export class ObjectLiteralPType extends ObjectPType {
...props,
namePrefix: `ObjectLiteral`,
immutable: false,
runtimeOnly: false,
})
}

Expand All @@ -985,13 +995,15 @@ export class ObjectLiteralPType extends ObjectPType {
alias: this.alias,
properties: this.properties,
description: this.description,
runtimeOnly: this.runtimeOnly,
})
}
getMutable(): MutableObjectPType {
return new MutableObjectPType({
alias: this.alias,
properties: this.properties,
description: this.description,
runtimeOnly: this.runtimeOnly,
})
}

Expand All @@ -1016,7 +1028,7 @@ export class ObjectLiteralPType extends ObjectPType {
export class ImmutableObjectPType extends ObjectPType {
readonly [PType.IdSymbol] = 'ImmutableObjectPType'

constructor(props: { alias?: SymbolName | null; properties: Record<string, PType>; description?: string }) {
constructor(props: { alias?: SymbolName | null; properties: Record<string, PType>; description?: string; runtimeOnly: boolean }) {
super({
...props,
namePrefix: `ReadonlyObject`,
Expand Down Expand Up @@ -1049,7 +1061,7 @@ export class ImmutableObjectPType extends ObjectPType {
export class MutableObjectPType extends ObjectPType {
readonly [PType.IdSymbol] = 'MutableObjectPType'

constructor(props: { alias?: SymbolName | null; properties: Record<string, PType>; description?: string }) {
constructor(props: { alias?: SymbolName | null; properties: Record<string, PType>; description?: string; runtimeOnly: boolean }) {
super({
...props,
namePrefix: `Object`,
Expand All @@ -1071,6 +1083,7 @@ export class MutableObjectPType extends ObjectPType {
alias: this.alias,
properties: this.properties,
description: this.description,
runtimeOnly: this.runtimeOnly,
})
}

Expand Down Expand Up @@ -1859,6 +1872,7 @@ export const compiledContractType = new ImmutableObjectPType({
localUints: uint64PType,
localBytes: uint64PType,
},
runtimeOnly: false,
})
export const compiledLogicSigType = new ImmutableObjectPType({
alias: new SymbolName({
Expand All @@ -1869,6 +1883,7 @@ export const compiledLogicSigType = new ImmutableObjectPType({
properties: {
account: accountPType,
},
runtimeOnly: false,
})

export const arc28EmitFunction = new LibFunctionType({
Expand Down Expand Up @@ -1967,3 +1982,36 @@ export const validateEncodingFunctionPType = new LibFunctionType({
name: 'validateEncoding',
module: Constants.moduleNames.algoTs.util,
})

function objectTypeFromIntersectionParts(
constructor: typeof ImmutableObjectPType | typeof MutableObjectPType,
types: ObjectPType[],
sourceLocation: SourceLocation,
description?: string,
alias?: SymbolName,
) {
const allProperties = new Map<string, PType[]>()
for (const type of types) {
for (const [propName, propType] of type.orderedProperties()) {
let propTypes = allProperties.get(propName)
if (propTypes === undefined) {
propTypes = []
allProperties.set(propName, propTypes)
}
propTypes.push(propType)
}
}

const properties: Record<string, PType> = {}
for (const [propName, propTypes] of allProperties.entries()) {
if (propName.startsWith('__@')) {
// Symbol property - ignore
// TODO: Check AST nodes to confirm?
continue
}
const ptype = propTypes.length === 1 ? propTypes[0] : IntersectionPType.fromTypes(propTypes, sourceLocation, undefined)
properties[propName] = ptype
}

return new constructor({ alias, properties, description, runtimeOnly: true })
}
4 changes: 2 additions & 2 deletions src/awst_build/ptypes/transient-type-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export const transientTypeErrors = {
usedInExpression: `Union types are only valid in boolean expressions. Expression type is ${typeName}`,
}),
intersectionTypes: (typeName) => ({
usedAsType: `Intersection types are not valid as a variable, parameter, return, or property type. Expression type is ${typeName}`,
usedInExpression: `Intersection types not valid here. Expression type is ${typeName}`,
usedAsType: `Some intersection types are not valid as a variable, parameter, return, or property type. Expression type is ${typeName}`,
usedInExpression: `Some intersection types not valid here. Expression type is ${typeName}`,
}),
optionalFields: (typeName) => ({
usedAsType: `${typeName} type should not be used explicitly as it contains optional fields which cannot be interrogated at runtime. Either remove the type annotation or use \`EXPRESSION satisfies ${typeName}\``,
Expand Down
Loading