Skip to content

Commit

Permalink
refactor: Defer evaulation of function bodies until after contract me…
Browse files Browse the repository at this point in the history
…tadata has been gathered
  • Loading branch information
tristanmenzel committed Feb 20, 2025
1 parent 35f2dd5 commit 002f91b
Show file tree
Hide file tree
Showing 259 changed files with 8,734 additions and 3,185 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"dev:examples": "tsx src/cli.ts build examples --output-awst --output-awst-json",
"dev:approvals": "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/native-arrays.algo.ts tests/approvals/mutable-arrays.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --out-dir out/unoptimized/[name] --optimization-level=1",
"dev:testing": "tsx src/cli.ts build tests/approvals/arc4-method-selector.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --out-dir out/unoptimized/[name] --optimization-level=1",
"dev:testing2": "tsx src/cli.ts build tests/expected-output/null-values.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --out-dir out/unoptimized/[name] --optimization-level=1",
"audit": "better-npm-audit audit",
"format": "prettier --write .",
"lint": "eslint \"src/**/*.ts\"",
Expand Down
41 changes: 18 additions & 23 deletions src/awst_build/ast-visitors/constructor-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,50 @@ import ts from 'typescript'
import type { ContractReference } from '../../awst/models'
import { nodeFactory } from '../../awst/node-factory'
import * as awst from '../../awst/nodes'
import { AwstBuildFailureError } from '../../errors'
import { logger } from '../../logger'
import { codeInvariant, invariant } from '../../util'
import { AwstBuildContext } from '../context/awst-build-context'
import type { ContractClassPType } from '../ptypes'
import { voidPType } from '../ptypes'
import { ContractMethodBaseVisitor } from './contract-method-visitor'
import { visitInChildContext } from './util'

export interface ConstructorInfo {
propertyInitializerStatements: awst.Statement[]
cref: ContractReference
}

export class ConstructorVisitor extends ContractMethodBaseVisitor {
private readonly _result: awst.ContractMethod
private _foundSuperCall = false
private readonly _propertyInitializerStatements: awst.Statement[]
constructor(node: ts.ConstructorDeclaration, contractType: ContractClassPType, contractInfo: ConstructorInfo) {
constructor(
node: ts.ConstructorDeclaration,
contractType: ContractClassPType,
private readonly contractInfo: ConstructorInfo,
) {
super(node, contractType)
this._propertyInitializerStatements = contractInfo.propertyInitializerStatements
const sourceLocation = this.sourceLocation(node)

const { args, body, documentation } = this.buildFunctionAwst(node)
}

this._result = new awst.ContractMethod({
get result() {
const sourceLocation = this.sourceLocation(this.node)
const { args, body, documentation } = this.buildFunctionAwst()
return new awst.ContractMethod({
arc4MethodConfig: null,
memberName: this._functionType.name,
sourceLocation,
args,
returnType: voidPType.wtype,
body,
cref: contractInfo.cref,
cref: this.contractInfo.cref,
documentation,
inline: null,
})
}

get result() {
return this._result
}

public static buildConstructor(
node: ts.ConstructorDeclaration,
contractType: ContractClassPType,
constructorMethodInfo: ConstructorInfo,
) {
return AwstBuildContext.current.runInChildContext(() => {
const result = new ConstructorVisitor(node, contractType, constructorMethodInfo).result
invariant(result instanceof awst.ContractMethod, "result must be ContractMethod'")
return result
})
return visitInChildContext(this, node, contractType, constructorMethodInfo)
}

visitBlock(node: ts.Block): awst.Block {
Expand All @@ -71,13 +65,14 @@ export class ConstructorVisitor extends ContractMethodBaseVisitor {
sourceLocation: this.sourceLocation(s),
},
...(Array.isArray(statement) ? statement : [statement]),
...this._propertyInitializerStatements,
...this.contractInfo.propertyInitializerStatements,
)
}
return statement
} catch (e) {
if (e instanceof AwstBuildFailureError) return []
throw e
invariant(e instanceof Error, 'Only errors should be thrown')
logger.error(e)
return []
}
}),
)
Expand Down
48 changes: 32 additions & 16 deletions src/awst_build/ast-visitors/contract-method-visitor.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import type ts from 'typescript'
import { ContractReference, OnCompletionAction } from '../../awst/models'
import { nodeFactory } from '../../awst/node-factory'
import type { ABIMethodArgConstantDefault, ABIMethodArgMemberDefault } from '../../awst/nodes'
import type { ABIMethodArgConstantDefault, ABIMethodArgMemberDefault, ARC4MethodConfig } from '../../awst/nodes'
import * as awst from '../../awst/nodes'
import { ARC4ABIMethodConfig, ARC4BareMethodConfig, ARC4CreateOption } from '../../awst/nodes'
import type { SourceLocation } from '../../awst/source-location'
import { Constants } from '../../constants'
import { CodeError } from '../../errors'
import { logger } from '../../logger'
import { codeInvariant, invariant, isIn } from '../../util'
import { AwstBuildContext } from '../context/awst-build-context'
import type { NodeBuilder } from '../eb'
import { ContractSuperBuilder, ContractThisBuilder } from '../eb/contract-builder'
import { requireExpressionOfType } from '../eb/util'
Expand All @@ -18,6 +17,7 @@ import type { ContractClassPType, FunctionPType } from '../ptypes'
import { GlobalStateType, LocalStateType } from '../ptypes'
import { DecoratorVisitor } from './decorator-visitor'
import { FunctionVisitor } from './function-visitor'
import { visitInChildContext } from './util'

