Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Support emit calls which include an explicit signature #70

Merged
merged 3 commits into from
Jan 8, 2025
Merged
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: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 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/abi-decorators.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/arc-28-events.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 Expand Up @@ -74,6 +74,7 @@
"vitest": "2.1.8"
},
"dependencies": {
"arcsecond": "^5.0.0",
"chalk": "^5.3.0",
"change-case": "^5.4.4",
"commander": "^12.1.0",
Expand Down
4 changes: 3 additions & 1 deletion src/awst/to-code-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,10 @@ export class ToCodeVisitor
}

visitContractMethod(statement: nodes.ContractMethod): string[] {
const args = statement.args.map((a) => `${a.name}: ${a.wtype}`).join(', ')

const prefix = statement.cref.id === this.currentContract.at(-1)?.id ? '' : `${statement.cref.className}::`
return [`${prefix}${statement.memberName}(): ${statement.returnType}`, '{', ...indent(statement.body.accept(this)), '}', '']
return [`${prefix}${statement.memberName}(${args}): ${statement.returnType}`, '{', ...indent(statement.body.accept(this)), '}', '']
}
visitLogicSignature(moduleStatement: nodes.LogicSignature): string[] {
return ['', `logicsig ${moduleStatement.id} {`, ...indent(moduleStatement.program.body.accept(this)), '}']
Expand Down
6 changes: 6 additions & 0 deletions src/awst/wtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ export namespace wtypes {
nativeType: n <= 64 ? uint64WType : biguintWType,
arc4Name: arc4Name ?? `uint${n}`,
})
invariant(n >= 8n && n <= 512n, 'Invalid uint: n must be between 8 and 512')
invariant(n % 8n === 0n, 'Invalid uint: n must be multiple of 8')
this.n = n
}
}
Expand All @@ -242,6 +244,10 @@ export namespace wtypes {
nativeType: n <= 64 ? uint64WType : biguintWType,
arc4Name: `ufixed${n}x${m}`,
})

