Skip to content

Commit

Permalink
refactor: Introduce array literal ptype to differentiate between expl…
Browse files Browse the repository at this point in the history
…icit tuples and implicit ones
  • Loading branch information
tristanmenzel committed Oct 8, 2024
1 parent 063134f commit fcb12c1
Show file tree
Hide file tree
Showing 47 changed files with 6,501 additions and 1,584 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"dev:tealscript": "tsx src/cli.ts build examples/tealscript/example.algo.ts",
"dev:approvals": "tsx src/cli.ts build tests/approvals --output-awst --output-awst-json --dry-run",
"dev:expected-output": "tsx src/cli.ts build tests/expected-output --dry-run",
"dev:testing": "tsx src/cli.ts build tests/approvals/local-state.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level debug",
"dev:testing": "tsx src/cli.ts build tests/approvals/array-literals.algo.ts --output-awst --output-awst-json --dry-run",
"audit": "better-npm-audit audit",
"format": "prettier --write .",
"lint": "eslint \"src/**/*.ts\"",
Expand Down
2 changes: 1 addition & 1 deletion src/awst/node-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ const explicitNodeFactory = {
tupleExpression(props: Omit<Props<TupleExpression>, 'wtype'>) {
return new TupleExpression({
...props,
wtype: new WTuple({ types: props.items.map((i) => i.wtype), immutable: true }),
wtype: new WTuple({ types: props.items.map((i) => i.wtype), immutable: false }),
})
},
methodDocumentation(props?: { description?: string | null; args?: Map<string, string>; returns?: string | null }) {
Expand Down
56 changes: 30 additions & 26 deletions src/awst_build/base-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,19 @@ import { OmittedExpressionBuilder } from './eb/omitted-expression-builder'
import { StringExpressionBuilder, StringFunctionBuilder } from './eb/string-expression-builder'
import { requireExpressionOfType, requireInstanceBuilder } from './eb/util'
import type { PType } from './ptypes'
import { ArrayPType, BigIntPType, biguintPType, boolPType, NumberPType, ObjectPType, TuplePType, uint64PType, UnionPType } from './ptypes'
import {
ArrayLiteralPType,
ArrayPType,
BigIntPType,
biguintPType,
boolPType,
NumberPType,
ObjectPType,
TransientType,
TuplePType,
uint64PType,
UnionPType,
} from './ptypes'
import { TextVisitor } from './text-visitor'
import { instanceEb, typeRegistry } from './type-registry'

Expand Down Expand Up @@ -379,7 +391,7 @@ export abstract class BaseVisitor implements Visitor<Expressions, NodeBuilder> {
}): InstanceBuilder {
// If the expression has a wtype, we can resolve it immediately - if not, we defer the resolution until we have more context
// (eg. the type of the assignment target)
if (ptype.wtype) {
if (!(ptype instanceof TransientType) && ptype.wtype) {
return typeRegistry.getInstanceEb(
nodeFactory.conditionalExpression({
sourceLocation: sourceLocation,
Expand Down Expand Up @@ -445,26 +457,15 @@ export abstract class BaseVisitor implements Visitor<Expressions, NodeBuilder> {
}

handleAssignment(target: InstanceBuilder, source: InstanceBuilder, sourceLocation: SourceLocation): InstanceBuilder {
if (source.ptype.equals(target.ptype)) {
return instanceEb(
nodeFactory.assignmentExpression({
target: target.resolveLValue(),
sourceLocation,
value: source.resolve(),
}),
target.ptype,
)
} else {
const assignmentType = this.buildAssignmentExpressionType(target.ptype, source.ptype, sourceLocation)
return instanceEb(
nodeFactory.assignmentExpression({
target: this.buildLValue(target, assignmentType, sourceLocation),
sourceLocation,
value: source.resolveToPType(assignmentType).resolve(),
}),
assignmentType,
)
}
const assignmentType = this.buildAssignmentExpressionType(target.ptype, source.ptype, sourceLocation)
return instanceEb(
nodeFactory.assignmentExpression({
target: this.buildLValue(target, assignmentType, sourceLocation),
sourceLocation,
value: source.resolveToPType(assignmentType).resolve(),
}),
assignmentType,
)
}

/**
Expand All @@ -480,6 +481,10 @@ export abstract class BaseVisitor implements Visitor<Expressions, NodeBuilder> {
* @private
*/
private buildAssignmentExpressionType(targetType: PType, sourceType: PType, sourceLocation: SourceLocation): PType {
if (targetType instanceof ArrayLiteralPType)
// Puya does not support assigning to array targets, but we can treat array literals as tuples
return this.buildAssignmentExpressionType(targetType.getTupleType(), sourceType, sourceLocation)

const errorMessage = `Value of type ${sourceType.name} cannot be assigned to target of type ${targetType.name}`
if (sourceType.equals(targetType)) {
return targetType
Expand All @@ -498,9 +503,9 @@ export abstract class BaseVisitor implements Visitor<Expressions, NodeBuilder> {
// Narrow `biguint | bigint` or `bigint` to target type
return targetType
}
if (sourceType instanceof TuplePType) {
if (sourceType instanceof ArrayLiteralPType) {
if (targetType instanceof TuplePType) {
// Narrow tuple item types recursively
// Narrow array literal types to tuple item types
codeInvariant(targetType.items.length <= sourceType.items.length, errorMessage, sourceLocation)
return new TuplePType({
items: sourceType.items.map((item, index) =>
Expand All @@ -509,15 +514,14 @@ export abstract class BaseVisitor implements Visitor<Expressions, NodeBuilder> {
immutable: sourceType.immutable,
})
} else if (targetType instanceof ArrayPType) {
// Widen tuple type to array
// Narrow array literal types to array type
codeInvariant(
sourceType.items.every((i) =>
this.buildAssignmentExpressionType(targetType.itemType, i, sourceLocation).equals(targetType.itemType),
),
errorMessage,
sourceLocation,
)
// TODO: Tuples should widen to an immutable array only, but array literals are represented as tuples currently. They should have their own type
return targetType
}
}
Expand Down
21 changes: 13 additions & 8 deletions src/awst_build/eb/arc4-bare-method-decorator-builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ARC4CreateOption, OnCompletionAction } from '../../awst/models'
import type { Expression } from '../../awst/nodes'
import { StringConstant, TupleExpression } from '../../awst/nodes'
import { NewArray, StringConstant, TupleExpression } from '../../awst/nodes'
import type { SourceLocation } from '../../awst/source-location'
import { Constants } from '../../constants'
import { CodeError } from '../../errors'
Expand Down Expand Up @@ -109,18 +109,23 @@ function mapStringConstant<T>(map: Record<string, T>, expr: Expression) {
function resolveOnCompletionActions(oca: InstanceBuilder | undefined): OnCompletionAction[] {
if (!oca) return [OnCompletionAction.NoOp]
const value = oca.resolve()
let ocaRawExpr: Expression[]
if (value instanceof StringConstant) {
return [mapStringConstant(ocaMap, value)]
ocaRawExpr = [value]
} else if (value instanceof TupleExpression) {
const ocas = value.items.map((item) => mapStringConstant(ocaMap, item))
const distinctOcas = Array.from(new Set(ocas))
if (distinctOcas.length !== ocas.length) {
logger.warn(oca.sourceLocation, 'Duplicate on completion actions')
}
return ocas
ocaRawExpr = value.items
} else if (value instanceof NewArray) {
ocaRawExpr = value.values
} else {
throw new CodeError('Unexpected value for onComplete', { sourceLocation: oca.sourceLocation })
}

const ocas = ocaRawExpr.map((item) => mapStringConstant(ocaMap, item))
const distinctOcas = Array.from(new Set(ocas))
if (distinctOcas.length !== ocas.length) {
logger.warn(oca.sourceLocation, 'Duplicate on completion actions')
}
return ocas
}

function resolveDefaultArguments(
Expand Down
47 changes: 25 additions & 22 deletions src/awst_build/eb/literal/array-literal-expression-builder.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
import { nodeFactory } from '../../../awst/node-factory'
import type { Expression, LValue } from '../../../awst/nodes'
import type { SourceLocation } from '../../../awst/source-location'
import { CodeError } from '../../../errors'
import { codeInvariant } from '../../../util'
import type { PTypeOrClass } from '../../ptypes'
import { ArrayPType, TuplePType } from '../../ptypes'
import { ArrayLiteralPType, ArrayPType, TuplePType } from '../../ptypes'
import type { NodeBuilder } from '../index'
import { InstanceBuilder } from '../index'
import { TupleExpressionBuilder } from '../tuple-expression-builder'
import { requireExpressionOfType, requireIntegerConstant } from '../util'
import { requireIntegerConstant } from '../util'

export class ArrayLiteralExpressionBuilder extends InstanceBuilder {
readonly ptype: TuplePType
readonly ptype: ArrayLiteralPType
constructor(
sourceLocation: SourceLocation,
private readonly items: InstanceBuilder[],
) {
super(sourceLocation)
this.ptype = new TuplePType({ items: items.map((i) => i.ptype) })
this.ptype = new ArrayLiteralPType({ items: items.map((i) => i.ptype) })
}

resolve(): Expression {
// Resolve object to a tuple using its own inferred types
return this.toTuple(this.ptype, this.sourceLocation)
const arrayType = this.ptype.getArrayType()

return nodeFactory.newArray({
sourceLocation: this.sourceLocation,
values: this.items.map((i) => i.resolve()),
wtype: arrayType.wtype,
})
}

resolveLValue(): LValue {
throw new CodeError('Array literal is not a valid lvalue')
// return nodeFactory.tupleExpression({
// items: this.items.map((i) => i.resolveLValue()),
// sourceLocation: this.sourceLocation,
// })
}

singleEvaluation(): InstanceBuilder {
return this
return new ArrayLiteralExpressionBuilder(
this.sourceLocation,
this.items.map((i) => i.singleEvaluation()),
)
}

indexAccess(index: InstanceBuilder, sourceLocation: SourceLocation): NodeBuilder {
Expand All @@ -34,24 +51,10 @@ export class ArrayLiteralExpressionBuilder extends InstanceBuilder {
return this.items[indexNum]
}

private toTuple(ptype: TuplePType, sourceLocation: SourceLocation): Expression {
return nodeFactory.tupleExpression({
items: this.items.map((item, index) => requireExpressionOfType(item, ptype.items[index])),
sourceLocation,
})
}

resolveLValue(): LValue {
return nodeFactory.tupleExpression({
items: this.items.map((i) => i.resolveLValue()),
sourceLocation: this.sourceLocation,
})
}

resolveToPType(ptype: PTypeOrClass): InstanceBuilder {
if (ptype instanceof TuplePType) {
codeInvariant(
ptype.items.length === this.items.length,
ptype.items.length <= this.items.length,
`Value of length ${this.items.length} cannot be resolved to type of length ${ptype.items.length}`,
)
return new TupleExpressionBuilder(
Expand Down
21 changes: 19 additions & 2 deletions src/awst_build/function-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import type { AwstBuildContext } from './context/awst-build-context'
import type { InstanceBuilder } from './eb'
import { ArrayLiteralExpressionBuilder } from './eb/literal/array-literal-expression-builder'
import { ObjectLiteralExpressionBuilder } from './eb/literal/object-literal-expression-builder'
import { NativeArrayExpressionBuilder } from './eb/native-array-expression-builder'
import { OmittedExpressionBuilder } from './eb/omitted-expression-builder'
import { TupleExpressionBuilder } from './eb/tuple-expression-builder'
import { requireExpressionOfType, requireInstanceBuilder } from './eb/util'
import type { PType } from './ptypes'
import { FunctionPType, ObjectPType, TransientType } from './ptypes'
Expand Down Expand Up @@ -103,10 +105,25 @@ export class FunctionVisitor
if (ts.isOmittedExpression(element)) {
items.push(new OmittedExpressionBuilder(sourceLocation))
} else {
codeInvariant(!element.dotDotDotToken, 'Spread operator is not supported', sourceLocation)
codeInvariant(!element.initializer, 'Initializer on array binding expression is not supported', sourceLocation)
codeInvariant(!element.propertyName, 'Property name on array binding expression is not supported', sourceLocation)
items.push(this.buildAssignmentTarget(element.name, sourceLocation))

if (element.dotDotDotToken) {
const spreadResult = this.buildAssignmentTarget(element.name, sourceLocation)
if (spreadResult instanceof NativeArrayExpressionBuilder) {
throw new CodeError(
'Spread operator is not supported in assignment expressions where the resulting type is a variadic array',
{ sourceLocation },
)
} else if (spreadResult instanceof TupleExpressionBuilder) {
throw new CodeError('Spread operator is not currently supported with tuple expressions', { sourceLocation })
} else {
// TODO: What would this context be?
throw new CodeError('The spread operator is not supported in this context', { sourceLocation })
}
} else {
items.push(this.buildAssignmentTarget(element.name, sourceLocation))
}
}
}
return new ArrayLiteralExpressionBuilder(sourceLocation, items)
Expand Down
36 changes: 36 additions & 0 deletions src/awst_build/ptypes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,42 @@ export class FunctionPType extends PType {
this.parameters = props.parameters
}
}
export class ArrayLiteralPType extends TransientType {
get fullName() {
return `${this.module}::[${this.items.map((i) => i.fullName).join(', ')}]`
}

readonly items: PType[]
readonly immutable = true
constructor(props: { items: PType[] }) {
super({
module: 'lib.d.ts',
name: `[${props.items.map((i) => i.name).join(', ')}]`,
typeMessage:
'Native array types are not valid as variable, parameter, return, or property types. Please define a static tuple type or use an `as const` expression',
singleton: false,
expressionMessage: '',
})
this.items = props.items
}

getArrayType(): ArrayPType {
const itemType = UnionPType.fromTypes(this.items)

return new ArrayPType({
immutable: false,
itemType,
})
}

getTupleType(): TuplePType {
return new TuplePType({
immutable: false,
items: this.items,
})
}
}

export class TuplePType extends PType {
readonly module: string = 'lib.d.ts'
get name() {
Expand Down
14 changes: 14 additions & 0 deletions tests/approvals/array-literals.algo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { uint64 } from '@algorandfoundation/algorand-typescript'

function test(a: uint64, b: uint64) {
const inferTuple = [a, b] as const
const explicitTuple: [uint64, uint64] = [a, b]

const conditionalExplicitTuple: [uint64, uint64] = a < b ? [a, b] : [b, a]

const [c, d] = [a, b]

//const [...f] = [a, b] as const
const [, g] = [a, b] as const
const [h] = [a, b] as const
}
14 changes: 7 additions & 7 deletions tests/approvals/out/abi-decorators.awst
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ contract AbiDecorators extends @algorandfoundation/algorand-typescript/arc4/inde
void
var GlobalState["globalValue"]: uint64 = 123
}

justNoop(): void
{
}

createMethod(): void
{
}

allActions(): void
{
}

readonly(): uint64
{
return 5
}

methodWithDefaults(): uint64
{
return a * b + c
}

}
}
2 changes: 1 addition & 1 deletion tests/approvals/out/abi-decorators.awst.json
Original file line number Diff line number Diff line change
Expand Up @@ -708,4 +708,4 @@
},
"docstring": null
}
]
]
6 changes: 3 additions & 3 deletions tests/approvals/out/accounts.awst
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ contract AccountsContract extends @algorandfoundation/algorand-typescript/arc4/i
{
void
}

getAccountInfo(): tuple[bytes, uint64, bytes, bool, bool, uint64, uint64, uint64, uint64, uint64, uint64, uint64, uint64, uint64, uint64]
{
return <tuple>[reinterpret_cast<bytes>(checked_maybe(acct_params_get<AcctAuthAddr>(account), comment=account funded)), checked_maybe(acct_params_get<AcctBalance>(account), comment=account funded), reinterpret_cast<bytes>(account), app_opted_in(account, global<CurrentApplicationID>()), asset_holding_get<AssetBalance>(account, asset).1, checked_maybe(acct_params_get<AcctMinBalance>(account), comment=account funded), checked_maybe(acct_params_get<AcctTotalAppsCreated>(account), comment=account funded), checked_maybe(acct_params_get<AcctTotalAppsOptedIn>(account), comment=account funded), checked_maybe(acct_params_get<AcctTotalAssets>(account), comment=account funded), checked_maybe(acct_params_get<AcctTotalAssetsCreated>(account), comment=account funded), checked_maybe(acct_params_get<AcctTotalBoxBytes>(account), comment=account funded), checked_maybe(acct_params_get<AcctTotalBoxes>(account), comment=account funded), checked_maybe(acct_params_get<AcctTotalExtraAppPages>(account), comment=account funded), checked_maybe(acct_params_get<AcctTotalNumByteSlice>(account), comment=account funded), checked_maybe(acct_params_get<AcctTotalNumUint>(account), comment=account funded)]
}

}
}
Loading

0 comments on commit fcb12c1

Please sign in to comment.