export class ContractMethodBaseVisitor extends FunctionVisitor {
protected readonly _contractType: ContractClassPType
Expand All @@ -41,15 +41,18 @@ export class ContractMethodBaseVisitor extends FunctionVisitor {
}

export class ContractMethodVisitor extends ContractMethodBaseVisitor {
private readonly _result: awst.ContractMethod
private readonly metaData: {
cref: ContractReference
arc4MethodConfig: ARC4MethodConfig | null
sourceLocation: SourceLocation
}

constructor(node: ts.MethodDeclaration, contractType: ContractClassPType) {
super(node, contractType)
const sourceLocation = this.sourceLocation(node)
const { args, body, documentation } = this.buildFunctionAwst(node)
const cref = ContractReference.fromPType(this._contractType)

const decorator = DecoratorVisitor.buildContractMethodData(node)
const cref = ContractReference.fromPType(this._contractType)

const modifiers = this.parseMemberModifiers(node)

Expand All @@ -60,25 +63,38 @@ export class ContractMethodVisitor extends ContractMethodBaseVisitor {
methodLocation: sourceLocation,
})

this._result = new awst.ContractMethod({
arc4MethodConfig: arc4MethodConfig ?? null,
memberName: this._functionType.name,
if (arc4MethodConfig)
this.context.addArc4Config({
contractReference: cref,
sourceLocation,
arc4MethodConfig,
memberName: this._functionType.name,
})
this.metaData = {
arc4MethodConfig,
cref,
sourceLocation,
}
}

get result() {
const { args, body, documentation } = this.buildFunctionAwst()

return new awst.ContractMethod({
arc4MethodConfig: this.metaData.arc4MethodConfig,
memberName: this._functionType.name,
sourceLocation: this.metaData.sourceLocation,
args,
returnType: this._functionType.returnType.wtypeOrThrow,
body,
cref,
cref: this.metaData.cref,
documentation,
inline: null,
})
}

get result() {
return this._result
}

public static buildContractMethod(node: ts.MethodDeclaration, contractType: ContractClassPType): awst.ContractMethod {
return AwstBuildContext.current.runInChildContext(() => new ContractMethodVisitor(node, contractType).result)
public static buildContractMethod(node: ts.MethodDeclaration, contractType: ContractClassPType): () => awst.ContractMethod {
return visitInChildContext(this, node, contractType)
}

private buildArc4Config({
Expand All @@ -91,7 +107,7 @@ export class ContractMethodVisitor extends ContractMethodBaseVisitor {
decorator: DecoratorData | undefined
modifiers: { isPublic: boolean; isStatic: boolean }
methodLocation: SourceLocation
}): awst.ContractMethod['arc4MethodConfig'] | null {
}): awst.ARC4MethodConfig | null {
const isProgramMethod = isIn(functionType.name, [Constants.approvalProgramMethodName, Constants.clearStateProgramMethodName])

if (decorator && isIn(decorator.type, [Constants.arc4BareDecoratorName, Constants.arc4AbiDecoratorName])) {
Expand Down
90 changes: 51 additions & 39 deletions src/awst_build/ast-visitors/contract-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type { ContractMethod } from '../../awst/nodes'
import type { SourceLocation } from '../../awst/source-location'
import { wtypes } from '../../awst/wtypes'
import { Constants } from '../../constants'
import { AwstBuildFailureError } from '../../errors'
import { logger } from '../../logger'
import { codeInvariant, invariant } from '../../util'
import type { ClassElements } from '../../visitor/syntax-names'
Expand All @@ -18,21 +17,28 @@ import { GlobalStateFunctionResultBuilder } from '../eb/storage/global-state'
import { LocalStateFunctionResultBuilder } from '../eb/storage/local-state'
import { requireInstanceBuilder } from '../eb/util'
import { ContractClassModel } from '../models/contract-class-model'
import type { ContractOptionsDecoratorData } from '../models/decorator-data'
import type { ContractClassPType } from '../ptypes'
import { BaseVisitor } from './base-visitor'
import { ConstructorVisitor } from './constructor-visitor'
import { ContractMethodVisitor } from './contract-method-visitor'
import { DecoratorVisitor } from './decorator-visitor'
import { visitInChildContext } from './util'

export class ContractVisitor extends BaseVisitor implements Visitor<ClassElements, void> {
private _ctor?: ContractMethod
private _methods: ContractMethod[] = []
private _approvalProgram: ContractMethod | null = null
private _clearStateProgram: ContractMethod | null = null
private _ctor?: () => ContractMethod
private _methods: Array<() => ContractMethod> = []
private readonly _contractPType: ContractClassPType
private readonly _propertyInitialization: awst.Statement[] = []
public accept = <TNode extends ts.Node>(node: TNode) => accept<ContractVisitor, TNode>(this, node)

private readonly metaData: {
isAbstract: boolean
contractOptions: ContractOptionsDecoratorData | undefined
sourceLocation: SourceLocation
description: string | null
}

constructor(classDec: ts.ClassDeclaration, ptype: ContractClassPType) {
super()
const sourceLocation = this.context.getSourceLocation(classDec)
Expand All @@ -56,23 +62,50 @@ export class ContractVisitor extends BaseVisitor implements Visitor<ClassElement
}
}

this.metaData = {
isAbstract,
sourceLocation,
contractOptions,
description: this.getNodeDescription(classDec),
}
}

get result(): [] | [awst.Contract] {
const { isAbstract, sourceLocation, contractOptions, description } = this.metaData

let approvalProgram: ContractMethod | null = null
let clearProgram: ContractMethod | null = null
const methods: ContractMethod[] = []
const ctor: ContractMethod | null = this._ctor?.() ?? this.makeDefaultConstructor(sourceLocation)

for (const deferredMethod of this._methods) {
const contractMethod = deferredMethod()
switch (contractMethod.memberName) {
case Constants.approvalProgramMethodName:
approvalProgram = contractMethod
break
case Constants.clearStateProgramMethodName:
clearProgram = contractMethod
break
default:
methods.push(contractMethod)
}
}

const contract = new ContractClassModel({
type: this._contractPType,
propertyInitialization: this._propertyInitialization,
isAbstract: isAbstract,
appState: this.context.getStorageDefinitionsForContract(this._contractPType),
ctor: this._ctor ?? this.makeDefaultConstructor(sourceLocation),
methods: this._methods,
description: this.getNodeDescription(classDec),
approvalProgram: this._approvalProgram,
clearProgram: this._clearStateProgram,
ctor,
methods,
description,
approvalProgram,
clearProgram,
options: contractOptions,
sourceLocation: sourceLocation,
})
this.context.addToCompilationSet(contract.id, contract)
}

private getContract(): [] | [awst.Contract] {
const contractClass = this.context.compilationSet.getContractClass(ContractReference.fromPType(this._contractPType))
if (!contractClass.isAbstract) {
return [contractClass.buildContract(this.context.compilationSet)]
Expand All @@ -84,10 +117,8 @@ export class ContractVisitor extends BaseVisitor implements Visitor<ClassElement
try {
this.accept(node)
} catch (e) {
// Ignore this error and continue visiting other members, so we can show additional errors
if (!(e instanceof AwstBuildFailureError)) {
throw e
}
invariant(e instanceof Error, 'Only errors should be thrown')
logger.error(e)
}
}

Expand Down Expand Up @@ -134,26 +165,7 @@ export class ContractVisitor extends BaseVisitor implements Visitor<ClassElement
}

visitMethodDeclaration(node: ts.MethodDeclaration): void {
const contractMethod = ContractMethodVisitor.buildContractMethod(node, this._contractPType)
if (contractMethod.arc4MethodConfig) {
this.context.addArc4Config({
contractReference: contractMethod.cref,
arc4MethodConfig: contractMethod.arc4MethodConfig,
memberName: contractMethod.memberName,
sourceLocation: contractMethod.sourceLocation,
})
}

switch (contractMethod.memberName) {
case Constants.approvalProgramMethodName:
this._approvalProgram = contractMethod
break
case Constants.clearStateProgramMethodName:
this._clearStateProgram = contractMethod
break
default:
this._methods.push(contractMethod)
}
this._methods.push(ContractMethodVisitor.buildContractMethod(node, this._contractPType))
}
visitPropertyDeclaration(node: ts.PropertyDeclaration): void {
const sourceLocation = this.sourceLocation(node)
Expand Down Expand Up @@ -213,7 +225,7 @@ export class ContractVisitor extends BaseVisitor implements Visitor<ClassElement
this.throwNotSupported(node, 'set accessors')
}

public static buildContract(classDec: ts.ClassDeclaration, ptype: ContractClassPType): [] | [awst.Contract] {
return new ContractVisitor(classDec, ptype).getContract()
public static buildContract(classDec: ts.ClassDeclaration, ptype: ContractClassPType) {
return visitInChildContext(this, classDec, ptype)
}
}
8 changes: 3 additions & 5 deletions src/awst_build/ast-visitors/decorator-visitor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import ts from 'typescript'
import { AwstBuildFailureError } from '../../errors'
import { logger } from '../../logger'
import { invariant, isIn } from '../../util'
import { accept } from '../../visitor/visitor'
Expand Down Expand Up @@ -28,10 +27,9 @@ export class DecoratorVisitor extends BaseVisitor {
try {
return AwstBuildContext.current.runInChildContext(() => new DecoratorVisitor(modifier).result)
} catch (e) {
if (e instanceof AwstBuildFailureError) {
return []
}
throw e
invariant(e instanceof Error, 'Only errors should be thrown')
logger.error(e)
return []
}
}) ?? []
)
Expand Down
Loading

0 comments on commit 002f91b

Please sign in to comment.