Skip to content

Commit

Permalink
fix: Generate a name for anonymous types used in ABI methods + stabil…
Browse files Browse the repository at this point in the history
…ise output for SingleEval nodes
  • Loading branch information
tristanmenzel committed Nov 25, 2024
1 parent 9747254 commit 49f0cd6
Show file tree
Hide file tree
Showing 43 changed files with 27,838 additions and 1,838 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
"scripts": {
"postinstall": "npx patch-package",
"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 --output-awst --output-awst-json --output-ssa-ir --out-dir out/[name] --optimization-level 0",
"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/biguint-expressions.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --log-level info --out-dir out/[name]",
"dev:testing": "tsx src/cli.ts build tests/approvals/named-types.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --log-level debug --out-dir out/[name]",
"audit": "better-npm-audit audit",
"format": "prettier --write .",
"lint": "eslint \"src/**/*.ts\"",
Expand Down
11 changes: 10 additions & 1 deletion src/awst/json-serialize-awst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { invariant } from '../util'
import { buildBase85Encoder } from '../util/base-85'
import { ARC4ABIMethodConfig, ContractReference, LogicSigReference } from './models'
import type { RootNode } from './nodes'
import { IntrinsicCall } from './nodes'
import { IntrinsicCall, SingleEvaluation } from './nodes'
import { SourceLocation } from './source-location'
import { SymbolToNumber } from './util'

export class SnakeCaseSerializer<T> {
constructor(private readonly spaces = 2) {}
Expand All @@ -35,6 +36,7 @@ export class AwstSerializer extends SnakeCaseSerializer<RootNode[]> {
) {
super()
}
#singleEvals = new SymbolToNumber()
private b85 = buildBase85Encoder()

protected serializerFunction(key: string, value: unknown): unknown {
Expand Down Expand Up @@ -97,6 +99,13 @@ export class AwstSerializer extends SnakeCaseSerializer<RootNode[]> {
file: filePath,
}
}
if (value instanceof SingleEvaluation) {
return {
_type: SingleEvaluation.name,
...(super.serializerFunction(key, value) as object),
id: String(this.#singleEvals.forSymbol(value.id)[0]),
}
}
if (value instanceof ARC4ABIMethodConfig) {
// TODO: This can be removed once puya has been updated to support a more advanced default args schema
return {
Expand Down
3 changes: 1 addition & 2 deletions src/awst/node-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import { wtypes } from './wtypes'

type ConcreteNodes = typeof concreteNodes

let singleEval = 0n
const explicitNodeFactory = {
voidConstant(props: { sourceLocation: SourceLocation }): VoidConstant {
return new VoidConstant({
Expand Down Expand Up @@ -133,7 +132,7 @@ const explicitNodeFactory = {
},
singleEvaluation({ source }: { source: Expression }) {
return new SingleEvaluation({
id: singleEval++,
id: Symbol(),
sourceLocation: source.sourceLocation,
wtype: source.wtype,
source,
Expand Down
21 changes: 18 additions & 3 deletions src/awst/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ export class DecimalConstant extends Expression {
export class BoolConstant extends Expression {
constructor(props: Props<BoolConstant>) {
super(props)
this.wtype = props.wtype
this.value = props.value
this.wtype = props.wtype
}
value: boolean
accept<T>(visitor: ExpressionVisitor<T>): T {
Expand Down Expand Up @@ -193,8 +193,8 @@ export class BytesConstant extends Expression {
export class StringConstant extends Expression {
constructor(props: Props<StringConstant>) {
super(props)
this.wtype = props.wtype
this.value = props.value
this.wtype = props.wtype
}
value: string
accept<T>(visitor: ExpressionVisitor<T>): T {
Expand Down Expand Up @@ -538,7 +538,7 @@ export class SingleEvaluation extends Expression {
this.sourceLocation = props.sourceLocation
}
source: Expression
id: bigint
id: symbol
sourceLocation: SourceLocation
accept<T>(visitor: ExpressionVisitor<T>): T {
return visitor.visitSingleEvaluation(this)
Expand Down Expand Up @@ -969,6 +969,19 @@ export class BytesAugmentedAssignment extends Statement {
return visitor.visitBytesAugmentedAssignment(this)
}
}
export class Emit extends Expression {
constructor(props: Props<Emit>) {
super(props)
this.signature = props.signature
this.value = props.value
this.wtype = props.wtype
}
signature: string
value: Expression
accept<T>(visitor: ExpressionVisitor<T>): T {
return visitor.visitEmit(this)
}
}
export class Range extends Expression {
constructor(props: Props<Range>) {
super(props)
Expand Down Expand Up @@ -1355,6 +1368,7 @@ export const concreteNodes = {
uInt64AugmentedAssignment: UInt64AugmentedAssignment,
bigUIntAugmentedAssignment: BigUIntAugmentedAssignment,
bytesAugmentedAssignment: BytesAugmentedAssignment,
emit: Emit,
range: Range,
enumeration: Enumeration,
reversed: Reversed,
Expand Down Expand Up @@ -1429,6 +1443,7 @@ export interface ExpressionVisitor<T> {
visitBytesBinaryOperation(expression: BytesBinaryOperation): T
visitBooleanBinaryOperation(expression: BooleanBinaryOperation): T
visitNot(expression: Not): T
visitEmit(expression: Emit): T
visitRange(expression: Range): T
visitEnumeration(expression: Enumeration): T
visitReversed(expression: Reversed): T
Expand Down
17 changes: 11 additions & 6 deletions src/awst/to-code-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { TodoError } from '../errors'
import { logger } from '../logger'
import { uint8ArrayToBase32, uint8ArrayToUtf8 } from '../util'
import type { ContractReference } from './models'
import type { AppStorageDefinition, ContractMemberNodeVisitor, ExpressionVisitor, RootNodeVisitor, StatementVisitor } from './nodes'
import type { AppStorageDefinition, ContractMemberNodeVisitor, Emit, ExpressionVisitor, RootNodeVisitor, StatementVisitor } from './nodes'
import * as nodes from './nodes'
import { AppStorageKind, BytesEncoding, ContractMethodTarget, InstanceMethodTarget, InstanceSuperMethodTarget, SubroutineID } from './nodes'
import { SymbolToNumber } from './util'
import { wtypes } from './wtypes'

function printBytes(value: Uint8Array, encoding: BytesEncoding) {
Expand Down Expand Up @@ -41,7 +42,7 @@ export class ToCodeVisitor
visitAppStorageDefinition(contractMemberNode: AppStorageDefinition): string[] {
throw new Error('Method not implemented.')
}
#singleEval = new Set<bigint>()
#singleEval = new SymbolToNumber()
visitUInt64PostfixUnaryOperation(expression: nodes.UInt64PostfixUnaryOperation): string {
return `${expression.target.accept(this)}${expression.op}`
}
Expand Down Expand Up @@ -175,11 +176,11 @@ export class ToCodeVisitor
return `LocalState[${expression.account.accept(this)}][${expression.key.accept(this)}]`
}
visitSingleEvaluation(expression: nodes.SingleEvaluation): string {
if (this.#singleEval.has(expression.id)) {
return `#${expression.id}`
const [id, isNew] = this.#singleEval.forSymbol(expression.id)
if (!isNew) {
return `#${id}`
}
this.#singleEval.add(expression.id)
return `(#${expression.id} = ${expression.source.accept(this)})`
return `(#${id} = ${expression.source.accept(this)})`
}
visitReinterpretCast(expression: nodes.ReinterpretCast): string {
const target = expression.expr.accept(this)
Expand Down Expand Up @@ -319,6 +320,10 @@ export class ToCodeVisitor
'}',
]
}
visitEmit(expression: Emit): string {
throw new TodoError('Method not implemented.', { sourceLocation: expression.sourceLocation })
}

visitContractMethod(statement: nodes.ContractMethod): string[] {
const prefix = statement.cref.id === this.currentContract.at(-1)?.id ? '' : `${statement.cref.className}::`
return [`${prefix}${statement.memberName}(): ${statement.returnType}`, '{', ...indent(statement.body.accept(this)), '}', '']
Expand Down
14 changes: 14 additions & 0 deletions src/awst/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,17 @@ import { BoolConstant, BytesConstant, IntegerConstant, StringConstant } from './
export function isConstant(expr: Expression): expr is Constant {
return expr instanceof StringConstant || expr instanceof BytesConstant || expr instanceof IntegerConstant || expr instanceof BoolConstant
}

export class SymbolToNumber {
#symbols = new Map<symbol, number>()

forSymbol(sym: symbol): [number, boolean] {
let val = this.#symbols.get(sym)
if (val !== undefined) {
return [val, false]
}
val = this.#symbols.size
this.#symbols.set(sym, val)
return [val, true]
}
}
5 changes: 3 additions & 2 deletions src/awst/wtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,11 @@ export namespace wtypes {
}

toString(): string {
const displayName = this.name.split('::').at(-1) ?? this.name
if (this.names) {
return `${this.name === 'Anonymous' ? '' : this.name}{ ${this.names.map((n, i) => `${n}: ${this.types[i]}`).join(', ')} }`
return `${displayName}{ ${this.names.map((n, i) => `${n}: ${this.types[i]}`).join(', ')} }`
}
return `${this.immutable ? 'readonly' : ''}${this.name ?? ''}[${this.types.join(', ')}]`
return `${this.immutable ? 'readonly' : ''}${displayName}[${this.types.join(', ')}]`
}
}
export class WArray extends WType {
Expand Down
2 changes: 2 additions & 0 deletions src/awst_build/arc4-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ export function getFunctionTypes(ptype: FunctionPType, sourceLocation: SourceLoc
if ('output' in result) {
logger.error(sourceLocation, 'for compatibility with ARC-32, ARC-4 methods cannot have an argument named output')
}

result['output'] = ptype.returnType

return result
}

Expand Down
1 change: 0 additions & 1 deletion src/awst_build/ast-visitors/contract-method-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ export class ContractMethodVisitor extends ContractMethodBaseVisitor {
return new ARC4ABIMethodConfig({
sourceLocation: methodLocation,
allowedCompletionTypes: [OnCompletionAction.NoOp],

create: ARC4CreateOption.Disallow,
name: functionType.name,
readonly: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class ObjectLiteralExpressionBuilder extends LiteralExpressionBuilder {
// Resolve this object to a tuple using declared order but using the target property types.
// This will resolve numeric literals to algo-ts types if available
const tempType = new ObjectPType({
name: undefined,
isAnonymous: true,
properties: Object.fromEntries(this.ptype.orderedProperties().map(([p]) => [p, ptype.getPropertyType(p)] as const)),
})

Expand Down
24 changes: 19 additions & 5 deletions src/awst_build/ptypes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,15 @@ export class FunctionPType extends PType {
super()
this.name = props.name
this.module = props.module
this.returnType = props.returnType
if (props.returnType instanceof ObjectPType && props.returnType.isAnonymous) {
this.returnType = new ObjectPType({
name: `${props.name}Result`,
module: props.module,
properties: props.returnType.properties,
})
} else {
this.returnType = props.returnType
}
this.parameters = props.parameters
}
}
Expand Down Expand Up @@ -608,24 +616,30 @@ export class ArrayPType extends TransientType {
}
}

type ObjectPTypeArgs =
| { module: string; name: string; properties: Record<string, PType>; isAnonymous?: false }
| { module?: undefined; name?: undefined; properties: Record<string, PType>; isAnonymous: true }

export class ObjectPType extends PType {
readonly name: string
readonly module: string
readonly properties: Record<string, PType>
readonly singleton = false
readonly isAnonymous: boolean

constructor(props: { module?: string; name?: string; properties: Record<string, PType> }) {
constructor(props: ObjectPTypeArgs) {
super()
this.name = props.name ?? ''
this.module = props.module ?? ''
this.properties = props.properties
this.isAnonymous = props.isAnonymous ?? false
}

static anonymous(props: Record<string, PType> | Array<[string, PType]>) {
const properties = Array.isArray(props) ? Object.fromEntries(props) : props
return new ObjectPType({
name: 'Anonymous',
properties,
isAnonymous: true,
})
}

Expand All @@ -640,15 +654,15 @@ export class ObjectPType extends PType {
tupleNames.push(propName)
}
return new wtypes.WTuple({
name: this.name,
name: this.fullName,
names: tupleNames,
types: tupleTypes,
immutable: true,
})
}

orderedProperties() {
return Object.entries(this.properties) //.toSorted(sortBy(([key]) => key))
return Object.entries(this.properties)
}

getPropertyType(name: string): PType {
Expand Down
8 changes: 4 additions & 4 deletions tests/approvals.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ describe('Approvals', () => {
})

it('There should be no differences to committed changes', () => {
// Run git add to force line ending changes
sync('git', ['add', '.'], {
stdio: 'inherit',
})
// // Run git add to force line ending changes
// sync('git', ['add', '.'], {
// stdio: 'inherit',
// })
const result = sync('git', ['status', '--porcelain'], {
stdio: 'pipe',
})
Expand Down
13 changes: 4 additions & 9 deletions tests/approvals/named-types.algo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { uint64 } from '@algorandfoundation/algorand-typescript'
import { Contract, Uint64 } from '@algorandfoundation/algorand-typescript'
import { assertMatch, Contract, Uint64 } from '@algorandfoundation/algorand-typescript'

type XY = {
x: uint64
Expand All @@ -11,13 +11,6 @@ type YX = {
x: uint64
}

/**
* In TypeScript, objects with the same properties are considered equal regardless of declaration order however puya-ts
* should respect the declaration order when encoding an object as an ARC4 tuple. Ie. XY should be assignable to YX but
* when encoded as an ARC4 tuple they should be encoded as [X, Y] and [Y, X] respectively.
*
* TODO: This is not currently the case.
*/
export class MyContract extends Contract {
public getXY(): XY {
return {
Expand All @@ -40,7 +33,9 @@ export class MyContract extends Contract {
}
}

public test(x: XY, y: YX) {}
public test(x: XY, y: YX) {
assertMatch(x, { ...y })
}

public testing() {
const a = this.getXY()
Expand Down
Loading

0 comments on commit 49f0cd6

Please sign in to comment.