invariant(n >= 8n && n <= 512n, 'Invalid ufixed: n must be between 8 and 512')
invariant(n % 8n === 0n, 'Invalid ufixed: n must be multiple of 8')
invariant(m >= 0n && m <= 160n, 'Invalid ufixed: m must be between 0 and 160')
this.n = n
this.m = m
}
Expand Down
9 changes: 5 additions & 4 deletions src/awst_build/arc4-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import {
voidPType,
} from './ptypes'
import {
ARC4BooleanType,
arc4BooleanType,
ARC4EncodedType,
ARC4StringType,
arc4StringType,
ARC4StructType,
ARC4TupleType,
DynamicBytesType,
Expand Down Expand Up @@ -58,15 +58,16 @@ export function ptypeToArc4EncodedType(ptype: ObjectPType, sourceLocation: Sourc
export function ptypeToArc4EncodedType(ptype: PType, sourceLocation: SourceLocation): ARC4EncodedType
export function ptypeToArc4EncodedType(ptype: PType, sourceLocation: SourceLocation): ARC4EncodedType {
if (ptype instanceof ARC4EncodedType) return ptype
if (ptype.equals(boolPType)) return ARC4BooleanType
if (ptype.equals(boolPType)) return arc4BooleanType
if (ptype.equals(uint64PType)) return new UintNType({ n: 64n })
if (ptype.equals(biguintPType)) return new UintNType({ n: 512n })
if (ptype.equals(bytesPType)) return DynamicBytesType
if (ptype.equals(stringPType)) return ARC4StringType
if (ptype.equals(stringPType)) return arc4StringType
if (ptype instanceof NativeNumericType) {
throw new CodeError(numberPType.expressionMessage, { sourceLocation })
}
if (ptype instanceof TuplePType) return new ARC4TupleType({ types: ptype.items.map((i) => ptypeToArc4EncodedType(i, sourceLocation)) })

if (ptype instanceof ObjectPType)
return new ARC4StructType({
name: ptype.name,
Expand Down
73 changes: 64 additions & 9 deletions src/awst_build/eb/arc28/arc-28-emit-function-builder.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { nodeFactory } from '../../../awst/node-factory'
import type { Expression } from '../../../awst/nodes'
import type { SourceLocation } from '../../../awst/source-location'
import { InternalError } from '../../../errors'
import type { Expression, StringConstant } from '../../../awst/nodes'
import { SourceLocation } from '../../../awst/source-location'
import { CodeError, InternalError } from '../../../errors'
import { logger } from '../../../logger'
import { codeInvariant } from '../../../util'
import { Arc4ParseError, parseArc4Type } from '../../../util/arc4-signature-parser'
import { ptypeToArc4EncodedType } from '../../arc4-util'
import type { PType } from '../../ptypes'
import { arc28EmitFunction, ObjectPType, stringPType, voidPType } from '../../ptypes'
import { ARC4EncodedType, ARC4StructType } from '../../ptypes/arc4-types'
import { ARC4EncodedType, ARC4StructType, ARC4TupleType } from '../../ptypes/arc4-types'
import { instanceEb } from '../../type-registry'
import type { NodeBuilder } from '../index'
import type { InstanceBuilder, NodeBuilder } from '../index'
import { FunctionBuilder } from '../index'
import { requireStringConstant } from '../util'
import { parseFunctionArgs } from '../util/arg-parsing'
Expand All @@ -31,17 +32,28 @@ export class Arc28EmitFunctionBuilder extends FunctionBuilder {
})

if (nameOrObj.ptype.equals(stringPType)) {
const name = requireStringConstant(nameOrObj).value
const thisModule = nameOrObj.sourceLocation.file ?? ''

const fields: Record<string, ARC4EncodedType> = {}
const values = new Map<string, Expression>()

for (const [index, prop] of Object.entries(props)) {
const { name, propTypes } = parseEventName(nameOrObj)

for (const [index, prop] of props.entries()) {
const arc4Type = ptypeToArc4EncodedType(prop.ptype, prop.sourceLocation)

const expectedType = propTypes?.[index]
if (expectedType) {
codeInvariant(
expectedType.wtype.equals(arc4Type.wtype),
`Expected type ${expectedType} does not match actual type ${arc4Type}`,
prop.sourceLocation,
)
}

fields[index] = arc4Type
values.set(
index,
index.toString(),
prop.ptype instanceof ARC4EncodedType
? prop.resolve()
: nodeFactory.aRC4Encode({
Expand All @@ -51,9 +63,14 @@ export class Arc28EmitFunctionBuilder extends FunctionBuilder {
}),
)
}
if (propTypes && propTypes.length !== values.size) {
throw new CodeError(`Event signature length (${propTypes.length}) does not match number of provided values (${values.size}).`, {
sourceLocation: sourceLocation,
})
}

const structType = new ARC4StructType({
name,
name: name.value,
module: thisModule,
fields,
description: undefined,
Expand Down Expand Up @@ -107,3 +124,41 @@ function emitStruct(ptype: ARC4StructType, expression: Expression, sourceLocatio
voidPType,
)
}

function parseEventName(nameBuilder: InstanceBuilder): {
name: StringConstant
propTypes?: ARC4EncodedType[]
} {
const name = requireStringConstant(nameBuilder)
const parenthesisIndex = name.value.indexOf('(')
if (parenthesisIndex === -1) {
return {
name,
}
}
const signature = name.value.substring(parenthesisIndex)

try {
const signatureType = parseArc4Type(signature)
codeInvariant(signatureType instanceof ARC4TupleType, 'Event signature must be a tuple type', name.sourceLocation)
return {
name: nodeFactory.stringConstant({
value: name.value.substring(0, parenthesisIndex),
sourceLocation: name.sourceLocation,
}),
propTypes: signatureType.items,
}
} catch (e) {
if (e instanceof Arc4ParseError) {
// Assumes StringConstant is all on one line
throw new CodeError(`Invalid signature: ${e.message}`, {
sourceLocation: new SourceLocation({
...name.sourceLocation,
column: name.sourceLocation.column + parenthesisIndex + e.index,
}),
})
} else {
throw e
}
}
}
4 changes: 2 additions & 2 deletions src/awst_build/eb/arc4/bool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { SourceLocation } from '../../../awst/source-location'
import { wtypes } from '../../../awst/wtypes'
import type { PType } from '../../ptypes'
import { boolPType } from '../../ptypes'
import { ARC4BoolClass, ARC4BooleanType, type ARC4EncodedType } from '../../ptypes/arc4-types'
import { ARC4BoolClass, arc4BooleanType, type ARC4EncodedType } from '../../ptypes/arc4-types'
import type { InstanceBuilder, NodeBuilder } from '../index'
import { ClassBuilder } from '../index'
import { parseFunctionArgs } from '../util/arg-parsing'
Expand Down Expand Up @@ -55,6 +55,6 @@ export class BoolClassBuilder extends ClassBuilder {

export class BoolExpressionBuilder extends Arc4EncodedBaseExpressionBuilder<ARC4EncodedType> {
constructor(expression: Expression) {
super(expression, ARC4BooleanType)
super(expression, arc4BooleanType)
}
}
4 changes: 2 additions & 2 deletions src/awst_build/eb/arc4/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { wtypes } from '../../../awst/wtypes'
import type { PType } from '../../ptypes'
import { stringPType } from '../../ptypes'
import type { ARC4EncodedType } from '../../ptypes/arc4-types'
import { ARC4StrClass, ARC4StringType } from '../../ptypes/arc4-types'
import { ARC4StrClass, arc4StringType } from '../../ptypes/arc4-types'
import type { InstanceBuilder, NodeBuilder } from '../index'
import { ClassBuilder } from '../index'
import { parseFunctionArgs } from '../util/arg-parsing'
Expand Down Expand Up @@ -59,6 +59,6 @@ export class StrClassBuilder extends ClassBuilder {

export class StrExpressionBuilder extends Arc4EncodedBaseExpressionBuilder<ARC4EncodedType> {
constructor(expression: Expression) {
super(expression, ARC4StringType)
super(expression, arc4StringType)
}
}
4 changes: 2 additions & 2 deletions src/awst_build/ptypes/arc4-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ export const ARC4StrClass = new LibClassType({
module: Constants.arc4EncodedTypesModuleName,
})

export const ARC4BooleanType = new ARC4InstanceType({
export const arc4BooleanType = new ARC4InstanceType({
name: 'Bool',
wtype: wtypes.arc4BooleanWType,
nativeType: boolPType,
})

export const ARC4StringType = new ARC4InstanceType({
export const arc4StringType = new ARC4InstanceType({
name: 'Str',
wtype: wtypes.arc4StringAliasWType,
nativeType: stringPType,
Expand Down
8 changes: 4 additions & 4 deletions src/awst_build/ptypes/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ import {
AddressClass,
arc4AddressAlias,
ARC4BoolClass,
ARC4BooleanType,
arc4BooleanType,
arc4ByteAlias,
ARC4StrClass,
ARC4StringType,
arc4StringType,
ARC4StructClass,
ARC4StructType,
Arc4TupleClass,
Expand Down Expand Up @@ -351,8 +351,8 @@ export function registerPTypes(typeRegistry: TypeRegistry) {
typeRegistry.register({ ptype: arc4AddressAlias, instanceEb: AddressExpressionBuilder })
typeRegistry.register({ ptype: AddressClass, singletonEb: AddressClassBuilder })
typeRegistry.register({ ptype: ARC4BoolClass, singletonEb: BoolClassBuilder })
typeRegistry.register({ ptype: ARC4BooleanType, instanceEb: BoolExpressionBuilder })
typeRegistry.register({ ptype: ARC4StringType, instanceEb: StrExpressionBuilder })
typeRegistry.register({ ptype: arc4BooleanType, instanceEb: BoolExpressionBuilder })
typeRegistry.register({ ptype: arc4StringType, instanceEb: StrExpressionBuilder })
typeRegistry.register({ ptype: ARC4StrClass, singletonEb: StrClassBuilder })
typeRegistry.register({ ptype: Arc4TupleClass, singletonEb: Arc4TupleClassBuilder })
typeRegistry.registerGeneric({ generic: Arc4TupleGeneric, ptype: ARC4TupleType, instanceEb: Arc4TupleExpressionBuilder })
Expand Down
67 changes: 67 additions & 0 deletions src/util/arc4-signature-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest'
import { Arc4ParseError, parseArc4Type } from './arc4-signature-parser'
import { invariant } from './index'

describe('arc4 signature parser', () => {
describe('can parse valid types', () => {
it.each([
['byte'],
['byte[]'],
['byte[5]'],
['bool'],
['bool[]'],
['bool[12]'],
['uint8'],
['uint8[]'],
['uint8[12]'],
['uint16'],
['uint16[]'],
['uint16[12]'],
['uint32'],
['uint32[]'],
['uint32[12]'],
['uint64'],
['uint64[]'],
['uint64[12]'],
['uint128'],
['uint128[]'],
['uint128[12]'],
['uint256'],
['uint256[]'],
['uint256[12]'],
['string'],
['string[]'],
['string[12]'],
['byte[][][]'],
['byte[2][2][2]'],
['(bool)'],
['(bool,string,uint8,(uint16,string[]),bool[])[1][]'],
])('%s parses', (arc4TypeString) => {
const match = parseArc4Type(arc4TypeString)

expect(match.wtype.arc4Name).toBe(arc4TypeString)
})
})
describe('errors on invalid types', () => {
it.each([
['float', "Expecting string 'uint', got 'floa...'", 0],
['uint2', 'n must be between 8 and 512, and a multiple of 8', 0],
['(uint8,uint2)', 'n must be between 8 and 512, and a multiple of 8', 7],
['uint8[', "Expecting character ']', but got end of input.", 6],
['uint8]', "Expecting ',', '[', or ')', but got ]", 6],
['uint8,uint8', 'Signature contained more than one type. Wrap multiple types in parentheses to declare a tuple type', 0],
['(uint8', 'Tuple has not been closed', 6],
['(uint8,bool))', "Char ')' has no matching opening '('", 13],
])('%s returns error %s', (typeString, errorMessage, errorIndex) => {
try {
parseArc4Type(typeString)
} catch (e) {
invariant(e instanceof Arc4ParseError, 'e must be instance of Arc4ParseError')
expect(e.message).toBe(errorMessage)
expect(e.index).toBe(errorIndex)
return
}
expect.fail('Expected error but none was thrown')
})
})
})
Loading
Loading